@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.7.34 - 2026-10-26
4
+ - Fixed: `starci app db restamp` considers only active migrations when validating names and choosing unoccupied versions. Legacy archive names no longer block preview or apply, and archived files stay untouched.
5
+ - Changed: the loosening check reads the tree as it stands and never its history. The bundled `scripts/lib/loosening-state.mjs` (with `js-lex.mjs` and `loosening-config.mjs`) replaces the diff detector; `HFS_BYPASS_MARKER` (R303) reads it: a suppression word opens a comment, a test marker stands in code, a swallowed failure stands in a shell line or in the string a script hands a shell, a package.json is judged by its scripts, and the same words quoted as data are no bypass. It also refuses a coverage target or threshold below the canon, a relaxing switch, a disabled ESLint rule, a tsconfig strictness option turned off and an exception file that `knowledge/hfs/design-truths.yaml` does not list. `--passWithNoTests`, `--coverageThreshold`, `--no-error-on-unmatched-pattern` and a `--max-warnings` budget are bypasses in a script, a hook or a workflow.
6
+ - Added: `starci app emit` carries class-validator `MaxLength`, `ArrayMaxSize`, `Min` and `Max` of GraphQL input types and REST operation DTOs into the contract (a GraphQL `@inputBounds` directive and an OpenAPI constraint), and the `@starci/hfs/form-bounds` codegen plugin turns them into typed `INPUT_BOUNDS` and `inputBoundsIssues` for the form canon. The app codegen template refuses legacy or unsupported bounds metadata.
7
+ - Changed: the operation emit refuses a non-strict `z.object` (open on the wire), publishes the real maximum length of fixed-format strings (`z.uuid()`, `z.ipv4()` and kin) and keeps `readOnly` out of request schemas.
8
+ - Changed: operation tables declare runtime input/output schemas from owner-local `*.schema.ts` exports. `starci app emit` derives OpenAPI 3.1 with the app's `z.toJSONSchema`, preserving bounds and defaults, refusing open or unrepresentable wire values, and evaluating only schema modules. Package specs cover conversion, isolated worker execution and rejection paths.
9
+
10
+ ## 4.7.33 - 2026-10-25
11
+ - Changed: the bundled canon pins select @starci/stylelint-canon 2.0.11, @starci/cli 1.7.33 and the canons 3.4.41 and 8.4.40.
12
+
3
13
  ## 4.7.32 - 2026-10-25
4
14
  - Changed: the bundled canon pins select @starci/grammar 0.11.0, @starci/cli 1.7.32 and the canons 3.4.40 and 8.4.39.
5
15
 
package/README.md CHANGED
@@ -12,8 +12,8 @@ starci app scaffold <name> [--into <dir>] [--edition full|lite] [--cwd <dir>]
12
12
  starci app add <api|webhook|realtime|saga|job|reactor|queue|projection|cli|table|app> <name> [--fe] [--no-types] [options] [--cwd <dir>]
13
13
  starci app lint [--cwd <dir>] [--changed <file>...] [--workspace <dir>] [--fix] [--format text|json] [--sonar <file>]
14
14
  starci app sync (--check | --write) [--cwd <dir>]
15
- starci app check [--cwd <dir>] [--json] [--fast] [--base <ref>] [--edition full] [--db-types]
16
- starci app upgrade [--edition full] [--plan]
15
+ starci app check [--cwd <dir>] [--json] [--fast] [--base <ref>] [--db-types]
16
+ starci app upgrade [--plan]
17
17
  starci app stack <up|down|status> [--stack <dir>] [--services <list>] [--k3d] [--force] [--json] [--cwd <dir>]
18
18
  starci app explain <path> [--cwd <dir>] [--json]
19
19
  starci app emit [--cwd <dir>]
@@ -56,10 +56,17 @@ The lite additions are deliberately narrow:
56
56
  `starci app emit` continues to emit the enabled back-end snapshots and also writes `supabase/types/database.types.ts` when
57
57
  the `app.supabase.types` slot is enabled. The local Supabase stack must be running for generation.
58
58
 
59
- Plan the transition with `starci app upgrade --edition full --plan`, then use `starci app check --edition full [--db-types]` to judge the
60
- unchanged lite tree through the full-edition view. The check is read-only; `--db-types` includes a fresh generated-types
61
- comparison when the local stack is available. Applying `starci app upgrade --edition full` adds the full-only managed configuration,
62
- test layers and cli requirements without rewriting product code. Downgrade to lite is not supported.
59
+ Input bounds stay with the backend DTO: `MaxLength`, `ArrayMaxSize`, `Min` and `Max` become GraphQL `@inputBounds`
60
+ input-field directives or JSON Schema constraints on the REST operation table's DTO properties. Codegen derives
61
+ `INPUT_BOUNDS` beside GraphQL wire types and `REST_INPUT_BOUNDS` under the REST API owner's `__generated__/` directory.
62
+ Each generated module also exports `inputBoundsIssues`. Forms consume the reader and their input's bounds through the API
63
+ owner and call it from each input object's zod `superRefine`, converting every returned field path into a translated custom issue.
64
+ Nested input schemas compose their own refinement. The reader
65
+ applies changed and newly added bounds automatically; required fields, types and formats remain in zod. Conflicting GraphQL
66
+ bounds across services and undecidable backend bounds refuse generation; snapshot checks detect changed limits.
67
+ Emit contracts with matching HFS and CLI versions before codegen; legacy or unsupported bounds metadata refuses generation.
68
+
69
+ The edition is chosen once at scaffold: a lite app stays lite and a full app stays full, and `starci app upgrade` refuses a lite app (`HFS_UPGRADE_EDITION_FIXED`). `starci app check --edition <edition>` accepts only the edition `hfs.json` declares.
63
70
 
