@starci/hfs 4.3.0 → 4.4.0

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 (515) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/db/migrate.mjs +361 -0
  3. package/db/post-cutover.mjs +136 -0
  4. package/emit/db-types.mjs +10 -1
  5. package/lint/run.mjs +1 -1
  6. package/package.json +4 -1
  7. package/release/gate.mjs +298 -0
  8. package/release/index.mjs +103 -0
  9. package/release/push.mjs +110 -0
  10. package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
  11. package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
  12. package/runtime/knowledge/hfs/rules.yaml +3401 -152
  13. package/runtime/knowledge/hfs/slots.yaml +344 -62
  14. package/runtime/knowledge/patterns/be/api.yaml +32 -5
  15. package/runtime/knowledge/patterns/be/cli.yaml +14 -4
  16. package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
  17. package/runtime/knowledge/patterns/be/jobs.yaml +20 -2
  18. package/runtime/knowledge/patterns/be/projections.yaml +4 -2
  19. package/runtime/knowledge/patterns/be/queues.yaml +11 -3
  20. package/runtime/knowledge/patterns/be/reactors.yaml +20 -1
  21. package/runtime/knowledge/patterns/be/realtime.yaml +25 -3
  22. package/runtime/knowledge/patterns/be/saga.yaml +20 -3
  23. package/runtime/knowledge/patterns/be/webhooks.yaml +23 -4
  24. package/runtime/knowledge/patterns/fe/folder.yaml +1 -0
  25. package/runtime/knowledge/sonar-gate.yaml +1 -1
  26. package/runtime/modules/kernel/failure-codes.yaml +3113 -43
  27. package/runtime/modules/models/runtimes.yaml +1 -1
  28. package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
  29. package/runtime/packages/cli/src/catalog.generated.mjs +14104 -0
  30. package/runtime/packages/cli/src/validate-args.mjs +213 -0
  31. package/runtime/scripts/api/git/add.mjs +5 -0
  32. package/runtime/scripts/api/git/init.mjs +7 -0
  33. package/runtime/scripts/api/git/lib.mjs +3 -0
  34. package/runtime/scripts/api/git/tag.mjs +5 -0
  35. package/runtime/scripts/api/node/lib.mjs +20 -0
  36. package/runtime/scripts/api/node/run-node.mjs +6 -0
  37. package/runtime/scripts/api/sops/lib.mjs +41 -0
  38. package/runtime/scripts/api/sops/selected-age-recipient.mjs +7 -0
  39. package/runtime/scripts/api/supabase/database-dump.mjs +5 -0
  40. package/runtime/scripts/api/supabase/database-query.mjs +5 -0
  41. package/runtime/scripts/api/supabase/lib.mjs +44 -0
  42. package/runtime/scripts/api/supabase/migration-repair.mjs +5 -0
  43. package/runtime/scripts/hfs/app-wiring.mjs +201 -0
  44. package/runtime/scripts/hfs/architecture/backend-app-files.mjs +1 -8
  45. package/runtime/scripts/hfs/architecture/backend.mjs +2 -7
  46. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
  47. package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
  48. package/runtime/scripts/hfs/architecture/dead-exports.mjs +9 -41
  49. package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
  50. package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
  51. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
  52. package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
  53. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +28 -13
  54. package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
  55. package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
  56. package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
  57. package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
  58. package/runtime/scripts/hfs/architecture/register-once.mjs +2 -2
  59. package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
  60. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +211 -11
  61. package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -1
  62. package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
  63. package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
  64. package/runtime/scripts/hfs/architecture/tiers.mjs +3 -3
  65. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +8 -2
  66. package/runtime/scripts/hfs/check-results.mjs +28 -0
  67. package/runtime/scripts/hfs/check.mjs +46 -47
  68. package/runtime/scripts/hfs/coverage-from.mjs +70 -0
  69. package/runtime/scripts/hfs/db-baseline-verb.mjs +276 -0
  70. package/runtime/scripts/hfs/db-plan-verb.mjs +254 -0
  71. package/runtime/scripts/hfs/db-restamp-verb.mjs +60 -0
  72. package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
  73. package/runtime/scripts/hfs/doc-commands.mjs +367 -0
  74. package/runtime/scripts/hfs/edition-slots.mjs +2 -2
  75. package/runtime/scripts/hfs/example-secrets.mjs +158 -0
  76. package/runtime/scripts/hfs/manifest-shape.mjs +67 -7
  77. package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
  78. package/runtime/scripts/hfs/rule-catalog.mjs +156 -11
  79. package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
  80. package/runtime/scripts/hfs/rules/a15-findings.mjs +88 -0
  81. package/runtime/scripts/hfs/rules/app-env-required.mjs +20 -0
  82. package/runtime/scripts/hfs/rules/app-root.mjs +2 -2
  83. package/runtime/scripts/hfs/rules/assets.mjs +79 -0
  84. package/runtime/scripts/hfs/rules/boot-proof.mjs +16 -0
  85. package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +59 -0
  86. package/runtime/scripts/hfs/rules/contract.mjs +66 -5
  87. package/runtime/scripts/hfs/rules/coverage-below.mjs +230 -0
  88. package/runtime/scripts/hfs/rules/database-access.mjs +343 -0
  89. package/runtime/scripts/hfs/rules/database-baseline.mjs +142 -0
  90. package/runtime/scripts/hfs/rules/database-config.mjs +250 -2
  91. package/runtime/scripts/hfs/rules/database-constants.mjs +9 -0
  92. package/runtime/scripts/hfs/rules/database-migrations.mjs +286 -7
  93. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +119 -9
  94. package/runtime/scripts/hfs/rules/database-queue-sources.mjs +176 -0
  95. package/runtime/scripts/hfs/rules/database-queues.mjs +361 -0
  96. package/runtime/scripts/hfs/rules/database-sql.mjs +91 -9
  97. package/runtime/scripts/hfs/rules/database.mjs +9 -0
  98. package/runtime/scripts/hfs/rules/deploy-scan.mjs +192 -0
  99. package/runtime/scripts/hfs/rules/deploy-workflow.mjs +219 -0
  100. package/runtime/scripts/hfs/rules/doc-fresh.mjs +286 -0
  101. package/runtime/scripts/hfs/rules/docker-health.mjs +249 -0
  102. package/runtime/scripts/hfs/rules/docker.mjs +24 -7
  103. package/runtime/scripts/hfs/rules/edition.mjs +9 -8
  104. package/runtime/scripts/hfs/rules/env-parity.mjs +373 -0
  105. package/runtime/scripts/hfs/rules/env-value-shape.mjs +113 -0
  106. package/runtime/scripts/hfs/rules/event-bus.mjs +3 -2
  107. package/runtime/scripts/hfs/rules/event-tables.mjs +100 -0
  108. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +115 -15
  109. package/runtime/scripts/hfs/rules/fe-stack.mjs +57 -0
  110. package/runtime/scripts/hfs/rules/feature-files.mjs +28 -0
  111. package/runtime/scripts/hfs/rules/frontend-tree.mjs +54 -35
  112. package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -2
  113. package/runtime/scripts/hfs/rules/lexicon.mjs +62 -0
  114. package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +89 -0
  115. package/runtime/scripts/hfs/rules/monorepo.mjs +14 -1
  116. package/runtime/scripts/hfs/rules/pipeline.mjs +8 -1
  117. package/runtime/scripts/hfs/rules/release-provenance.mjs +211 -0
  118. package/runtime/scripts/hfs/rules/saga-delivery.mjs +66 -0
  119. package/runtime/scripts/hfs/rules/saga.mjs +8 -16
  120. package/runtime/scripts/hfs/rules/script-hygiene.mjs +151 -0
  121. package/runtime/scripts/hfs/rules/secret-custody-only.mjs +65 -0
  122. package/runtime/scripts/hfs/rules/secrets.mjs +13 -0
  123. package/runtime/scripts/hfs/rules/services.mjs +4 -4
  124. package/runtime/scripts/hfs/rules/stack-tooling.mjs +93 -0
  125. package/runtime/scripts/hfs/rules/stacks-deploy.mjs +76 -0
  126. package/runtime/scripts/hfs/rules/stacks-ingress.mjs +87 -0
  127. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -0
  128. package/runtime/scripts/hfs/rules/stacks-release.mjs +366 -0
  129. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +41 -1
  130. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
  131. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +1 -1
  132. package/runtime/scripts/hfs/rules/supply-chain.mjs +167 -0
  133. package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +101 -0
  134. package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +77 -0
  135. package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +165 -0
  136. package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +103 -0
  137. package/runtime/scripts/hfs/rules/unused-symbol.mjs +320 -0
  138. package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +185 -0
  139. package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +19 -0
  140. package/runtime/scripts/hfs/sealed-members.mjs +101 -0
  141. package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +200 -0
  142. package/runtime/scripts/hfs/secret-edit.mjs +170 -0
  143. package/runtime/scripts/hfs/secret-init.mjs +127 -19
  144. package/runtime/scripts/hfs/secret-sops.mjs +38 -6
  145. package/runtime/scripts/hfs/secret-store.mjs +56 -54
  146. package/runtime/scripts/hfs/secret.mjs +34 -5
  147. package/runtime/scripts/hfs/slot-imports.mjs +18 -5
  148. package/runtime/scripts/hfs/slots.mjs +1 -0
  149. package/runtime/scripts/hfs/stack-config-kit.mjs +34 -0
  150. package/runtime/scripts/hfs/stack-deploy-config.mjs +313 -0
  151. package/runtime/scripts/hfs/stack-ingress-config.mjs +237 -0
  152. package/runtime/scripts/hfs/stacks-gate.mjs +414 -0
  153. package/runtime/scripts/hfs/starcistacks-runtime.mjs +61 -0
  154. package/runtime/scripts/lib/command-tokens.mjs +126 -0
  155. package/runtime/scripts/lib/compose-values.mjs +11 -0
  156. package/runtime/scripts/lib/deployment-findings.mjs +7 -0
  157. package/runtime/scripts/lib/json-clone.mjs +61 -0
  158. package/runtime/scripts/lib/list.mjs +4 -0
  159. package/runtime/scripts/lib/runtime-example.mjs +13 -0
  160. package/runtime/scripts/lib/secret-patterns.mjs +60 -5
  161. package/runtime/scripts/lib/sql-ast.mjs +32 -0
  162. package/runtime/scripts/lib/stack-services.mjs +1 -1
  163. package/runtime/scripts/lib/swarm-stack.mjs +5 -18
  164. package/runtime/scripts/lib/trim-end.mjs +8 -0
  165. package/runtime/scripts/lib/ts-analysis.mjs +5 -2
  166. package/runtime/scripts/lib/ts-ast.mjs +27 -2
  167. package/runtime/scripts/lib/workflow-events.mjs +8 -0
  168. package/scaffold/add-app.mjs +77 -59
  169. package/scaffold/add-cli-lite.mjs +14 -6
  170. package/scaffold/add-compose.mjs +79 -0
  171. package/scaffold/add-database-lite.mjs +14 -0
  172. package/scaffold/add-fe-overlay.mjs +122 -0
  173. package/scaffold/add-fe-page.mjs +269 -0
  174. package/scaffold/add-inbox-lite.mjs +74 -42
  175. package/scaffold/add-table.mjs +110 -87
  176. package/scaffold/add.mjs +159 -44
  177. package/scaffold/app.mjs +124 -39
  178. package/scaffold/edition-gate.mjs +3 -3
  179. package/scaffold/lite-exports.mjs +2 -1
  180. package/scaffold/service.mjs +2 -2
  181. package/src/main.mjs +229 -116
  182. package/stack/age-install.mjs +37 -0
  183. package/stack/backup.mjs +100 -0
  184. package/stack/build-args.mjs +42 -0
  185. package/stack/build.mjs +173 -0
  186. package/stack/cli.mjs +130 -0
  187. package/stack/config.mjs +57 -0
  188. package/stack/context.mjs +57 -0
  189. package/stack/converge.mjs +107 -0
  190. package/stack/deploy.mjs +301 -0
  191. package/stack/digest.mjs +16 -0
  192. package/stack/docker-transport.mjs +53 -0
  193. package/stack/edge-alias.mjs +118 -0
  194. package/stack/errors.mjs +21 -0
  195. package/stack/image-scan.mjs +152 -0
  196. package/stack/ingress-create.mjs +114 -0
  197. package/stack/ingress-publish.mjs +273 -0
  198. package/stack/ingress.mjs +706 -0
  199. package/stack/jobs.mjs +51 -0
  200. package/stack/live-restore.mjs +177 -0
  201. package/stack/prepare.mjs +266 -0
  202. package/stack/private-file.mjs +22 -0
  203. package/stack/provision.mjs +90 -0
  204. package/stack/prune.mjs +46 -0
  205. package/stack/release-store.mjs +87 -0
  206. package/stack/remote.mjs +205 -0
  207. package/stack/render.mjs +149 -0
  208. package/stack/retire.mjs +47 -0
  209. package/stack/sealed.mjs +149 -0
  210. package/stack/task.mjs +42 -0
  211. package/stack/verify.mjs +99 -0
  212. package/sync/ci-commands.mjs +13 -0
  213. package/sync/index.mjs +108 -52
  214. package/sync/stack-images.mjs +127 -0
  215. package/templates/app/ci-workflows/github/workflows/ci.yml +5 -1
  216. package/templates/app/ci-workflows/github/workflows/images.yml +15 -3
  217. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +148 -0
  218. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +7 -3
  219. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +38 -6
  220. package/templates/app/ci-workflows-lite/github/workflows/images.yml +14 -2
  221. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +148 -0
  222. package/templates/app/codegen-graphql.mjs.partial +13 -2
  223. package/templates/app/codegen-lite/scripts/codegen.mjs +6 -1
  224. package/templates/app/codegen-prelude.mjs.partial +8 -0
  225. package/templates/app/gitignore +3 -0
  226. package/templates/app/gitignore-lite +3 -0
  227. package/templates/app/package-scripts/package.json +1 -0
  228. package/templates/app/package-scripts/supabase.json +7 -0
  229. package/templates/app/package-scripts-lite/package.json +1 -7
  230. package/templates/app/skeleton/.editorconfig +1 -0
  231. package/templates/app/skeleton/.gitattributes +7 -1
  232. package/templates/app/skeleton/.github/dependabot.yml +17 -0
  233. package/templates/app/skeleton/.nvmrc +1 -1
  234. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +134 -1
  235. package/templates/app/skeleton/.starcistacks/dev/README.md +16 -0
  236. package/templates/app/skeleton/.starcistacks/dev/environment.json +5 -0
  237. package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +71 -0
  238. package/templates/app/skeleton/.starcistacks/dev/runtime/env/KEYS.md +14 -0
  239. package/templates/app/skeleton/.starcistacks/production/README.md +37 -0
  240. package/templates/app/skeleton/.starcistacks/production/environment.json +5 -0
  241. package/templates/app/skeleton/.starcistacks/production/infra/swarm/Caddyfile +15 -0
  242. package/templates/app/skeleton/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  243. package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  244. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  245. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +9 -0
  246. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  247. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +142 -0
  248. package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +14 -0
  249. package/templates/app/skeleton/README.md +6 -6
  250. package/templates/app/skeleton/scripts/codegen.mjs +1 -2
  251. package/templates/app/skeleton-lite/.editorconfig +16 -0
  252. package/templates/app/skeleton-lite/.gitattributes +8 -0
  253. package/templates/app/skeleton-lite/.github/dependabot.yml +17 -0
  254. package/templates/app/skeleton-lite/.nvmrc +1 -0
  255. package/templates/app/skeleton-lite/.sops.yaml +3 -2
  256. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +178 -1
  257. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +20 -0
  258. package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +2 -1
  259. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +16 -0
  260. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +11 -0
  261. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +7 -0
  262. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +6 -0
  263. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +8 -0
  264. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +6 -0
  265. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +120 -0
  266. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -0
  267. package/templates/app/skeleton-lite/.starcistacks/production/README.md +38 -0
  268. package/templates/app/skeleton-lite/.starcistacks/production/environment.json +6 -0
  269. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/Caddyfile +11 -0
  270. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  271. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  272. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  273. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/images.lock.yaml +8 -0
  274. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  275. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +121 -0
  276. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +21 -0
  277. package/templates/app/skeleton-lite/.starciwork/brand/index.yaml +12 -0
  278. package/templates/app/skeleton-lite/README.md +5 -3
  279. package/templates/app/skeleton-lite/supabase/config.toml +8 -1
  280. package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +15 -6
  281. package/templates/app/upgrade-full/.starcistacks/dev/README.md +13 -7
  282. package/templates/be/image/worker/Dockerfile +1 -1
  283. package/templates/be/inbox/database-client.spec.ts.tpl +17 -18
  284. package/templates/be/inbox/database-client.ts.tpl +12 -11
  285. package/templates/be/inbox/delivery-recorded.event.ts.tpl +36 -0
  286. package/templates/be/inbox/event-index.ts.tpl +1 -0
  287. package/templates/be/inbox/events.json.tpl +10 -0
  288. package/templates/be/inbox/migration.sql.tpl +1 -0
  289. package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +42 -0
  290. package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +9 -0
  291. package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +6 -0
  292. package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +19 -0
  293. package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +2 -0
  294. package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +16 -0
  295. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +45 -0
  296. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +15 -0
  297. package/templates/be/inbox/platform/event-bus/index.ts.tpl +5 -0
  298. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +5 -0
  299. package/templates/be/inbox/platform/inbox/inbox.decorators.ts.tpl +9 -0
  300. package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +6 -0
  301. package/templates/be/inbox/platform/inbox/inbox.module.ts.tpl +19 -0
  302. package/templates/be/inbox/platform/inbox/inbox.options.ts.tpl +2 -0
  303. package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +10 -0
  304. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +41 -0
  305. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +19 -0
  306. package/templates/be/inbox/platform/inbox/index.ts.tpl +3 -0
  307. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +7 -0
  308. package/templates/be/inbox/platform/migration.sql.tpl +48 -0
  309. package/templates/be/inbox/service.spec.ts.tpl +42 -9
  310. package/templates/be/inbox/service.ts.tpl +23 -8
  311. package/templates/be/inbox/webhook-door.spec.ts.tpl +1 -1
  312. package/templates/be/inbox/webhook-door.ts.tpl +2 -2
  313. package/templates/be/patterns/api/feature.module-definition.ts.tpl +6 -0
  314. package/templates/be/patterns/api/feature.module.ts.tpl +2 -1
  315. package/templates/be/patterns/api/feature.options.ts.tpl +2 -0
  316. package/templates/be/patterns/api/graphql.module-definition.ts.tpl +6 -0
  317. package/templates/be/patterns/api/graphql.module.ts.tpl +3 -2
  318. package/templates/be/patterns/api/graphql.options.ts.tpl +2 -0
  319. package/templates/be/patterns/cli/group.module-definition.ts.tpl +6 -0
  320. package/templates/be/patterns/cli/group.module.ts.tpl +2 -1
  321. package/templates/be/patterns/cli/group.options.ts.tpl +2 -0
  322. package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +0 -3
  323. package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +0 -2
  324. package/templates/be/patterns/jobs/job.module-definition.ts.tpl +6 -0
  325. package/templates/be/patterns/jobs/job.module.ts.tpl +2 -1
  326. package/templates/be/patterns/jobs/job.options.ts.tpl +2 -0
  327. package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +6 -0
  328. package/templates/be/patterns/jobs/job.queue.module.ts.tpl +6 -3
  329. package/templates/be/patterns/jobs/job.queue.options.ts.tpl +2 -0
  330. package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +3 -1
  331. package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +25 -9
  332. package/templates/be/patterns/queues/platform/queue.config.ts.tpl +1 -1
  333. package/templates/be/patterns/queues/platform/queue.index.ts.tpl +1 -1
  334. package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +2 -2
  335. package/templates/be/patterns/queues/platform/queue.module.ts.tpl +11 -0
  336. package/templates/be/patterns/queues/platform/queue.options.ts.tpl +10 -0
  337. package/templates/be/patterns/queues/queue.module-definition.ts.tpl +6 -0
  338. package/templates/be/patterns/queues/queue.module.ts.tpl +2 -1
  339. package/templates/be/patterns/queues/queue.options.ts.tpl +2 -0
  340. package/templates/be/patterns/reactors/message.module-definition.ts.tpl +6 -0
  341. package/templates/be/patterns/reactors/message.module.ts.tpl +6 -3
  342. package/templates/be/patterns/reactors/message.options.ts.tpl +2 -0
  343. package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +6 -0
  344. package/templates/be/patterns/reactors/reactor.module.ts.tpl +2 -1
  345. package/templates/be/patterns/reactors/reactor.options.ts.tpl +2 -0
  346. package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +188 -0
  347. package/templates/be/patterns/realtime/channel.gateway.ts.tpl +22 -0
  348. package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +6 -0
  349. package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +2 -1
  350. package/templates/be/patterns/realtime/channel.graphql.options.ts.tpl +2 -0
  351. package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +6 -0
  352. package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +110 -0
  353. package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +7 -0
  354. package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +6 -0
  355. package/templates/be/patterns/saga/saga.message.module.ts.tpl +6 -3
  356. package/templates/be/patterns/saga/saga.message.options.ts.tpl +2 -0
  357. package/templates/be/patterns/saga/saga.module-definition.ts.tpl +6 -0
  358. package/templates/be/patterns/saga/saga.module.ts.tpl +2 -1
  359. package/templates/be/patterns/saga/saga.options.ts.tpl +2 -0
  360. package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +6 -0
  361. package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +2 -1
  362. package/templates/be/patterns/webhooks/webhook.http.options.ts.tpl +2 -0
  363. package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +80 -26
  364. package/templates/be/platform/graphql/localize-errors.mapper.ts +2 -5
  365. package/templates/be/skeleton/apps/__app__/src/app.module.ts +11 -9
  366. package/templates/be/skeleton/apps/__app__/src/compose/api.ts +4 -0
  367. package/templates/be/skeleton/apps/__app__/src/compose/domain.ts +11 -0
  368. package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +2 -0
  369. package/templates/be/skeleton/apps/__app__/src/compose/system.ts +5 -0
  370. package/templates/be/skeleton/apps/__app__/src/compose/webhooks.ts +4 -0
  371. package/templates/be/skeleton/apps/__app__/src/main.ts +15 -1
  372. package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
  373. package/templates/be/skeleton/apps/cli/src/cli.options.ts +6 -14
  374. package/templates/be/skeleton/apps/cli/src/compose/cli.ts +5 -0
  375. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +24 -0
  376. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.contracts.ts +2 -0
  377. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.handler.ts +22 -0
  378. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.query.ts +11 -0
  379. package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +6 -0
  380. package/templates/be/skeleton/src/features/api/system-health/system-health.module.ts +5 -3
  381. package/templates/be/skeleton/src/features/api/system-health/system-health.options.ts +2 -0
  382. package/templates/be/skeleton/src/features/api/system-health/transport/http/ready.controller.ts +19 -0
  383. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +6 -0
  384. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module.ts +8 -3
  385. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.options.ts +2 -0
  386. package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +6 -0
  387. package/templates/be/skeleton/src/features/cli/cli.module.ts +3 -2
  388. package/templates/be/skeleton/src/features/cli/cli.options.ts +2 -0
  389. package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +6 -0
  390. package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +2 -1
  391. package/templates/be/skeleton/src/features/cli/migrate/migrate.options.ts +2 -0
  392. package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +6 -0
  393. package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +2 -1
  394. package/templates/be/skeleton/src/features/cli/seed/seed.options.ts +2 -0
  395. package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +5 -6
  396. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +56 -4
  397. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +36 -5
  398. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +39 -8
  399. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +14 -8
  400. package/templates/be/skeleton/src/modules/domain/identity/credential.contracts.ts +26 -0
  401. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +119 -0
  402. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +43 -0
  403. package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +16 -0
  404. package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -1
  405. package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +22 -4
  406. package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +8 -2
  407. package/templates/be/skeleton/src/modules/domain/identity/index.ts +1 -0
  408. package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +11 -4
  409. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.spec.ts +32 -0
  410. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.ts +14 -0
  411. package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
  412. package/templates/be/skeleton/src/modules/platform/database/index.ts +2 -0
  413. package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +8 -0
  414. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +25 -6
  415. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +16 -6
  416. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +130 -0
  417. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +58 -0
  418. package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +3 -0
  419. package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -4
  420. package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -10
  421. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +3 -1
  422. package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +2 -0
  423. package/templates/be/skeleton/src/modules/platform/http-security/index.ts +5 -0
  424. package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +12 -5
  425. package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +13 -8
  426. package/templates/be/skeleton/src/modules/platform/http-security/readiness.contracts.ts +5 -0
  427. package/templates/be/skeleton/src/modules/platform/http-security/readiness.decorators.ts +7 -0
  428. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +86 -0
  429. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +39 -0
  430. package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +139 -131
  431. package/templates/be/skeleton/src/modules/platform/i18n/index.ts +1 -1
  432. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +91 -67
  433. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +5 -4
  434. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +0 -2
  435. package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +20 -6
  436. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +16 -4
  437. package/templates/be/skeleton/src/modules/platform/retry/index.ts +3 -0
  438. package/templates/be/skeleton/src/modules/platform/retry/retry.decorators.ts +9 -0
  439. package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +7 -0
  440. package/templates/be/skeleton/src/modules/platform/retry/retry.module.ts +19 -0
  441. package/templates/be/skeleton/src/modules/platform/retry/retry.options.ts +2 -0
  442. package/templates/be/skeleton/src/modules/platform/retry/retry.port.ts +11 -0
  443. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.spec.ts +36 -0
  444. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.ts +10 -0
  445. package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +54 -0
  446. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +14 -18
  447. package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +4 -0
  448. package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +10 -0
  449. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +16 -0
  450. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +2 -0
  451. package/templates/be/skeleton-lite/apps/api/src/compose/system.ts +5 -0
  452. package/templates/be/skeleton-lite/apps/api/src/compose/webhooks.ts +4 -0
  453. package/templates/be/skeleton-lite/apps/api/src/main.ts +2 -3
  454. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +12 -0
  455. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +71 -5
  456. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +29 -2
  457. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +68 -15
  458. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +14 -9
  459. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +9 -2
  460. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +16 -1
  461. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +19 -4
  462. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +5 -1
  463. package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -1
  464. package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -3
  465. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +17 -5
  466. package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +24 -13
  467. package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +5 -0
  468. package/templates/be/skeleton-lite/src/modules/platform/database/errors/database.error.ts +16 -0
  469. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +3 -0
  470. package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +5 -2
  471. package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +4 -1
  472. package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
  473. package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +7 -0
  474. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +29 -0
  475. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +16 -0
  476. package/templates/be/table/api/feature.module-definition.ts.tpl +6 -0
  477. package/templates/be/table/api/feature.module.ts.tpl +2 -1
  478. package/templates/be/table/api/feature.options.ts.tpl +2 -0
  479. package/templates/be/table/api/graphql.module-definition.ts.tpl +6 -0
  480. package/templates/be/table/api/graphql.module.ts.tpl +3 -2
  481. package/templates/be/table/api/graphql.options.ts.tpl +2 -0
  482. package/templates/be/table/migration.sql.tpl +1 -0
  483. package/templates/be/upgrade-full/apps/cli/src/app.module.ts +4 -6
  484. package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +7 -0
  485. package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +5 -0
  486. package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +9 -0
  487. package/templates/be/upgrade-full/apps/cli/src/main.ts +3 -0
  488. package/templates/be/upgrade-full/src/features/cli/cli.module.ts +3 -2
  489. package/templates/fe/auth/supabase-client.ts.tpl +34 -0
  490. package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +11 -0
  491. package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/index.tsx +6 -1
  492. package/templates/fe/skeleton/apps/__app__/src/components/leaves/Icon/index.tsx +4 -0
  493. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/component.tsx +31 -0
  494. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/index.tsx +24 -0
  495. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -1
  496. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +3 -4
  497. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/index.ts +1 -0
  498. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +15 -0
  499. package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +18 -0
  500. package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +19 -0
  501. package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +7 -1
  502. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/en.json +57 -0
  503. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +6 -0
  504. package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/en.json +31 -0
  505. package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +1 -1
  506. package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -0
  507. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +1 -34
  508. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +7 -1
  509. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +57 -0
  510. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +30 -24
  511. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +1 -1
  512. package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
  513. package/templates/fe/transport/graphql/clients/links/locale.ts +2 -2
  514. package/upgrade/check.mjs +3 -1
  515. package/upgrade/index.mjs +53 -40
