@starci/hfs 4.7.31 → 4.7.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (473) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +13 -6
  3. package/emit/contracts.mjs +4 -3
  4. package/emit/form-bounds-reader.ts.partial +58 -0
  5. package/emit/form-bounds.mjs +155 -0
  6. package/emit/graphql-input-bounds.mjs +97 -0
  7. package/emit/input-bounds.mjs +44 -0
  8. package/emit/operation-schemas.mjs +137 -0
  9. package/emit/operations-worker.mjs +2 -2
  10. package/emit/operations.mjs +52 -16
  11. package/emit/schema-worker.mjs +17 -4
  12. package/emit/type-schema.mjs +17 -5
  13. package/package.json +9 -2
  14. package/release/gate.mjs +18 -23
  15. package/release/index.mjs +1 -1
  16. package/release/push.mjs +4 -5
  17. package/runtime/engine/admission.mjs +331 -0
  18. package/runtime/engine/canonical-json.mjs +14 -0
  19. package/runtime/engine/db/blob.mjs +380 -0
  20. package/runtime/engine/db/event-compact.mjs +73 -0
  21. package/runtime/engine/db/ledger-open.mjs +38 -0
  22. package/runtime/engine/db/ledger-paths.mjs +87 -0
  23. package/runtime/engine/db/ledger.mjs +1068 -0
  24. package/runtime/engine/db/machine-connection.mjs +150 -0
  25. package/runtime/engine/db/machine-open.mjs +60 -0
  26. package/runtime/engine/db/machine-schema.mjs +105 -0
  27. package/runtime/engine/db/machine.mjs +1317 -0
  28. package/runtime/engine/db/provider-reservations.mjs +127 -0
  29. package/runtime/engine/db/provision-authority.mjs +76 -0
  30. package/runtime/engine/db/provision-lock.mjs +29 -0
  31. package/runtime/engine/db/provision-operations.mjs +146 -0
  32. package/runtime/engine/db/ref-value.mjs +67 -0
  33. package/runtime/engine/db/schema/machine.sql +920 -0
  34. package/runtime/engine/db/schema/runtime.sql +1113 -0
  35. package/runtime/engine/refuse.mjs +11 -0
  36. package/runtime/knowledge/hfs/canon-pins.yaml +65 -9
  37. package/runtime/knowledge/hfs/design-truths.yaml +149 -0
  38. package/runtime/knowledge/hfs/fe-stack.yaml +23 -5
  39. package/runtime/knowledge/hfs/rules.yaml +269 -48
  40. package/runtime/knowledge/hfs/slots.yaml +113 -53
  41. package/runtime/knowledge/patterns/be/api.yaml +46 -20
  42. package/runtime/knowledge/patterns/be/event-bus.yaml +19 -12
  43. package/runtime/knowledge/patterns/be/idempotency.yaml +265 -0
  44. package/runtime/knowledge/patterns/be/jobs.yaml +17 -12
  45. package/runtime/knowledge/patterns/be/projections.yaml +2 -2
  46. package/runtime/knowledge/patterns/be/queues.yaml +57 -22
  47. package/runtime/knowledge/patterns/be/reactors.yaml +1 -1
  48. package/runtime/knowledge/patterns/be/realtime.yaml +326 -27
  49. package/runtime/knowledge/patterns/be/saga.yaml +2 -2
  50. package/runtime/knowledge/sonar-gate.yaml +1 -1
  51. package/runtime/modules/kernel/command-policy.yaml +249 -0
  52. package/runtime/modules/kernel/failure-codes.yaml +211 -52
  53. package/runtime/modules/kernel/hook-input.yaml +6 -0
  54. package/runtime/modules/kernel/protected-zone.yaml +81 -0
  55. package/runtime/modules/kernel/removed-vocabulary.yaml +62 -0
  56. package/runtime/modules/kernel/roles.yaml +405 -0
  57. package/runtime/modules/models/registry.yaml +3 -1
  58. package/runtime/modules/models/runtimes.yaml +2 -0
  59. package/runtime/modules/schemas/application-stacks.schema.yaml +10 -2
  60. package/runtime/packages/cli/src/catalog.generated.mjs +46 -59
  61. package/runtime/packages/grammar/scripts/build-stamp.mjs +98 -0
  62. package/runtime/scripts/agent/codex-app-server.mjs +79 -0
  63. package/runtime/scripts/agent/codex-mirror-source.mjs +34 -0
  64. package/runtime/scripts/agent/host-agents.mjs +80 -0
  65. package/runtime/scripts/agent/launch-trust-policy.mjs +53 -0
  66. package/runtime/scripts/agent/trust-launch.mjs +191 -0
  67. package/runtime/scripts/agent/trust.mjs +523 -0
  68. package/runtime/scripts/api/fs/rename-over.mjs +21 -0
  69. package/runtime/scripts/api/fs/temp-path.mjs +8 -0
  70. package/runtime/scripts/api/fs/write-json-file.mjs +10 -0
  71. package/runtime/scripts/api/git/diff-names.mjs +8 -0
  72. package/runtime/scripts/api/git/head-time.mjs +10 -0
  73. package/runtime/scripts/api/git/index-lock.mjs +78 -0
  74. package/runtime/scripts/api/git/is-ancestor.mjs +5 -0
  75. package/runtime/scripts/api/git/log.mjs +5 -0
  76. package/runtime/scripts/api/git/merge-base-query.mjs +5 -0
  77. package/runtime/scripts/api/git/merge-tree.mjs +5 -0
  78. package/runtime/scripts/api/git/rev-list.mjs +5 -0
  79. package/runtime/scripts/api/git/rev-parse.mjs +13 -0
  80. package/runtime/scripts/api/git/status-query.mjs +5 -0
  81. package/runtime/scripts/api/git/worktree-list-query.mjs +5 -0
  82. package/runtime/scripts/api/node/spawn-node.mjs +6 -0
  83. package/runtime/scripts/api/npm/dist-tag.mjs +8 -0
  84. package/runtime/scripts/api/npm/lib.mjs +32 -0
  85. package/runtime/scripts/api/npm/pack-dry-run.mjs +9 -0
  86. package/runtime/scripts/api/npm/run-npm.mjs +6 -0
  87. package/runtime/scripts/api/npm/view.mjs +30 -0
  88. package/runtime/scripts/api/npm/whoami.mjs +8 -0
  89. package/runtime/scripts/api/process/hide-child-windows.mjs +31 -0
  90. package/runtime/scripts/api/process/kill-tree.mjs +22 -0
  91. package/runtime/scripts/api/process/lib.mjs +125 -0
  92. package/runtime/scripts/api/process/probe-guard-command.mjs +74 -0
  93. package/runtime/scripts/api/process/process-list.mjs +28 -0
  94. package/runtime/scripts/api/process/system-tool.mjs +37 -0
  95. package/runtime/scripts/gates/canon-digest.mjs +81 -0
  96. package/runtime/scripts/gates/canon-pins.mjs +16 -0
  97. package/runtime/scripts/gates/commit-proof.mjs +53 -0
  98. package/runtime/scripts/gates/gate-input.mjs +98 -0
  99. package/runtime/scripts/gates/gate.mjs +669 -0
  100. package/runtime/scripts/gates/grammar-dist.mjs +206 -0
  101. package/runtime/scripts/gates/runtime-package-selection.mjs +27 -0
  102. package/runtime/scripts/gates/test-world-run.mjs +254 -0
  103. package/runtime/scripts/gates/type-impact.mjs +277 -0
  104. package/runtime/scripts/gates/type-program.mjs +113 -0
  105. package/runtime/scripts/guards/assigned-command.mjs +12 -0
  106. package/runtime/scripts/guards/call-rights.mjs +47 -0
  107. package/runtime/scripts/guards/command-guard.mjs +468 -0
  108. package/runtime/scripts/guards/command-policy.mjs +342 -0
  109. package/runtime/scripts/guards/critic-reach.mjs +82 -0
  110. package/runtime/scripts/guards/deps-guard.mjs +150 -0
  111. package/runtime/scripts/guards/env-dump-verdict.mjs +89 -0
  112. package/runtime/scripts/guards/git-pathspec.mjs +225 -0
  113. package/runtime/scripts/guards/git-policy.mjs +355 -0
  114. package/runtime/scripts/guards/guards-root.mjs +27 -0
  115. package/runtime/scripts/guards/hook-install.mjs +343 -0
  116. package/runtime/scripts/guards/hook-io.mjs +76 -0
  117. package/runtime/scripts/guards/hook-verdict.mjs +12 -0
  118. package/runtime/scripts/guards/install-verdict.mjs +41 -0
  119. package/runtime/scripts/guards/kernel-seat.mjs +63 -0
  120. package/runtime/scripts/guards/launch-verdict.mjs +74 -0
  121. package/runtime/scripts/guards/op-starci.mjs +16 -0
  122. package/runtime/scripts/guards/op-write-scope.mjs +27 -0
  123. package/runtime/scripts/guards/orca-self-lifecycle.mjs +39 -0
  124. package/runtime/scripts/guards/process-kill-verdict.mjs +106 -0
  125. package/runtime/scripts/guards/protected-zone.mjs +56 -0
  126. package/runtime/scripts/guards/refusals.mjs +27 -0
  127. package/runtime/scripts/guards/release-definition.mjs +185 -0
  128. package/runtime/scripts/guards/release-push-gate.mjs +72 -0
  129. package/runtime/scripts/guards/release-record.mjs +58 -0
  130. package/runtime/scripts/guards/release-suite-mode.mjs +15 -0
  131. package/runtime/scripts/guards/rights.mjs +268 -0
  132. package/runtime/scripts/guards/seat-tools.mjs +47 -0
  133. package/runtime/scripts/guards/shell-commands.mjs +328 -0
  134. package/runtime/scripts/guards/supervisor-seat.mjs +80 -0
  135. package/runtime/scripts/guards/verdict-process.mjs +81 -0
  136. package/runtime/scripts/hfs/architecture/dead-exports.mjs +10 -10
  137. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +1 -1
  138. package/runtime/scripts/hfs/architecture/operation-contract-roots.mjs +53 -0
  139. package/runtime/scripts/hfs/architecture/reachability.mjs +1 -1
  140. package/runtime/scripts/hfs/architecture/sql-owner.mjs +17 -6
  141. package/runtime/scripts/hfs/architecture/sql-token-forms.mjs +105 -0
  142. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +38 -52
  143. package/runtime/scripts/hfs/architecture/supabase-keys.mjs +261 -0
  144. package/runtime/scripts/hfs/architecture/transport-owner.mjs +49 -3
  145. package/runtime/scripts/hfs/architecture/typescript.mjs +1 -1
  146. package/runtime/scripts/hfs/check.mjs +6 -4
  147. package/runtime/scripts/hfs/coverage-scope.mjs +28 -1
  148. package/runtime/scripts/hfs/db-restamp-verb.mjs +54 -14
  149. package/runtime/scripts/hfs/declaration-shape.mjs +4 -4
  150. package/runtime/scripts/hfs/declaration-slots.mjs +14 -3
  151. package/runtime/scripts/hfs/manifest-shape.mjs +10 -6
  152. package/runtime/scripts/hfs/rule-params-shape.mjs +18 -6
  153. package/runtime/scripts/hfs/rules/a15-findings.mjs +4 -1
  154. package/runtime/scripts/hfs/rules/bypass-markers.mjs +22 -7
  155. package/runtime/scripts/hfs/rules/database-migrations.mjs +69 -14
  156. package/runtime/scripts/hfs/rules/database-queue-sources.mjs +9 -7
  157. package/runtime/scripts/hfs/rules/database-queues.mjs +68 -18
  158. package/runtime/scripts/hfs/rules/edition.mjs +56 -12
  159. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +1 -10
  160. package/runtime/scripts/hfs/rules/fe-stack.mjs +12 -7
  161. package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +1 -1
  162. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +75 -9
  163. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +1 -1
  164. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +15 -4
  165. package/runtime/scripts/hfs/rules/text-integrity.mjs +73 -0
  166. package/runtime/scripts/hfs/runtime-rules/hook-shape.mjs +74 -0
  167. package/runtime/scripts/hfs/runtime-rules/release-notes.mjs +44 -0
  168. package/runtime/scripts/hfs/sealed-members.mjs +3 -3
  169. package/runtime/scripts/hfs/slot-side-problems.mjs +3 -3
  170. package/runtime/scripts/hfs/slots.mjs +2 -1
  171. package/runtime/scripts/hfs/stack-deploy-config.mjs +22 -5
  172. package/runtime/scripts/hfs/stacks-gate.mjs +28 -7
  173. package/runtime/scripts/hfs/view.mjs +2 -1
  174. package/runtime/scripts/lib/app-data.mjs +11 -0
  175. package/runtime/scripts/lib/bypass-markers.mjs +272 -75
  176. package/runtime/scripts/lib/check-format.mjs +7 -0
  177. package/runtime/scripts/lib/cli-arg.mjs +120 -0
  178. package/runtime/scripts/lib/css-rules.mjs +220 -0
  179. package/runtime/scripts/lib/dom-tree.mjs +4 -0
  180. package/runtime/scripts/lib/escape.mjs +16 -0
  181. package/runtime/scripts/lib/event-items.mjs +22 -0
  182. package/runtime/scripts/lib/failure-code-template-errors.mjs +161 -0
  183. package/runtime/scripts/lib/git-dir.mjs +120 -0
  184. package/runtime/scripts/lib/grammar-vendor-parity.mjs +377 -0
  185. package/runtime/scripts/lib/guard-command.mjs +24 -0
  186. package/runtime/scripts/lib/hook-limits.mjs +5 -0
  187. package/runtime/scripts/lib/interrupt-cleanup.mjs +38 -0
  188. package/runtime/scripts/lib/js-lex.mjs +200 -0
  189. package/runtime/scripts/lib/json-formats.mjs +0 -1
  190. package/runtime/scripts/lib/language.mjs +94 -1
  191. package/runtime/scripts/lib/loosening-budgets.mjs +44 -0
  192. package/runtime/scripts/lib/loosening-config.mjs +361 -0
  193. package/runtime/scripts/lib/loosening-state.mjs +223 -0
  194. package/runtime/scripts/lib/menu-parts.mjs +11 -0
  195. package/runtime/scripts/lib/normalize.mjs +25 -0
  196. package/runtime/scripts/lib/op-gate.mjs +15 -0
  197. package/runtime/scripts/lib/redact.mjs +208 -0
  198. package/runtime/scripts/lib/runtime-location.mjs +21 -0
  199. package/runtime/scripts/lib/runtime-package-inventory.mjs +21 -0
  200. package/runtime/scripts/lib/runtime-package-manifests.mjs +84 -0
  201. package/runtime/scripts/lib/runtime-publication-tags.mjs +99 -0
  202. package/runtime/scripts/lib/seat-env.mjs +43 -0
  203. package/runtime/scripts/lib/secret-patterns.mjs +58 -9
  204. package/runtime/scripts/lib/shell-source.mjs +4 -0
  205. package/runtime/scripts/lib/source-phrases.mjs +34 -0
  206. package/runtime/scripts/lib/sqlite.mjs +21 -0
  207. package/runtime/scripts/lib/swarm-stack.mjs +2 -1
  208. package/runtime/scripts/lib/tar-files.mjs +39 -0
  209. package/runtime/scripts/lib/ts-analysis.mjs +10 -0
  210. package/runtime/scripts/lib/worktree-exclude.mjs +35 -0
  211. package/runtime/scripts/lib/worktree-record.mjs +14 -0
  212. package/runtime/scripts/machine/artifact-hold.mjs +52 -0
  213. package/runtime/scripts/machine/ask-receipts.mjs +113 -0
  214. package/runtime/scripts/machine/evidence-store.mjs +223 -0
  215. package/runtime/scripts/machine/home.mjs +216 -0
  216. package/runtime/scripts/machine/host-lock.mjs +184 -0
  217. package/runtime/scripts/machine/lock-recovery.mjs +55 -0
  218. package/runtime/scripts/machine/roles-contract.mjs +90 -0
  219. package/runtime/scripts/machine/runtime-change.mjs +20 -0
  220. package/runtime/scripts/machine/workflow-tree.mjs +122 -0
  221. package/runtime/scripts/reconciler/checklist-items.mjs +7 -0
  222. package/runtime/scripts/reconciler/guard-row.mjs +25 -0
  223. package/runtime/scripts/work/asset-slot.mjs +284 -0
  224. package/runtime/scripts/work/brand/brand-colour.mjs +125 -0
  225. package/runtime/scripts/work/brand/brand-tokens.mjs +167 -0
  226. package/runtime/scripts/work/brand/brand-value-source.mjs +106 -0
  227. package/runtime/scripts/work/brand/brand.mjs +892 -0
  228. package/runtime/scripts/work/draw/draw-dna-anatomy.mjs +60 -0
  229. package/runtime/scripts/work/draw/draw-dna-findings.mjs +256 -0
  230. package/runtime/scripts/work/draw/draw-dna-selector.mjs +38 -0
  231. package/runtime/scripts/work/draw/draw-dna.mjs +483 -0
  232. package/runtime/scripts/work/draw/draw-layer.mjs +305 -0
  233. package/runtime/scripts/work/draw/draw-rationale-page.mjs +237 -0
  234. package/runtime/scripts/work/draw/draw-rationale-ref.mjs +14 -0
  235. package/runtime/scripts/work/draw/draw-rationale.mjs +353 -0
  236. package/runtime/scripts/work/draw/draw-source.mjs +412 -0
  237. package/runtime/scripts/work/draw/draw-taste.mjs +278 -0
  238. package/runtime/scripts/work/draw/draw-typecheck.mjs +72 -0
  239. package/runtime/scripts/work/draw/rationale-findings.mjs +205 -0
  240. package/runtime/scripts/work/draw-grammar.mjs +175 -0
  241. package/runtime/scripts/work/draw-render-page.mjs +188 -0
  242. package/runtime/scripts/work/draw-render.mjs +626 -0
  243. package/runtime/scripts/work/grammar-proposal.mjs +279 -0
  244. package/runtime/scripts/work/png.mjs +237 -0
  245. package/runtime/scripts/work/render-tools.mjs +64 -0
  246. package/runtime/scripts/work/work-io.mjs +119 -0
  247. package/scaffold/add-inbox-lite.mjs +2 -2
  248. package/scaffold/add-plan.mjs +40 -13
  249. package/scaffold/add-queue-lite.mjs +372 -0
  250. package/scaffold/add.mjs +5 -2
  251. package/scaffold/app.mjs +1 -2
  252. package/scaffold/edition-gate.mjs +11 -7
  253. package/scaffold/lite-exports.mjs +10 -0
  254. package/scaffold/pattern-file-slots.mjs +33 -0
  255. package/scaffold/test-world.mjs +1 -1
  256. package/scaffold/yaml-block-edit.mjs +117 -0
  257. package/src/main.mjs +5 -4
  258. package/stack/deploy.mjs +7 -3
  259. package/stack/post-deploy.mjs +48 -0
  260. package/sync/index.mjs +10 -0
  261. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +1 -1
  262. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +1 -1
  263. package/templates/app/codegen-graphql.mjs.partial +59 -12
  264. package/templates/app/hooks/husky/pre-commit +2 -2
  265. package/templates/app/hooks/husky/pre-push +10 -3
  266. package/templates/app/package-scripts/redis-dev.json +3 -0
  267. package/templates/app/package-scripts-lite/package.json +1 -0
  268. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +36 -8
  269. package/templates/app/skeleton/.starcistacks/dev/README.md +1 -1
  270. package/templates/app/skeleton/.starcistacks/production/README.md +10 -9
  271. package/templates/app/skeleton/.starcistacks/production/infra/swarm/postgres-init.sh +21 -0
  272. package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +1 -1
  273. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +1 -0
  274. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +47 -5
  275. package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +5 -3
  276. package/templates/app/skeleton/README.md +2 -0
  277. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +5 -0
  278. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +2 -2
  279. package/templates/app/skeleton-lite/.starcistacks/production/README.md +1 -1
  280. package/templates/app/skeleton-lite/README.md +1 -1
  281. package/templates/be/inbox/platform/client-operation-outcomes.sql.tpl +18 -0
  282. package/templates/be/inbox/platform/event-bus/errors/event-bus.error.ts.tpl +16 -0
  283. package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +10 -0
  284. package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +1 -1
  285. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +15 -3
  286. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +11 -3
  287. package/templates/be/inbox/platform/event-bus/index.ts.tpl +2 -0
  288. package/templates/be/inbox/platform/event-bus/messages/event-bus.messages.ts.tpl +7 -0
  289. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +2 -2
  290. package/templates/be/inbox/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
  291. package/templates/be/inbox/platform/idempotency/index.ts.tpl +2 -0
  292. package/templates/be/inbox/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
  293. package/templates/be/inbox/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
  294. package/templates/be/inbox/platform/inbox/client-operation.policy.ts.tpl +17 -0
  295. package/templates/be/inbox/platform/inbox/errors/inbox.error.ts.tpl +22 -0
  296. package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +21 -0
  297. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +334 -11
  298. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +55 -4
  299. package/templates/be/inbox/platform/inbox/index.ts.tpl +2 -0
  300. package/templates/be/inbox/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
  301. package/templates/be/inbox/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
  302. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +26 -0
  303. package/templates/be/inbox/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
  304. package/templates/be/inbox/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
  305. package/templates/be/inbox/platform/migration.sql.tpl +1 -0
  306. package/templates/be/inbox/service.spec.ts.tpl +4 -4
  307. package/templates/be/inbox/service.ts.tpl +3 -1
  308. package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -2
  309. package/templates/be/inbox/webhook-door.ts.tpl +3 -2
  310. package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +3 -0
  311. package/templates/be/patterns/event-bus/platform/event-bus.contracts.ts.tpl +10 -0
  312. package/templates/be/patterns/event-bus/platform/event-bus.port.ts.tpl +2 -2
  313. package/templates/be/patterns/event-bus/platform/event-bus.service.spec.ts.tpl +16 -3
  314. package/templates/be/patterns/event-bus/platform/event-bus.service.ts.tpl +11 -3
  315. package/templates/be/patterns/event-bus/platform/event-envelope.policy.spec.ts.tpl +139 -50
  316. package/templates/be/patterns/event-bus/platform/event-envelope.policy.ts.tpl +25 -2
  317. package/templates/be/patterns/event-bus/platform/event-relay.service.spec.ts.tpl +139 -78
  318. package/templates/be/patterns/event-bus/platform/event-relay.service.ts.tpl +10 -6
  319. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +262 -159
  320. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +32 -5
  321. package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +2 -0
  322. package/templates/be/patterns/event-bus/platform/persistence/entities/event-outbox.entity.ts.tpl +5 -1
  323. package/templates/be/patterns/event-bus/platform/persistence/event-bus.rows.ts.tpl +2 -0
  324. package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +4 -4
  325. package/templates/be/patterns/event-bus/platform/persistence/migrations/1789800009000-create-event-outbox.ts.tpl +1 -0
  326. package/templates/be/patterns/idempotency/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
  327. package/templates/be/patterns/idempotency/platform/idempotency/index.ts.tpl +2 -0
  328. package/templates/be/patterns/idempotency/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
  329. package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
  330. package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.ts.tpl +17 -0
  331. package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.spec.ts.tpl +66 -0
  332. package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.ts.tpl +24 -0
  333. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.spec.ts.tpl +60 -0
  334. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.ts.tpl +12 -0
  335. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.spec.ts.tpl +49 -0
  336. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.ts.tpl +12 -0
  337. package/templates/be/patterns/idempotency/platform/inbox/effectful.decorators.ts.tpl +7 -0
  338. package/templates/be/patterns/idempotency/platform/inbox/errors/inbox.error.ts.tpl +22 -0
  339. package/templates/be/patterns/idempotency/platform/inbox/inbox.decorators.ts.tpl +17 -0
  340. package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +7 -0
  341. package/templates/be/patterns/idempotency/platform/inbox/inbox.module.ts.tpl +28 -0
  342. package/templates/be/patterns/idempotency/platform/inbox/inbox.options.ts.tpl +7 -0
  343. package/templates/be/patterns/idempotency/platform/inbox/inbox.port.ts.tpl +44 -0
  344. package/templates/be/patterns/idempotency/platform/inbox/inbox.service.spec.ts.tpl +388 -0
  345. package/templates/be/patterns/idempotency/platform/inbox/inbox.service.ts.tpl +77 -0
  346. package/templates/be/patterns/idempotency/platform/inbox/index.ts.tpl +10 -0
  347. package/templates/be/patterns/idempotency/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
  348. package/templates/be/patterns/idempotency/platform/inbox/persistence/connection.ts.tpl +9 -0
  349. package/templates/be/patterns/idempotency/platform/inbox/persistence/entities/inbox-claim.entity.ts.tpl +25 -0
  350. package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
  351. package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.sql.ts.tpl +39 -0
  352. package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1789800005000-create-inbox-claims.ts.tpl +21 -0
  353. package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1792800000000-client-operation-outcomes.ts.tpl +21 -0
  354. package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
  355. package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
  356. package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +1 -0
  357. package/templates/be/patterns/jobs/platform/job-claim.service.spec.ts.tpl +4 -3
  358. package/templates/be/patterns/jobs/platform/job-claim.service.ts.tpl +2 -0
  359. package/templates/be/patterns/jobs/platform/job-runner.service.spec.ts.tpl +20 -2
  360. package/templates/be/patterns/jobs/platform/job-runner.service.ts.tpl +1 -0
  361. package/templates/be/patterns/jobs/platform/jobs.contracts.ts.tpl +4 -0
  362. package/templates/be/patterns/jobs/platform-lite/jobs.index.ts.tpl +10 -0
  363. package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +44 -0
  364. package/templates/be/patterns/jobs/platform-lite/persistence.jobs.sql.ts.tpl +33 -0
  365. package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.spec.ts.tpl +203 -18
  366. package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.ts.tpl +78 -22
  367. package/templates/be/patterns/queues/platform/errors/queue.error.ts.tpl +19 -0
  368. package/templates/be/patterns/queues/platform/messages/queue.messages.ts.tpl +13 -0
  369. package/templates/be/patterns/queues/platform/persistence.migration.ts.tpl +1 -0
  370. package/templates/be/patterns/queues/platform/persistence.queue-outbox.entity.ts.tpl +4 -0
  371. package/templates/be/patterns/queues/platform/persistence.queue.rows.ts.tpl +2 -0
  372. package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +4 -4
  373. package/templates/be/patterns/queues/platform/queue-outbox.service.spec.ts.tpl +14 -2
  374. package/templates/be/patterns/queues/platform/queue-outbox.service.ts.tpl +5 -2
  375. package/templates/be/patterns/queues/platform/queue-relay.service.spec.ts.tpl +72 -74
  376. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +1 -1
  377. package/templates/be/patterns/queues/platform/queue-transport.port.ts.tpl +23 -8
  378. package/templates/be/patterns/queues/platform/queue-worker.service.spec.ts.tpl +29 -8
  379. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +7 -3
  380. package/templates/be/patterns/queues/platform/queue.contracts.ts.tpl +5 -0
  381. package/templates/be/patterns/queues/platform/queue.index.ts.tpl +2 -0
  382. package/templates/be/patterns/queues/platform/queue.log-events.ts.tpl +4 -0
  383. package/templates/be/patterns/queues/platform/queue.module.ts.tpl +2 -1
  384. package/templates/be/patterns/queues/platform/queue.port.ts.tpl +1 -1
  385. package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.spec.ts.tpl +417 -0
  386. package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.ts.tpl +172 -0
  387. package/templates/be/patterns/queues/platform-lite/persistence.queue.sql.ts.tpl +12 -0
  388. package/templates/be/patterns/queues/platform-lite/queue-outbox.sql.tpl +32 -0
  389. package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +148 -0
  390. package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +51 -0
  391. package/templates/be/patterns/queues/platform-lite/queue-transport.port.ts.tpl +163 -0
  392. package/templates/be/patterns/queues/platform-lite/queue.config.ts.tpl +17 -0
  393. package/templates/be/patterns/queues/platform-lite/queue.error.ts.tpl +25 -0
  394. package/templates/be/patterns/queues/platform-lite/queue.index.ts.tpl +8 -0
  395. package/templates/be/patterns/queues/platform-lite/queue.messages.ts.tpl +21 -0
  396. package/templates/be/patterns/queues/platform-lite/queue.module.ts.tpl +74 -0
  397. package/templates/be/patterns/queues/platform-lite/queue.options.ts.tpl +38 -0
  398. package/templates/be/patterns/queues/queue.queue.ts.tpl +2 -2
  399. package/templates/be/patterns/reactors/application.contracts.ts.tpl +2 -0
  400. package/templates/be/patterns/reactors/consumer.ts.tpl +5 -1
  401. package/templates/be/platform/graphql/graphql.contracts.ts +2 -1
  402. package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +1 -1
  403. package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +6 -1
  404. package/templates/be/skeleton/apps/__app__/src/main.ts +7 -4
  405. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +4 -0
  406. package/templates/be/skeleton/apps/cli/src/main.ts +7 -4
  407. package/templates/be/skeleton/src/modules/platform/database/index.ts +1 -1
  408. package/templates/be/skeleton/src/modules/platform/http/errors/http.error.ts +22 -0
  409. package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.spec.ts +235 -0
  410. package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.ts +85 -0
  411. package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.spec.ts +92 -0
  412. package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.ts +52 -0
  413. package/templates/be/skeleton/src/modules/platform/http/http.decorators.ts +9 -0
  414. package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +7 -0
  415. package/templates/be/skeleton/src/modules/platform/http/http.module.ts +19 -0
  416. package/templates/be/skeleton/src/modules/platform/http/http.options.ts +2 -0
  417. package/templates/be/skeleton/src/modules/platform/http/http.port.ts +40 -0
  418. package/templates/be/skeleton/src/modules/platform/http/index.ts +7 -0
  419. package/templates/be/skeleton/src/modules/platform/http/messages/http.messages.ts +15 -0
  420. package/templates/be/skeleton/src/modules/platform/ids/ids.decorators.ts +9 -0
  421. package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +7 -0
  422. package/templates/be/skeleton/src/modules/platform/ids/ids.module.ts +19 -0
  423. package/templates/be/skeleton/src/modules/platform/ids/ids.options.ts +2 -0
  424. package/templates/be/skeleton/src/modules/platform/ids/ids.port.ts +5 -0
  425. package/templates/be/skeleton/src/modules/platform/ids/index.ts +4 -0
  426. package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.spec.ts +15 -0
  427. package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.ts +10 -0
  428. package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.spec.ts +59 -0
  429. package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.ts +21 -0
  430. package/templates/be/skeleton/src/modules/platform/logging/correlation.contracts.ts +16 -0
  431. package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.spec.ts +150 -0
  432. package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.ts +42 -0
  433. package/templates/be/skeleton/src/modules/platform/logging/correlation.port.ts +15 -0
  434. package/templates/be/skeleton/src/modules/platform/logging/correlation.service.spec.ts +23 -0
  435. package/templates/be/skeleton/src/modules/platform/logging/correlation.service.ts +28 -0
  436. package/templates/be/skeleton/src/modules/platform/logging/index.ts +14 -1
  437. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +39 -6
  438. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +15 -3
  439. package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +17 -2
  440. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +13 -1
  441. package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +13 -3
  442. package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.spec.ts +69 -0
  443. package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.ts +48 -0
  444. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  445. package/templates/be/skeleton/src/modules/platform/primitives/record.policy.spec.ts +12 -0
  446. package/templates/be/skeleton/src/modules/platform/primitives/record.policy.ts +3 -0
  447. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +6 -1
  448. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +4 -0
  449. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
  450. package/templates/be/skeleton-lite/src/modules/platform/logging/index.ts +14 -1
  451. package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +12 -0
  452. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  453. package/templates/be/test-world/world/test-apps.options.ts.tpl +2 -0
  454. package/templates/be/test-world/world/test-world.config.ts.tpl +1 -0
  455. package/upgrade/check.mjs +12 -135
  456. package/upgrade/file-state.mjs +1 -16
  457. package/upgrade/index.mjs +34 -361
  458. package/runtime/scripts/lib/check-code-loosening.mjs +0 -128
  459. package/runtime/scripts/lib/gate-loosening.mjs +0 -292
  460. package/templates/app/upgrade-full/.starcistacks/application-stacks.yaml +0 -89
  461. package/templates/app/upgrade-full/.starcistacks/dev/README.md +0 -16
  462. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/api.yaml +0 -13
  463. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/compose.yaml +0 -3
  464. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/postgres.yaml +0 -12
  465. package/templates/app/upgrade-full/.starcistacks/dev/infra/metadata.json +0 -7
  466. package/templates/be/skeleton-lite/apps/api/src/main.ts +0 -26
  467. package/templates/be/upgrade-full/apps/cli/src/app.module.ts +0 -12
  468. package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +0 -7
  469. package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +0 -5
  470. package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +0 -9
  471. package/templates/be/upgrade-full/apps/cli/src/main.ts +0 -29
  472. package/templates/be/upgrade-full/src/features/cli/cli.module.ts +0 -7
  473. package/templates/be/upgrade-full/src/features/cli/migrate/subs/run.cli.ts +0 -23