64
71
  A new full app ships its test world (`scaffold/test-world.mjs`): the `@starci/test-world` declaration under `be/src/tests/world/`
65
72
  (the dev stack's Postgres per connection, the api apps, the cli app's migration runner as the migrate step), an integration spec of
@@ -2,10 +2,11 @@
2
2
  * `starci app emit`: writes `contracts/<service>/events.json` for every service that declares typed event classes under
3
3
  * `src/modules/events/<service>/` (events.mjs: the async contract, R166), `contracts/<app>/schema.graphql` for every api app of hfs.json that serves GraphQL, and
4
4
  * `contracts/<app>/openapi.json` for every api app whose `apps/<app>/src/operations.ts` exports the typed operation table
5
- * `OPERATIONS` (operations.mjs: OpenAPI 3.1 read from the TypeScript checker; nothing is executed).
5
+ * `OPERATIONS` (operations.mjs: OpenAPI 3.1 derived from zod schema modules, with closed refusal codes proved by the checker).
6
6
  *
7
- * The snapshot is `printSchema(lexicographicSortSchema(schema))`, nothing else, so it is deterministic and never carries a
8
- * generator banner. It needs no environment, no database and no network, and never boots the app.
7
+ * The GraphQL snapshot prints the lexicographically sorted schema with class-validator bounds on input fields as
8
+ * `@inputBounds` directives. OpenAPI carries the equivalent JSON Schema property and item constraints. It needs no
9
+ * environment, no database and no network, and never boots the app.
9
10
  *
10
11
  * What an app serves is decided from its source (`static-graph.mjs`): the module graph is walked from `AppModule` of