@@ -0,0 +1,298 @@
1
+ // gate.mjs - `starci app release gate`: the honest release gate. It runs the app's own gates on a clean HEAD and ONLY when every one is green it
2
+ // writes the L4 record <git-common-dir>/starci-release/<HEAD sha>.l4.json (head, tag, per-gate log path and sha256) and creates the annotated tag.
3
+ // The canon pre-push hook lets main and v* tags through only for a HEAD with that tag and that record. Nothing is pushed here (see push.mjs).
4
+ // The gate list comes from the optional `release:` block of .starcistacks/application-stacks.yaml, else from the app's managed package scripts.
5
+ // Commands are argument arrays (never shell strings) run through ctx.spawn; a git tool call always goes through the same seam.
6
+ import { createHash } from 'node:crypto';
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { isPlainObject as plain } from '../runtime/engine/plain-object.mjs';
10
+ import { firstLine, lines, tool } from '../stack/context.mjs';
11
+ import { failure, refusal } from '../stack/errors.mjs';
12
+
13
+ export const RELEASE_CODE = 'RELEASE_CONFIG_INVALID';
14
+
15
+ const FIELDS = new Set(['gates', 'tagPrefix', 'defaultBranch', 'tagMatchesVersion']);
16
+ const GATE_FIELDS = new Set(['name', 'command', 'summary', 'env']);
17
+ const GATE_NAME = /^[a-z][a-z0-9-]*$/u;
18
+ const TOKEN = /^[A-Za-z0-9_@%+=:,./\\-]+$/u;
19
+ const ENV_KEY = /^[A-Z][A-Z0-9_]*$/u;
20
+ const REF_NAME = /^[A-Za-z0-9._/][A-Za-z0-9._/-]*$/u;
21
+ const PREFIX = /^[A-Za-z][A-Za-z0-9._-]{0,19}$/u;
22
+ export const VERSION = /^(\d+)\.(\d+)\.(\d+)$/u;
23
+ const THRESHOLD_MISS = /coverage threshold .* not met/gu;
24
+ const WRAPPED = new Set(['npm', 'npx']);
25
+ const SCRIPT_GATES = ['typecheck', 'build:be', 'lint', 'build:fe', 'test'];
26
+ const MAX_PATCH = 100;
27
+
28
+ /** True for a branch or remote name git can take as one argument (no leading dash, only path-like characters). */
29
+ export const refName = (value) => typeof value === 'string' && REF_NAME.test(value);
30
+
31
+ const scriptGate = (script) => ({
32
+ name: script.replaceAll(':', '-'),
33
+ command: ['npm', 'run', script],
34
+ summary: script === 'test' ? 'jest' : null,
35
+ env: {},
36
+ });
37
+
38
+ /** The gates derived from the managed package scripts: the app check first, then every script the sync renders that the app has. */
39
+ export function defaultGates(scripts) {
40
+ const present = SCRIPT_GATES.filter((script) => typeof scripts?.[script] === 'string');
41
+ return [{ name: 'app-check', command: ['npx', 'starci', 'app', 'check'], summary: null, env: {} }, ...present.map(scriptGate)];
42
+ }
43
+
44
+ function gateProblems(entry, at, seen) {
45
+ const found = [];
46
+ for (const key of Object.keys(entry)) if (!GATE_FIELDS.has(key)) found.push(`${at}.${key}: is not a field of a gate`);
47
+ if (typeof entry.name !== 'string' || !GATE_NAME.test(entry.name)) found.push(`${at}.name: must match ${GATE_NAME}`);
48
+ else if (seen.has(entry.name)) found.push(`${at}.name: ${entry.name} is declared twice`);
49
+ const command = entry.command;
50
+ if (!Array.isArray(command) || command.length === 0 || !command.every((word) => typeof word === 'string' && TOKEN.test(word))) found.push(`${at}.command: must be a non-empty list of plain words (no shell strings)`);
51
+ if (entry.summary !== undefined && entry.summary !== 'jest') found.push(`${at}.summary: must be jest`);
52
+ const env = entry.env ?? {};
53
+ if (!plain(env) || !Object.entries(env).every(([key, value]) => ENV_KEY.test(key) && typeof value === 'string')) found.push(`${at}.env: must map UPPER_CASE names to string values`);
54
+ return found;
55
+ }
56
+
57
+ function gatesOf(list, problems) {
58
+ if (!Array.isArray(list) || list.length === 0) {
59
+ problems.push('release.gates: must be a non-empty list');
60
+ return [];
61
+ }
62
+ const seen = new Set();
63
+ const gates = [];
64
+ list.forEach((entry, index) => {
65
+ const at = `release.gates[${index}]`;
66
+ const found = plain(entry) ? gateProblems(entry, at, seen) : [`${at}: must be a mapping`];
67
+ problems.push(...found);
68
+ if (found.length === 0) {
69
+ seen.add(entry.name);
70
+ gates.push({ name: entry.name, command: entry.command, summary: entry.summary ?? null, env: entry.env ?? {} });
71
+ }
72
+ });
73
+ return gates;
74
+ }
75
+
76
+ /** Judges the optional `release:` block. Absence is fine (defaults). Returns { config } or { problems }. */
77
+ export function normalizeRelease(block, scripts = {}) {
78
+ const raw = block ?? {};
79
+ if (!plain(raw)) return { problems: ['release: must be a mapping'] };
80
+ const problems = Object.keys(raw).filter((key) => !FIELDS.has(key)).map((key) => `release.${key}: is not a field of the release block`);
81
+ const tagPrefix = raw.tagPrefix ?? 'v';
82
+ const defaultBranch = raw.defaultBranch ?? 'main';
83
+ const tagMatchesVersion = raw.tagMatchesVersion ?? false;
84
+ if (typeof tagPrefix !== 'string' || !PREFIX.test(tagPrefix)) problems.push(`release.tagPrefix: must match ${PREFIX}`);
85
+ if (!refName(defaultBranch)) problems.push(`release.defaultBranch: must match ${REF_NAME}`);
86
+ if (typeof tagMatchesVersion !== 'boolean') problems.push('release.tagMatchesVersion: must be true or false');
87
+ const gates = raw.gates === undefined ? defaultGates(scripts) : gatesOf(raw.gates, problems);
88
+ return problems.length === 0 ? { config: { gates, tagPrefix, defaultBranch, tagMatchesVersion } } : { problems };
89
+ }
90
+
91
+ /* ---- git through the seam */
92
+
93
+ /** Runs git in the app root; a missing git or (unless allowFail) a non-zero exit is a failure whose message names the first stderr line. */
94
+ export function git(ctx, args, { allowFail = false } = {}) {
95
+ const run = tool(ctx, 'git', args, { cwd: ctx.cwd });
96
+ if (run.error) throw failure('RELEASE_GIT_MISSING', `cannot run git (${run.error.message})`);
97
+ if (run.status !== 0 && !allowFail) throw failure('RELEASE_GIT_FAILED', `git ${args.slice(0, 2).join(' ')}: ${firstLine(run.stderr)}`);
98
+ return { code: run.status, stdout: String(run.stdout ?? '').trim(), stderr: String(run.stderr ?? '') };
99
+ }
100
+
101
+ /** The directory that holds the L4 records: <git common dir>/starci-release. */
102
+ export const releaseDir = (ctx) => path.join(path.resolve(ctx.cwd, git(ctx, ['rev-parse', '--git-common-dir']).stdout), 'starci-release');
103
+
104
+ /** Annotated tags `<prefix><digit>*` that point at HEAD (lightweight tags do not count: the hook ignores them). */
105
+ export function annotatedOnHead(ctx, prefix) {
106
+ const names = lines(git(ctx, ['tag', '--points-at', 'HEAD', '--list', `${prefix}[0-9]*`]).stdout);
107
+ return names.filter((name) => git(ctx, ['cat-file', '-t', `refs/tags/${name}`], { allowFail: true }).stdout === 'tag');
108
+ }
109
+
110
+ const dirtyPaths = (ctx) => lines(git(ctx, ['status', '--porcelain', '--untracked-files=normal']).stdout);
111
+
112
+ /** Tag names that exist locally or on the remote. */
113
+ function takenTags(ctx, prefix, remote) {
114
+ const local = lines(git(ctx, ['tag', '--list', `${prefix}*`]).stdout);
115
+ const listed = lines(git(ctx, ['ls-remote', '--tags', remote], { allowFail: true }).stdout);
116
+ const remoteNames = listed.map((line) => line.split('refs/tags/')[1]?.replace(/\^\{\}$/u, '')).filter(Boolean);
117
+ return new Set([...local, ...remoteNames]);
118
+ }
119
+
120
+ /** Next free `<prefix><major>.<minor>.<n>` for the package version. */
121
+ function nextFree(prefix, version, taken) {
122
+ const [, major, minor] = VERSION.exec(version);
123
+ for (let patch = 0; patch < MAX_PATCH; patch += 1) {
124
+ const candidate = `${prefix}${major}.${minor}.${patch}`;
125
+ if (!taken.has(candidate)) return candidate;
126
+ }
127
+ throw refusal('RELEASE_NO_FREE_TAG', `no free tag ${prefix}${major}.${minor}.0 to ${prefix}${major}.${minor}.${MAX_PATCH - 1}`);
128
+ }
129
+
130
+ /** The tag this release uses and its state: on-head (reused), free (will be created) or taken (exists on another commit). */
131
+ export function chooseTag(ctx, config, { prefix, remote }) {
132
+ const onHead = annotatedOnHead(ctx, prefix);
133
+ const taken = takenTags(ctx, prefix, remote);
134
+ const exact = `${prefix}${config.version}`;
135
+ const tag = config.tagMatchesVersion ? exact : (onHead[0] ?? nextFree(prefix, config.version, taken));
136
+ if (onHead.includes(tag)) return { tag, state: 'on-head' };
137
+ return { tag, state: taken.has(tag) ? 'taken' : 'free' };
138
+ }
139
+
140
+ /* ---- gate results */
141
+
142
+ const sha256 = (file) => createHash('sha256').update(fs.readFileSync(file)).digest('hex');
143
+
144
+ const lineAfter = (text, label) => {
145
+ const found = text.split('\n').find((line) => line.startsWith(label));
146
+ return found === undefined ? null : found.slice(label.length).trim();
147
+ };
148
+
149
+ /** The jest summary lines of a log: { suites, failedSuites, tests, failed, passed } or null when jest printed none. */
150
+ export function jestSummary(text) {
151
+ const suites = lineAfter(text, 'Test Suites:');
152
+ const tests = lineAfter(text, 'Tests:');
153
+ if (suites === null || tests === null) return null;
154
+ const count = (line, word) => Number(new RegExp(String.raw`(\d+) ${word}`, 'u').exec(line)?.[1] ?? 0);
155
+ return {
156
+ suites: count(suites, 'total'), failedSuites: count(suites, 'failed'),
157
+ tests: count(tests, 'total'), failed: count(tests, 'failed'), passed: count(tests, 'passed'),
158
+ };
159
+ }
160
+
161
+ /** Judges a jest-style gate from its log: green needs suites that all passed, every test passed and no coverage threshold miss. */
162
+ export function judgeJest(text) {
163
+ const summary = jestSummary(text);
164
+ const misses = (text.match(THRESHOLD_MISS) ?? []).length;
165
+ if (!summary) return { pass: false, detail: 'no jest summary in the log', summary: null, misses };
166
+ const pass = summary.suites > 0 && summary.failedSuites === 0 && summary.failed === 0 && summary.passed === summary.tests && misses === 0;
167
+ const detail = `suites ${summary.suites - summary.failedSuites}/${summary.suites}, tests ${summary.passed}/${summary.tests}, coverage-threshold misses ${misses}`;
168
+ return { pass, detail, summary, misses };
169
+ }
170
+
171
+ /** [command, args] to start an argv; on Windows npm and npx are .cmd files, so they start through cmd.exe (words are validated, never a shell string). */
172
+ export function launch(argv, platform) {
173
+ if (platform === 'win32' && WRAPPED.has(argv[0])) return ['cmd.exe', ['/d', '/s', '/c', ...argv]];
174
+ return [argv[0], argv.slice(1)];
175
+ }
176
+
177
+ function runGate(ctx, gate, { dir, platform }) {
178
+ const log = path.join(dir, `${gate.name}.log`);
179
+ const started = ctx.now();
180
+ const [command, args] = launch(gate.command, platform);
181
+ const run = tool(ctx, command, args, { cwd: ctx.cwd, env: { ...ctx.env, ...gate.env } });
182
+ const text = `${run.stdout ?? ''}${run.stderr ?? ''}`;
183
+ fs.writeFileSync(log, text);
184
+ const verdict = judgeVerdict(run, gate, text);
185
+ const seconds = Math.round((ctx.now() - started) / 1000);
186
+ return {
187
+ name: gate.name, command: gate.command.join(' '), pass: verdict.pass, detail: verdict.detail, seconds,
188
+ summary: verdict.summary ?? null, log: path.resolve(log), logSha256: sha256(log), env: Object.keys(gate.env),
189
+ };
190
+ }
191
+
192
+ function judgeVerdict(run, gate, text) {
193
+ if (run.error) return { pass: false, detail: `cannot run (${run.error.message})` };
194
+ if (run.status !== 0) return { pass: false, detail: `exit ${run.status}` };
195
+ if (gate.summary === 'jest') return judgeJest(text);
196
+ return { pass: true, detail: 'exit 0' };
197
+ }
198
+
199
+ /* ---- the gate */
200
+
201
+ function sourceProblem(ctx, source, head) {
202
+ if (source === 'HEAD') return null;
203
+ if (!refName(source)) return `--source ${source} is not a ref name`;
204
+ const resolved = git(ctx, ['rev-parse', '--verify', `${source}^{commit}`], { allowFail: true });
205
+ if (resolved.code !== 0) return `cannot resolve --source ${source}`;
206
+ if (resolved.stdout !== head) return `--source ${source} is ${resolved.stdout.slice(0, 10)} but HEAD is ${head.slice(0, 10)}: the hook and the L4 record are about HEAD; check the candidate out first`;
207
+ return null;
208
+ }
209
+
210
+ function tagLine({ tag, state }) {
211
+ if (state === 'on-head') return `tag ${tag} (annotated, already on HEAD)`;
212
+ if (state === 'taken') return `tag ${tag} (TAKEN by another commit)`;
213
+ return `tag ${tag} (free: will be created, annotated)`;
214
+ }
215
+
216
+ /** Everything that can be known without running a gate: the facts, the red preconditions and the lines that describe them. */
217
+ function inspect(ctx, config, options) {
218
+ const head = git(ctx, ['rev-parse', 'HEAD']).stdout;
219
+ const branch = git(ctx, ['rev-parse', '--abbrev-ref', 'HEAD'], { allowFail: true }).stdout;
220
+ const out = [`checkout ${ctx.cwd}`, `HEAD ${head} (${branch})`];
221
+ const red = [];
222
+ const problem = sourceProblem(ctx, options.source ?? 'HEAD', head);
223
+ if (problem) red.push(problem);
224
+ const dirty = dirtyPaths(ctx);
225
+ if (dirty.length > 0) red.push(`working tree is not clean (${dirty.length} path(s), first: ${dirty[0].slice(0, 100)}): the record would vouch for files that are not in HEAD`);
226
+ else out.push('worktree clean');
227
+ const prefix = options.tagPrefix ?? config.tagPrefix;
228
+ if (!PREFIX.test(prefix)) throw refusal('RELEASE_USAGE', `--tag-prefix must match ${PREFIX}`);
229
+ const chosen = chooseTag(ctx, config, { prefix, remote: 'origin' });
230
+ if (chosen.state === 'taken') red.push(`tag ${chosen.tag} already exists on another commit (local or remote)`);
231
+ out.push(tagLine(chosen));
232
+ return { head, chosen, red, out };
233
+ }
234
+
235
+ function planLines(config, facts, dir, recordPath) {
236
+ const out = [`L4 record ${recordPath}`, `logs ${dir}`, '', 'gates (all must pass, in this order):'];
237
+ for (const gate of config.gates) {
238
+ const summary = gate.summary ? ` (summary: ${gate.summary})` : '';
239
+ out.push(` ${gate.name.padEnd(17)} ${gate.command.join(' ')}${summary}`);
240
+ }
241
+ out.push(`then: write ${path.basename(recordPath)} and \`git tag -a ${facts.chosen.tag}\` on ${facts.head.slice(0, 10)}; push it with \`starci app release push\`.`);
242
+ return out;
243
+ }
244
+
245
+ function assertUnmoved(ctx, head) {
246
+ const now = git(ctx, ['rev-parse', 'HEAD']).stdout;
247
+ if (now !== head) throw failure('RELEASE_HEAD_MOVED', `HEAD moved during the gates (${head.slice(0, 10)} -> ${now.slice(0, 10)}). No record.`);
248
+ const dirty = dirtyPaths(ctx);
249
+ if (dirty.length > 0) throw failure('RELEASE_TREE_DIRTY', `the gates left ${dirty.length} changed or untracked path(s) (first: ${dirty[0].slice(0, 100)}): generated files differ from HEAD. Commit or ignore them properly, then rerun. No record.`);
250
+ }
251
+
252
+ function writeRecord(ctx, { head, tag, results, recordPath }) {
253
+ const record = {
254
+ schema: 'starci-app-l4-record/1', head, tag, createdAt: new Date(ctx.now()).toISOString(), producedBy: 'starci app release gate',
255
+ note: 'The full gates ran on a clean HEAD; every entry links the real log and its sha256.',
256
+ gates: results,
257
+ };
258
+ fs.mkdirSync(path.dirname(recordPath), { recursive: true });
259
+ fs.writeFileSync(recordPath, `${JSON.stringify(record, null, 2)}\n`);
260
+ }
261
+
262
+ function runAll(ctx, config, { dir, platform }) {
263
+ fs.mkdirSync(dir, { recursive: true });
264
+ const results = [];
265
+ for (const gate of config.gates) {
266
+ const result = runGate(ctx, gate, { dir, platform });
267
+ results.push(result);
268
+ if (!result.pass) break;
269
+ }
270
+ return results;
271
+ }
272
+
273
+ const summaryOf = (result) => `${result.pass ? 'PASS' : 'FAIL'} ${result.name} (${result.detail}, ${result.seconds}s) log ${result.log}`;
274
+
275
+ /**
276
+ * `starci app release gate`: dry run prints the preconditions, the gate list, the head and the tag; --execute runs every gate and, only if all are green,
277
+ * writes the L4 record and creates the annotated tag. options: { execute, source, tagPrefix, platform }.
278
+ */
279
+ export async function releaseGate(ctx, config, options = {}) {
280
+ const facts = inspect(ctx, config, options);
281
+ const common = releaseDir(ctx);
282
+ const recordPath = path.join(common, `${facts.head}.l4.json`);
283
+ const dir = path.join(common, facts.head.slice(0, 12));
284
+ const plan = [...facts.out, ...planLines(config, facts, dir, recordPath)];
285
+ const base = { head: facts.head, tag: facts.chosen.tag, record: recordPath, gates: config.gates.map((gate) => gate.name) };
286
+ if (facts.red.length > 0) return { ok: false, lines: [...plan, ...facts.red.map((item) => `RED: ${item}`), 'nothing was run.'], data: { ...base, red: facts.red, executed: false } };
287
+ if (options.execute !== true) return { ok: true, lines: [...plan, '', 'dry run: no gate was run, nothing written. preconditions OK.'], data: { ...base, red: [], executed: false } };
288
+ const results = runAll(ctx, config, { dir, platform: options.platform ?? process.platform });
289
+ const report = results.map(summaryOf);
290
+ const failed = results.filter((result) => !result.pass);
291
+ if (failed.length > 0) return { ok: false, lines: [...plan, ...report, `gate(s) red: ${failed.map((item) => item.name).join(', ')}. No L4 record, no tag.`], data: { ...base, executed: true, results } };
292
+ assertUnmoved(ctx, facts.head);
293
+ writeRecord(ctx, { head: facts.head, tag: facts.chosen.tag, results, recordPath });
294
+ const created = facts.chosen.state !== 'on-head';
295
+ if (created) git(ctx, ['tag', '-a', facts.chosen.tag, '-m', `release ${facts.chosen.tag}: gates green on ${facts.head.slice(0, 12)} (L4 record ${path.basename(recordPath)})`, facts.head]);
296
+ const tagged = created ? `annotated tag ${facts.chosen.tag} created on ${facts.head.slice(0, 10)}` : `annotated tag ${facts.chosen.tag} already on ${facts.head.slice(0, 10)}`;
297
+ return { ok: true, lines: [...plan, ...report, `L4 record written: ${recordPath}`, tagged], data: { ...base, executed: true, results, tagCreated: created } };
298
+ }
@@ -0,0 +1,103 @@
1
+ // index.mjs - `starci app release <gate|push>` and the plumbing the release and database verbs share: the context built from the app's IO, strict flag
2
+ // parsing, the optional `release:` / `database:` blocks of .starcistacks/application-stacks.yaml, text and --json output and the exit-code mapping.
3
+ // starci app release gate [--execute] [--source <ref>] [--tag-prefix <prefix>] [--json]
4
+ // starci app release push [--execute] [--remote <name>] [--json]
5
+ // Every action is a dry run unless --execute is given. A refusal is a StackError (exit 2); a failed step returns ok false (exit 1).
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { isPlainObject as plain } from '../runtime/engine/plain-object.mjs';
9
+ import { parseYaml } from '../runtime/engine/yaml.mjs';
10
+ import { STACKS_DIR, STACKS_FILE } from '../stack/config.mjs';
11
+ import { stackContext } from '../stack/context.mjs';
12
+ import { EXIT, StackError, refusal } from '../stack/errors.mjs';
13
+ import { RELEASE_CODE, VERSION, normalizeRelease, releaseGate } from './gate.mjs';
14
+ import { releasePush } from './push.mjs';
15
+
16
+ const IO_KEYS = ['cwd', 'stdout', 'stderr', 'env', 'sops'];
17
+
18
+ /** The stack context of one invocation: the IO of the app (cwd, writers, env, sops) over the injectable seams of `io.stackIo` (spawn, sleep, now, fetch). */
19
+ export function contextOf(io = {}) {
20
+ const own = Object.fromEntries(IO_KEYS.filter((key) => io[key] !== undefined).map((key) => [key, io[key]]));
21
+ return stackContext({ ...io.stackIo, ...own });
22
+ }
23
+
24
+ /** Splits argv into { flags, positional }; an unknown flag or a flag without its value is a refusal. */
25
+ export function parseVerbFlags(argv, spec, code = 'RELEASE_USAGE') {
26
+ const flags = {};
27
+ const positional = [];
28
+ for (let index = 0; index < argv.length; index += 1) {
29
+ const arg = argv[index];
30
+ if (spec.boolean.includes(arg)) flags[arg.slice(2)] = true;
31
+ else if (spec.value.includes(arg)) {
32
+ const value = argv[index + 1];
33
+ if (value === undefined || value.startsWith('--')) throw refusal(code, `${arg} needs a value`);
34
+ index += 1;
35
+ flags[arg.slice(2)] = value;
36
+ } else if (arg.startsWith('--')) throw refusal(code, `unknown flag ${arg}`);
37
+ else positional.push(arg);
38
+ }
39
+ return { flags, positional };
40
+ }
41
+
42
+ /** The top-level mapping of .starcistacks/application-stacks.yaml; a missing file is an empty declaration, an unreadable one a refusal. */
43
+ export function readDeclaration(root) {
44
+ let source;
45
+ try { source = fs.readFileSync(path.join(root, STACKS_DIR, STACKS_FILE), 'utf8'); } catch { return {}; }
46
+ let doc;
47
+ try { doc = parseYaml(source); } catch { throw refusal('STACKS_DEPLOY_FILE_INVALID', `${STACKS_FILE} is not valid YAML`); }
48
+ return plain(doc) ? doc : {};
49
+ }
50
+
51
+ function readPackage(root) {
52
+ try { return JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); } catch { throw refusal('RELEASE_PACKAGE_UNREADABLE', 'package.json is not readable JSON'); }
53
+ }
54
+
55
+ /** The normalised `release:` block (tolerant of absence) with the package version: { root, version, gates, tagPrefix, defaultBranch, tagMatchesVersion }. */
56
+ export function loadReleaseConfig(root) {
57
+ const pkg = readPackage(root);
58
+ if (typeof pkg.version !== 'string' || !VERSION.test(pkg.version)) throw refusal(RELEASE_CODE, 'package.json needs a version like 1.2.3 (the release tag derives from it)');
59
+ const { config, problems } = normalizeRelease(readDeclaration(root).release, pkg.scripts);
60
+ if (!config) throw refusal(RELEASE_CODE, `${problems.length} problem(s) in release: ${problems.slice(0, 6).join('; ')}`);
61
+ return { root, version: pkg.version, ...config };
62
+ }
63
+
64
+ function print(ctx, name, flags, result) {
65
+ if (flags.json) ctx.stdout(`${JSON.stringify({ verb: name, ok: result.ok, ...result.data }, null, 2)}\n`);
66
+ else for (const line of result.lines ?? []) ctx.stdout(`${line}\n`);
67
+ }
68
+
69
+ /** Parses the flags, runs `run(ctx, flags, positional)` and prints its result; returns the exit code. Only StackError is mapped, anything else is a bug and propagates. */
70
+ export async function runVerb({ label, name, io, argv, spec, run, code = 'RELEASE_USAGE' }) {
71
+ const ctx = contextOf(io);
72
+ try {
73
+ const { flags, positional } = parseVerbFlags(argv, spec, code);
74
+ const result = await run(ctx, flags, positional);
75
+ print(ctx, name, flags, result);
76
+ return result.ok ? EXIT.ok : EXIT.failed;
77
+ } catch (error) {
78
+ if (!(error instanceof StackError)) throw error;
79
+ ctx.stderr(`${label}: ${error.message}\n`);
80
+ return error.exit;
81
+ }
82
+ }
83
+
84
+ const ACTIONS = {
85
+ gate: {
86
+ spec: { boolean: ['--execute', '--json'], value: ['--source', '--tag-prefix'] },
87
+ run: (ctx, flags) => releaseGate(ctx, loadReleaseConfig(ctx.cwd), { execute: flags.execute === true, source: flags.source, tagPrefix: flags['tag-prefix'] }),
88
+ },
89
+ push: {
90
+ spec: { boolean: ['--execute', '--json'], value: ['--remote'] },
91
+ run: (ctx, flags) => releasePush(ctx, loadReleaseConfig(ctx.cwd), { execute: flags.execute === true, remote: flags.remote }),
92
+ },
93
+ };
94
+
95
+ /** Runs `starci app release <gate|push>`; `io` is the app IO (cwd, stdout, stderr, env, sops, stackIo). Returns the exit code. */
96
+ export async function releaseMain(argv, io = {}) {
97
+ const action = Object.hasOwn(ACTIONS, argv[0]) ? ACTIONS[argv[0]] : null;
98
+ if (!action) {
99
+ contextOf(io).stderr('starci app release: takes gate or push\n');
100
+ return EXIT.refused;
101
+ }
102
+ return runVerb({ label: 'starci app release', name: argv[0], io, argv: argv.slice(1), spec: action.spec, run: action.run });
103
+ }
@@ -0,0 +1,110 @@
1
+ // push.mjs - `starci app release push`: sends the default branch and the release tag to the remote in ONE atomic push through the repository's own
2
+ // pre-push hook (never --no-verify). It requires what the hook requires: an annotated release tag on HEAD and the L4 record of the gate for that
3
+ // head and tag. `git push --dry-run --atomic` always runs first (the hook's verdict is the proof) and any failure refuses the push.
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { firstLine } from '../stack/context.mjs';
7
+ import { failure, refusal } from '../stack/errors.mjs';
8
+ import { annotatedOnHead, git, refName, releaseDir } from './gate.mjs';
9
+
10
+ const VERDICT = /RIGHTS_PUSH_NOT_RELEASE|pre-push|failed to push|Everything up|->/u;
11
+
12
+ function readRecord(file) {
13
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
14
+ }
15
+
16
+ const greenRecord = (record, head, tag) => record?.head === head && record?.tag === tag
17
+ && Array.isArray(record.gates) && record.gates.length > 0 && record.gates.every((gate) => gate?.pass === true);
18
+
19
+ /** The release gate as the hook reads it: the annotated tag on HEAD whose L4 record names head and tag. Returns { tag } or { reason }. */
20
+ export function gateState(ctx, head, prefix) {
21
+ const tags = annotatedOnHead(ctx, prefix);
22
+ if (tags.length === 0) return { reason: `no ANNOTATED tag ${prefix}[0-9]* on HEAD: run starci app release gate --execute first` };
23
+ const file = path.join(releaseDir(ctx), `${head}.l4.json`);
24
+ if (!fs.existsSync(file)) return { reason: `no L4 record ${file}: run starci app release gate --execute first` };
25
+ const record = readRecord(file);
26
+ const tag = tags.find((name) => greenRecord(record, head, name));
27
+ return tag ? { tag } : { reason: `the L4 record does not name head ${head.slice(0, 10)} and one of ${tags.join(', ')} with every gate green` };
28
+ }
29
+
30
+ /** The remote branch tip, or null on an empty remote; an unreachable remote is a failure (the state cannot be judged). */
31
+ function remoteTip(ctx, remote, branch) {
32
+ const listed = git(ctx, ['ls-remote', '--heads', remote, branch], { allowFail: true });
33
+ if (listed.code !== 0) throw failure('RELEASE_REMOTE_UNREACHABLE', `cannot read ${remote}: ${firstLine(listed.stderr)}`);
34
+ if (listed.stdout === '') return null;
35
+ git(ctx, ['fetch', remote, branch, '--quiet']);
36
+ return listed.stdout.split(/\s/u)[0];
37
+ }
38
+
39
+ const isAncestor = (ctx, older, newer) => git(ctx, ['merge-base', '--is-ancestor', older, newer], { allowFail: true }).code === 0;
40
+
41
+ /** The remote object of a tag ('' when absent). */
42
+ function remoteTagObject(ctx, remote, tag) {
43
+ const wanted = `refs/tags/${tag}`;
44
+ const listed = git(ctx, ['ls-remote', '--tags', remote, wanted], { allowFail: true }).stdout.split('\n');
45
+ return listed.map((line) => line.split('\t')).find((parts) => parts[1] === wanted)?.[0] ?? '';
46
+ }
47
+
48
+ function branchRefs(ctx, { head, remote, branch }, red) {
49
+ const tip = remoteTip(ctx, remote, branch);
50
+ if (tip === null || isAncestor(ctx, tip, head)) return { refs: tip === head ? [] : [`${head}:refs/heads/${branch}`], tip };
51
+ if (isAncestor(ctx, head, tip)) return { refs: [], tip };
52
+ red.push(`${remote}/${branch} (${tip.slice(0, 10)}) is not an ancestor of HEAD: not a fast-forward; merge it first`);
53
+ return { refs: [], tip };
54
+ }
55
+
56
+ function tagRefs(ctx, { remote, tag }, red) {
57
+ const object = remoteTagObject(ctx, remote, tag);
58
+ if (object === '') return [`refs/tags/${tag}`];
59
+ if (object !== git(ctx, ['rev-parse', `refs/tags/${tag}`]).stdout) red.push(`tag ${tag} exists on ${remote} as a different object`);
60
+ return [];
61
+ }
62
+
63
+ function probe(ctx, remote, refs, red) {
64
+ const run = git(ctx, ['push', '--dry-run', '--atomic', remote, ...refs], { allowFail: true });
65
+ const verdict = `${run.stderr}\n${run.stdout}`.split('\n').filter((line) => VERDICT.test(line)).slice(0, 4);
66
+ if (run.code !== 0) red.push('the pre-push hook refuses this push as things stand');
67
+ return { exit: run.code, lines: [`hook dry run git push --dry-run --atomic: exit ${run.code}`, ...verdict.map((line) => ` ${line.slice(0, 200)}`)] };
68
+ }
69
+
70
+ const NAME_RULE = '^[A-Za-z0-9._/-]+$';
71
+
72
+ function assertNames(remote, branch) {
73
+ if (!refName(remote)) throw refusal('RELEASE_USAGE', `--remote ${remote} must match ${NAME_RULE}`);
74
+ if (!refName(branch)) throw refusal('RELEASE_USAGE', `the default branch ${branch} must match ${NAME_RULE}`);
75
+ }
76
+
77
+ const gateLine = (gate) => (gate.tag ? `release gate OK: annotated ${gate.tag} on HEAD, L4 record names head and tag` : 'release gate CLOSED (see RED below)');
78
+
79
+ /** A refusal result: the lines so far, every red item and a closing line; `data.red` lists the red items. */
80
+ const refuse = (out, red, data, closing) => ({ ok: false, lines: [...out, ...red.map((item) => `RED: ${item}`), closing], data: { ...data, red } });
81
+
82
+ /**
83
+ * `starci app release push`. options: { execute, remote }. The remote and branch names are validated; the branch is the declared default branch.
84
+ * Dry run (and --execute) first prove the push with `git push --dry-run --atomic`; --execute then pushes for real, still through the hook.
85
+ */
86
+ export async function releasePush(ctx, config, options = {}) {
87
+ const remote = options.remote ?? 'origin';
88
+ const branch = config.defaultBranch;
89
+ assertNames(remote, branch);
90
+ const head = git(ctx, ['rev-parse', 'HEAD']).stdout;
91
+ const gate = gateState(ctx, head, config.tagPrefix);
92
+ const out = [`HEAD ${head}`, gateLine(gate)];
93
+ const data = { head, tag: gate.tag ?? null, remote, branch, refs: [], hook: null, pushed: false };
94
+ if (!gate.tag) return refuse(out, [gate.reason], data, 'nothing pushed.');
95
+ const red = [];
96
+ const refs = [...branchRefs(ctx, { head, remote, branch }, red).refs, ...tagRefs(ctx, { remote, tag: gate.tag }, red)];
97
+ data.refs = refs;
98
+ out.push(`plan git push --atomic ${remote} ${refs.join(' ') || '(nothing)'}`);
99
+ if (refs.length === 0 && red.length === 0) return { ok: true, lines: [...out, `already pushed: ${remote}/${branch} holds HEAD and ${gate.tag} is on ${remote}. Nothing to do.`], data };
100
+ if (refs.length > 0) {
101
+ const hook = probe(ctx, remote, refs, red);
102
+ data.hook = { exit: hook.exit };
103
+ out.push(...hook.lines);
104
+ }
105
+ if (red.length > 0) return refuse(out, red, data, options.execute === true ? 'nothing pushed.' : 'dry run: nothing pushed.');
106
+ if (options.execute !== true) return { ok: true, lines: [...out, 'dry run: nothing pushed. ready; re-run with --execute.'], data };
107
+ const run = git(ctx, ['push', '--atomic', remote, ...refs], { allowFail: true });
108
+ if (run.code !== 0) return { ok: false, lines: [...out, `push failed: ${firstLine(run.stderr)}`], data };
109
+ return { ok: true, lines: [...out, `pushed to ${remote}: ${refs.join(' ')}`], data: { ...data, pushed: true } };
110
+ }
@@ -29,14 +29,14 @@ pins:
29
29
  source: packages/grammar/package.json
