@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
@@ -2,16 +2,19 @@
2
2
  * The versioned operations of an api app, read from its typed operation table and printed as OpenAPI 3.1.
3
3
  *
4
4
  * The table is the canon's one declaration of an app's operation surface (knowledge/patterns/be/api.yaml, BE-OPERATIONS-1):
5
- * `apps/<app>/src/operations.ts` exports `OPERATIONS`, made by `defineOperations({ "<id>@<version>": operation<Input, Output,
6
- * RefusalCode>("query" | "mutation") })`. The types come from the TypeScript checker; nothing is executed. An input, an output or a
7
- * refusal set the checker cannot express (`any`, `unknown`, `Record<string, unknown>`, an unbound generic, `string` as a refusal code)
8
- * is an error naming the operation.
5
+ * `apps/<app>/src/operations.ts` exports `OPERATIONS`, made by `defineOperations({ "<id>@<version>": query({ input, output }) })`.
6
+ * Input and output are exported zod schemas from *.schema.ts. Only schema modules are evaluated, never app modules; the app's
7
+ * z.toJSONSchema carries their constraints into OpenAPI. Open schemas and unrepresentable wire values are refused; the
8
+ * TypeScript checker separately proves closed parsed input/output types and refusal codes. A failure always names the operation.
9
9
  *
10
10
  * The wire is one route, `POST /operations`: the body is `{ operation, requestId, input }` and the answer is `{ operation, requestId,
11
11
  * outcome }`, `outcome` being `{ kind: "ok", value }` or `{ kind: "refused", code, params? }`. Each operation contributes one request
12
12
  * and one reply component carrying `x-operation: "<id>@<version>"`; the route's body and answer are the oneOf over them.
13
13
  */
14
14
  import { createSchemaBuilder, stable } from './type-schema.mjs';
15
+ import { INPUT_BOUNDS_OPENAPI_EXTENSION, INPUT_BOUNDS_VERSION } from './input-bounds.mjs';
16
+ import { operationSchemaReader } from './operation-schemas.mjs';
17
+ import path from 'node:path';
15
18
 
16
19
  /** The export of `apps/<app>/src/operations.ts` that holds the table. */
17
20
  const OPERATIONS_EXPORT = 'OPERATIONS';
@@ -27,19 +30,32 @@ const ID = /^[a-z][A-Za-z0-9.]*@[1-9][0-9]*$/;
27
30
  /** The component name stem of an operation id: `sales.policy@1` gives `sales.policy.v1`. */
28
31
  const stemOf = (id) => id.replace('@', '.v');
29
32
 
33
+ /** Preserve R95 on parsed values even when an input transform still has a closed JSON wire representation. */
34
+ function assertParsedTypes(builder, member, fail) {
35
+ for (const label of ['input', 'output']) {
36
+ const type = member(label);
37
+ try {
38
+ builder.schemaOf(type, `parsed ${label}`);
39
+ } catch (error) {
40
+ fail(error.message);
41
+ }
42
+ }
43
+ }
44
+
30
45
  /**
31
46
  * Reads the operation table of a source file of a program. Answers null when the file is not in the program; throws when it
32
47
  * exports no `OPERATIONS` or an operation is not decidable.
33
48
  */