package/scaffold/add.mjs CHANGED
@@ -22,6 +22,7 @@ import { addModuleImport, compositionCode, readComposeGroup, writeComposeGroup }
22
22
  import { addApp } from './add-app.mjs';
23
23
  import { ensureLiteCli } from './add-cli-lite.mjs';
24
24
  import { addLiteWebhookInbox } from './add-inbox-lite.mjs';
25
+ import { planLiteQueue } from './add-queue-lite.mjs';
25
26
  import { addBuiltinLiteCli, fillKnownForms, formsOf, planKind, readAddContext } from './add-plan.mjs';
26
27
  export { liteApiTemplates } from './add-plan.mjs';
27
28
  import { bundledPins, jsonText, packageJsonText, peerDependencies } from './app.mjs';
@@ -58,8 +59,9 @@ function registerFeatureExports({ repoRoot, repo, noun, values }) {
58
59
  }
59
60
  }
60
61
 
61
- function writeKind({ repoRoot, noun, name, now, manifest, repo, spec, declarationFile, values, forms, beRoot, patterns, planned }) {
62
+ function writeKind({ repoRoot, noun, name, now, options, manifest, repo, spec, declarationFile, values, forms, beRoot, patterns, planned }) {
62
63
  if (noun === 'api' || noun === 'webhook') preflightFeatureComposition({ repoRoot, repo, noun, name });
64
+ const liteQueue = planLiteQueue({ repoRoot, repo, noun, patterns, now, emitTypes: options.noTypes === true ? false : options.emitTypes });
63
65
  const bootstrapCreated = noun === 'cli' && repo.edition === 'lite' ? ensureLiteCli({ root: repoRoot, manifest, repo }) : [];
64
66
  for (const file of planned) {
65
67
  fs.mkdirSync(path.dirname(file.absolute), { recursive: true });
@@ -68,10 +70,11 @@ function writeKind({ repoRoot, noun, name, now, manifest, repo, spec, declaratio
68
70
  registerFeatureExports({ repoRoot, repo, noun, values });
69
71
  if (spec.wire) wire({ beRoot, wire: spec.wire, forms, noun, name });
70
72
  const inboxCreated = addLiteWebhookInbox({ root: repoRoot, repo, noun, provider: name, values, now });
73
+ const queueBacking = liteQueue ? liteQueue.apply() : [];
71
74
  const platformCreated = noun === 'api' || noun === 'webhook' ? wireFeature({ repoRoot, repo, noun, name }) : [];
72
75
  const graphqlDoor = noun === 'api' ? ensureGraphqlPeers(repoRoot) : [];
73
76
  const registered = register({ declarationFile, spec: { patterns, trigger: spec.trigger, optionalSlots: noun === 'api' ? [GRAPHQL_CONTRACT_SLOT] : [] } });
74
- return { created: [...bootstrapCreated, ...planned.map((file) => `be/${file.target}`), ...platformCreated, ...inboxCreated, ...graphqlDoor], registered };
77
+ return { created: [...bootstrapCreated, ...planned.map((file) => path.relative(repoRoot, file.absolute).replaceAll('\\', '/')), ...platformCreated, ...inboxCreated, ...queueBacking, ...graphqlDoor], registered };
75
78
  }
76
79
 
77
80
  /** The slot of the emitted GraphQL snapshot (contracts/<app>/schema.graphql), which an app serving a GraphQL api feature opts into. */
package/scaffold/app.mjs CHANGED
@@ -417,7 +417,6 @@ const liteBaseSource = (rel) => !rel.startsWith('apps/cli/')
417
417
  && rel !== 'src/modules/platform/database/connection-source.client.ts'
418
418
  && rel !== 'src/modules/platform/database/migrate-connections.client.ts'
419
419
  && rel !== 'src/modules/platform/database/seed-connections.client.ts'
420
- && rel !== 'src/modules/platform/primitives/sequence.policy.ts'
421
420
  && !LITE_DEFERRED_GENERATOR_FILES.has(rel)
422
421
  && !rel.startsWith('src/tests/');
423
422
  /** The be files the lite overlay replaces: a base unit spec of such a file tests the full variant, so lite carries its own. */
@@ -488,7 +487,7 @@ function skeletonDirectory(scope, directory, app, vars, include = () => true) {
488
487
  }
489
488
 
490
489
  /** The canonical full Work workspace, catalog and feature nodes an edition upgrade may need. */
491
- export function scaffoldWorkSeeds(project) {
490
+ function scaffoldWorkSeeds(project) {
492
491
  return skeletonDirectory('app', 'skeleton', { project }, {},
493
492
  rel => rel === '.starciwork/workspace.yaml' || rel === '.starciwork/index.yaml' || rel.startsWith('.starciwork/features/'));
494
493
  }
@@ -7,28 +7,32 @@
7
7
 
8
8
  /** The refusal of a verb a non-full edition does not carry: printed verbatim by the CLI, exit 2, nothing written. */
9
9
  export class EditionRefusal extends Error {
10
- constructor(command, message = `${command}: full edition only; run starci app upgrade --edition full`) {
10
+ constructor(command, message = `${command}: full edition only; editions are fixed at scaffold, a lite app stays lite`) {
11
11
  super(message);
12
12
  this.name = 'EditionRefusal';
13
13
  }
14
14
  }
15
15
 
16
16
  /**
17
- * The refusal of a pattern noun in a lite app whose topic carries a `liteAdapter` (ruling:R212/case-1, amended by ruling:R212/case-4):
18
- * lite has the pattern, but the package carries only its full-adapter templates, so generating them would write adapter paths
17
+ * The refusal of a pattern noun in a lite app whose topic carries a `liteAdapter` (ruling:R212/case-1, amended by ruling:R212/case-4)
18
+ * and no lite template in this package: lite has the pattern, but generating its full-adapter templates would write adapter paths
19
19
  * unavailable in lite. The pattern is written by hand using the topic's declared lite adapter until the package carries a lite template.
20
20
  */
21
21
  export class LiteAdapterRefusal extends EditionRefusal {
22
22
  constructor(command, topics) {
23
- super(command, `${command}: the package carries only the full adapter of ${topics.join(', ')}; in a lite app write it on its Supabase adapter (knowledge/patterns/be/${topics[0]}.yaml liteAdapter, knowledge/patterns/be/supabase-queues.yaml)`);
23
+ super(command, `${command}: the package carries only the full adapter of ${topics.join(', ')}; in a lite app write it on its lite adapter (knowledge/patterns/be/${topics[0]}.yaml liteAdapter)`);
24
24
  this.name = 'LiteAdapterRefusal';
25
25
  }
26
26
  }
27
27
 
28
- /** Refuses `command` in a lite app when one of `topics` (pattern topic ids with their parsed documents) carries a `liteAdapter`. */
29
- export function refuseFullAdapterInLite({ edition, topics, command }) {
28
+ /**
29
+ * Refuses `command` in a lite app when one of `topics` (pattern topic ids with their parsed documents) carries a `liteAdapter` and
30
+ * has no complete edition-specific file tree: neither a `liteFiles` list nor a place among `liteTemplated`, the topics whose lite
31
+ * templates the package carries.
32
+ */
33
+ export function refuseFullAdapterInLite({ edition, topics, command, liteTemplated = [] }) {
30
34
  if (edition !== 'lite') return;
31
- const adapted = topics.filter(({ doc }) => doc?.liteAdapter !== undefined).map(({ id }) => id).sort();
35
+ const adapted = topics.filter(({ id, doc }) => doc?.liteAdapter !== undefined && !liteTemplated.includes(id) && (!Array.isArray(doc.liteFiles) || doc.liteFiles.length === 0)).map(({ id }) => id).sort();
32
36
  if (adapted.length) throw new LiteAdapterRefusal(command, adapted);
33
37
  }
34
38
 
@@ -36,6 +36,16 @@ const REGISTRATIONS = Object.freeze({
36
36
  names: ["InjectPrimaryEntityManager", "PRIMARY_ENTITY_MANAGER"],
37
37
  },
38
38
  ],
39
+ // The queue and jobs capabilities state their outbox and claim statements with the sql tag (the lite starter already carries the
40
+ // in-order relay and the record guard the BullMQ client reads stored jobs with).
41
+ queue: [
42
+ {
43
+ file: "be/src/modules/platform/database/index.ts",
44
+ source: "./database.sql",
45
+ names: ["sql"],
46
+ beforeSource: "./errors/database.error",
47
+ },
48
+ ],
39
49
  webhook: [
40
50
  {
41
51
  file: "be/src/modules/platform/database/index.ts",
@@ -0,0 +1,33 @@
1
+ /** The app-relative path of a topic file, whose root defaults to the back end. */
2
+ export const patternFilePath = (file, root = 'be') => root === 'app' ? file : `${root}/${file}`;
3
+
4
+ const unquote = (item) => item.trim().replace(/^["']|["']$/g, '');
5
+
6
+ /** Derives slots for files/liteFiles entries through the app resolver supplied by the caller. */
7
+ export function derivePatternFileSlots(text, classify) {
8
+ const lines = text.split('\n');
9
+ const problems = [];
10
+ let inFiles = false;
11
+ for (let i = 0; i < lines.length; i += 1) {
12
+ if (/^[A-Za-z][A-Za-z0-9_-]*:/.test(lines[i])) inFiles = /^(?:files|liteFiles):\s*$/.test(lines[i]);
13
+ if (!inFiles) continue;
14
+ const match = /^ {2}- path: (.+)$/.exec(lines[i]);
15
+ if (!match) continue;
16
+ const entry = deriveFileSlot(lines, i, match[1], classify);
17
+ if (entry.problem) problems.push(entry.problem);
18
+ else lines[entry.slotAt] = ` slot: ${entry.slot}`;
19
+ }
20
+ return { text: lines.join('\n'), problems };
21
+ }
22
+
23
+ /** The fields of one file entry stop at the next entry or top-level YAML key. */
24
+ function deriveFileSlot(lines, start, file, classify) {
25
+ let end = start + 1;
26
+ while (end < lines.length && !/^(?:[A-Za-z][A-Za-z0-9_-]*:| {2}- )/.test(lines[end])) end += 1;
27
+ const slotAt = lines.findIndex((line, index) => index > start && index < end && /^ {4}slot: /.test(line));
28
+ if (slotAt < 0) return { problem: `${file} has no slot line` };
29
+ const rootLine = lines.slice(start + 1, end).find((line) => /^ {4}root: /.test(line));
30
+ const root = rootLine ? unquote(rootLine.replace(/^ {4}root: /, '')) : 'be';
31
+ const slot = classify(patternFilePath(unquote(file), root));
32
+ return slot ? { slotAt, slot } : { problem: `${file} is owned by no slot` };
33
+ }
@@ -57,7 +57,7 @@ function worldConfig({ apis, connections }) {
57
57
  appModuleImports: apis.map((api) => `import { AppModule as ${pascalOf(api.name)}App } from "../../../apps/${api.name}/src/app.module"`).join('\n'),
58
58
  optionsImports: ['cliOptionsOf', ...apis.map((api) => `${camelOf(api.name)}OptionsOf`), 'migrateCli', 'platformBase'].sort().join(', '),
59
59
  connectionDeclarations: connections.map((connection) => `{ name: ${JSON.stringify(connection.name)} }`).join(', '),
60
- worldApps: apis.map((api) => ` ${IDENTIFIER.test(api.name) ? api.name : JSON.stringify(api.name)}: { module: ${pascalOf(api.name)}App, options: ${camelOf(api.name)}OptionsOf },`).join('\n'),
60
+ worldApps: apis.map((api) => ` ${IDENTIFIER.test(api.name) ? api.name : JSON.stringify(api.name)}: { module: ${pascalOf(api.name)}App, options: ${camelOf(api.name)}OptionsOf, configure: applyCorrelation },`).join('\n'),
61
61
  });
62
62
  }
63
63
 
@@ -0,0 +1,117 @@
1
+ // Comment-preserving additions to a block-style YAML file an app owns (its stack declaration, its Swarm model). The caller
2
+ // applies every addition to the parsed document as well and accepts the edited text only when it parses to that document, so
3
+ // a shape this editor does not understand refuses instead of writing a wrong file.
4
+ import { isDeepStrictEqual } from 'node:util';
5
+ import { parseYaml } from '../runtime/engine/yaml.mjs';
6
+ import { ScaffoldError } from './service.mjs';
7
+
8
+ const significant = (line) => line.trim() !== '' && !line.trimStart().startsWith('#');
9
+ const indentOf = (line) => line.length - line.trimStart().length;
10
+ const isItem = (line) => /^\s*-(?:\s|$)/.test(line);
11
+
12
+ /** The key's own indent: a `- key:` item line carries its first key two columns after the dash. */
13
+ const keyIndentOf = (line) => (isItem(line) ? indentOf(line) + 2 : indentOf(line));
14
+
15
+ /** The body of the value that starts after line `at` (a key line, or an item line for an item's body): its lines and their indent. */
16
+ function bodyOf(lines, at, ownIndent) {
17
+ let last = at;
18
+ let childIndent = null;
19
+ for (let index = at + 1; index < lines.length; index += 1) {
20
+ const line = lines[index];
21
+ if (!significant(line)) continue;
22
+ const indent = indentOf(line);
23
+ const indentless = indent === ownIndent && isItem(line) && (childIndent === null || childIndent === ownIndent);
24
+ if (indent <= ownIndent && !indentless) break;
25
+ childIndent ??= indent;
26
+ last = index;
27
+ }
28
+ return { from: at + 1, to: last + 1, childIndent };
29
+ }
30
+
31
+ /** The line of `key` among the direct children (indent `indent`) of lines [from, to); `itemLine` may hold the first key of an item. */
32
+ function keyLine(lines, { from, to }, indent, key, itemLine = -1) {
33
+ const pattern = new RegExp(`^\\s*(?:- )?${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}:(?:\\s|$)`);
34
+ for (let index = itemLine >= 0 ? itemLine : from; index < to; index += 1) {
35
+ const line = lines[index];
36
+ if (!significant(line) || keyIndentOf(line) !== indent || (isItem(line) && index !== itemLine)) continue;
37
+ if (pattern.test(line)) return index;
38
+ }
39
+ return -1;
40
+ }
41
+
42
+ /**
43
+ * Resolves `path` to the value it names: a string steps into a mapping key, `{ item: key, is: value }` into the sequence item whose
44
+ * `key` is `value`. Returns the line that opens the value (the key or item line), the indent of its keys and its body.
45
+ */
46
+ function locate(lines, path, file) {
47
+ let at = { line: -1, indent: -1, itemLine: -1, body: { from: 0, to: lines.length, childIndent: 0 } };
48
+ for (const step of path) {
49
+ const { body } = at;
50
+ if (typeof step === 'string') {
51
+ const indent = at.itemLine >= 0 ? at.indent : body.childIndent;
52
+ const line = keyLine(lines, body, indent, step, at.itemLine);
53
+ if (line < 0) throw new ScaffoldError('HFS_ADD_STACK_SHAPE', `${file} has no ${path.map(describe).join('.')}; add the declared Redis by hand`);
54
+ at = { line, indent: keyIndentOf(lines[line]), itemLine: -1, body: bodyOf(lines, line, keyIndentOf(lines[line])) };
55
+ continue;
56
+ }
57
+ const items = [];
58
+ for (let index = body.from; index < body.to; index += 1) if (significant(lines[index]) && isItem(lines[index]) && indentOf(lines[index]) === body.childIndent) items.push(index);
59
+ const match = items.find((line, position) => {
60
+ const end = items[position + 1] ?? body.to;
61
+ return keyLine(lines, { from: line, to: end }, body.childIndent + 2, step.item, line) >= 0
62
+ && new RegExp(`^\\s*(?:- )?${step.item}:\\s*${step.is}\\s*$`).test(lines[keyLine(lines, { from: line, to: end }, body.childIndent + 2, step.item, line)]);
63
+ });
64
+ if (match === undefined) throw new ScaffoldError('HFS_ADD_STACK_SHAPE', `${file} has no ${path.map(describe).join('.')}; add the declared Redis by hand`);
65
+ const itemBody = bodyOf(lines, match, body.childIndent);
66
+ at = { line: match, indent: body.childIndent + 2, itemLine: match, body: { ...itemBody, from: match, childIndent: body.childIndent + 2 } };
67
+ }
68
+ return at;
69
+ }
70
+
71
+ const describe = (step) => (typeof step === 'string' ? step : `[${step.item}=${step.is}]`);
72
+
73
+ /** The lines of `block` (written at indent 0) moved to `indent`. */
74
+ const indented = (block, indent) => block.replace(/\n$/, '').split('\n').map((line) => (line ? ' '.repeat(indent) + line : line));
75
+
76
+ /** A YAML text that adds entries under one mapping or items to one block sequence, and applies the same change to its document. */
77
+ export class YamlBlockEdit {
78
+ constructor(text, file) {
79
+ this.file = file;
80
+ this.newline = text.includes('\r\n') ? '\r\n' : '\n';
81
+ this.lines = text.replace(/\r\n/g, '\n').split('\n');
82
+ this.document = parseYaml(text);
83
+ this.expected = structuredClone(this.document);
84
+ }
85
+
86
+ /** The parsed document as the edits leave it; mutate it in step with each text edit. */
87
+ get value() { return this.expected; }
88
+
89
+ /** Appends `block` (YAML lines at indent 0) as the last children of the mapping or sequence at `path`. */
90
+ append(path, block) {
91
+ const at = locate(this.lines, path, this.file);
92
+ const { body } = at;
93
+ const indent = body.to > body.from && body.childIndent !== null ? body.childIndent : at.indent + 2;
94
+ this.lines.splice(body.to, 0, ...indented(block, indent));
95
+ return this;
96
+ }
97
+
98
+ /** Adds `value` to the flow sequence (`key: [a, b]`) at `path`. */
99
+ extendFlow(path, value) {
100
+ const at = locate(this.lines, path, this.file);
101
+ const line = this.lines[at.line];
102
+ const flow = /^(.*:\s*\[)(.*)\](\s*)$/.exec(line);
103
+ if (!flow) throw new ScaffoldError('HFS_ADD_STACK_SHAPE', `${this.file} writes ${path.map(describe).join('.')} as no one-line flow sequence; add the declared Redis by hand`);
104
+ const items = flow[2].trim();
105
+ this.lines[at.line] = `${flow[1]}${items ? `${items}, ` : ''}${value}]${flow[3]}`;
106
+ return this;
107
+ }
108
+
109
+ /** The edited text, or a refusal when it does not parse to the expected document. */
110
+ text() {
111
+ const text = this.lines.join(this.newline);
112
+ let parsed;
113
+ try { parsed = parseYaml(text); } catch { parsed = undefined; }
114
+ if (!isDeepStrictEqual(parsed, this.expected)) throw new ScaffoldError('HFS_ADD_STACK_SHAPE', `${this.file} is not in a shape the declared Redis can be added to; add it by hand`);
115
+ return text;
116
+ }
117
+ }
package/src/main.mjs CHANGED
@@ -97,7 +97,7 @@ const USAGE = `starci app scaffold <name> [--into <dir>] [--edition full|lite] [
97
97
  starci app add <api|webhook|realtime|saga|job|reactor|queue|projection|cli|table|app> <name> [options] [--cwd <dir>]
98
98
  starci app lint [--cwd <dir>] [--changed <file>...] [--workspace <dir>] [--side be|fe] [--fix] [--format text|json] [--sonar <file>]
99
99
  starci app sync (--check | --write) [--cwd <dir>]
100
- starci app check [--cwd <dir>] [--json] [--fast] [--base <ref>] [--edition full] [--db-types] [--wiring] [--coverage-from <lcov>]
100
+ starci app check [--cwd <dir>] [--json] [--fast] [--base <ref>] [--db-types] [--wiring] [--coverage-from <lcov>]
101
101
  starci app db baseline [--stamp <version>] [--local] [--apply] [--cwd <dir>]
102
102
  starci app db restamp --map <restamp.yaml> [--local] [--apply] [--cwd <dir>]
103
103
  starci app db plan [--local] [--json] [--cwd <dir>]
@@ -107,7 +107,7 @@ starci app db migrate [--execute] [--local | --linked | --url-env <NAME>] [--jso
107
107
  starci app db post-cutover [--execute] [--green-at <ISO> | --owner-confirms-old-app-gone] [--linked | --url-env <NAME>] [--json] [--cwd <dir>]
108
108
  starci app release gate [--execute] [--source <ref>] [--tag-prefix <prefix>] [--json] [--cwd <dir>]
109
109
  starci app release push [--execute] [--remote <name>] [--json] [--cwd <dir>]
110
- starci app upgrade [--edition full] [--plan] [--cwd <dir>]
110
+ starci app upgrade [--plan] [--cwd <dir>]
111
111
  starci app stack <up|down|status> [--stack <dir>] [--services <list>] [--k3d] [--force] [--json] [--cwd <dir>]
112
112
  starci app stack <prepare|doctor|connect|disconnect|build-args|render|deploy|converge|verify|rollback|prune|backup|jobs> --env <env> [--execute] [--json] [--cwd <dir>]
113
113
  starci app explain <path> [--cwd <dir>] [--json]
@@ -397,8 +397,9 @@ function explainMain(opts, { cwd: repoRoot, stdout: out }) {
397
397
  return explained.status === 'no-slot' || explained.status === 'ambiguous' ? 1 : 0;
398
398
  }
399
399
 
400
- function upgradeAppMain(argv, { cwd, edition, stdout, presets }) {
401
- return upgradeMain(['--repo', cwd, ...(edition ? ['--edition', edition] : []), ...argv], { stdout, presets });
400
+ async function upgradeAppMain(argv, { cwd, edition, stdout }) {
401
+ await checkEdition({ repoRoot: cwd, edition, normal: () => null });
402
+ return upgradeMain(['--repo', cwd, ...argv], { stdout });
402
403
  }
403
404
 
404
405
  /** The edition `hfs.json` declares, or undefined when it cannot be read (the test-world verbs then decide on their own). */
package/stack/deploy.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // deploy.mjs - the whole release (deployRelease) and the redeploy of a stored release (rollbackRelease).
2
- // deploy: render, provision, store the release record, pre-pull, `docker stack deploy`, converge, verify, accept, prune. A failure at or
2
+ // deploy: render, provision, store the release record, pre-pull, `docker stack deploy`, converge, post-deploy tasks (deploy.postDeploy, when declared), verify, accept, prune. A failure at or
3
3
  // after the stack deploy redeploys the previous accepted release from its STORED bytes; a failure before it leaves the services untouched.
4
4
  // Without `execute` a deploy renders locally and prints the plan: NO remote call at all. A rollback cannot undo data effects.
5
5
  //
@@ -17,6 +17,7 @@ import { describeClash, edgeCollisions, reconcileEdgeAliases } from './edge-alia
17
17
  import { firstLine, tool } from './context.mjs';
18
18
  import { hasPlaceholderDigest, isReleaseId } from './digest.mjs';
19
19
  import { failure, refusal } from './errors.mjs';
20
+ import { postDeployStack } from './post-deploy.mjs';
20
21
  import { pruneStack } from './prune.mjs';
21
22
  import { parseYaml } from '../runtime/engine/yaml.mjs';
22
23
  import { createReleaseStore } from './release-store.mjs';
@@ -33,6 +34,7 @@ export const DEFAULT_DEPS = Object.freeze({
33
34
  provisionRelease: lazy('./provision.mjs', 'provisionRelease'),
34
35
  convergeStack,
35
36
  reconcileEdge: reconcileEdgeAliases,
37
+ postDeployStack,
36
38
  verifyStack,
37
39
  pruneStack,
38
40
  releaseStore: createReleaseStore,
@@ -190,6 +192,7 @@ async function deployExecute(ctx, config, options, d, out, cleanups) {
190
192
  ['deploy', () => stackDeploy(ctx, config, path.join(out, 'stack.yaml'))],
191
193
  ['edge', async () => d.reconcileEdge(ctx, config, path.join(out, 'stack.yaml'))],
192
194
  ['converge', () => d.convergeStack(ctx, config, { since, timeoutSeconds: options.timeoutSeconds })],
195
+ ...(config.postDeploy.length > 0 ? [['post-deploy', () => d.postDeployStack(ctx, config)]] : []),
193
196
  ['verify', () => d.verifyStack(ctx, config, { retrySeconds: options.retrySeconds, skipPublic: skipPublic === true, stackFile: path.join(out, 'stack.yaml') })],
194
197
  ...(skipPublic ? [] : [['accept', async () => { await store.accept(releaseId); return { ok: true, lines: [`accepted release ${releaseId}`] }; }]]),
195
198
  ], lines, steps);
@@ -224,9 +227,10 @@ export async function deployRelease(ctx, config, options = {}, deps = {}) {
224
227
  const rendered = await d.renderRelease(ctx, config, renderOptions(options, out, null));
225
228
  const { releaseId } = inspectRendered(out, config);
226
229
  if (options.execute !== true) {
230
+ const post = config.postDeploy.length > 0 ? ` post-deploy tasks (${config.postDeploy.join(', ')}),` : '';
227
231
  const plan = options.skipPublic
228
- ? `dry run: release ${releaseId} would be provisioned, stored, pre-pulled, deployed, converged and verified inside the stack (public hosts skipped); it would stay pending until ingress publish and stack accept`
229
- : `dry run: release ${releaseId} would be provisioned, stored, pre-pulled, deployed, converged, verified, accepted`;
232
+ ? `dry run: release ${releaseId} would be provisioned, stored, pre-pulled, deployed, converged,${post} and verified inside the stack (public hosts skipped); it would stay pending until ingress publish and stack accept`
233
+ : `dry run: release ${releaseId} would be provisioned, stored, pre-pulled, deployed, converged,${post} verified, accepted`;
230
234
  return { ok: true, lines: [...(rendered.lines ?? []), plan], data: { releaseId, steps: [{ step: 'render', ok: true }], executed: false } };
231
235
  }
232
236
  return await deployExecute(ctx, config, options, d, out, cleanups);
@@ -0,0 +1,48 @@
1
+ // post-deploy.mjs - the release tasks of a deploy (deploy.postDeploy): after the stack converged and before verification, each declared task
2
+ // (deploy.tasks.<name>) runs in order inside the running task of its service, through the daemon of this invocation like verify's checks.
3
+ // A post-deploy task is the place for an idempotent write a release owes the data (a catalogue sync, a seed upsert), so verification stays read-only.
4
+ // The first task that exits non-zero, or runs past its timeout, fails the step and the deploy rolls back; a rollback cannot undo what a task wrote.
5
+ // The deploy output carries the task's name, its exit and its line count only: the container holds decrypted secrets and the output reaches the
6
+ // CI log, so stdout and stderr are never relayed (`starci app stack task <name>` is the operator verb that shows them).
7
+ import { findInOrder } from '../runtime/scripts/lib/in-order.mjs';
8
+ import { lines as splitLines, tool } from './context.mjs';
9
+
10
+ /** The running container of a service on this daemon, or undefined. */
11
+ function runningContainer(ctx, config, service) {
12
+ const found = tool(ctx, 'docker', ['ps', '-q', '--filter', `label=com.docker.swarm.service.name=${config.stack}_${service}`, '--filter', 'status=running']);
13
+ return splitLines(found.stdout)[0];
14
+ }
15
+
16
+ /** The verdict of one finished `docker exec`: a timeout, a launch failure or a non-zero exit is a failure, whatever it printed. */
17
+ function verdictOf(run, timeoutSeconds) {
18
+ if (run.error?.code === 'ETIMEDOUT') return { ok: false, detail: `timed out after ${timeoutSeconds}s` };
19
+ if (run.error) return { ok: false, detail: 'could not run' };
20
+ if (run.status === null || run.status === undefined) return { ok: false, detail: 'ended without an exit status' };
21
+ return run.status === 0 ? { ok: true, detail: 'ok' } : { ok: false, detail: `exit ${run.status}` };
22
+ }
23
+
24
+ /** Runs one declared task; returns { name, ok, detail, outputLines }. */
25
+ function runTask(ctx, config, name) {
26
+ const declared = config.tasks[name];
27
+ const container = runningContainer(ctx, config, declared.service);
28
+ if (!container) return { name, ok: false, detail: `no running container of ${config.stack}_${declared.service} on the daemon`, outputLines: 0 };
29
+ const run = tool(ctx, 'docker', ['exec', container, ...declared.command], { timeout: declared.timeoutSeconds * 1000, killSignal: 'SIGKILL' });
30
+ return { name, ...verdictOf(run, declared.timeoutSeconds), outputLines: splitLines(run.stdout).length + splitLines(run.stderr).length };
31
+ }
32
+
33
+ /**
34
+ * Runs the declared post-deploy tasks in order and stops at the first failure.
35
+ * Returns { ok, lines, data: { tasks: [{ name, ok }] } }; the lines read `PASS|FAIL post-deploy <name> detail (N output lines withheld)`.
36
+ */
37
+ export async function postDeployStack(ctx, config) {
38
+ const results = [];
39
+ await findInOrder(config.postDeploy, async (name) => {
40
+ results.push(runTask(ctx, config, name));
41
+ return !results.at(-1).ok;
42
+ });
43
+ return {
44
+ ok: results.every((entry) => entry.ok),
45
+ lines: results.map((entry) => `${entry.ok ? 'PASS' : 'FAIL'} post-deploy ${entry.name} ${entry.detail} (${entry.outputLines} output line${entry.outputLines === 1 ? '' : 's'} withheld)`),
46
+ data: { tasks: results.map(({ name, ok }) => ({ name, ok })) },
47
+ };
48
+ }
package/sync/index.mjs CHANGED
@@ -150,6 +150,14 @@ function trailingNewline(text) {
150
150
  return text.slice(0, end) + '\n';
151
151
  }
152
152
 
153
+ /** The digest-pinned image of a lite app's declared Redis, in every environment that runs it (ruling:R212/case-10). */
154
+ export const REDIS_IMAGE = 'redis:7@sha256:c6eabf748fc7a61dbb5a705c78bcf3d6377b1127a97d0ce965c11c44ba46896f';
155
+
156
+ const redisDevText = project => render(readBundled('app/package-scripts/redis-dev.json'), { project, redisImage: REDIS_IMAGE });
157
+
158
+ /** The development scripts of a lite app's declared Redis (create once, then start and stop it), as a scripts object. */
159
+ export const redisDevScripts = project => JSON.parse(`{${redisDevText(project).trim().replace(/,$/, '')}}`);
160
+
153
161
  /** What the app's sync reads from the jest preset it installs for its be side: the Sonar exclusions ({ sonarExclusions }); both editions install it (a lite back end keeps full's unit run, ruling:R212/case-1, amended by ruling:R212/case-4). */
154
162
  export async function loadPresets(root) {
155
163
  const name = '@starci/jest-preset';
@@ -341,6 +349,8 @@ export function variables(app, scope, presets, sonarKey, manifest = loadSlotMani
341
349
  appScripts: appScripts(app, source),
342
350
  // A full upgrade keeps the app's declared Supabase connection and its local database commands.
343
351
  supabaseScripts: providers.includes('supabase') ? render(readBundled('app/package-scripts/supabase.json'), { appBinary: APP_BINARY }) : '',
352
+ // A lite app's declared Redis (sides.be.backing, ruling:R212/case-10) runs in development as one container of its pinned image.
353
+ backingScripts: lite && (app.sides.be.backing ?? []).includes('redis') ? redisDevText(app.project) : '',
344
354
  // turbo builds every fe app workspace; ^build (turbo.json) builds the packages each app imports first, and caches both.
345
355
  buildFe: 'npm run codegen --silent && turbo run build --filter=./fe/apps/*',
346
356
  // The be side with its tsc; every fe workspace with its own typecheck script through turbo (the packages are built first: ^build).
@@ -3,7 +3,7 @@
3
3
  # rollback and disconnect of "starci app stack". It runs by hand only (Actions > deploy stack > Run workflow) from a release tag v*
4
4
  # whose version is the package.json version; it has no push or schedule trigger.
5
5
  # operation deploy builds and pushes one image per environments.<env>.deploy.images entry of .starcistacks/application-stacks.yaml,
6
- # then deploys their immutable digests (render, provision, converge, verify, accept; a failure redeploys the previous release)
6
+ # then deploys their immutable digests (render, provision, converge, post-deploy tasks, verify, accept; a failure redeploys the previous release)
7
7
  # operation rollback redeploys a stored release; "release" is 16 lowercase hex characters, empty means the previous accepted release
8
8
  # The environment input names a protected GitHub environment (required reviewers) that holds:
9
9
  # vars VPS_HOST, VPS_USER (the swarm manager); a variable named in deploy.passEnv, when the stack interpolates one
@@ -3,7 +3,7 @@
3
3
  # rollback and disconnect of "starci app stack". It runs by hand only (Actions > deploy stack > Run workflow) from a release tag v*
4
4
  # whose version is the package.json version; it has no push or schedule trigger.
5
5
  # operation deploy builds and pushes one image per environments.<env>.deploy.images entry of .starcistacks/application-stacks.yaml,
6
- # then deploys their immutable digests (render, provision, converge, verify, accept; a failure redeploys the previous release)
6
+ # then deploys their immutable digests (render, provision, converge, post-deploy tasks, verify, accept; a failure redeploys the previous release)
7
7
  # operation rollback redeploys a stored release; "release" is 16 lowercase hex characters, empty means the previous accepted release
8
8
  # The environment input names a protected GitHub environment (required reviewers) that holds:
9
9
  # vars VPS_HOST, VPS_USER (the swarm manager); a variable named in deploy.passEnv, when the stack interpolates one
@@ -1,36 +1,66 @@
1
1
  // GraphQL wire types, the same in both editions (owner ruling 2026-10-13): every fe app with operations under
2
- // src/modules/api/graphql/{queries,mutations} gets its typed documents generated from the contract snapshots the back end emits
2
+ // src/modules/api/graphql/{queries,mutations,subscriptions} gets its typed documents generated from the contract snapshots the back end emits
3
3
  // (be/contracts/<app>/schema.graphql, `npm run contract:emit`) into the gitignored src/modules/api/graphql/__generated__/.
4
+ // The input-bounds output carries the backend validators into translated form schemas through modules/api.
4
5
  // Nothing to generate (no snapshot yet, or no fe app with a modules/api/graphql folder) is not an error.
5
6
  const CONTRACTS = path.join("be", "contracts")
6
7
  const FE_APPS = path.join("fe", "apps")
7
8
  const posix = (file) => file.split(path.sep).join("/")
8
- const schemas = (fs.existsSync(CONTRACTS) ? fs.readdirSync(CONTRACTS) : [])
9
+ const services = (fs.existsSync(CONTRACTS) ? fs.readdirSync(CONTRACTS) : []).sort(byCodeUnit)
10
+ const frontendApps = (fs.existsSync(FE_APPS) ? fs.readdirSync(FE_APPS) : []).sort(byCodeUnit)
11
+ const schemas = services
9
12
  .map((app) => path.join(CONTRACTS, app, "schema.graphql"))
10
13
  .filter((file) => fs.existsSync(file))
11
14
  .sort(byCodeUnit)
12
15
  .map(posix)
13
- const operationRoots = (fs.existsSync(FE_APPS) ? fs.readdirSync(FE_APPS) : [])
16
+ const operationRoots = frontendApps
14
17
  .map((app) => path.join(FE_APPS, app, "src", "modules", "api", "graphql"))
15
18
  .filter((dir) => fs.existsSync(dir))
16
19
  .sort(byCodeUnit)
17
20
  .map(posix)
21
+ const writeFormBounds = (roots, source) => {
22
+ for (const root of roots) {
23
+ const generated = `${root}/__generated__`
24
+ fs.mkdirSync(generated, { recursive: true })
25
+ fs.writeFileSync(`${generated}/input-bounds.ts`, source)
26
+ }
27
+ }
18
28
  if (schemas.length === 0 || operationRoots.length === 0) {
19
29
  process.stdout.write("codegen: no GraphQL contract under be/contracts yet\n")
30
+ if (schemas.length === 0 && operationRoots.length) {
31
+ const { emptyInputBoundsSource } = await import("@starci/cli/input-bounds")
32
+ writeFormBounds(operationRoots, emptyInputBoundsSource())
33
+ }
20
34
  } else {
35
+ const { buildSchema } = await import("graphql")
36
+ const { assertCompatibleInputBounds } = await import("@starci/cli/input-bounds")
37
+ assertCompatibleInputBounds(
38
+ Object.fromEntries(schemas.map((file) => [file, buildSchema(fs.readFileSync(file, "utf8"))])),
39
+ )
21
40
  const { generate } = await import("@graphql-codegen/cli")
22
41
  const generates = Object.fromEntries(
23
- operationRoots.map((root) => [
24
- `${root}/__generated__/`,
25
- {
26
- preset: "client",
27
- documents: [`${root}/queries/*.ts`, `${root}/mutations/*.ts`],
28
- presetConfig: { fragmentMasking: false },
29
- config: { useTypeImports: true, enumsAsTypes: true, avoidOptionals: true },
30
- },
42
+ operationRoots.flatMap((root) => [
43
+ [
44
+ `${root}/__generated__/`,
45
+ {
46
+ preset: "client",
47
+ documents: [`${root}/queries/*.ts`, `${root}/mutations/*.ts`, `${root}/subscriptions/*.ts`],
48
+ presetConfig: { fragmentMasking: false },
49
+ config: { useTypeImports: true, enumsAsTypes: true, avoidOptionals: true },
50
+ },
51
+ ],
52
+ [`${root}/__generated__/input-bounds.ts`, { plugins: [{ "@starci/cli/input-bounds": {} }] }],
31
53
  ]),
32
54
  )
33
- await generate({ schema: schemas, generates, ignoreNoDocuments: true, silent: true }, true)
55
+ await generate(
56
+ {
57
+ schema: schemas,
58
+ generates,
59
+ ignoreNoDocuments: true,
60
+ silent: true,
61
+ },
62
+ true,
63
+ )
34
64
  // Stamp only a successful generation, using the same sorted paths and LF-normalized schemas as R52.
35
65
  const hash = createHash("sha256")
36
66
  for (const schema of schemas) {
@@ -43,3 +73,20 @@ if (schemas.length === 0 || operationRoots.length === 0) {
43
73
  if (fs.existsSync(generated)) fs.writeFileSync(`${generated}/.schema-sha256`, `${digest}\n`)
44
74
  }
45
75
  }
76
+
77
+ // REST forms consume the same backend DTO bounds through their API owner's generated output, even without GraphQL.
78
+ const restRoots = frontendApps
79
+ .map((app) => path.join(FE_APPS, app, "src", "modules", "api", "rest"))
80
+ .filter((dir) => fs.existsSync(dir))
81
+ .map(posix)
82
+ if (restRoots.length) {
83
+ const { openApiBoundsSource } = await import("@starci/cli/input-bounds")
84
+ const contracts = Object.fromEntries(
85
+ services
86
+ .map((service) => [service, path.join(CONTRACTS, service, "openapi.json")])
87
+ .filter(([, file]) => fs.existsSync(file))
88
+ .map(([service, file]) => [service, JSON.parse(fs.readFileSync(file, "utf8"))]),
89
+ )
90
+ // Generate an empty map when a contract disappears so stale form limits cannot survive a successful codegen.
91
+ writeFormBounds(restRoots, openApiBoundsSource(contracts))
92
+ }
@@ -1,7 +1,7 @@
1
1
  {{header}}
2
2
  # Commit gate (L0 only): work hygiene, the secrets guard and the bypass guard (no inline disable, type-check escape, skipped test or
3
3
  # swallowed failure: HFS_BYPASS_MARKER) of the staged files, managed files equal to their render, eslint of the staged files of each
4
- # side, stylelint over the staged CSS of fe, a prettier check of the staged files. Every step ends the hook when it fails: none can be
4
+ # side (the fe packages are built first: the apps import their emitted types, the way the managed lint script does), stylelint over the staged CSS of fe, a prettier check of the staged files. Every step ends the hook when it fails: none can be
5
5
  # skipped. Types and the targeted specs belong to the op gate.
6
6
  npx starci app hygiene || exit $?
7
7
  npx starci app sync --check || exit $?
@@ -9,7 +9,7 @@ npx starci app sync --check || exit $?
9
9
  git diff --cached --name-only --diff-filter=ACMR -z --relative=be -- '*.ts' '*.mts' '*.cts' '*.js' '*.mjs' '*.cjs' |
10
10
  xargs -0 -n 50 sh -c 'if [ "$#" -gt 0 ]; then cd be && npx eslint -- "$@"; fi' sh || exit $?
11
11
  git diff --cached --name-only --diff-filter=ACMR -z --relative=fe -- '*.ts' '*.tsx' '*.mts' '*.cts' '*.js' '*.jsx' '*.mjs' '*.cjs' |
12
- xargs -0 -n 50 sh -c 'if [ "$#" -gt 0 ]; then cd fe && npx eslint --max-warnings=0 --no-warn-ignored -- "$@"; fi' sh || exit $?
12
+ xargs -0 -n 50 sh -c 'if [ "$#" -gt 0 ]; then if [ -d fe/packages ]; then npx turbo run build --filter="./fe/packages/*" --output-logs=errors-only || exit $?; fi; cd fe && npx eslint --max-warnings=0 --no-warn-ignored -- "$@"; fi' sh || exit $?
13
13
  git diff --cached --name-only --diff-filter=ACMR -z --relative=fe -- '*.css' |
14
14
  xargs -0 -n 50 sh -c 'if [ "$#" -gt 0 ]; then cd fe && npx stylelint "$@"; fi' sh || exit $?
15
15
  git diff --cached --name-only --diff-filter=ACMR -z |
@@ -3,7 +3,8 @@
3
3
  # backs work up under refs/backup/*. Git hands this hook one line per ref: <local ref> <local sha> <remote ref> <remote sha>.
4
4
  # refs/backup/* allowed: the coordinator backup namespace
5
5
  # refs/heads/main, refs/tags/v* allowed only when HEAD carries an annotated tag v[0-9]* and the release cut recorded the
6
- # full suite of HEAD: <git common dir>/starci-release/<HEAD sha>.l4.json names that head and tag
6
+ # full suite of HEAD: <git common dir>/starci-release/<HEAD sha>.l4.json names that head and tag;
7
+ # a pushed tag must be that tag and must point at HEAD
7
8
  # refs/heads/release/candidate-* the release candidate (starci app release push sends the release sha there first and moves main
8
9
  # and the tag only when its ci.yml run is green): allowed under the same release gate, never deleted
9
10
  # any other ref, a delete refused
@@ -13,6 +14,7 @@ head=$(git rev-parse HEAD)
13
14
  record="$common/starci-release/$head.l4.json"
14
15
  status=0
15
16
  gate=""
17
+ gate_tag=""
16
18
  refuse() {
17
19
  echo "RIGHTS_PUSH_NOT_RELEASE: $1 - the allowed path is the release cut (main, v* tags and the release/candidate-* ref) or refs/backup/* (backups)" >&2
18
20
  status=1
@@ -28,7 +30,7 @@ release_gate() {
28
30
  if [ ! -f "$record" ]; then gate="no L4 record $record"; continue; fi
29
31
  flat=$(tr -d ' \t\r\n' < "$record")
30
32
  case "$flat" in
31
- *'"head":"'"$head"'"'*'"tag":"'"$tag"'"'* | *'"tag":"'"$tag"'"'*'"head":"'"$head"'"'*) gate="ok"; return 0 ;;
33
+ *'"head":"'"$head"'"'*'"tag":"'"$tag"'"'* | *'"tag":"'"$tag"'"'*'"head":"'"$head"'"'*) gate="ok"; gate_tag="$tag"; return 0 ;;
32
34
  esac
33
35
  gate="the L4 record $record does not name head $head and tag $tag"
34
36
  done
@@ -43,7 +45,12 @@ while read -r local_ref local_sha remote_ref remote_sha; do
43
45
  *) refuse "refusing to delete $remote_ref"; continue ;;
44
46
  esac
45
47
  release_gate
46
- if [ "$gate" != "ok" ]; then refuse "$remote_ref is closed: $gate"; fi ;;
48
+ if [ "$gate" != "ok" ]; then refuse "$remote_ref is closed: $gate"; continue; fi
49
+ case "$remote_ref" in
50
+ refs/tags/*)
51
+ if [ "$remote_ref" != "refs/tags/$gate_tag" ]; then refuse "$remote_ref is not the release tag $gate_tag of HEAD $head"
52
+ elif [ "$(git rev-parse "$local_sha^{commit}" 2>/dev/null)" != "$head" ]; then refuse "$remote_ref does not point at HEAD $head"; fi ;;
53
+ esac ;;
47
54
  *) refuse "refusing to push $remote_ref" ;;
48
55
  esac
49
56
  done
@@ -0,0 +1,3 @@
1
+ "redis:create": "docker create --name {{project}}-dev-redis -p 127.0.0.1:6379:6379 -v {{project}}-dev-redis-data:/data {{redisImage}} redis-server --appendonly yes",
2
+ "redis:start": "docker start {{project}}-dev-redis",
3
+ "redis:stop": "docker stop {{project}}-dev-redis",
@@ -7,6 +7,7 @@
7
7
  "test": "cd be && jest --selectProjects unit --coverage",
8
8
  "test:integration": "npm run typecheck:tests && cd be && jest --selectProjects integration",
9
9
  {{> app/package-scripts/supabase.json}}
10
+ {{backingScripts}}
10
11
  "contract:emit": "{{appBinary}} emit"
11
12
  }
12
13
  }