30
30
  why: 'the grammar front ends read: the brand layer sets `--font-sans` and `--font-mono` and the grammar reads them; it carries the Input tel kind, the copyable and read-only Input, the IconButton disclosure props and count badge, ChatBubble, FillViewport, Textarea mentions and Enter-to-send, the leading PrimaryRailLayout rail, the plate-less Image, ListRow, the soft Alert, Progress steps, the floating primary IconButton, FlowGraph, the STATE-8 loading views and the bordered selectable TagGroup chips. The runtime source version wins for a @starci package.'
31
31
  '@starci/eslint-canon-be':
32
- version: 3.3.1
32
+ version: 3.4.0
33
33
  group: starci
34
34
  install: registry
35
35
  side: be
36
36
  source: packages/eslint/be/package.json
37
37
  why: 'the back-end ESLint canon: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; `starciBeConfig({ hfs: loadHfs(import.meta.url) })` is the typed factory and the BE-CONVENTION laws are its rules.'
38
38
  '@starci/eslint-canon-fe':
39
- version: 8.3.0
39
+ version: 8.4.0
40
40
  group: starci
41
41
  install: registry
42
42
  side: fe
@@ -62,28 +62,28 @@ pins:
62
62
  side: both
63
63
  source: packages/prettier-config/package.json