34
- export function readOperations({ ts, program, file }) {
49
+ export function readOperations({ ts, program, file, repoRoot = path.resolve(path.dirname(file), '../../..') }) {
35
50
  const checker = program.getTypeChecker();
36
51
  const sourceFile = program.getSourceFile(file);
37
52
  if (!sourceFile) return null;
38
53
  const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
39
54
  const exported = moduleSymbol ? checker.getExportsOfModule(moduleSymbol).find((symbol) => symbol.getName() === OPERATIONS_EXPORT) : null;
40
55
  if (!exported) throw new Error(`${file} does not export ${OPERATIONS_EXPORT}, the app's operation table`);
41
- const builder = createSchemaBuilder({ ts, checker });
42
- const table = checker.getTypeOfSymbol(exported);
56
+ const readSchema = operationSchemaReader({ ts, program, repoRoot });
57
+ const parsedTypes = createSchemaBuilder({ ts, checker, schemaBacked: true });
58
+ const table = checker.getTypeOfSymbol(exported.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(exported) : exported);
43
59
  const operations = [];
44
60
  for (const prop of [...checker.getPropertiesOfType(table)].sort((a, b) => (a.getName() < b.getName() ? -1 : 1))) {
45
61
  const id = prop.getName();
@@ -47,6 +63,7 @@ export function readOperations({ ts, program, file }) {
47
63
  throw new Error(`operation ${id}: ${why}`);
48
64
  };
49
65
  if (!ID.test(id)) fail('the key must be "<name>@<positive version>", for example "sales.policy@1"');
66
+ if (id.length > 128) fail('the operation id exceeds the runtime envelope limit of 128 characters');
50
67
  const contract = checker.getTypeOfSymbol(prop);
51
68
  const kindType = contract.getProperty('kind') ? checker.getTypeOfSymbol(contract.getProperty('kind')) : null;
52
69
  const kind = kindType?.isStringLiteral() ? kindType.value : null;
@@ -59,15 +76,17 @@ export function readOperations({ ts, program, file }) {
59
76
  if (!symbol) fail(`the contract has no ${name} type`);
60
77
  return checker.getTypeOfSymbol(symbol);
61
78
  };
62
- const guard = (label, type) => {
79
+ const schema = (label) => {
63
80
  try {
64
- return builder.schemaOf(type, label);
81
+ return readSchema(prop, label);
65
82
  } catch (error) {
66
- return fail(error.message);
83
+ return fail(`${label} schema: ${error.message}`);
67
84
  }
68
85
  };
69
- const input = guard('input', member('input'));
70
- const output = guard('output', member('output'));
86
+ const input = schema('input');
87
+ const output = schema('output');
88
+ // This check never supplies emitted schemas: z.toJSONSchema remains the wire authority.
89
+ assertParsedTypes(parsedTypes, member, fail);
71
90
  const refusalType = member('refusal');
72
91
  let refusal = [];
73
92
  if ((refusalType.flags & ts.TypeFlags.Never) === 0) {
@@ -78,12 +97,23 @@ export function readOperations({ ts, program, file }) {
78
97
  operations.push({ id, kind, input, output, refusal });
79
98
  }
80
99
  if (operations.length === 0) throw new Error(`${file}: ${OPERATIONS_EXPORT} declares no operation`);
81
- return { operations, components: builder.components() };
100
+ return { operations, components: {} };
82
101
  }
83
102
 
84
103
  /** The OpenAPI 3.1 document of an app's operations, as sorted-key JSON text with one final newline. */
85
104
  export function openApiText({ app, operations, components }) {
86
105
  const schemas = { ...components };
106
+ schemas['Operation.InvalidRequest'] = {
107
+ type: 'object',
108
+ additionalProperties: false,
109
+ required: ['code', 'kind', 'message', 'params'],
110
+ properties: {
111
+ code: { const: 'OPERATIONS_INVALID_REQUEST' },
112
+ kind: { const: 'invalid' },
113
+ message: { type: 'string', description: 'The localized canonical error message.' },
114
+ params: { type: 'object', additionalProperties: false },
115
+ },
116
+ };
87
117
  const requests = [];
88
118
  const replies = [];
89
119
  for (const operation of operations) {
@@ -92,15 +122,17 @@ export function openApiText({ app, operations, components }) {
92
122
  const refusedOutcome = { type: 'object', required: ['code', 'kind'], properties: { code: { enum: operation.refusal, type: 'string' }, kind: { const: 'refused' }, params: { type: 'object', additionalProperties: { type: ['string', 'number', 'boolean'] } } } };
93
123
  schemas[`Operation.${stem}.Request`] = {
94
124
  type: 'object',
125
+ additionalProperties: false,
95
126
  required: ['input', 'operation', 'requestId'],
96
- properties: { input: operation.input, operation: { const: operation.id }, requestId: { type: 'string', minLength: 1 } },
127
+ properties: { input: operation.input, operation: { const: operation.id }, requestId: { type: 'string', minLength: 1, maxLength: 128 } },
97
128
  'x-kind': operation.kind,
98
129
  'x-operation': operation.id,
99
130
  };
100
131
  schemas[`Operation.${stem}.Reply`] = {
101
132
  type: 'object',
133
+ additionalProperties: false,
102
134
  required: ['operation', 'outcome', 'requestId'],
103
- properties: { operation: { const: operation.id }, outcome: operation.refusal.length ? { oneOf: [okOutcome, refusedOutcome] } : okOutcome, requestId: { type: 'string' } },
135
+ properties: { operation: { const: operation.id }, outcome: operation.refusal.length ? { oneOf: [okOutcome, refusedOutcome] } : okOutcome, requestId: { type: 'string', minLength: 1, maxLength: 128 } },
104
136
  'x-kind': operation.kind,
105
137
  'x-operation': operation.id,
106
138
  };
@@ -109,6 +141,7 @@ export function openApiText({ app, operations, components }) {
109
141
  }
110
142
  const document = {
111
143
  openapi: '3.1.0',
144
+ [INPUT_BOUNDS_OPENAPI_EXTENSION]: INPUT_BOUNDS_VERSION,
112
145
  info: { title: `${app} operations`, version: '1' },
113
146
  'x-operations': operations.map((operation) => ({ id: operation.id, kind: operation.kind })),
114
147
  paths: {
@@ -116,7 +149,10 @@ export function openApiText({ app, operations, components }) {
116
149
  post: {
117
150
  operationId: 'invokeOperation',
118
151
  requestBody: { required: true, content: { 'application/json': { schema: { oneOf: requests } } } },
119
- responses: { 200: { description: 'The reply of the invoked operation: its outcome is ok or a declared refusal.', content: { 'application/json': { schema: { oneOf: replies } } } } },
152
+ responses: {
153
+ 200: { description: 'The reply of the invoked operation: its outcome is ok or a declared refusal.', content: { 'application/json': { schema: { oneOf: replies } } } },
154
+ 400: { description: 'The envelope, operation id or input failed runtime schema validation before dispatch.', content: { 'application/json': { schema: { $ref: '#/components/schemas/Operation.InvalidRequest' } } } },
155
+ },
120
156
  },
121
157
  },
122
158
  },
@@ -1,6 +1,6 @@
1
1
  // The child process of `starci app emit` for one app.
2
2
  // Prints the schema the app serves on stdout; exit 3 when the app's module graph has no GraphQL server.
3
- // Loads the repository's own typescript, @nestjs/* and graphql (resolved from the repository, never from hfs).
3
+ // Loads the repository's own typescript, @nestjs/*, graphql and (when installed) class-validator (resolved from the repository, never from hfs).
4
4
  //
5
5
  // Two steps: (1) the module graph of the app root is read from source (static-graph.mjs) and yields the resolver classes the
6
6
  // GraphQL server would collect; (2) only those classes are loaded, and Nest's own GraphQLSchemaFactory builds the schema.
@@ -13,6 +13,7 @@ import path from 'node:path';
13
13
  import { aliasTarget, aliasesOf, appModulePath } from './contracts.mjs';
14
14
  import { compilerOptionsOf } from './compiler.mjs';
15
15
  import { createGraphReader } from './static-graph.mjs';
16
+ import { graphqlInputBounds, withInputBounds } from './graphql-input-bounds.mjs';
16
17
 
17
18
  const [rootArgument, app] = process.argv.slice(2);
18
19
  const repoRoot = path.resolve(rootArgument);
@@ -94,7 +95,7 @@ const classOf = ({ file, name }) => {
94
95
  };
95
96
 
96
97
  // The packages of the repository under emission, resolved from it (never from hfs, which declares none of them).
97
- const FROM_REPOSITORY = { nestGraphql: '@nestjs/graphql', nestCore: '@nestjs/core', graphql: 'graphql' };
98
+ const FROM_REPOSITORY = { nestGraphql: '@nestjs/graphql', nestCore: '@nestjs/core', graphql: 'graphql', classValidator: 'class-validator' };
98
99
  const graphql = require(FROM_REPOSITORY.nestGraphql);
99
100
  // the constants and the scalar factory are internals of the package: reached by file, its `exports` map hides them
100
101
  const graphqlDist = path.dirname(require.resolve(FROM_REPOSITORY.nestGraphql));
@@ -110,8 +111,20 @@ const includeModules = composition.include.map(classOf);
110
111
 
111
112
  const { NestFactory } = require(FROM_REPOSITORY.nestCore);
112
113
  const { GraphQLSchemaBuilderModule, GraphQLSchemaFactory } = graphql;
113
- const { lexicographicSortSchema, printSchema } = require(FROM_REPOSITORY.graphql);
114
+ const { lexicographicSortSchema, parse, print, printSchema } = require(FROM_REPOSITORY.graphql);
114
115
  const context = await NestFactory.createApplicationContext(GraphQLSchemaBuilderModule, { logger: false });
115
116
  const schema = await context.get(GraphQLSchemaFactory).create(resolvers, { scalarsMap, includeModules });
116
- process.stdout.write(printSchema(lexicographicSortSchema(schema)));
117
+ const printed = printSchema(lexicographicSortSchema(schema));
118
+ const { TypeMetadataStorage } = require(path.join(graphqlDist, 'schema-builder', 'storages', 'type-metadata.storage.js'));
119
+ // A repository without class-validator has no validators, hence no bounds: only that package's absence is tolerated.
120
+ let validationStorage = { getTargetValidationMetadatas: () => [] };
121
+ try {
122
+ validationStorage = require(FROM_REPOSITORY.classValidator).getMetadataStorage();
123
+ } catch (error) {
124
+ if (error?.code !== 'MODULE_NOT_FOUND' || !String(error.message).includes(`'${FROM_REPOSITORY.classValidator}'`)) throw error;
125
+ }
126
+ const bounds = graphqlInputBounds({ schema, typeStorage: TypeMetadataStorage, validationStorage });
127
+ const document = parse(printed);
128
+ const bounded = withInputBounds(document, bounds);
129
+ process.stdout.write(bounded === document ? printed : print(bounded));
117
130
  await context.close();
@@ -18,8 +18,12 @@ export function stable(value) {
18
18
 
19
19
  const canonical = (value) => JSON.stringify(stable(value));
20
20
 
21
- /** A builder over one checker; `components` fills as named object types are met. */
22
- export function createSchemaBuilder({ ts, checker }) {
21
+ /**
22
+ * A builder over one checker; `components` fills as named object types are met. A schema-backed caller has already proved
23
+ * the wire schema is closed, so its inferred empty object is meaningful (for example z.strictObject({})). Other callers
24
+ * retain the strict type-only rule: an empty object type alone proves no wire shape.
25
+ */
26
+ export function createSchemaBuilder({ ts, checker, schemaBacked = false }) {
23
27
  const F = ts.TypeFlags;
24
28
  const components = new Map();
25
29
  const nameOf = new Map();
@@ -98,6 +102,12 @@ export function createSchemaBuilder({ ts, checker }) {
98
102
  return { $ref: `#/components/schemas/${name}` };
99
103
  }
100
104
 
105
+ function indexSchema(index, path) {
106
+ // z.strictObject({}) infers Record<string, never>: this index forbids keys rather than admitting a never-valued field.
107
+ if (schemaBacked && (index.type.flags & F.Never) !== 0) return false;
108
+ return schemaOf(index.type, `${path}[key]`);
109
+ }
110
+
101
111
  function plainObject(type, path) {
102
112
  const properties = {};
103
113
  const required = [];
@@ -109,15 +119,17 @@ export function createSchemaBuilder({ ts, checker }) {
109
119
  const kept = withoutUndefined(propType);
110
120
  if (kept.length === 0) fail(`${path}.${prop.getName()}`, 'undefined is not a wire value');
111
121
  const hadUndefined = kept.length < members.length;
112
- properties[prop.getName()] = !hadUndefined ? schemaOf(propType, `${path}.${prop.getName()}`) : kept.length === 1 ? schemaOf(kept[0], `${path}.${prop.getName()}`) : membersSchema(kept, `${path}.${prop.getName()}`);
122
+ const propertyPath = `${path}.${prop.getName()}`;
123
+ const schema = !hadUndefined ? schemaOf(propType, propertyPath) : kept.length === 1 ? schemaOf(kept[0], propertyPath) : membersSchema(kept, propertyPath);
124
+ properties[prop.getName()] = schema;
113
125
  if (!optional && !hadUndefined) required.push(prop.getName());
114
126
  }
115
127
  const index = checker.getIndexInfosOfType(type).find((info) => info.keyType.flags & F.String);
116
- if (props.length === 0 && !index) fail(path, 'an object type with no members and no index signature says nothing');
128
+ if (props.length === 0 && !index && !schemaBacked) fail(path, 'an object type with no members and no index signature says nothing');
117
129
  const result = { type: 'object' };
118
130
  if (props.length) result.properties = properties;
119
131
  if (required.length) result.required = required;
120
- if (index) result.additionalProperties = schemaOf(index.type, `${path}[key]`);
132
+ if (index) result.additionalProperties = indexSchema(index, path);
121
133
  return result;
122
134
  }
123
135
 
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@starci/hfs",
3
- "version": "4.7.32",
3
+ "version": "4.7.34",
4
4
  "description": "The StarCi app command implementation for scaffolding, linting, checking and synchronizing one product repository.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "private": false,
8
8
  "exports": {
9
9
  ".": "./src/main.mjs",
10
+ "./form-bounds": "./emit/form-bounds.mjs",
10
11
  "./package.json": "./package.json",
11
12
  "./runtime/*": "./runtime/*"
12
13
  },
@@ -15,13 +16,19 @@
15
16
  "smol-toml": "1.9.0"
16
17
  },
17
18
  "devDependencies": {
19
+ "@graphql-codegen/cli": "7.4.6",
20
+ "@graphql-codegen/client-preset": "6.2.3",
18
21
  "@nestjs/common": "11.2.5",
19
22
  "@nestjs/core": "11.2.5",
23
+ "@nestjs/graphql": "13.4.5",
20
24
  "@nestjs/testing": "11.2.5",
25
+ "class-validator": "0.15.1",
26
+ "graphql": "16.14.2",
21
27
  "prettier": "3.9.6",
22
28
  "reflect-metadata": "0.2.2",
23
29
  "rxjs": "7.8.2",
24
- "typescript": "5.9.3"
30
+ "typescript": "5.9.3",
31
+ "zod": "4.6.7"
25
32
  },
26
33
  "files": [
27
34
  "LICENSE",
package/release/gate.mjs CHANGED
@@ -16,7 +16,9 @@ import { sonarQualityGate } from './sonar-quality-gate.mjs';
16
16
  export const RELEASE_CODE = 'RELEASE_CONFIG_INVALID';
17
17
  const SONAR_UNREACHABLE = 'SONAR_UNREACHABLE';
18
18
 
19
- const FIELDS = new Set(['gates', 'tagPrefix', 'defaultBranch', 'tagMatchesVersion', 'requireCi']);
19
+ const FIELDS = new Set(['gates', 'tagPrefix', 'defaultBranch', 'requireCi']);
20
+ /** Release-block keys that no longer exist, each with what replaced it. */
21
+ const REMOVED_FIELDS = new Map([['tagMatchesVersion', 'the release tag is always <tagPrefix><package.json version>; delete the key']]);
20
22
  /** The note a release that does not wait for CI carries in its L4 record and its push output (owner ruling release-require-ci-per-project). */
21
23
  export const CI_NOT_REQUIRED = 'CI not required by release.requireCi=false (owner ruling release-require-ci-per-project): the local release gate, this L4 record and the annotated tag, is the release proof';
22
24
  const GATE_FIELDS = new Set(['name', 'command', 'summary', 'env']);
@@ -36,7 +38,6 @@ const SCRIPT_GATES = ['format:check', 'typecheck', 'build:be', 'lint', 'db:lint'
36
38
  const WORLD_SCRIPTS = ['test:integration', 'test:e2e', 'test:contract'];
37
39
  const DB_TYPES_SCRIPTS = ['db:start', 'db:reset', 'db:types', 'db:stop'];
38
40
  const DB_TYPES_COMMITTED = { name: 'db-types-committed', command: ['git', 'diff', '--exit-code', '--', 'supabase/types/database.types.ts'], summary: null, env: {} };
39
- const MAX_PATCH = 100;
40
41
 
41
42
  /** True for a branch or remote name git can take as one argument (no leading dash, only path-like characters). */
42
43
  export const refName = (value) => typeof value === 'string' && REF_NAME.test(value);
@@ -133,21 +134,23 @@ function gatesOf(list, problems) {
133
134
  export function normalizeRelease(block, scripts = {}, services = {}, options = {}) {
134
135
  const raw = block ?? {};
135
136
  if (!plain(raw)) return { problems: ['release: must be a mapping'] };
136
- const problems = Object.keys(raw).filter((key) => !FIELDS.has(key)).map((key) => `release.${key}: is not a field of the release block`);
137
+ const problems = [];
138
+ for (const key of Object.keys(raw)) {
139
+ if (REMOVED_FIELDS.has(key)) problems.push(`release.${key}: is removed (${REMOVED_FIELDS.get(key)})`);
140
+ else if (!FIELDS.has(key)) problems.push(`release.${key}: is not a field of the release block`);
141
+ }
137
142
  const tagPrefix = raw.tagPrefix ?? 'v';
138
143
  const defaultBranch = raw.defaultBranch ?? 'main';
139
- const tagMatchesVersion = raw.tagMatchesVersion ?? false;
140
144
  // The candidate CI of `release push` is an app's choice and off by default (owner ruling release-require-ci-per-project); the local gate never is.
141
145
  const requireCi = raw.requireCi ?? false;
142
146
  if (typeof tagPrefix !== 'string' || !PREFIX.test(tagPrefix)) problems.push(`release.tagPrefix: must match ${PREFIX}`);
143
147
  if (!refName(defaultBranch)) problems.push(`release.defaultBranch: must match ${REF_NAME}`);
144
- if (typeof tagMatchesVersion !== 'boolean') problems.push('release.tagMatchesVersion: must be true or false');
145
148
  if (typeof requireCi !== 'boolean') problems.push('release.requireCi: must be true or false');
146
149
  const quality = releaseServices(services, options);
147
150
  const audit = auditGate(quality.audit);
148
151
  const gates = raw.gates === undefined ? defaultGates(scripts, audit) : gatesOf(raw.gates, problems);
149
152
  if (raw.gates !== undefined && audit && !gates.some((gate) => gate.name === 'audit')) gates.push(audit);
150
- return problems.length === 0 ? { config: { gates, tagPrefix, defaultBranch, tagMatchesVersion, requireCi, services: quality } } : { problems };
153
+ return problems.length === 0 ? { config: { gates, tagPrefix, defaultBranch, requireCi, services: quality } } : { problems };
151
154
  }
152
155
 
153
156
  /* ---- git through the seam */
@@ -169,6 +172,9 @@ export function annotatedOnHead(ctx, prefix) {
169
172
  return names.filter((name) => git(ctx, ['cat-file', '-t', `refs/tags/${name}`], { allowFail: true }).stdout === 'tag');
170
173
  }
171
174
 
175
+ /** Every tag on HEAD whose name ends in a version, annotated or not, under any prefix: a release commit carries exactly one. */
176
+ const versionTagsOnHead = (ctx) => lines(git(ctx, ['tag', '--points-at', 'HEAD']).stdout).filter((name) => /[0-9]+[.][0-9]+[.][0-9]+$/u.test(name));
177
+
172
178
  const dirtyPaths = (ctx) => lines(git(ctx, ['status', '--porcelain', '--untracked-files=normal']).stdout);
173
179
 
174
180
  /** Tag names that exist locally or on the remote. */
@@ -179,24 +185,11 @@ function takenTags(ctx, prefix, remote) {
179
185
  return new Set([...local, ...remoteNames]);
180
186
  }
181
187
 
182
- /** Next free `<prefix><major>.<minor>.<n>` for the package version. */
183
- function nextFree(prefix, version, taken) {
184
- const [, major, minor] = VERSION.exec(version);
185
- for (let patch = 0; patch < MAX_PATCH; patch += 1) {
186
- const candidate = `${prefix}${major}.${minor}.${patch}`;
187
- if (!taken.has(candidate)) return candidate;
188
- }
189
- throw refusal('RELEASE_NO_FREE_TAG', `no free tag ${prefix}${major}.${minor}.0 to ${prefix}${major}.${minor}.${MAX_PATCH - 1}`);
190
- }
191
-
192
- /** The tag this release uses and its state: on-head (reused), free (will be created) or taken (exists on another commit). */
188
+ /** The tag this release uses is `<prefix><package version>`; its state is on-head (reused), free (will be created) or taken (exists on another commit). */
193
189
  function chooseTag(ctx, config, { prefix, remote }) {
194
- const onHead = annotatedOnHead(ctx, prefix);
195
- const taken = takenTags(ctx, prefix, remote);
196
- const exact = `${prefix}${config.version}`;
197
- const tag = config.tagMatchesVersion ? exact : (onHead[0] ?? nextFree(prefix, config.version, taken));
198
- if (onHead.includes(tag)) return { tag, state: 'on-head' };
199
- return { tag, state: taken.has(tag) ? 'taken' : 'free' };
190
+ const tag = `${prefix}${config.version}`;
191
+ if (annotatedOnHead(ctx, prefix).includes(tag)) return { tag, state: 'on-head' };
192
+ return { tag, state: takenTags(ctx, prefix, remote).has(tag) ? 'taken' : 'free' };
200
193
  }
201
194
 
202
195
  /* ---- gate results */
@@ -344,6 +337,8 @@ function inspect(ctx, config, options) {
344
337
  if (!PREFIX.test(prefix)) throw refusal('RELEASE_USAGE', `--tag-prefix must match ${PREFIX}`);
345
338
  const chosen = chooseTag(ctx, config, { prefix, remote: 'origin' });
346
339
  if (chosen.state === 'taken') red.push(`tag ${chosen.tag} already exists on another commit (local or remote)`);
340
+ const stray = versionTagsOnHead(ctx).filter((name) => name !== chosen.tag);
341
+ if (stray.length > 0) red.push(`HEAD already carries the release tag ${stray[0]} but package.json version ${config.version} gives ${chosen.tag}: a release commit carries exactly one tag, so set the version to match the tag or release a new commit`);
347
342
  out.push(tagLine(chosen));
348
343
  return { head, chosen, red, out };
349
344
  }
package/release/index.mjs CHANGED
@@ -53,7 +53,7 @@ function readPackage(root) {
53
53
  try { return JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); } catch { throw refusal('RELEASE_PACKAGE_UNREADABLE', 'package.json is not readable JSON'); }
54
54
  }
55
55
 
56
- /** The normalised `release:` block (tolerant of absence) with the package version: { root, version, gates, tagPrefix, defaultBranch, tagMatchesVersion, requireCi }. */
56
+ /** The normalised `release:` block (tolerant of absence) with the package version: { root, version, gates, tagPrefix, defaultBranch, requireCi }. */
57
57
  function loadReleaseConfig(root) {
58
58
  const pkg = readPackage(root);
59
59
  if (typeof pkg.version !== 'string' || !VERSION.test(pkg.version)) throw refusal(RELEASE_CODE, 'package.json needs a version like 1.2.3 (the release tag derives from it)');
package/release/push.mjs CHANGED
@@ -22,14 +22,13 @@ const greenRecord = (record, head, tag) => record?.head === head && record?.tag
22
22
  && Array.isArray(record.gates) && record.gates.length > 0 && record.gates.every((gate) => gate?.pass === true);
23
23
 
24
24
  /** The release gate as the hook reads it: the annotated tag on HEAD whose L4 record names head and tag. Returns { tag } or { reason }. */
25
- function gateState(ctx, head, prefix) {
25
+ function gateState(ctx, head, prefix, expected) {
26
26
  const tags = annotatedOnHead(ctx, prefix);
27
- if (tags.length === 0) return { reason: `no ANNOTATED tag ${prefix}[0-9]* on HEAD: run starci app release gate --execute first` };
27
+ if (!tags.includes(expected)) return { reason: `no ANNOTATED tag ${expected} (the package.json version) on HEAD${tags.length > 0 ? `, which carries ${tags.join(', ')}` : ''}: run starci app release gate --execute first` };
28
28
  const file = path.join(releaseDir(ctx), `${head}.l4.json`);
29
29
  if (!fs.existsSync(file)) return { reason: `no L4 record ${file}: run starci app release gate --execute first` };
30
30
  const record = readRecord(file);
31
- const tag = tags.find((name) => greenRecord(record, head, name));
32
- return tag ? { tag } : { reason: `the L4 record does not name head ${head.slice(0, 10)} and one of ${tags.join(', ')} with every gate green` };
31
+ return greenRecord(record, head, expected) ? { tag: expected } : { reason: `the L4 record does not name head ${head.slice(0, 10)} and ${expected} with every gate green` };
33
32
  }
34
33
 
35
34
  /** The remote branch tip, or null on an empty remote; an unreachable remote is a failure (the state cannot be judged). */
@@ -93,7 +92,7 @@ export async function releasePush(ctx, config, options = {}) {
93
92
  const branch = config.defaultBranch;
94
93
  assertNames(remote, branch);
95
94
  const head = git(ctx, ['rev-parse', 'HEAD']).stdout;
96
- const gate = gateState(ctx, head, config.tagPrefix);
95
+ const gate = gateState(ctx, head, config.tagPrefix, `${config.tagPrefix}${config.version}`);
97
96
  const out = [`HEAD ${head}`, gateLine(gate)];
98
97
  const data = { head, tag: gate.tag ?? null, remote, branch, refs: [], hook: null, pushed: false };
99
98
  if (!gate.tag) return refuse(out, [gate.reason], data, 'nothing pushed.');