11
12
  * `apps/<app>/src/app.module.ts` through every import (imports of imports, `register`/`forRoot`/`forRootAsync` dynamic modules,
@@ -0,0 +1,58 @@
1
+ /** A form constraint failure; the form supplies translated copy for its field. */
2
+ export type InputBoundIssue = {
3
+ readonly path: readonly (string | number)[]
4
+ readonly kind: "maxLength" | "maxItems" | "min" | "max"
5
+ readonly bound: number
6
+ }
7
+
8
+ /** Checks limits only; the form's zod schema owns required values, types and formats. */
9
+ const fieldBoundIssues = (
10
+ value: unknown,
11
+ limits: InputBoundField,
12
+ path: readonly (string | number)[],
13
+ ): InputBoundIssue[] => {
14
+ const issues: InputBoundIssue[] = []
15
+ if (typeof value === "string" && limits.maxLength !== undefined && value.length > limits.maxLength) {
16
+ issues.push({ path, kind: "maxLength", bound: limits.maxLength })
17
+ }
18
+ if (Array.isArray(value) && limits.maxItems !== undefined && value.length > limits.maxItems) {
19
+ issues.push({ path, kind: "maxItems", bound: limits.maxItems })
20
+ }
21
+ if (typeof value === "number") {
22
+ if (limits.min !== undefined && (!Number.isFinite(value) || value < limits.min)) {
23
+ issues.push({ path, kind: "min", bound: limits.min })
24
+ }
25
+ if (limits.max !== undefined && (!Number.isFinite(value) || value > limits.max)) {
26
+ issues.push({ path, kind: "max", bound: limits.max })
27
+ }
28
+ }
29
+ return issues
30
+ }
31
+
32
+ /**
33
+ * Checks one input object's bounds from its zod superRefine callback, so newly declared limits apply automatically.
34
+ * Compose nested input object schemas with their own bounds refinement; zod prefixes their relative issue paths.
35
+ * Missing optional values are left to zod. Maximum string length uses UTF-16 units, conservatively stricter than
36
+ * class-validator. Array item checks stop when the array itself exceeds its maximum size.
37
+ */
38
+ export const inputBoundsIssues = (
39
+ bounds: Readonly<Record<string, InputBoundField>>,
40
+ values: Readonly<Record<string, unknown>>,
41
+ ): readonly InputBoundIssue[] => {
42
+ const issues: InputBoundIssue[] = []
43
+ for (const [field, limits] of Object.entries(bounds)) {
44
+ const value = values[field]
45
+ issues.push(...fieldBoundIssues(value, limits, [field]))
46
+ if (!Array.isArray(value) || (limits.maxItems !== undefined && value.length > limits.maxItems)) continue
47
+ const itemLimits: InputBoundField = {
48
+ maxLength: limits.itemMaxLength,
49
+ maxItems: limits.itemMaxItems,
50
+ min: limits.itemMin,
51
+ max: limits.itemMax,
52
+ }
53
+ value.forEach((item: unknown, index: number) => {
54
+ issues.push(...fieldBoundIssues(item, itemLimits, [field, index]))
55
+ })
56
+ }
57
+ return issues
58
+ }
@@ -0,0 +1,155 @@
1
+ import fs from 'node:fs';
2
+ import { INPUT_BOUND_ARGUMENTS, INPUT_BOUNDS_DIRECTIVE, INPUT_BOUNDS_VERSION, INPUT_BOUNDS_VERSION_ARGUMENT, INPUT_BOUNDS_OPENAPI_EXTENSION, JSON_SCHEMA_BOUND_KEYS } from './input-bounds.mjs';
3
+ import { byCodeUnit } from '../report/order.mjs';
4
+
5
+ const READER = `/** One field's backend bounds, including each-item validators. */\ntype InputBoundField = Readonly<Partial<Record<${Object.keys(INPUT_BOUND_ARGUMENTS).map(key => JSON.stringify(key)).join(' | ')}, number | undefined>>>\n\n${fs.readFileSync(new URL('./form-bounds-reader.ts.partial', import.meta.url), 'utf8')}`;
6
+ const graphqlBoundsText = inputs => `/** Backend input bounds, generated from the contract. */\nexport const INPUT_BOUNDS = ${JSON.stringify(inputs, null, 2)} as const\n\n${READER}`;
7
+
8
+ /** Clears generated GraphQL form limits when the last backend schema snapshot is removed. */
9
+ export function emptyInputBoundsSource() {
10
+ return graphqlBoundsText({});
11
+ }
12
+
13
+ const inputTypes = schema => Object.values(schema.getTypeMap()).filter(type => type.astNode?.kind === 'InputObjectTypeDefinition');
14
+
15
+ function requireGraphqlBoundsFormat(schema, label = 'GraphQL contract') {
16
+ if (inputTypes(schema).length === 0) return;
17
+ const directive = schema.getDirective(INPUT_BOUNDS_DIRECTIVE);
18
+ const args = new Map((directive?.args ?? []).map(argument => [argument.name, argument]));
19
+ const version = args.get(INPUT_BOUNDS_VERSION_ARGUMENT);
20
+ const argumentsMatch = Object.entries(INPUT_BOUND_ARGUMENTS).every(([name, type]) => String(args.get(name)?.type) === type);
21
+ if (version?.defaultValue !== INPUT_BOUNDS_VERSION || String(version?.type) !== 'Int' ||
22
+ args.size !== Object.keys(INPUT_BOUND_ARGUMENTS).length + 1 || !argumentsMatch) {
23
+ throw new Error(`${label}: missing or unsupported input-bounds metadata; run starci app emit with matching HFS/CLI versions`);
24
+ }
25
+ }
26
+
27
+ function fieldBounds(type, field) {
28
+ const directives = field.astNode?.directives?.filter((item) => item.name.value === INPUT_BOUNDS_DIRECTIVE) ?? [];
29
+ if (directives.length === 0) return null;
30
+ const label = `${type.name}.${field.name}`;
31
+ if (directives.length !== 1) throw new Error(`${label}: duplicate ${INPUT_BOUNDS_DIRECTIVE} directive`);
32
+ const bounds = {};
33
+ for (const argument of directives[0].arguments ?? []) {
34
+ const name = argument.name.value;
35
+ const scalar = INPUT_BOUND_ARGUMENTS[name];
36
+ const value = Number(argument.value.value);
37
+ const numeric = argument.value.kind === 'IntValue' || argument.value.kind === 'FloatValue';
38
+ if (name === INPUT_BOUNDS_VERSION_ARGUMENT) {
39
+ if (argument.value.kind !== 'IntValue' || value !== INPUT_BOUNDS_VERSION) throw new Error(`${label}: unsupported input-bounds version`);
40
+ continue;
41
+ }
42
+ if (!Object.hasOwn(INPUT_BOUND_ARGUMENTS, name) || !numeric || !Number.isFinite(value) ||
43
+ (scalar === 'Int' && (!Number.isSafeInteger(value) || value < 0))) {
44
+ throw new Error(`${label}: invalid input bound ${name}`);
45
+ }
46
+ if (Object.hasOwn(bounds, name)) throw new Error(`${label}: duplicate input bound ${name}`);
47
+ bounds[name] = value;
48
+ }
49
+ if (Object.keys(bounds).length === 0) throw new Error(`${label}: empty ${INPUT_BOUNDS_DIRECTIVE} directive`);
50
+ return Object.fromEntries(Object.entries(bounds).sort(([a], [b]) => byCodeUnit(a, b)));
51
+ }
52
+
53
+ /**
54
+ * GraphQL Code Generator plugin: derives typed form limits from the emitted input-field directives.
55
+ * Generate beside the wire types and import through the API owner. Legacy or unsupported metadata refuses generation;
56
+ * forms apply all carried limits from their translated zod refinements without importing backend code.
57
+ */
58
+ export function inputBoundsPlugin(schema) {
59
+ requireGraphqlBoundsFormat(schema);
60
+ const inputs = {};
61
+ for (const type of inputTypes(schema).sort((a, b) => byCodeUnit(a.name, b.name))) {
62
+ const fields = {};
63
+ for (const field of Object.values(type.getFields()).sort((a, b) => byCodeUnit(a.name, b.name))) {
64
+ const bounds = fieldBounds(type, field);
65
+ if (bounds !== null) fields[field.name] = bounds;
66
+ }
67
+ inputs[type.name] = fields;
68
+ }
69
+ return graphqlBoundsText(inputs);
70
+ }
71
+
72
+ /** Refuses differing bounds on shared input names before Codegen merges multiple service schemas. */
73
+ export function assertCompatibleInputBounds(schemas) {
74
+ const seen = new Map();
75
+ for (const [service, schema] of Object.entries(schemas)) {
76
+ requireGraphqlBoundsFormat(schema, service);
77
+ for (const type of inputTypes(schema)) {
78
+ for (const field of Object.values(type.getFields())) {
79
+ const key = `${type.name}.${field.name}`;
80
+ const bounds = JSON.stringify(fieldBounds(type, field));
81
+ const prior = seen.get(key);
82
+ if (prior && prior.bounds !== bounds) throw new Error(`conflicting input bounds for ${key}: ${prior.service} and ${service}`);
83
+ seen.set(key, { service, bounds });
84
+ }
85
+ }
86
+ }
87
+ }
88
+
89
+ function mergeBounds(target, source) {
90
+ for (const [name, value] of Object.entries(source)) {
91
+ if (!Number.isFinite(value) || (INPUT_BOUND_ARGUMENTS[name] === 'Int' && (!Number.isSafeInteger(value) || value < 0))) {
92
+ throw new Error(`invalid OpenAPI input bound ${name}`);
93
+ }
94
+ const previous = target[name];
95
+ target[name] = previous === undefined ? value : name.toLowerCase().includes('min') ? Math.max(previous, value) : Math.min(previous, value);
96
+ }
97
+ return target;
98
+ }
99
+
100
+ function schemaBounds(schema, components, seen = new Set()) {
101
+ if (!schema || typeof schema !== 'object') throw new Error('an OpenAPI property needs a schema');
102
+ if (seen.has(schema)) return {};
103
+ const next = new Set([...seen, schema]);
104
+ const bounds = {};
105
+ if (schema.$ref) {
106
+ const prefix = '#/components/schemas/';
107
+ const name = schema.$ref.startsWith(prefix) ? schema.$ref.slice(prefix.length).replaceAll('~1', '/').replaceAll('~0', '~') : null;
108
+ if (name === null || !Object.hasOwn(components, name)) throw new Error(`unresolved OpenAPI input reference ${schema.$ref}`);
109
+ mergeBounds(bounds, schemaBounds(components[name], components, next));
110
+ }
111
+ for (const key of Object.keys(INPUT_BOUND_ARGUMENTS).filter(name => !name.startsWith('item'))) {
112
+ const value = schema[JSON_SCHEMA_BOUND_KEYS[key] ?? key];
113
+ if (value !== undefined) mergeBounds(bounds, { [key]: value });
114
+ }
115
+ for (const part of [...(schema.allOf ?? []), ...(schema.anyOf ?? []), ...(schema.oneOf ?? [])]) {
116
+ mergeBounds(bounds, schemaBounds(part, components, next));
117
+ }
118
+ const itemSchemas = [...(schema.prefixItems ?? []), ...(schema.items ? [schema.items] : [])];
119
+ if (itemSchemas.length) {
120
+ const items = {};
121
+ for (const item of itemSchemas) mergeBounds(items, schemaBounds(item, components, next));
122
+ for (const [name, value] of Object.entries(items)) {
123
+ const key = `item${name[0].toUpperCase()}${name.slice(1)}`;
124
+ if (Object.hasOwn(INPUT_BOUND_ARGUMENTS, key)) mergeBounds(bounds, { [key]: value });
125
+ }
126
+ }
127
+ return Object.fromEntries(Object.entries(bounds).sort(([a], [b]) => byCodeUnit(a, b)));
128
+ }
129
+
130
+ /**
131
+ * Generates REST_INPUT_BOUNDS from emitted OpenAPI contracts, keyed by service, DTO and property.
132
+ * REST-only forms use the API owner's generated module. Union constraints are conservatively intersected;
133
+ * unresolved references and malformed limits refuse generation rather than retaining stale client limits.
134
+ */
135
+ export function openApiBoundsSource(contracts) {
136
+ const services = {};
137
+ for (const [service, document] of Object.entries(contracts).sort(([a], [b]) => byCodeUnit(a, b))) {
138
+ if (document.openapi !== '3.1.0' || !document.components?.schemas) throw new Error(`${service}: expected an emitted OpenAPI 3.1 contract`);
139
+ if (document[INPUT_BOUNDS_OPENAPI_EXTENSION] !== INPUT_BOUNDS_VERSION) {
140
+ throw new Error(`${service}: missing or unsupported input-bounds metadata; run starci app emit with matching HFS/CLI versions`);
141
+ }
142
+ const inputs = {};
143
+ const components = document.components.schemas;
144
+ for (const [name, schema] of Object.entries(components).sort(([a], [b]) => byCodeUnit(a, b))) {
145
+ const fields = {};
146
+ for (const [property, definition] of Object.entries(schema.properties ?? {}).sort(([a], [b]) => byCodeUnit(a, b))) {
147
+ const bounds = schemaBounds(definition, components);
148
+ if (Object.keys(bounds).length) fields[property] = bounds;
149
+ }
150
+ if (schema.type === 'object' || schema.properties) inputs[name] = fields;
151
+ }
152
+ if (Object.keys(inputs).length) services[service] = inputs;
153
+ }
154
+ return `/** Backend input bounds, generated from OpenAPI contracts. */\nexport const REST_INPUT_BOUNDS = ${JSON.stringify(services, null, 2)} as const\n\n${READER}`;
155
+ }
@@ -0,0 +1,97 @@
1
+ /** Adds backend class-validator input bounds to the emitted GraphQL contract. */
2
+ import { boundsOf, INPUT_BOUND_ARGUMENTS, INPUT_BOUNDS_DIRECTIVE, INPUT_BOUNDS_VERSION, INPUT_BOUNDS_VERSION_ARGUMENT, validateBoundType } from './input-bounds.mjs';
3
+
4
+ const ARGUMENTS = Object.keys(INPUT_BOUND_ARGUMENTS);
5
+
6
+ function graphqlShape(type) {
7
+ const written = String(type);
8
+ if (written.endsWith('!')) return graphqlShape(type.ofType);
9
+ if (written.startsWith('[')) return { type: 'array', items: graphqlShape(type.ofType) };
10
+ if (type.name === 'Int' || type.name === 'Float') return { type: 'number' };
11
+ if (type.name === 'String' || type.name === 'ID' || type.getValues) return { type: 'string' };
12
+ return { type: null };
13
+ }
14
+
15
+ /** A bound must constrain the field or the exact collection-item type class-validator receives. */
16
+ function validateFieldBounds(type, bounds, where) {
17
+ const shape = graphqlShape(type);
18
+ for (const key of Object.keys(bounds)) {
19
+ const each = key.startsWith('item');
20
+ if (each && !shape.items) throw new Error(`${where}: each:true bounds require an array`);
21
+ const plainKey = each ? `${key[4].toLowerCase()}${key.slice(5)}` : key;
22
+ validateBoundType((each ? shape.items : shape).type, plainKey, where);
23
+ }
24
+ }
25
+
26
+ function inputFields(inputs, input) {
27
+ const fields = new Map();
28
+ let target = input.target;
29
+ while (target && target !== Function.prototype) {
30
+ for (const property of inputs.get(target)?.properties ?? []) {
31
+ if (!fields.has(property.schemaName)) fields.set(property.schemaName, property.name);
32
+ }
33
+ target = Object.getPrototypeOf(target);
34
+ }
35
+ return fields;
36
+ }
37
+
38
+ function boundedFields(input, type, fields, validators) {
39
+ const bounded = new Map();
40
+ for (const [name, field] of Object.entries(type.getFields())) {
41
+ const propertyName = fields.get(name);
42
+ const where = `${input.name}.${name}`;
43
+ if (propertyName === undefined) throw new Error(`${where}: cannot resolve backend input property`);
44
+ const bounds = boundsOf(validators.filter(validator => validator.propertyName === propertyName), where);
45
+ validateFieldBounds(field.type, bounds, where);
46
+ if (Object.keys(bounds).length) bounded.set(name, bounds);
47
+ }
48
+ return bounded;
49
+ }
50
+
51
+ /** Bounds by schema input name and schema field name, using Nest's class/property names only to find validators. */
52
+ export function graphqlInputBounds({ schema, typeStorage, validationStorage }) {
53
+ const inputs = new Map(typeStorage.getInputTypesMetadata().map(input => [input.target, input]));
54
+ const result = new Map();
55
+ const names = new Set();
56
+ for (const input of inputs.values()) {
57
+ const type = schema.getType(input.name);
58
+ if (!type?.getFields) continue;
59
+ if (names.has(input.name)) throw new Error(`${input.name}: duplicate backend input type name`);
60
+ names.add(input.name);
61
+ const validators = validationStorage.getTargetValidationMetadatas(input.target, '', false, false);
62
+ const bounded = boundedFields(input, type, inputFields(inputs, input), validators);
63
+ if (bounded.size) result.set(input.name, bounded);
64
+ }
65
+ return result;
66
+ }
67
+
68
+ /** Adds directive AST nodes without changing the schema's types, fields or ordering. */
69
+ export function withInputBounds(document, bounds) {
70
+ if (!bounds.size && !document.definitions.some(definition => definition.kind === 'InputObjectTypeDefinition')) return document;
71
+ const nameNode = value => ({ kind: 'Name', value });
72
+ const definitions = document.definitions.map(definition => {
73
+ const fields = definition.kind === 'InputObjectTypeDefinition' ? bounds.get(definition.name.value) : null;
74
+ if (!fields) return definition;
75
+ return { ...definition, fields: definition.fields.map(field => {
76
+ const limits = fields.get(field.name.value);
77
+ if (!limits) return field;
78
+ const args = ARGUMENTS.filter(name => limits[name] !== undefined).map(name => {
79
+ const value = limits[name];
80
+ const integer = INPUT_BOUND_ARGUMENTS[name] === 'Int';
81
+ if (integer && value > 2147483647) throw new Error(`${definition.name.value}.${field.name.value}: ${name} exceeds GraphQL Int`);
82
+ return { kind: 'Argument', name: nameNode(name), value: { kind: integer ? 'IntValue' : 'FloatValue', value: String(value) } };
83
+ });
84
+ return { ...field, directives: [...(field.directives ?? []), { kind: 'Directive', name: nameNode(INPUT_BOUNDS_DIRECTIVE), arguments: args }] };
85
+ }) };
86
+ });
87
+ const definition = {
88
+ kind: 'DirectiveDefinition', name: nameNode(INPUT_BOUNDS_DIRECTIVE), repeatable: false,
89
+ arguments: [
90
+ { kind: 'InputValueDefinition', name: nameNode(INPUT_BOUNDS_VERSION_ARGUMENT), type: { kind: 'NamedType', name: nameNode('Int') }, defaultValue: { kind: 'IntValue', value: String(INPUT_BOUNDS_VERSION) }, directives: [] },
91
+ ...ARGUMENTS.map(name => ({ kind: 'InputValueDefinition', name: nameNode(name), type: { kind: 'NamedType', name: nameNode(INPUT_BOUND_ARGUMENTS[name]) }, directives: [] })),
92
+ ],
93
+ locations: [nameNode('INPUT_FIELD_DEFINITION')],
94
+ };
95
+ if (definitions.some(item => item.kind === 'DirectiveDefinition' && item.name.value === INPUT_BOUNDS_DIRECTIVE)) throw new Error(`@${INPUT_BOUNDS_DIRECTIVE} is reserved for emitted input bounds`);
96
+ return { ...document, definitions: [definition, ...definitions] };
97
+ }
@@ -0,0 +1,44 @@
1
+ /** Input-bounds contract metadata and class-validator bounds for the GraphQL emitter. */
2
+ export const INPUT_BOUNDS_DIRECTIVE = 'inputBounds';
3
+ export const INPUT_BOUNDS_VERSION = 1;
4
+ export const INPUT_BOUNDS_VERSION_ARGUMENT = 'version';
5
+ export const INPUT_BOUNDS_OPENAPI_EXTENSION = 'x-input-bounds';
6
+ export const INPUT_BOUND_ARGUMENTS = Object.freeze({
7
+ maxLength: 'Int', maxItems: 'Int', min: 'Float', max: 'Float',
8
+ itemMaxLength: 'Int', itemMaxItems: 'Int', itemMin: 'Float', itemMax: 'Float',
9
+ });
10
+ export const JSON_SCHEMA_BOUND_KEYS = Object.freeze({ min: 'minimum', max: 'maximum' });
11
+
12
+ const VALIDATORS = {
13
+ maxLength: ['maxLength', 'max'],
14
+ arrayMaxSize: ['maxItems', 'max'],
15
+ min: ['min', 'min'],
16
+ max: ['max', 'max'],
17
+ };
18
+
19
+ /** The tightest bound of each kind, retaining whether class-validator applies it to collection items. */
20
+ export function boundsOf(validators, where) {
21
+ const bounds = {};
22
+ for (const validator of validators) {
23
+ if (!Object.hasOwn(VALIDATORS, validator.name)) continue;
24
+ const rule = VALIDATORS[validator.name];
25
+ const [name, direction] = rule;
26
+ const value = validator.constraints?.[0];
27
+ if (typeof value !== 'number' || !Number.isFinite(value) || (/Length$|Items$/.test(name) && (!Number.isSafeInteger(value) || value < 0))) {
28
+ throw new Error(`${where}: ${validator.name} needs a finite${/Length$|Items$/.test(name) ? ' nonnegative integer' : ' numeric'} bound`);
29
+ }
30
+ const key = validator.each ? `item${name[0].toUpperCase()}${name.slice(1)}` : name;
31
+ bounds[key] = bounds[key] === undefined ? value : direction === 'min' ? Math.max(bounds[key], value) : Math.min(bounds[key], value);
32
+ }
33
+ for (const prefix of ['', 'item']) {
34
+ const key = name => prefix ? `${prefix}${name[0].toUpperCase()}${name.slice(1)}` : name;
35
+ if (bounds[key('min')] > bounds[key('max')]) throw new Error(`${where}: ${key('min')} exceeds ${key('max')}`);
36
+ }
37
+ return bounds;
38
+ }
39
+
40
+ /** A bound must constrain the GraphQL field's scalar or collection-item type. */
41
+ export function validateBoundType(type, key, where) {
42
+ const expected = key === 'maxItems' ? ['array'] : key === 'min' || key === 'max' ? ['number', 'integer'] : ['string'];
43
+ if (!expected.includes(type)) throw new Error(`${where}: ${key} requires ${expected.join(' or ')}, found ${type ?? 'an unknown type'}`);
44
+ }
@@ -0,0 +1,137 @@
1
+ /** Read only the schema declarations of an operation; importing the app table never boots the app during emit. */
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { createRequire } from 'node:module';
5
+ import vm from 'node:vm';
6
+
7
+ /** The longest value of each fixed-format string that R305 leaves without an explicit bound; the emit publishes it as maxLength. */
8
+ const FIXED_FORMAT_MAX = Object.freeze({ uuid: 36, guid: 36, cuid: 25, ulid: 26, nanoid: 21, ksuid: 27, xid: 20, ipv4: 15, ipv6: 45, e164: 16 });
9
+
10
+ /** Reject schemas whose wire values remain unconstrained. */
11
+ function assertClosedShape(schema, location) {
12
+ if (!schema || typeof schema !== 'object' || (!schema.type && !schema.enum && !Object.hasOwn(schema, 'const') && !schema.anyOf && !schema.oneOf && !schema.allOf)) {
13
+ throw new Error(`${location}: an open schema (any or unknown) is not an operation wire value`);
14
+ }
15
+ if (schema.additionalProperties === true) throw new Error(`${location}: an open object is not an operation wire value`);
16
+ if (schema.properties && schema.additionalProperties === undefined && schema.patternProperties === undefined) {
17
+ throw new Error(`${location}: an open object is not an operation wire value; use z.strictObject so unknown keys are refused`);
18
+ }
19
+ }
20
+
21
+ /** A runtime bound must survive the JSON conversion; custom refinements alone cannot publish a bounded contract. */
22
+ function assertEmittedBounds(schema, location) {
23
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type];
24
+ const bounded = (keyword) => Number.isFinite(schema[keyword]) && schema[keyword] >= 0;
25
+ if (types.includes('string') && !schema.enum && !Object.hasOwn(schema, 'const') && !bounded('maxLength')) {
26
+ throw new Error(`${location}: the string bound must be representable as maxLength in JSON Schema`);
27
+ }
28
+ if (types.includes('array') && !bounded('maxItems')) throw new Error(`${location}: the array bound must be representable as maxItems in JSON Schema`);
29
+ const dictionary = (schema.additionalProperties && typeof schema.additionalProperties === 'object') || schema.patternProperties;
30
+ if (dictionary && !bounded('maxProperties')) {
31
+ throw new Error(`${location}: the dictionary bound must be representable as maxProperties in JSON Schema; custom refinements are not emitted by z.toJSONSchema`);
32
+ }
33
+ }
34
+
35
+ /** The wire-bearing descendants of a JSON schema, each with an actionable field path. */
36
+ function* schemaChildren(schema, location) {
37
+ for (const keyword of ['anyOf', 'oneOf', 'allOf', 'prefixItems']) {
38
+ for (const [index, child] of (schema[keyword] ?? []).entries()) yield [child, `${location}.${keyword}[${index}]`];
39
+ }
40
+ for (const [name, child] of Object.entries(schema.properties ?? {})) yield [child, `${location}.${name}`];
41
+ for (const [pattern, child] of Object.entries(schema.patternProperties ?? {})) yield [child, `${location}.${pattern}`];
42
+ if (schema.propertyNames) yield [schema.propertyNames, `${location}.key`];
43
+ if (schema.items) yield [schema.items, `${location}[]`];
44
+ if (schema.additionalProperties && typeof schema.additionalProperties === 'object') yield [schema.additionalProperties, `${location}.*`];
45
+ }
46
+
47
+ function assertClosedJsonSchema(schema, location) {
48
+ assertClosedShape(schema, location);
49
+ assertEmittedBounds(schema, location);
50
+ for (const [child, childLocation] of schemaChildren(schema, location)) assertClosedJsonSchema(child, childLocation);
51
+ }
52
+
53
+ /** Loads schema-only modules and converts their exported zod values with the app's own z.toJSONSchema. */
54
+ export function operationSchemaReader({ ts, program, repoRoot }) {
55
+ const checker = program.getTypeChecker();
56
+ const requireApp = createRequire(path.join(repoRoot, 'package.json'));
57
+ const loaded = new Map();
58
+ const actual = (symbol) => symbol?.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
59
+ const unwrap = (node) => {
60
+ while (node && (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isSatisfiesExpression(node) || ts.isParenthesizedExpression(node) || ts.isNonNullExpression(node))) node = node.expression;
61
+ return node;
62
+ };
63
+ const initializer = (node, seen = new Set()) => {
64
+ node = unwrap(node);
65
+ if (node && (ts.isIdentifier(node) || ts.isPropertyAccessExpression(node))) {
66
+ const declaration = actual(checker.getSymbolAtLocation(node))?.valueDeclaration;
67
+ const value = declaration && valueOf(declaration);
68
+ if (value && !seen.has(declaration)) {
69
+ seen.add(declaration);
70
+ return initializer(value, seen);
71
+ }
72
+ }
73
+ return node;
74
+ };
75
+ const valueOf = (property) => {
76
+ if (!property || !ts.isShorthandPropertyAssignment(property)) return property?.initializer;
77
+ const value = actual(checker.getShorthandAssignmentValueSymbol(property));
78
+ return value?.valueDeclaration?.initializer && !value.valueDeclaration.getSourceFile().fileName.endsWith('.schema.ts')
79
+ ? value.valueDeclaration.initializer
80
+ : value?.valueDeclaration?.name ?? property.name;
81
+ };
82
+ const schemaSymbol = (node, seen = new Set()) => {
83
+ node = unwrap(node);
84
+ const symbol = node && actual(checker.getSymbolAtLocation(node));
85
+ const declaration = symbol?.valueDeclaration;
86
+ if (!declaration || seen.has(declaration)) return null;
87
+ if (declaration.getSourceFile().fileName.endsWith('.schema.ts')) return symbol;
88
+ seen.add(declaration);
89
+ return declaration.initializer ? schemaSymbol(declaration.initializer, seen) : null;
90
+ };
91
+ function load(file) {
92
+ if (loaded.has(file)) return loaded.get(file).exports;
93
+ if (!file.endsWith('.schema.ts')) throw new Error(`schema dependencies must be *.schema.ts, found ${file}`);
94
+ const source = program.getSourceFile(file)?.text ?? fs.readFileSync(file, 'utf8');
95
+ const compiled = ts.transpileModule(source, { fileName: file, compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS, esModuleInterop: true } }).outputText;
96
+ const module = { exports: {} };
97
+ loaded.set(file, module);
98
+ const requireSchema = (specifier) => {
99
+ if (/^zod(?:\/|$)/.test(specifier)) return requireApp(specifier);
100
+ const resolved = ts.resolveModuleName(specifier, file, program.getCompilerOptions(), ts.sys).resolvedModule?.resolvedFileName;
101
+ if (!resolved?.endsWith('.schema.ts')) throw new Error(`schema modules may import only zod and *.schema.ts, found ${specifier} in ${file}`);
102
+ return load(resolved);
103
+ };
104
+ new vm.Script(compiled, { filename: file }).runInNewContext({ module, exports: module.exports, require: requireSchema }, { timeout: 1000 });
105
+ return module.exports;
106
+ }
107
+
108
+ return (property, label) => {
109
+ const entry = initializer(valueOf(property.valueDeclaration));
110
+ const options = entry && ts.isCallExpression(entry) ? initializer(entry.arguments[0]) : null;
111
+ const field = options && checker.getTypeAtLocation(options).getProperty(label)?.getDeclarations()?.find((item) => ts.isPropertyAssignment(item) || ts.isShorthandPropertyAssignment(item));
112
+ const reference = field && unwrap(valueOf(field));
113
+ if (!reference) throw new Error(`${label}: declare input and output zod schemas in *.schema.ts and pass { input, output } to query or mutation`);
114
+ const symbol = schemaSymbol(reference);
115
+ const declaration = symbol?.valueDeclaration;
116
+ const sourceFile = declaration?.getSourceFile();
117
+ if (!sourceFile?.fileName.endsWith('.schema.ts')) throw new Error(`${label}: the schema must be an exported declaration from *.schema.ts`);
118
+ const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
119
+ const exported = moduleSymbol && checker.getExportsOfModule(moduleSymbol).find((item) => actual(item) === symbol);
120
+ if (!exported) throw new Error(`${label}: the schema must be exported from ${sourceFile.fileName}`);
121
+ const schema = load(sourceFile.fileName)[exported.getName()];
122
+ // Inline reused schemas and refuse cycles: a recursive value has no bounded wire depth, and a local $ref would point at
123
+ // the OpenAPI document root instead of this nested schema. Unrepresentable values (Date, transforms) also fail closed.
124
+ const json = requireApp('zod').z.toJSONSchema(schema, {
125
+ io: label, target: 'draft-2020-12', cycles: 'throw', reused: 'inline',
126
+ // A request property marked readOnly would tell OpenAPI clients not to send it; a fixed-format string carries its real maximum.
127
+ override: ({ jsonSchema }) => {
128
+ if (label === 'input') delete jsonSchema.readOnly;
129
+ const fixed = FIXED_FORMAT_MAX[jsonSchema.format];
130
+ if (jsonSchema.type === 'string' && fixed && jsonSchema.maxLength === undefined) jsonSchema.maxLength = fixed;
131
+ },
132
+ });
133
+ delete json.$schema;
134
+ assertClosedJsonSchema(json, label);
135
+ return json;
136
+ };
137
+ }
@@ -1,6 +1,6 @@
1
1
  // The child process of `starci app emit` for the operations of one app.
2
2
  // Prints the OpenAPI 3.1 document of the app's typed operation table on stdout; exit 3 when the app has no `apps/<app>/src/operations.ts`.
3
- // Loads the repository's own typescript (resolved from the repository, never from hfs). Nothing of the repository is executed.
3
+ // Loads the repository's own TypeScript and zod. Only *.schema.ts modules are evaluated; the app is never booted.
4
4
  import { createRequire } from 'node:module';
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
@@ -15,7 +15,7 @@ const ts = createRequire(path.join(repoRoot, 'package.json'))('typescript');
15
15
  const options = compilerOptionsOf(ts, repoRoot, (message) => process.stderr.write(`stand-in ${message}\n`));
16
16
  const program = ts.createProgram({ rootNames: [file], options });
17
17
  try {
18
- const { operations, components } = readOperations({ ts, program, file });
18
+ const { operations, components } = readOperations({ ts, program, file, repoRoot });
19
19
  process.stdout.write(openApiText({ app, operations, components }));
20
20
  } catch (error) {
21
21
  process.stderr.write(`${error.message}