64
64
  '@starci/jest-preset':
65
- version: 2.2.7
65
+ version: 2.3.0
66
66
  group: starci
67
67
  install: registry
68
68
  side: be
69
69
  source: packages/jest-preset/package.json
70
70
  why: 'the Jest preset of a back end: the world runner runs up to min(--maxWorkers, slots) world files at once, each in its own process bound to one data slot of the test world (STARCI_TEST_WORLD_SLOT), and reads test-world state protocol 2 only: it pairs exactly with @starci/test-world, and any other state file is JEST_PRESET_WORLD_PAIR_MISMATCH. The unit run writes the lcov Sonar and Codecov import (services only); the integration, e2e and contract projects run every spec file in a worker process of its own (world-runner.cjs), so a process-global framework registry (@nestjs/graphql type metadata) never leaks between e2e files; it ships the unit kit (mockEntityManager, fakeTransaction, fakeIds, FakeClock, Outcome matchers) and the recordingOutbox claim side.'
71
71
  '@starci/test-world':
72
- version: 1.2.1
72
+ version: 1.2.2
73
73
  group: starci
74
74
  install: registry
75
75
  side: be
76
76
  source: packages/test-world/package.json
77
77
  why: 'the shared e2e library of every back end (R47, R48), a devDependency of every back end: the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs and the outage lock. Kafka is real own infra (the one digest-pinned apache/kafka KRaft image, a broker listener and proxy per slot, slot-prefixed topics, consumer groups and client ids) and Postgres is schema-per-context (a schema and a login role per context in a shared database). Each worker has its own data slot (<ns>_w<k>) with its own databases, Keycloak realm, leased Redis DB, bucket/collection/topic prefixes, fakes host, toxiproxy proxies and outage lock; a run provisions min(jest workers, workers cap, default 2) slots, and state file protocol 2 pairs exactly with @starci/jest-preset (TEST_WORLD_PAIR_MISMATCH otherwise). A modules world boots real peer apps beside its modules, world.apps.<name>.during(fn) is the outage of a peer app, w.keycloak.clientSecret(client) answers the run-generated secret of a confidential realm client, world.keycloak.events/sessions read the realm user events and live sessions, world.infra.postgresql.connection(name) takes one database connection down while the others serve, and world.resolve and scope.resolve take any Nest token. Every path a declaration names (`stack`, seeds, the realm, Dockerfiles) resolves from the app root (the directory of hfs.json); the globalSetup registers the path aliases as TypeScript resolves them. Its CLI main is exported at ./cli for starci app stack.'
78
78
  '@starci/cli':
79
- version: 1.3.0
79
+ version: 1.4.0
80
80
  group: starci
81
81
  install: registry
82
82
  side: both
83
83
  source: packages/cli/package.json
84
84
  why: 'the only StarCi command package; product repositories install it for starci app, while @starci/hfs remains its pinned transitive implementation.'
85
85
  '@starci/hfs':
86
- version: 4.3.0
86
+ version: 4.4.0
87
87
  group: starci
88
88
  install: registry
89
89
  side: both
@@ -180,7 +180,7 @@ pins:
180
180
  side: be
181
181
  # --- framework
182
182
  next:
183
- version: 16.1.6
183
+ version: 16.4.2
184
184
  group: framework
185
185
  side: fe
186
186
  react:
@@ -206,7 +206,7 @@ pins:
206
206
  group: framework
207
207
  side: be
208
208
  '@nestjs/platform-express':
209
- version: 11.2.5
209
+ version: 11.2.7
210
210
  group: framework
211
211
  side: be
212
212
  '@nestjs/testing':
@@ -316,7 +316,7 @@ pins:
316
316
  side: fe
317
317
  why: 'the codegen preset of the front-end wire types (fe-stack codegen); the latest registry version on 2026-10-13.'
318
318
  'axios':
319
- version: 1.15.2
319
+ version: 1.20.0
320
320
  group: framework
321
321
  side: fe
322
322
  why: 'the REST client of the front end for uploads, presigned URLs and auth endpoints (fe-stack rest); version read from the starci-academy package-lock on 2026-10-13.'
@@ -465,12 +465,12 @@ pins:
465
465
  version: 13.4.5
466
466
  group: framework
467
467
  side: be
468
- why: 'the GraphQL transport of every back end (be.transport.graphql) that the front end calls; the ecommerce-app resolution on 2026-10-13.'
468
+ why: 'the GraphQL transport of every back end (be.transport.graphql) that the front end calls; the ecommerce-app resolution on 2026-10-13. Stays on 13.x for alpha.18: the npm-audit advisory fixed by 14.0.3 (semver major) is a documented accepted exception, upgrade deferred to alpha.19 (owner ruling graphql-13-advisory-accepted-until-a19, 2026-10-16).'
469
469
  '@nestjs/apollo':
470
470
  version: 13.4.5
471
471
  group: framework
472
472
  side: be
473
- why: 'the Apollo driver of @nestjs/graphql; the ecommerce-app resolution on 2026-10-13.'
473
+ why: 'the Apollo driver of @nestjs/graphql; the ecommerce-app resolution on 2026-10-13. Stays on 13.x with @nestjs/graphql (accepted advisory, major deferred to alpha.19).'
474
474
  '@apollo/server':
475
475
  version: 5.5.1
476
476
  group: framework
@@ -1,6 +1,6 @@
1
1
  schema: starci/fe-stack@1
2
2
  purpose: >-
3
- The front-end stack of every app, lite and full alike (owner ruling 2026-10-13, R212 case-2): for each common front-end concern the
3
+ The front-end stack of every app, lite and full alike (owner ruling 2026-10-13, R212 case-2 (owner:lite-production-swarm), with the Work boundary amended by ruling:R212/case-4): for each common front-end concern the
4
4
  ONE canonical library, its HFS home (slot ids of knowledge/hfs/slots.yaml), the pattern id that teaches it
5
5
  (knowledge/patterns/fe/*.yaml) and the alternatives a front end never depends on or imports. Versions live in
6
6
  knowledge/hfs/canon-pins.yaml only. `tier: core` packages are installed by every scaffold; `opt` packages are added when a feature
@@ -219,3 +219,24 @@ concerns:
219
219
  pattern: FE-LINT
220
220
  forbids: []
221
221
  why: the canon lint and format configuration.
222
+ - id: auth-supabase
223
+ tier: opt
224
+ packages: ['@supabase/supabase-js', '@supabase/ssr']
225
+ home: [fe.modules.auth]
226
+ pattern: FE-AUTH
227
+ forbids: []
228
+ why: the lite auth provider client and PKCE server cookie adapter, confined to modules/auth; data still goes through the back end.
229
+ - id: framework-support
230
+ tier: opt
231
+ packages: [server-only, rxjs]
232
+ home: [fe.source-root-pinned]
233
+ pattern: FE-ROUTE
234
+ forbids: []
235
+ why: the Next server boundary marker and Apollo peer support used by canonical workspaces.
236
+ - id: tool-support
237
+ tier: opt
238
+ packages: ['@types/node', '@types/react', '@types/react-dom', '@starci/tsconfig', '@starci/stylelint-canon', '@tailwindcss/postcss', postcss-value-parser, stylelint, eslint-plugin-react-hooks]
239
+ home: [fe.tool-config]
240
+ pattern: FE-LINT
241
+ forbids: []
242
+ why: the compiler, CSS and lint support packages of the canonical front-end configs.