@starci/hfs 4.2.17 → 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 (564) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +7 -0
  3. package/db/migrate.mjs +361 -0
  4. package/db/post-cutover.mjs +136 -0
  5. package/emit/db-types.mjs +10 -1
  6. package/lint/run.mjs +1 -1
  7. package/package.json +4 -1
  8. package/release/gate.mjs +298 -0
  9. package/release/index.mjs +103 -0
  10. package/release/push.mjs +110 -0
  11. package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
  12. package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
  13. package/runtime/knowledge/hfs/rules.yaml +3437 -160
  14. package/runtime/knowledge/hfs/slots.yaml +430 -92
  15. package/runtime/knowledge/patterns/be/api.yaml +92 -2
  16. package/runtime/knowledge/patterns/be/cli.yaml +37 -9
  17. package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
  18. package/runtime/knowledge/patterns/be/jobs.yaml +20 -2
  19. package/runtime/knowledge/patterns/be/projections.yaml +4 -2
  20. package/runtime/knowledge/patterns/be/queues.yaml +11 -3
  21. package/runtime/knowledge/patterns/be/reactors.yaml +20 -1
  22. package/runtime/knowledge/patterns/be/realtime.yaml +25 -3
  23. package/runtime/knowledge/patterns/be/saga.yaml +20 -3
  24. package/runtime/knowledge/patterns/be/webhooks.yaml +210 -36
  25. package/runtime/knowledge/patterns/fe/folder.yaml +4 -0
  26. package/runtime/knowledge/sonar-gate.yaml +1 -1
  27. package/runtime/modules/kernel/failure-codes.yaml +3128 -47
  28. package/runtime/modules/models/runtimes.yaml +1 -1
  29. package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
  30. package/runtime/packages/cli/src/catalog.generated.mjs +14104 -0
  31. package/runtime/packages/cli/src/validate-args.mjs +213 -0
  32. package/runtime/scripts/api/git/add.mjs +5 -0
  33. package/runtime/scripts/api/git/cat-file.mjs +5 -0
  34. package/runtime/scripts/api/git/init.mjs +7 -0
  35. package/runtime/scripts/api/git/lib.mjs +3 -0
  36. package/runtime/scripts/api/git/tag.mjs +5 -0
  37. package/runtime/scripts/api/node/lib.mjs +20 -0
  38. package/runtime/scripts/api/node/run-node.mjs +6 -0
  39. package/runtime/scripts/api/sops/lib.mjs +41 -0
  40. package/runtime/scripts/api/sops/resolve-sops.mjs +7 -0
  41. package/runtime/scripts/api/sops/seal.mjs +1 -1
  42. package/runtime/scripts/api/sops/selected-age-recipient.mjs +7 -0
  43. package/runtime/scripts/api/supabase/database-dump.mjs +5 -0
  44. package/runtime/scripts/api/supabase/database-query.mjs +5 -0
  45. package/runtime/scripts/api/supabase/lib.mjs +44 -0
  46. package/runtime/scripts/api/supabase/migration-repair.mjs +5 -0
  47. package/runtime/scripts/hfs/app-wiring.mjs +201 -0
  48. package/runtime/scripts/hfs/architecture/backend-app-files.mjs +103 -0
  49. package/runtime/scripts/hfs/architecture/backend.mjs +22 -10
  50. package/runtime/scripts/hfs/architecture/background-unowned.mjs +157 -41
  51. package/runtime/scripts/hfs/architecture/canon-clones.mjs +240 -0
  52. package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +143 -0
  53. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
  54. package/runtime/scripts/hfs/architecture/clones.mjs +17 -8
  55. package/runtime/scripts/hfs/architecture/connection-map.mjs +14 -12
  56. package/runtime/scripts/hfs/architecture/context-map.mjs +25 -14
  57. package/runtime/scripts/hfs/architecture/context-owner.mjs +1 -2
  58. package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
  59. package/runtime/scripts/hfs/architecture/dead-exports.mjs +23 -50
  60. package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
  61. package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +104 -0
  62. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +30 -11
  63. package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
  64. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
  65. package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
  66. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +16 -3
  67. package/runtime/scripts/hfs/architecture/machine-ast.mjs +17 -0
  68. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +49 -25
  69. package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
  70. package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
  71. package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
  72. package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
  73. package/runtime/scripts/hfs/architecture/register-once.mjs +15 -7
  74. package/runtime/scripts/hfs/architecture/required-files.mjs +2 -1
  75. package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
  76. package/runtime/scripts/hfs/architecture/staged-consumers.mjs +37 -0
  77. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +214 -17
  78. package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -6
  79. package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
  80. package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
  81. package/runtime/scripts/hfs/architecture/tiers.mjs +11 -4
  82. package/runtime/scripts/hfs/architecture/typescript.mjs +1 -8
  83. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +9 -13
  84. package/runtime/scripts/hfs/check-results.mjs +28 -0
  85. package/runtime/scripts/hfs/check.mjs +63 -72
  86. package/runtime/scripts/hfs/coverage-from.mjs +70 -0
  87. package/runtime/scripts/hfs/db-baseline-verb.mjs +276 -0
  88. package/runtime/scripts/hfs/db-plan-verb.mjs +254 -0
  89. package/runtime/scripts/hfs/db-restamp-verb.mjs +60 -0
  90. package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
  91. package/runtime/scripts/hfs/doc-commands.mjs +367 -0
  92. package/runtime/scripts/hfs/edition-slots.mjs +2 -2
  93. package/runtime/scripts/hfs/example-secrets.mjs +158 -0
  94. package/runtime/scripts/hfs/manifest-shape.mjs +91 -6
  95. package/runtime/scripts/hfs/path-findings.mjs +32 -2
  96. package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
  97. package/runtime/scripts/hfs/rule-catalog.mjs +156 -11
  98. package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
  99. package/runtime/scripts/hfs/rules/a15-findings.mjs +88 -0
  100. package/runtime/scripts/hfs/rules/app-env-required.mjs +20 -0
  101. package/runtime/scripts/hfs/rules/app-root.mjs +2 -2
  102. package/runtime/scripts/hfs/rules/assets.mjs +79 -0
  103. package/runtime/scripts/hfs/rules/boot-proof.mjs +16 -0
  104. package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +59 -0
  105. package/runtime/scripts/hfs/rules/contract.mjs +66 -5
  106. package/runtime/scripts/hfs/rules/coverage-below.mjs +230 -0
  107. package/runtime/scripts/hfs/rules/database-access.mjs +343 -0
  108. package/runtime/scripts/hfs/rules/database-baseline.mjs +142 -0
  109. package/runtime/scripts/hfs/rules/database-config.mjs +250 -2
  110. package/runtime/scripts/hfs/rules/database-constants.mjs +9 -0
  111. package/runtime/scripts/hfs/rules/database-migrations.mjs +317 -37
  112. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +119 -9
  113. package/runtime/scripts/hfs/rules/database-queue-sources.mjs +176 -0
  114. package/runtime/scripts/hfs/rules/database-queues.mjs +361 -0
  115. package/runtime/scripts/hfs/rules/database-sql.mjs +91 -9
  116. package/runtime/scripts/hfs/rules/database.mjs +11 -2
  117. package/runtime/scripts/hfs/rules/deploy-scan.mjs +192 -0
  118. package/runtime/scripts/hfs/rules/deploy-workflow.mjs +219 -0
  119. package/runtime/scripts/hfs/rules/doc-fresh.mjs +286 -0
  120. package/runtime/scripts/hfs/rules/docker-health.mjs +249 -0
  121. package/runtime/scripts/hfs/rules/docker.mjs +24 -7
  122. package/runtime/scripts/hfs/rules/edition.mjs +11 -10
  123. package/runtime/scripts/hfs/rules/env-parity.mjs +373 -0
  124. package/runtime/scripts/hfs/rules/env-value-shape.mjs +113 -0
  125. package/runtime/scripts/hfs/rules/event-bus.mjs +3 -2
  126. package/runtime/scripts/hfs/rules/event-tables.mjs +100 -0
  127. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +115 -15
  128. package/runtime/scripts/hfs/rules/fe-stack.mjs +57 -0
  129. package/runtime/scripts/hfs/rules/feature-files.mjs +28 -0
  130. package/runtime/scripts/hfs/rules/frontend-tree.mjs +54 -35
  131. package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -2
  132. package/runtime/scripts/hfs/rules/lexicon.mjs +62 -0
  133. package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +89 -0
  134. package/runtime/scripts/hfs/rules/monorepo.mjs +14 -1
  135. package/runtime/scripts/hfs/rules/pipeline.mjs +8 -1
  136. package/runtime/scripts/hfs/rules/proof-commands.mjs +2 -0
  137. package/runtime/scripts/hfs/rules/release-provenance.mjs +211 -0
  138. package/runtime/scripts/hfs/rules/saga-delivery.mjs +66 -0
  139. package/runtime/scripts/hfs/rules/saga.mjs +8 -16
  140. package/runtime/scripts/hfs/rules/script-hygiene.mjs +151 -0
  141. package/runtime/scripts/hfs/rules/secret-custody-only.mjs +65 -0
  142. package/runtime/scripts/hfs/rules/secrets.mjs +26 -0
  143. package/runtime/scripts/hfs/rules/services.mjs +4 -4
  144. package/runtime/scripts/hfs/rules/stack-tooling.mjs +93 -0
  145. package/runtime/scripts/hfs/rules/stacks-deploy.mjs +76 -0
  146. package/runtime/scripts/hfs/rules/stacks-ingress.mjs +87 -0
  147. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -0
  148. package/runtime/scripts/hfs/rules/stacks-release.mjs +366 -0
  149. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +168 -0
  150. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
  151. package/runtime/scripts/hfs/rules/stacks.mjs +13 -8
  152. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +1 -1
  153. package/runtime/scripts/hfs/rules/supply-chain.mjs +167 -0
  154. package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +101 -0
  155. package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +77 -0
  156. package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +165 -0
  157. package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +103 -0
  158. package/runtime/scripts/hfs/rules/unused-symbol.mjs +320 -0
  159. package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +185 -0
  160. package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +19 -0
  161. package/runtime/scripts/hfs/sealed-members.mjs +101 -0
  162. package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +200 -0
  163. package/runtime/scripts/hfs/secret-document.mjs +97 -0
  164. package/runtime/scripts/hfs/secret-edit.mjs +170 -0
  165. package/runtime/scripts/hfs/secret-errors.mjs +20 -0
  166. package/runtime/scripts/hfs/secret-files.mjs +124 -0
  167. package/runtime/scripts/hfs/secret-init.mjs +300 -0
  168. package/runtime/scripts/hfs/secret-output.mjs +100 -0
  169. package/runtime/scripts/hfs/secret-sops.mjs +130 -0
  170. package/runtime/scripts/hfs/secret-store.mjs +163 -0
  171. package/runtime/scripts/hfs/secret.mjs +162 -193
  172. package/runtime/scripts/hfs/slot-imports.mjs +18 -3
  173. package/runtime/scripts/hfs/slot-required.mjs +13 -0
  174. package/runtime/scripts/hfs/slot-semantic-problems.mjs +2 -1
  175. package/runtime/scripts/hfs/slots.mjs +1 -0
  176. package/runtime/scripts/hfs/stack-config-kit.mjs +34 -0
  177. package/runtime/scripts/hfs/stack-deploy-config.mjs +313 -0
  178. package/runtime/scripts/hfs/stack-ingress-config.mjs +237 -0
  179. package/runtime/scripts/hfs/stacks-gate.mjs +414 -0
  180. package/runtime/scripts/hfs/starcistacks-runtime.mjs +61 -0
  181. package/runtime/scripts/hfs/tree-findings.mjs +22 -0
  182. package/runtime/scripts/lib/command-tokens.mjs +126 -0
  183. package/runtime/scripts/lib/compose-values.mjs +11 -0
  184. package/runtime/scripts/lib/deployment-findings.mjs +7 -0
  185. package/runtime/scripts/lib/json-clone.mjs +61 -0
  186. package/runtime/scripts/lib/list.mjs +4 -0
  187. package/runtime/scripts/lib/roots.mjs +59 -0
  188. package/runtime/scripts/lib/runtime-example.mjs +13 -0
  189. package/runtime/scripts/lib/secret-patterns.mjs +60 -5
  190. package/runtime/scripts/lib/sql-ast.mjs +32 -0
  191. package/runtime/scripts/lib/stack-services.mjs +1 -1
  192. package/runtime/scripts/lib/swarm-stack.mjs +126 -0
  193. package/runtime/scripts/lib/trim-end.mjs +8 -0
  194. package/runtime/scripts/lib/ts-analysis.mjs +23 -0
  195. package/runtime/scripts/lib/ts-ast.mjs +27 -2
  196. package/runtime/scripts/lib/workflow-events.mjs +8 -0
  197. package/scaffold/add-app.mjs +77 -59
  198. package/scaffold/add-cli-lite.mjs +24 -17
  199. package/scaffold/add-compose.mjs +79 -0
  200. package/scaffold/add-database-lite.mjs +14 -0
  201. package/scaffold/add-fe-overlay.mjs +122 -0
  202. package/scaffold/add-fe-page.mjs +269 -0
  203. package/scaffold/add-inbox-lite.mjs +87 -124
  204. package/scaffold/add-plan.mjs +183 -0
  205. package/scaffold/add-table.mjs +132 -70
  206. package/scaffold/add.mjs +186 -204
  207. package/scaffold/app.mjs +128 -43
  208. package/scaffold/edition-gate.mjs +3 -3
  209. package/scaffold/image.mjs +2 -2
  210. package/scaffold/lite-exports.mjs +2 -1
  211. package/scaffold/service.mjs +2 -2
  212. package/src/main.mjs +238 -119
  213. package/stack/age-install.mjs +37 -0
  214. package/stack/backup.mjs +100 -0
  215. package/stack/build-args.mjs +42 -0
  216. package/stack/build.mjs +173 -0
  217. package/stack/cli.mjs +130 -0
  218. package/stack/config.mjs +57 -0
  219. package/stack/context.mjs +57 -0
  220. package/stack/converge.mjs +107 -0
  221. package/stack/deploy.mjs +301 -0
  222. package/stack/digest.mjs +16 -0
  223. package/stack/docker-transport.mjs +53 -0
  224. package/stack/edge-alias.mjs +118 -0
  225. package/stack/errors.mjs +21 -0
  226. package/stack/image-scan.mjs +152 -0
  227. package/stack/ingress-create.mjs +114 -0
  228. package/stack/ingress-publish.mjs +273 -0
  229. package/stack/ingress.mjs +706 -0
  230. package/stack/jobs.mjs +51 -0
  231. package/stack/live-restore.mjs +177 -0
  232. package/stack/prepare.mjs +266 -0
  233. package/stack/private-file.mjs +22 -0
  234. package/stack/provision.mjs +90 -0
  235. package/stack/prune.mjs +46 -0
  236. package/stack/release-store.mjs +87 -0
  237. package/stack/remote.mjs +205 -0
  238. package/stack/render.mjs +149 -0
  239. package/stack/retire.mjs +47 -0
  240. package/stack/sealed.mjs +149 -0
  241. package/stack/task.mjs +42 -0
  242. package/stack/verify.mjs +99 -0
  243. package/sync/ci-commands.mjs +13 -0
  244. package/sync/index.mjs +127 -64
  245. package/sync/stack-images.mjs +127 -0
  246. package/templates/app/ci-workflows/github/workflows/ci.yml +5 -1
  247. package/templates/app/ci-workflows/github/workflows/images.yml +15 -3
  248. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +148 -0
  249. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +7 -3
  250. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +38 -6
  251. package/templates/app/ci-workflows-lite/github/workflows/images.yml +14 -2
  252. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +148 -0
  253. package/templates/app/codegen-graphql.mjs.partial +13 -2
  254. package/templates/app/codegen-lite/scripts/codegen.mjs +6 -1
  255. package/templates/app/codegen-prelude.mjs.partial +8 -0
  256. package/templates/app/gitignore +4 -0
  257. package/templates/app/gitignore-lite +4 -0
  258. package/templates/app/hooks/husky/pre-commit +7 -12
  259. package/templates/app/package-scripts/package.json +1 -0
  260. package/templates/app/package-scripts/supabase.json +7 -0
  261. package/templates/app/package-scripts-lite/package.json +1 -7
  262. package/templates/app/skeleton/.editorconfig +1 -0
  263. package/templates/app/skeleton/.gitattributes +7 -1
  264. package/templates/app/skeleton/.github/dependabot.yml +17 -0
  265. package/templates/app/skeleton/.nvmrc +1 -1
  266. package/templates/app/skeleton/.sops.yaml +1 -1
  267. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +134 -1
  268. package/templates/app/skeleton/.starcistacks/dev/README.md +16 -0
  269. package/templates/app/skeleton/.starcistacks/dev/environment.json +5 -0
  270. package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +71 -0
  271. package/templates/app/skeleton/.starcistacks/dev/runtime/env/KEYS.md +14 -0
  272. package/templates/app/skeleton/.starcistacks/production/README.md +37 -0
  273. package/templates/app/skeleton/.starcistacks/production/environment.json +5 -0
  274. package/templates/app/skeleton/.starcistacks/production/infra/swarm/Caddyfile +15 -0
  275. package/templates/app/skeleton/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  276. package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  277. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  278. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +9 -0
  279. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  280. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +142 -0
  281. package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +14 -0
  282. package/templates/app/skeleton/README.md +6 -6
  283. package/templates/app/skeleton/scripts/codegen.mjs +1 -2
  284. package/templates/app/skeleton-lite/.editorconfig +16 -0
  285. package/templates/app/skeleton-lite/.gitattributes +8 -0
  286. package/templates/app/skeleton-lite/.github/dependabot.yml +17 -0
  287. package/templates/app/skeleton-lite/.nvmrc +1 -0
  288. package/templates/app/skeleton-lite/.sops.yaml +3 -2
  289. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +193 -0
  290. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +20 -0
  291. package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +2 -1
  292. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +16 -0
  293. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +11 -0
  294. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +7 -0
  295. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +6 -0
  296. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +8 -0
  297. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +6 -0
  298. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +120 -0
  299. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -0
  300. package/templates/app/skeleton-lite/.starcistacks/production/README.md +38 -0
  301. package/templates/app/skeleton-lite/.starcistacks/production/environment.json +6 -0
  302. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/Caddyfile +11 -0
  303. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  304. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  305. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  306. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/images.lock.yaml +8 -0
  307. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  308. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +121 -0
  309. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +21 -0
  310. package/templates/app/skeleton-lite/.starciwork/brand/index.yaml +12 -0
  311. package/templates/app/skeleton-lite/.starciwork/workspace.yaml +9 -0
  312. package/templates/app/skeleton-lite/README.md +8 -4
  313. package/templates/app/skeleton-lite/supabase/config.toml +8 -1
  314. package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +15 -6
  315. package/templates/app/starciwork.gitignore-lite +6 -0
  316. package/templates/app/upgrade-full/.starcistacks/dev/README.md +13 -7
  317. package/templates/be/image/worker/Dockerfile +1 -1
  318. package/templates/be/inbox/database-client.spec.ts.tpl +22 -28
  319. package/templates/be/inbox/database-client.ts.tpl +14 -28
  320. package/templates/be/inbox/delivery-recorded.event.ts.tpl +36 -0
  321. package/templates/be/inbox/event-index.ts.tpl +1 -0
  322. package/templates/be/inbox/events.json.tpl +10 -0
  323. package/templates/be/inbox/migration.sql.tpl +1 -0
  324. package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +42 -0
  325. package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +9 -0
  326. package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +6 -0
  327. package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +19 -0
  328. package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +2 -0
  329. package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +16 -0
  330. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +45 -0
  331. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +15 -0
  332. package/templates/be/inbox/platform/event-bus/index.ts.tpl +5 -0
  333. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +5 -0
  334. package/templates/be/inbox/platform/inbox/inbox.decorators.ts.tpl +9 -0
  335. package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +6 -0
  336. package/templates/be/inbox/platform/inbox/inbox.module.ts.tpl +19 -0
  337. package/templates/be/inbox/platform/inbox/inbox.options.ts.tpl +2 -0
  338. package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +10 -0
  339. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +41 -0
  340. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +19 -0
  341. package/templates/be/inbox/platform/inbox/index.ts.tpl +3 -0
  342. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +7 -0
  343. package/templates/be/inbox/platform/migration.sql.tpl +48 -0
  344. package/templates/be/inbox/service.spec.ts.tpl +49 -20
  345. package/templates/be/inbox/service.ts.tpl +27 -16
  346. package/templates/be/inbox/sql.ts.tpl +2 -2
  347. package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -3
  348. package/templates/be/inbox/webhook-door.ts.tpl +4 -3
  349. package/templates/be/patterns/api/feature.module-definition.ts.tpl +6 -0
  350. package/templates/be/patterns/api/feature.module.ts.tpl +2 -1
  351. package/templates/be/patterns/api/feature.options.ts.tpl +2 -0
  352. package/templates/be/patterns/api/graphql.module-definition.ts.tpl +6 -0
  353. package/templates/be/patterns/api/graphql.module.ts.tpl +3 -2
  354. package/templates/be/patterns/api/graphql.options.ts.tpl +2 -0
  355. package/templates/be/patterns/cli/group.module-definition.ts.tpl +6 -0
  356. package/templates/be/patterns/cli/group.module.ts.tpl +2 -1
  357. package/templates/be/patterns/cli/group.options.ts.tpl +2 -0
  358. package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +0 -3
  359. package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +0 -2
  360. package/templates/be/patterns/jobs/job.module-definition.ts.tpl +6 -0
  361. package/templates/be/patterns/jobs/job.module.ts.tpl +2 -1
  362. package/templates/be/patterns/jobs/job.options.ts.tpl +2 -0
  363. package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +6 -0
  364. package/templates/be/patterns/jobs/job.queue.module.ts.tpl +6 -3
  365. package/templates/be/patterns/jobs/job.queue.options.ts.tpl +2 -0
  366. package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +3 -1
  367. package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +25 -9
  368. package/templates/be/patterns/queues/platform/queue.config.ts.tpl +1 -1
  369. package/templates/be/patterns/queues/platform/queue.index.ts.tpl +1 -1
  370. package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +2 -2
  371. package/templates/be/patterns/queues/platform/queue.module.ts.tpl +11 -0
  372. package/templates/be/patterns/queues/platform/queue.options.ts.tpl +10 -0
  373. package/templates/be/patterns/queues/queue.module-definition.ts.tpl +6 -0
  374. package/templates/be/patterns/queues/queue.module.ts.tpl +2 -1
  375. package/templates/be/patterns/queues/queue.options.ts.tpl +2 -0
  376. package/templates/be/patterns/reactors/message.module-definition.ts.tpl +6 -0
  377. package/templates/be/patterns/reactors/message.module.ts.tpl +6 -3
  378. package/templates/be/patterns/reactors/message.options.ts.tpl +2 -0
  379. package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +6 -0
  380. package/templates/be/patterns/reactors/reactor.module.ts.tpl +2 -1
  381. package/templates/be/patterns/reactors/reactor.options.ts.tpl +2 -0
  382. package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +188 -0
  383. package/templates/be/patterns/realtime/channel.gateway.ts.tpl +22 -0
  384. package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +6 -0
  385. package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +2 -1
  386. package/templates/be/patterns/realtime/channel.graphql.options.ts.tpl +2 -0
  387. package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +6 -0
  388. package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +110 -0
  389. package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +7 -0
  390. package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +6 -0
  391. package/templates/be/patterns/saga/saga.message.module.ts.tpl +6 -3
  392. package/templates/be/patterns/saga/saga.message.options.ts.tpl +2 -0
  393. package/templates/be/patterns/saga/saga.module-definition.ts.tpl +6 -0
  394. package/templates/be/patterns/saga/saga.module.ts.tpl +2 -1
  395. package/templates/be/patterns/saga/saga.options.ts.tpl +2 -0
  396. package/templates/be/patterns/webhooks/webhook.door.spec.ts.tpl +3 -3
  397. package/templates/be/patterns/webhooks/webhook.door.ts.tpl +3 -3
  398. package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +6 -0
  399. package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +2 -1
  400. package/templates/be/patterns/webhooks/webhook.http.options.ts.tpl +2 -0
  401. package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +80 -26
  402. package/templates/be/platform/graphql/localize-errors.mapper.ts +2 -5
  403. package/templates/be/skeleton/apps/__app__/src/__app__.env.ts +21 -0
  404. package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +1 -1
  405. package/templates/be/skeleton/apps/__app__/src/app.module.ts +15 -45
  406. package/templates/be/skeleton/apps/__app__/src/compose/api.ts +4 -0
  407. package/templates/be/skeleton/apps/__app__/src/compose/domain.ts +11 -0
  408. package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +44 -0
  409. package/templates/be/skeleton/apps/__app__/src/compose/system.ts +5 -0
  410. package/templates/be/skeleton/apps/__app__/src/compose/webhooks.ts +4 -0
  411. package/templates/be/skeleton/apps/__app__/src/main.ts +18 -10
  412. package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
  413. package/templates/be/skeleton/apps/cli/src/cli.env.ts +13 -0
  414. package/templates/be/skeleton/apps/cli/src/cli.options.ts +7 -17
  415. package/templates/be/skeleton/apps/cli/src/compose/cli.ts +5 -0
  416. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +24 -0
  417. package/templates/be/skeleton/apps/cli/src/main.ts +3 -2
  418. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.contracts.ts +2 -0
  419. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.handler.ts +22 -0
  420. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.query.ts +11 -0
  421. package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +6 -0
  422. package/templates/be/skeleton/src/features/api/system-health/system-health.module.ts +5 -3
  423. package/templates/be/skeleton/src/features/api/system-health/system-health.options.ts +2 -0
  424. package/templates/be/skeleton/src/features/api/system-health/transport/http/ready.controller.ts +19 -0
  425. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +6 -0
  426. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module.ts +8 -3
  427. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.options.ts +2 -0
  428. package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +6 -0
  429. package/templates/be/skeleton/src/features/cli/cli.module.ts +3 -2
  430. package/templates/be/skeleton/src/features/cli/cli.options.ts +2 -0
  431. package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +6 -0
  432. package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +2 -1
  433. package/templates/be/skeleton/src/features/cli/migrate/migrate.options.ts +2 -0
  434. package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +6 -0
  435. package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +2 -1
  436. package/templates/be/skeleton/src/features/cli/seed/seed.options.ts +2 -0
  437. package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +5 -6
  438. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +56 -4
  439. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +36 -5
  440. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +39 -8
  441. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +14 -8
  442. package/templates/be/skeleton/src/modules/domain/identity/credential.contracts.ts +26 -0
  443. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +119 -0
  444. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +43 -0
  445. package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +16 -0
  446. package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -1
  447. package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +22 -4
  448. package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +8 -2
  449. package/templates/be/skeleton/src/modules/domain/identity/index.ts +1 -0
  450. package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +6 -6
  451. package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +11 -4
  452. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.spec.ts +32 -0
  453. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.ts +14 -0
  454. package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
  455. package/templates/be/skeleton/src/modules/platform/database/index.ts +2 -0
  456. package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +8 -0
  457. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +25 -6
  458. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +16 -6
  459. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +130 -0
  460. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +58 -0
  461. package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +3 -0
  462. package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -4
  463. package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -10
  464. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +3 -1
  465. package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +2 -0
  466. package/templates/be/skeleton/src/modules/platform/http-security/index.ts +5 -0
  467. package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +12 -5
  468. package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +13 -8
  469. package/templates/be/skeleton/src/modules/platform/http-security/readiness.contracts.ts +5 -0
  470. package/templates/be/skeleton/src/modules/platform/http-security/readiness.decorators.ts +7 -0
  471. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +86 -0
  472. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +39 -0
  473. package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +139 -131
  474. package/templates/be/skeleton/src/modules/platform/i18n/index.ts +1 -1
  475. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +91 -67
  476. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +5 -4
  477. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +0 -2
  478. package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +20 -6
  479. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +16 -4
  480. package/templates/be/skeleton/src/modules/platform/retry/index.ts +3 -0
  481. package/templates/be/skeleton/src/modules/platform/retry/retry.decorators.ts +9 -0
  482. package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +7 -0
  483. package/templates/be/skeleton/src/modules/platform/retry/retry.module.ts +19 -0
  484. package/templates/be/skeleton/src/modules/platform/retry/retry.options.ts +2 -0
  485. package/templates/be/skeleton/src/modules/platform/retry/retry.port.ts +11 -0
  486. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.spec.ts +36 -0
  487. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.ts +10 -0
  488. package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +54 -0
  489. package/templates/be/skeleton-lite/apps/api/src/api.env.ts +25 -0
  490. package/templates/be/skeleton-lite/apps/api/src/api.options.ts +1 -1
  491. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +18 -58
  492. package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +4 -0
  493. package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +10 -0
  494. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +16 -0
  495. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +47 -0
  496. package/templates/be/skeleton-lite/apps/api/src/compose/system.ts +5 -0
  497. package/templates/be/skeleton-lite/apps/api/src/compose/webhooks.ts +4 -0
  498. package/templates/be/skeleton-lite/apps/api/src/main.ts +5 -13
  499. package/templates/be/skeleton-lite/apps/cli/src/cli.env.ts +17 -0
  500. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +12 -0
  501. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +71 -5
  502. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +29 -2
  503. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +68 -15
  504. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +14 -9
  505. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +9 -2
  506. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +16 -1
  507. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +19 -4
  508. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +5 -1
  509. package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -1
  510. package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -3
  511. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +17 -5
  512. package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +24 -13
  513. package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +5 -0
  514. package/templates/be/skeleton-lite/src/modules/platform/database/errors/database.error.ts +16 -0
  515. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +3 -0
  516. package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +5 -2
  517. package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +4 -1
  518. package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
  519. package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +7 -0
  520. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +29 -0
  521. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +16 -0
  522. package/templates/be/table/api/feature.module-definition.ts.tpl +6 -0
  523. package/templates/be/table/api/feature.module.ts.tpl +2 -1
  524. package/templates/be/table/api/feature.options.ts.tpl +2 -0
  525. package/templates/be/table/api/graphql.module-definition.ts.tpl +6 -0
  526. package/templates/be/table/api/graphql.module.ts.tpl +3 -2
  527. package/templates/be/table/api/graphql.options.ts.tpl +2 -0
  528. package/templates/be/table/migration.sql.tpl +1 -0
  529. package/templates/be/upgrade-full/apps/cli/src/app.module.ts +4 -6
  530. package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +7 -0
  531. package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +5 -0
  532. package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +9 -0
  533. package/templates/be/upgrade-full/apps/cli/src/main.ts +3 -0
  534. package/templates/be/upgrade-full/src/features/cli/cli.module.ts +3 -2
  535. package/templates/fe/auth/supabase-client.ts.tpl +34 -0
  536. package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +11 -0
  537. package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/index.tsx +6 -1
  538. package/templates/fe/skeleton/apps/__app__/src/components/leaves/Icon/index.tsx +4 -0
  539. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/component.tsx +31 -0
  540. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/index.tsx +24 -0
  541. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -1
  542. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +3 -4
  543. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/index.ts +1 -0
  544. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +15 -0
  545. package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +18 -0
  546. package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +19 -0
  547. package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +7 -1
  548. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/en.json +57 -0
  549. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +6 -0
  550. package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/en.json +31 -0
  551. package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +1 -1
  552. package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -0
  553. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +1 -34
  554. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +7 -1
  555. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +57 -0
  556. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +30 -24
  557. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +1 -1
  558. package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
  559. package/templates/fe/transport/graphql/clients/links/locale.ts +2 -2
  560. package/upgrade/check.mjs +3 -1
  561. package/upgrade/file-state.mjs +68 -0
  562. package/upgrade/index.mjs +58 -94
  563. package/runtime/scripts/api/git/log.mjs +0 -5
  564. package/templates/app/skeleton-lite/.starciwork/shell/index.yaml +0 -55
@@ -1,15 +1,16 @@
1
1
  // edition.mjs - L01, the edition rule. An hfs.json edition is `full` or `lite` (absent means full); under lite the standard is a
2
- // filter plus a few additions over the same manifest, never a second engine. Lite is the full architecture deployed without
3
- // self-hosted infrastructure (owner ruling 2026-10-12): the same slots, patterns and unit specs, Supabase adapters behind the same
4
- // ports, and no e2e or contract world. The slot layer already answers the paths lite removed (slots lite excludes, or whose
5
- // litePresence is forbidden, come back HFS_FORBIDDEN_PRESENT with their goesTo); this rule reports what no path can state:
6
- // - inside a package.json: an e2e, contract, stack or browser script (`test:e2e`, `test:contract`, `test:stack`, `test:browser`,
2
+ // filter plus a few additions over the same manifest, never a second engine. Lite declares Docker Swarm production infrastructure
3
+ // (owner:lite-production-swarm, ruling:R212/case-4): the same slots, patterns and unit specs, provider adapters behind the same
4
+ // ports, optional contract specs, and no e2e world (owner:lite-contract-specs). The slot layer already answers the paths lite
5
+ // removed (slots lite excludes, or whose litePresence is forbidden, come back HFS_FORBIDDEN_PRESENT with their goesTo);
6
+ // this rule reports what no path can state:
7
+ // - inside a package.json: an e2e, stack or browser script (`test:e2e`, `test:stack`, `test:browser`,
7
8
  // with a pre/post prefix too) or a dependency of the infrastructure lite does not run - a test-world container or proxy
8
9
  // (testcontainers, toxiproxy) or a browser runner (playwright, cypress), and every library the lite infraOwners own nowhere
9
10
  // while full owns them (BullMQ, Kafka, Redis: lite runs pgmq and Postgres locks instead);
10
11
  // - a declaration of what lite does not have: an app kind, pattern or trigger kind every slot of which is forbidden under
11
12
  // lite. The forbidden names are DERIVED from the manifest, so adding or restoring a kind is a manifest edit, not an edit here
12
- // (under the 2026-10-12 ruling no pattern, trigger or app kind is gone).
13
+ // (under the 2026-10-13 ruling no pattern, trigger or app kind is gone).
13
14
  // A full app answers []: the rule tests the edition once and a full view forbids none of these names.
14
15
  import { found, readJson } from './read.mjs';
15
16
  import { DEPENDENCY_SECTIONS } from './fe-no-tests.mjs';
@@ -18,8 +19,8 @@ import { editionRuleParams } from '../edition-slots.mjs';
18
19
  export const HFS_EDITION_FORBIDDEN_PRESENT = 'HFS_EDITION_FORBIDDEN_PRESENT';
19
20
 
20
21
  const PACKAGE_JSON = /(?:^|\/)package\.json$/;
21
- /** The scripts of a world lite does not have: the e2e and contract layers, the self-hosted stack and the browser journey. */
22
- const LITE_FORBIDDEN_SCRIPT = /^(?:pre|post)?test:(?:e2e|contract|stack|browser)$/;
22
+ /** The scripts of a world lite does not have: the e2e layer, the self-hosted stack and the browser journey. */
23
+ const LITE_FORBIDDEN_SCRIPT = /^(?:pre|post)?test:(?:e2e|stack|browser)$/;
23
24
  /** The dependencies of a test infrastructure lite does not run: containers, fault proxies and browser runners. */
24
25
  const LITE_FORBIDDEN_TEST_INFRA = /^(?:testcontainers|@testcontainers\/.+|toxiproxy-node-client|playwright|playwright-core|@playwright\/.+|cypress)$/;
25
26
 
@@ -68,7 +69,7 @@ function scriptFindings(rel, pkg) {
68
69
  const scripts = isPlainObject(pkg.scripts) ? pkg.scripts : {};
69
70
  return Object.keys(scripts)
70
71
  .filter((name) => LITE_FORBIDDEN_SCRIPT.test(name))
71
- .map((name) => found(HFS_EDITION_FORBIDDEN_PRESENT, rel, `${rel} has script ${JSON.stringify(name)}: a lite app has unit specs and optional integration specs on the local Supabase stack, never an e2e, contract, stack or browser run`, { name }));
72
+ .map((name) => found(HFS_EDITION_FORBIDDEN_PRESENT, rel, `${rel} has script ${JSON.stringify(name)}: a lite app has unit specs, optional integration specs on the local Supabase stack and optional contract specs, never an e2e, stack or browser run`, { name }));
72
73
  }
73
74
 
74
75
  function dependencyFindings(rel, pkg, removed) {
@@ -78,7 +79,7 @@ function dependencyFindings(rel, pkg, removed) {
78
79
  if (!isPlainObject(deps)) continue;
79
80
  for (const name of Object.keys(deps)) {
80
81
  if (LITE_FORBIDDEN_TEST_INFRA.test(name)) findings.push(found(HFS_EDITION_FORBIDDEN_PRESENT, rel, `${rel} ${section} has ${name}: a lite app runs no test containers, fault proxy or browser journey - its integration specs run against \`supabase start\``, { section, name }));
81
- else if (removed.has(name)) findings.push(found(HFS_EDITION_FORBIDDEN_PRESENT, rel, `${rel} ${section} has ${name}: lite runs no self-hosted infrastructure - the pattern's port is implemented on Supabase (pgmq, Postgres locks)`, { section, name }));
82
+ else if (removed.has(name)) findings.push(found(HFS_EDITION_FORBIDDEN_PRESENT, rel, `${rel} ${section} has ${name}: lite replaces backing infrastructure with Supabase - the pattern's port is implemented on Supabase (pgmq, Postgres locks)`, { section, name }));
82
83
  }
83
84
  }
84
85
  return findings;
@@ -0,0 +1,373 @@
1
+ // env-parity.mjs - R248 HFS_ENV_PARITY (OPS K5/K11): the environment keys a back end reads, the
2
+ // keys sealed under `.starcistacks/<env>/secrets/*.enc` and the keys the stack declares or passes are one set,
3
+ // compared by name only (owner ruling 0.1.7: every back-end app reads its environment only through the typed
4
+ // `EnvSource` of `<app>.env.ts` and the capability `*.config.ts` parse helpers).
5
+ // R = keys the typed `EnvSource` readers consume, found by a static TypeScript walk of `*.env.ts`, `*.config.ts`
6
+ // and `apps/<app>/src/main.ts`: `env.string|url|enum|secret` and `env.int|bool|duration` without a literal
7
+ // fallback are required reads; `env.optional`, `env.has`, `env.anyDeclared` and a fallback read are optional.
8
+ // A template key `${prefix}_WEBHOOK_SECRET` resolves through the literal arguments of its reader's call sites
9
+ // (`parseWebhookProviderConfig(env, "CALENDAR")` yields the key CALENDAR_WEBHOOK_SECRET).
10
+ // S = the key names of the sealed members: a sops envelope keeps its key names cleartext, so `envelopeOf` answers
11
+ // them without decrypting anything.
12
+ // D = keys declared as public config or passed by the stack: the backticked names of
13
+ // `.starcistacks/<env>/runtime/env/KEYS.md`, and the `environment:`/`env:` entries, `runtimeSecrets`, `secrets`
14
+ // and `vars[].name` of every `.starcistacks` yaml. A `<KEY>_FILE` name declares `<KEY>`.
15
+ // Findings: ENV_REQUIRED_UNSET a required read sits in neither S nor D; ENV_SEALED_UNREAD a sealed key sits in
16
+ // neither R nor a declared consumer; ENV_DUPLICATE one key in two members of one environment; ENV_PUBLIC_SEALED a NEXT_PUBLIC_* key in
17
+ // custody; ENV_EXAMPLE_DRIFT a `*.env.example` key nothing reads. The rule judges the app root (`isRoot`): the whole
18
+ // tree `all` is its input, both editions.
19
+ import path from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
21
+ import { loadTypescript } from '../../lib/package-at.mjs';
22
+ import { parseYaml } from '../../../engine/yaml.mjs';
23
+ import { envelopeOf } from '../secret-document.mjs';
24
+ import { found, readText } from './read.mjs';
25
+
26
+ export const ENV_REQUIRED_UNSET = 'ENV_REQUIRED_UNSET';
27
+ export const ENV_SEALED_UNREAD = 'ENV_SEALED_UNREAD';
28
+ export const ENV_DUPLICATE = 'ENV_DUPLICATE';
29
+ export const ENV_PUBLIC_SEALED = 'ENV_PUBLIC_SEALED';
30
+ export const ENV_EXAMPLE_DRIFT = 'ENV_EXAMPLE_DRIFT';
31
+
32
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
33
+ const ENV_SOURCE = /(?:\.env|\.config)\.ts$/u;
34
+ const APP_MAIN = /(?:^|\/)apps\/[^/]+\/src\/main\.ts$/u;
35
+ const SEALED_MEMBER = /^\.starcistacks\/[^/]+\/secrets\/[^/]+\.enc$/u;
36
+ const KEYS_ROSTER = /^\.starcistacks\/[^/]+\/runtime\/env\/KEYS\.md$/u;
37
+ const STACK_YAML = /^\.starcistacks\/.+\.ya?ml$/u;
38
+ const ENV_EXAMPLE = /\.env\.example$/u;
39
+ const PUBLIC_PREFIX = /^NEXT_PUBLIC_/u;
40
+ const KEY_NAME = /^[A-Za-z_]\w*$/u;
41
+ const FILE_SUFFIX = /_FILE$/u;
42
+ const EXAMPLE_LINE = /^[ \t]*(?:export[ \t]+)?([A-Za-z_]\w*)[ \t]*=(.*)$/u;
43
+ const ROSTER_KEY = /`([A-Za-z_][A-Za-z0-9_]*)`/gu;
44
+
45
+ const REQUIRED_ALWAYS = new Set(['string', 'url', 'secret', 'enum']);
46
+ const FALLBACK_OPTIONAL = new Set(['int', 'bool', 'duration']);
47
+ const OPTIONAL_ONLY = new Set(['optional', 'has', 'anyDeclared']);
48
+ const READER_METHODS = new Set([...REQUIRED_ALWAYS, ...FALLBACK_OPTIONAL, ...OPTIONAL_ONLY]);
49
+
50
+ const typescriptFor = (repoRoot) => loadTypescript(repoRoot, HERE);
51
+
52
+ /** A path the static walk reads: `<app>.env.ts`, `<capability>.config.ts` or an app `main.ts`. */
53
+ export const isEnvSourcePath = (file) => ENV_SOURCE.test(file) || APP_MAIN.test(file);
54
+
55
+ /** The `KEY=value` entries of one `*.env.example` text, comments and blank lines out, quotes unwrapped. */
56
+ export function envExampleEntries(text) {
57
+ const entries = [];
58
+ for (const line of String(text).split(/\r?\n/u)) {
59
+ if (!line.trim() || line.trimStart().startsWith('#')) continue;
60
+ const match = EXAMPLE_LINE.exec(line);
61
+ if (!match) continue;
62
+ let value = match[2].trim();
63
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) value = value.slice(1, -1);
64
+ entries.push({ key: match[1], value });
65
+ }
66
+ return entries;
67
+ }
68
+
69
+ /** The literal key or template parts a key argument holds; null when it is not statically a name. */
70
+ function keyParts(ts, node, params) {
71
+ if (ts.isStringLiteralLike(node)) return [node.text];
72
+ if (!ts.isTemplateExpression(node)) return null;
73
+ const parts = [node.head.text];
74
+ for (const span of node.templateSpans) {
75
+ if (!ts.isIdentifier(span.expression) || !params.includes(span.expression.text)) return null;
76
+ parts.push({ param: span.expression.text }, span.literal.text);
77
+ }
78
+ return parts;
79
+ }
80
+
81
+ /** The env reader context of a function: its EnvSource-typed (or bare `env`) parameter, or null. */
82
+ function envContextOf(ts, sourceFile, node) {
83
+ const params = node.parameters ?? [];
84
+ const names = params.map((param) => (ts.isIdentifier(param.name) ? param.name.text : null));
85
+ const index = params.findIndex((param, i) => names[i] !== null
86
+ && (param.type?.getText(sourceFile) === 'EnvSource' || (param.type === undefined && names[i] === 'env')));
87
+ if (index === -1) return null;
88
+ let fn = null;
89
+ if (node.name && ts.isIdentifier(node.name)) fn = node.name.text;
90
+ else if (ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) fn = node.parent.name.text;
91
+ return { envParam: names[index], params: names.filter(Boolean), fn };
92
+ }
93
+
94
+ const isFunction = (ts, node) => ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node);
95
+
96
+ /** The shape a reader method declares: `{kind}` for the typed readers, `{kind:'enum', allowed}` for env.enum, null else. */
97
+ function shapeOf(ts, method, args) {
98
+ if (method === 'enum') {
99
+ const list = args[1];
100
+ if (!list || !ts.isArrayLiteralExpression(list)) return null;
101
+ const allowed = list.elements.map((element) => (ts.isStringLiteralLike(element) ? element.text : null));
102
+ return allowed.every((value) => value !== null) ? { kind: 'enum', allowed } : null;
103
+ }
104
+ const kind = { string: 'string', secret: 'string', int: 'int', bool: 'bool', url: 'url', duration: 'duration' }[method] ?? null;
105
+ return kind === null ? null : { kind };
106
+ }
107
+
108
+ function noteReadParts(ctx, parts, entry, literals, templates) {
109
+ if (parts === null) return;
110
+ if (parts.every((part) => typeof part === 'string')) literals.push({ key: parts.join(''), ...entry });
111
+ else templates.push({ ...ctx, parts, ...entry });
112
+ }
113
+
114
+ function readAnyDeclared(ts, ctx, keyArg, entry, literals, templates) {
115
+ if (!keyArg || !ts.isArrayLiteralExpression(keyArg)) return;
116
+ const optionalEntry = { ...entry, required: false, shape: null };
117
+ for (const element of keyArg.elements) {
118
+ const parts = ts.isStringLiteralLike(element) ? [element.text] : keyParts(ts, element, ctx.params);
119
+ noteReadParts(ctx, parts, optionalEntry, literals, templates);
120
+ }
121
+ }
122
+
123
+ /** One `env.<method>(...)` call of context `ctx` as a read entry; `required` follows the reader's fallback contract. */
124
+ function readCall(ts, ctx, node, file, literals, templates) {
125
+ const method = node.expression.name.text;
126
+ const args = [...node.arguments];
127
+ const required = REQUIRED_ALWAYS.has(method) || (FALLBACK_OPTIONAL.has(method) && args.length < 2);
128
+ const entry = { method, required, shape: required || FALLBACK_OPTIONAL.has(method) ? shapeOf(ts, method, args) : null, file };
129
+ const keyArg = args[0];
130
+ if (method === 'anyDeclared') {
131
+ readAnyDeclared(ts, ctx, keyArg, entry, literals, templates);
132
+ return;
133
+ }
134
+ const parts = keyArg === undefined ? null : keyParts(ts, keyArg, ctx.params);
135
+ noteReadParts(ctx, parts, entry, literals, templates);
136
+ }
137
+
138
+ /** Every env read of one parsed file: concrete literals, per-reader templates and every named call site. */
139
+ function readsInFile(ts, sourceFile, rel) {
140
+ const literals = [];
141
+ const templates = [];
142
+ const calls = [];
143
+ const walk = (node, ctx) => {
144
+ if (isFunction(ts, node)) ctx = envContextOf(ts, sourceFile, node) ?? ctx;
145
+ if (ts.isCallExpression(node)) {
146
+ if (ctx && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression)
147
+ && node.expression.expression.text === ctx.envParam && READER_METHODS.has(node.expression.name.text)) {
148
+ readCall(ts, ctx, node, rel, literals, templates);
149
+ } else if (ts.isIdentifier(node.expression)) {
150
+ calls.push({ name: node.expression.text, args: node.arguments.map((arg) => (ts.isStringLiteralLike(arg) ? arg.text : null)), file: rel });
151
+ }
152
+ }
153
+ ts.forEachChild(node, (child) => walk(child, ctx));
154
+ };
155
+ walk(sourceFile, null);
156
+ return { literals, templates, calls };
157
+ }
158
+
159
+ function noteEnvRead(reads, { key, required, shape, file }) {
160
+ const held = reads.get(key);
161
+ if (!held) { reads.set(key, { key, required, shape, file }); return; }
162
+ held.required = held.required || required;
163
+ held.shape = held.shape ?? shape;
164
+ }
165
+
166
+ function noteReaderTemplates(readers, templates) {
167
+ for (const template of templates) {
168
+ if (!template.fn) continue;
169
+ if (!readers.has(template.fn)) readers.set(template.fn, { params: template.params, templates: [] });
170
+ readers.get(template.fn).templates.push(template);
171
+ }
172
+ }
173
+
174
+ function resolvedTemplateRead(reader, template, call) {
175
+ const parts = template.parts.map((part) => {
176
+ if (typeof part === 'string') return part;
177
+ const arg = call.args[reader.params.indexOf(part.param)];
178
+ return typeof arg === 'string' ? arg : null;
179
+ });
180
+ if (parts.includes(null)) return null;
181
+ return { key: parts.join(''), required: template.required, shape: template.shape, file: call.file };
182
+ }
183
+
184
+ function noteReaderCall(reads, readers, call) {
185
+ const reader = readers.get(call.name);
186
+ if (!reader) return;
187
+ for (const template of reader.templates) {
188
+ const read = resolvedTemplateRead(reader, template, call);
189
+ if (read !== null) noteEnvRead(reads, read);
190
+ }
191
+ }
192
+
193
+ /**
194
+ * The keys the repository's EnvSource readers consume: Map `KEY` -> {key, required, shape, file}. `relPaths` are
195
+ * repository-relative paths; the env source files (`isEnvSourcePath`) among them are walked. `ts` is the caller's
196
+ * already-resolved compiler; null when none resolves, so the caller decides what "cannot be judged" means.
197
+ */
198
+ export function collectEnvReads(repoRoot, relPaths, ts = typescriptFor(repoRoot)) {
199
+ if (!ts) return null;
200
+ const reads = new Map();
201
+ const readers = new Map();
202
+ const calls = [];
203
+ for (const rel of relPaths.filter(isEnvSourcePath)) {
204
+ const text = readText(repoRoot, rel);
205
+ if (text === null) continue;
206
+ const foundIn = readsInFile(ts, ts.createSourceFile(rel, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS), rel);
207
+ for (const literal of foundIn.literals) noteEnvRead(reads, literal);
208
+ noteReaderTemplates(readers, foundIn.templates);
209
+ calls.push(...foundIn.calls);
210
+ }
211
+ for (const call of calls) noteReaderCall(reads, readers, call);
212
+ return reads;
213
+ }
214
+
215
+ /** The sealed members' key names without decrypting: Map `KEY` -> the member paths holding it, sorted. */
216
+ function sealedKeys(repoRoot, all) {
217
+ const members = new Map();
218
+ for (const file of all) {
219
+ if (!SEALED_MEMBER.test(file)) continue;
220
+ const text = readText(repoRoot, file);
221
+ if (text === null) continue;
222
+ let envelope;
223
+ try { envelope = envelopeOf(text); } catch { continue; }
224
+ for (const key of envelope.keys) {
225
+ if (!members.has(key)) members.set(key, []);
226
+ members.get(key).push(file);
227
+ }
228
+ }
229
+ for (const list of members.values()) list.sort((left, right) => (left < right ? -1 : Number(left > right)));
230
+ return members;
231
+ }
232
+
233
+ function declaredEnvironment(value, add) {
234
+ if (!Array.isArray(value)) {
235
+ for (const name of Object.keys(value)) add(name);
236
+ return;
237
+ }
238
+ for (const entry of value) {
239
+ if (typeof entry !== 'string') continue;
240
+ const name = /^([A-Za-z_]\w*)\s*=/u.exec(entry)?.[1] ?? (KEY_NAME.test(entry) ? entry : null);
241
+ if (name) add(name);
242
+ }
243
+ }
244
+
245
+ function declaredEntryName(entry, allowString) {
246
+ if (allowString && typeof entry === 'string') return entry;
247
+ if (entry && typeof entry === 'object' && typeof entry.name === 'string') return entry.name;
248
+ return null;
249
+ }
250
+
251
+ function declaredNamedEntries(entries, add, allowString) {
252
+ for (const entry of entries) {
253
+ const name = declaredEntryName(entry, allowString);
254
+ if (name !== null) add(name);
255
+ }
256
+ }
257
+
258
+ function declaredYamlEntry(key, value, add) {
259
+ if ((key === 'environment' || key === 'env') && value && typeof value === 'object') {
260
+ declaredEnvironment(value, add);
261
+ return;
262
+ }
263
+ if ((key === 'runtimeSecrets' || key === 'secrets') && Array.isArray(value)) {
264
+ declaredNamedEntries(value, add, true);
265
+ return;
266
+ }
267
+ if (key === 'vars' && Array.isArray(value)) {
268
+ declaredNamedEntries(value, add, false);
269
+ return;
270
+ }
271
+ declaredFromYaml(value, add);
272
+ }
273
+
274
+ /** The env names one yaml node declares: `environment:`/`env:` keys or `K=V` entries, `runtimeSecrets`, `secrets` and `vars[].name`. */
275
+ function declaredFromYaml(node, add) {
276
+ if (!node || typeof node !== 'object') return;
277
+ if (Array.isArray(node)) { for (const item of node) declaredFromYaml(item, add); return; }
278
+ for (const [key, value] of Object.entries(node)) declaredYamlEntry(key, value, add);
279
+ }
280
+
281
+ function addDeclaredKey(declared, name) {
282
+ if (!KEY_NAME.test(name)) return;
283
+ declared.add(name);
284
+ if (FILE_SUFFIX.test(name)) declared.add(name.replace(FILE_SUFFIX, ''));
285
+ }
286
+
287
+ function declareKeysFromText(file, text, add) {
288
+ if (KEYS_ROSTER.test(file)) {
289
+ for (const match of text.matchAll(ROSTER_KEY)) if (/[A-Z]/u.test(match[1])) add(match[1]);
290
+ return;
291
+ }
292
+ try { declaredFromYaml(parseYaml(text), add); } catch { /* a malformed stacks yaml is the shape rule's finding, not this one's */ }
293
+ }
294
+
295
+ /** The keys declared as public config or passed by the stack: KEYS.md rosters plus `.starcistacks` yaml; `<KEY>_FILE` declares `<KEY>`. */
296
+ function declaredKeys(repoRoot, all) {
297
+ const declared = new Set();
298
+ const add = (name) => addDeclaredKey(declared, name);
299
+ for (const file of all) {
300
+ if (!KEYS_ROSTER.test(file) && !STACK_YAML.test(file)) continue;
301
+ const text = readText(repoRoot, file);
302
+ if (text !== null) declareKeysFromText(file, text, add);
303
+ }
304
+ return declared;
305
+ }
306
+
307
+ function addRequiredFindings(reads, sealed, declared, findings) {
308
+ for (const read of reads.values()) {
309
+ if (read.required && !sealed.has(read.key) && !declared.has(read.key)) {
310
+ findings.push(found(ENV_REQUIRED_UNSET, read.file, `${read.file} reads required ${read.key}, which is neither sealed under .starcistacks/<env>/secrets/*.enc nor declared by the stack or KEYS.md`));
311
+ }
312
+ }
313
+ }
314
+
315
+ function sealedMemberFinding(key, member, reads, declared) {
316
+ // The specific public-custody code wins over the generic unread one for a NEXT_PUBLIC_* key.
317
+ if (PUBLIC_PREFIX.test(key)) {
318
+ return found(ENV_PUBLIC_SEALED, member, `${member} seals the public key ${key}; NEXT_PUBLIC_* values are public configuration and never enter custody`);
319
+ }
320
+ if (!reads.has(key) && !declared.has(key)) {
321
+ return found(ENV_SEALED_UNREAD, member, `${member} seals ${key}, which no EnvSource reader and no declared consumer uses; unseal it or declare who consumes it`);
322
+ }
323
+ return null;
324
+ }
325
+
326
+ function addSealedFindings(sealed, reads, declared, findings) {
327
+ for (const [key, members] of sealed) {
328
+ for (const member of members) {
329
+ const finding = sealedMemberFinding(key, member, reads, declared);
330
+ if (finding !== null) findings.push(finding);
331
+ }
332
+ addDuplicateFindings(key, members, findings);
333
+ }
334
+ }
335
+
336
+ function addDuplicateFindings(key, members, findings) {
337
+ const firstByEnvironment = new Map();
338
+ for (const member of members) {
339
+ const environment = member.split('/')[1];
340
+ const first = firstByEnvironment.get(environment);
341
+ if (first === undefined) {
342
+ firstByEnvironment.set(environment, member);
343
+ continue;
344
+ }
345
+ findings.push(found(ENV_DUPLICATE, member, `${key} is sealed in ${first} and again in ${member}; one key lives in one member per environment`));
346
+ }
347
+ }
348
+
349
+ function addExampleFindings(repoRoot, all, reads, findings) {
350
+ for (const file of all.filter((rel) => ENV_EXAMPLE.test(rel))) {
351
+ const text = readText(repoRoot, file);
352
+ if (text === null) continue;
353
+ for (const { key } of envExampleEntries(text)) {
354
+ if (!reads.has(key)) findings.push(found(ENV_EXAMPLE_DRIFT, file, `${file} names ${key}, which no EnvSource reader consumes; align the example with the keys the code reads`));
355
+ }
356
+ }
357
+ }
358
+
359
+ /** The findings of R248 over the tracked paths `all` of an app judged at its root scope (both editions). */
360
+ export function envParityFindings({ repoRoot, all = [], isRoot }) {
361
+ if (!isRoot) return [];
362
+ const reads = collectEnvReads(repoRoot, all);
363
+ if (reads === null) {
364
+ return [found(ENV_REQUIRED_UNSET, 'be/', 'the env-key parity cannot be judged: no TypeScript resolves from the app; install the app\'s dependencies', { missing: ['typescript'] })];
365
+ }
366
+ const sealed = sealedKeys(repoRoot, all);
367
+ const declared = declaredKeys(repoRoot, all);
368
+ const findings = [];
369
+ addRequiredFindings(reads, sealed, declared, findings);
370
+ addSealedFindings(sealed, reads, declared, findings);
371
+ addExampleFindings(repoRoot, all, reads, findings);
372
+ return findings;
373
+ }
@@ -0,0 +1,113 @@
1
+ // env-value-shape.mjs - R249 HFS_ENV_VALUE_SHAPE (OPS K6: `OPENCLAW_MANAGE_AGENTS=1` for an `env.bool`
2
+ // read): a stored environment value parses as the type its `EnvSource` reader declares. The reader shapes come from
3
+ // the same static TypeScript walk as the env-key parity (collectEnvReads).
4
+ // - the finder judges the concrete values of `*.env.example` files: a value that is not a placeholder must parse;
5
+ // - `starci app secret verify <env>` (verifySecrets) decrypts every member of the environment in memory through the
6
+ // sops seam and checks each stored value against its reader's shape; output names the environment, the member,
7
+ // the key and the violated shape - never a value.
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { loadTypescript } from '../../lib/package-at.mjs';
12
+ import { byCodeUnit } from '../../lib/list.mjs';
13
+ import { envelopeOf } from '../secret-document.mjs';
14
+ import { sealedFiles } from '../secret-store.mjs';
15
+ import { SecretError } from '../secret-errors.mjs';
16
+ import { collectEnvReads, envExampleEntries, isEnvSourcePath } from './env-parity.mjs';
17
+ import { found, readText } from './read.mjs';
18
+
19
+ export const ENV_VALUE_SHAPE = 'ENV_VALUE_SHAPE';
20
+
21
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
22
+ const FILE_SUFFIX = /_FILE$/u;
23
+ const ENV_EXAMPLE = /\.env\.example$/u;
24
+ const SKIP_DIRECTORY = new Set(['node_modules', 'dist', 'coverage', '.git']);
25
+
26
+ const SHAPES = Object.freeze({
27
+ int: { text: 'an integer', ok: (value) => value.trim() !== '' && Number.isInteger(Number(value)) },
28
+ bool: { text: 'true or false', ok: (value) => value === 'true' || value === 'false' },
29
+ url: { text: 'an absolute URL', ok: (value) => URL.canParse(value) },
30
+ duration: { text: 'a duration (250, 250ms, 30s, 5m or 2h)', ok: (value) => /^\d+(?:ms|s|m|h)?$/u.test(value) },
31
+ string: { text: 'a nonempty value', ok: (value) => value.length > 0 },
32
+ });
33
+
34
+ /** What a shape violation is called in output; the enum shape names its allowed values. */
35
+ const shapeText = (shape) => (shape.kind === 'enum' ? `one of ${shape.allowed.join(', ')}` : SHAPES[shape.kind].text);
36
+
37
+ /** Whether `value` parses as `shape` declares. */
38
+ const shapeOk = (shape, value) => (shape.kind === 'enum' ? shape.allowed.includes(value) : SHAPES[shape.kind]?.ok(value) ?? true);
39
+
40
+ /** An example line holds a documented stand-in, not a value to parse: empty, an `<...>` name or a `${...}` reference. */
41
+ const isPlaceholder = (value) => value === '' || value.includes('${') || (value.startsWith('<') && value.endsWith('>'));
42
+
43
+ /** The repository-relative env source files on disk: the be side of an app, else the src tree of a back-end repository. */
44
+ function listEnvSourceFiles(repoRoot) {
45
+ const out = [];
46
+ const walk = (dir, rel) => {
47
+ let entries;
48
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
49
+ for (const entry of entries) {
50
+ if (entry.isDirectory()) {
51
+ if (!SKIP_DIRECTORY.has(entry.name)) walk(path.join(dir, entry.name), `${rel}${entry.name}/`);
52
+ } else if (isEnvSourcePath(rel + entry.name)) out.push(rel + entry.name);
53
+ }
54
+ };
55
+ const roots = ['be', 'src'].map((name) => path.join(repoRoot, name)).filter((dir) => { try { return fs.statSync(dir).isDirectory(); } catch { return false; } });
56
+ for (const root of roots) walk(root, `${path.basename(root)}/`);
57
+ return out.sort(byCodeUnit);
58
+ }
59
+
60
+ /** The ENV_VALUE_SHAPE violations of one decrypted map against the walked reader shapes; keys and shapes only. */
61
+ function shapeViolations(reads, map) {
62
+ const violations = [];
63
+ for (const [key, value] of Object.entries(map)) {
64
+ if (FILE_SUFFIX.test(key)) continue;
65
+ const shape = reads.get(key)?.shape;
66
+ if (shape && !shapeOk(shape, value)) violations.push({ key, expected: shapeText(shape) });
67
+ }
68
+ return violations;
69
+ }
70
+
71
+ /** The findings of R249 over the `*.env.example` files of an app judged at its root scope (both editions). */
72
+ export function envValueShapeFindings({ repoRoot, all = [], isRoot }) {
73
+ if (!isRoot) return [];
74
+ const examples = all.filter((file) => ENV_EXAMPLE.test(file));
75
+ if (!examples.length) return [];
76
+ const reads = collectEnvReads(repoRoot, all);
77
+ if (reads === null) return [];
78
+ const findings = [];
79
+ for (const file of examples) {
80
+ const text = readText(repoRoot, file);
81
+ if (text === null) continue;
82
+ for (const { key, value } of envExampleEntries(text)) {
83
+ if (isPlaceholder(value)) continue;
84
+ const shape = reads.get(key)?.shape;
85
+ if (shape && !shapeOk(shape, value)) {
86
+ findings.push(found(ENV_VALUE_SHAPE, file, `${file} gives ${key} a value that is not ${shapeText(shape)}; an env example must parse as the EnvSource reader declares`));
87
+ }
88
+ }
89
+ }
90
+ return findings;
91
+ }
92
+
93
+ /**
94
+ * `starci app secret verify <env>`: decrypt every member of `directory` in memory through the `sops` seam and check
95
+ * each value against the shape its EnvSource reader declares. Output names the environment, the member, the key and
96
+ * the violated shape, never a value. Returns the CLI exit code: 0 verified, 2 violations.
97
+ */
98
+ export function verifySecrets({ repoRoot, directory, environment, sops, stdout, typescript }) {
99
+ const ts = typescript ?? loadTypescript(repoRoot, HERE);
100
+ if (!ts) throw new SecretError('verify needs the app\'s TypeScript to read <app>.env.ts; install the app\'s dependencies');
101
+ const reads = collectEnvReads(repoRoot, listEnvSourceFiles(repoRoot), ts);
102
+ const violations = [];
103
+ let keys = 0;
104
+ for (const name of sealedFiles(directory)) {
105
+ const file = path.join(directory, name);
106
+ const map = sops.decrypt(file, envelopeOf(fs.readFileSync(file, 'utf8')).format);
107
+ keys += Object.keys(map).length;
108
+ for (const violation of shapeViolations(reads, map)) violations.push({ member: name, ...violation });
109
+ }
110
+ for (const violation of violations) stdout(`ENV_VALUE_SHAPE ${environment}/${violation.member}: ${violation.key} is not ${violation.expected}\n`);
111
+ if (!violations.length) stdout(`verified ${environment}: ${keys} key${keys === 1 ? '' : 's'} sealed\n`);
112
+ return violations.length ? 2 : 0;
113
+ }
@@ -2,7 +2,7 @@
2
2
  // BE_EVENT_CLASS_CONTRACT every typed event class `be/src/modules/events/<service>/<event>.event.ts` equals an entry of the vendored
3
3
  // contract `be/contracts/<service>/events.json` (the same name and version), every entry of a vendored
4
4
  // contract has its class, and a class file is named after its event (`order.placed` -> `order-placed.event.ts`);
5
- // BE_PATTERN_SPEC_MISSING a declared pattern has its required scenarios: ruleParams.be.patternScenarios.<pattern> lists scenario ids,
5
+ // BE_PATTERN_SPEC_MISSING a full-edition declared pattern has its required scenarios: ruleParams.be.patternScenarios.<pattern> lists scenario ids,
6
6
  // and an e2e or integration spec of the back end holds a test whose title starts `<pattern>/<scenario>:`.
7
7
  // Class members are read with TypeScript's own parser (a `static readonly eventName` and `version` with literal initializers), spec
8
8
  // titles are the first argument of `it(` and `test(` calls; no source text is matched by a regular expression.
@@ -181,8 +181,9 @@ let memoScenarios;
181
181
  /** ruleParams.be.patternScenarios of the slot manifest this runtime ships. */
182
182
  const scenariosTable = () => (memoScenarios ??= ruleParams(loadSlotManifest(), 'be').patternScenarios ?? {});
183
183
 
184
- /** Findings of BE_PATTERN_SPEC_MISSING: each declared pattern's required scenarios have a test. */
184
+ /** Full-edition proof scenarios; lite integration proofs are judged by lite-proof-lane.mjs. */
185
185
  export function patternSpecFindings({ repoRoot, files, repo, scenarios = scenariosTable() }) {
186
+ if (repo.edition === 'lite') return [];
186
187
  const patterns = (repo.sides?.be?.patterns ?? []).filter((name) => (scenarios[name] ?? []).length > 0);
187
188
  if (patterns.length === 0) return [];
188
189
  const ts = typescriptFor(repoRoot);
@@ -0,0 +1,100 @@
1
+ // Literal service event tables share the vendored contract with typed event classes.
2
+ import { isDeepStrictEqual } from 'node:util';
3
+ import { nameText } from '../../lib/ts-ast.mjs';
4
+ import { found, readJson, readText } from './read.mjs';
5
+
6
+ const CODE = 'HFS_EVENT_CONTRACT';
7
+ const TABLE = /^be\/apps\/([^/]+)\/src\/(events|consumes)\.ts$/;
8
+ const SNAPSHOT = /^be\/contracts\/([^/]+)\/events\.json$/;
9
+
10
+ function literal(ts, node) {
11
+ if (!node) throw new Error('missing initializer');
12
+ if (ts.isAsExpression(node) || ts.isParenthesizedExpression(node) || ts.isSatisfiesExpression(node)) return literal(ts, node.expression);
13
+ if (ts.isStringLiteralLike(node)) return node.text;
14
+ if (ts.isNumericLiteral(node)) return Number(node.text);
15
+ if (!ts.isObjectLiteralExpression(node)) throw new Error('expected a literal object, string or number');
16
+ return Object.fromEntries(node.properties.map((property) => {
17
+ const key = nameText(ts, property.name);
18
+ if (!ts.isPropertyAssignment(property) || key === null) throw new Error('expected a named literal property');
19
+ return [key, literal(ts, property.initializer)];
20
+ }));
21
+ }
22
+
23
+ function tableOf(ts, text, name) {
24
+ const source = ts.createSourceFile('table.ts', text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
25
+ if (source.parseDiagnostics.length) throw new Error('invalid TypeScript');
26
+ for (const statement of source.statements) {
27
+ if (!ts.isVariableStatement(statement)) continue;
28
+ const declaration = statement.declarationList.declarations.find((entry) => ts.isIdentifier(entry.name) && entry.name.text === name);
29
+ if (declaration) {
30
+ const value = literal(ts, declaration.initializer);
31
+ if (value === null || typeof value !== 'object') throw new Error('expected a literal table');
32
+ return value;
33
+ }
34
+ }
35
+ throw new Error(`missing ${name} table`);
36
+ }
37
+
38
+ function readTables(ts, repoRoot, files) {
39
+ const tables = [];
40
+ const findings = [];
41
+ for (const file of files.filter((candidate) => TABLE.test(candidate))) {
42
+ const [, service, kind] = TABLE.exec(file);
43
+ try {
44
+ const name = kind === 'events' ? 'EVENTS' : 'CONSUMES';
45
+ tables.push({ file, service, kind, value: tableOf(ts, readText(repoRoot, file) ?? '', name) });
46
+ } catch (error) {
47
+ findings.push(found(CODE, file, `${file}: ${error.message}; declare a literal event contract table.`));
48
+ }
49
+ }
50
+ return { tables, findings };
51
+ }
52
+
53
+ function providerFindings(table, snapshots, declared) {
54
+ const findings = [];
55
+ const expected = { schema: 'starci/event-contract@1', service: table.service, events: table.value };
56
+ if (!isDeepStrictEqual(snapshots.get(table.service), expected)) {
57
+ findings.push(found(CODE, table.file, `${table.file}: be/contracts/${table.service}/events.json is missing or differs from EVENTS; commit the emitted provider snapshot.`));
58
+ }
59
+ for (const [name, event] of Object.entries(table.value)) {
60
+ if (!Number.isInteger(event?.version) || event.version < 1 || !event.payload || typeof event.payload !== 'object') {
61
+ findings.push(found(CODE, table.file, `${table.file}: ${name} needs a positive integer version and a literal payload.`));
62
+ }
63
+ if (event?.compensates !== undefined && !declared.has(event.compensates)) {
64
+ findings.push(found(CODE, table.file, `${table.file}: ${name} compensates undeclared event ${event.compensates}.`));
65
+ }
66
+ }
67
+ return findings;
68
+ }
69
+
70
+ function consumerFindings(table, snapshots) {
71
+ const findings = [];
72
+ for (const [provider, events] of Object.entries(table.value)) {
73
+ if (events === null || typeof events !== 'object') {
74
+ findings.push(found(CODE, table.file, `${table.file}: CONSUMES.${provider} must be a literal event/version table.`));
75
+ continue;
76
+ }
77
+ for (const [name, version] of Object.entries(events)) {
78
+ const snapshot = snapshots.get(provider);
79
+ const event = snapshot?.events?.[name];
80
+ if (snapshot?.schema !== 'starci/event-contract@1' || snapshot?.service !== provider || !event || event.version !== version) {
81
+ findings.push(found(CODE, table.file, `${table.file}: ${provider} event ${name} at version ${version} is not declared by its vendored snapshot.`));
82
+ }
83
+ }
84
+ }
85
+ return findings;
86
+ }
87
+
88
+ export function eventTableFindings({ ts, repoRoot, files }) {
89
+ const snapshots = new Map(files.filter((file) => SNAPSHOT.test(file))
90
+ .map((file) => [SNAPSHOT.exec(file)[1], readJson(repoRoot, file)]));
91
+ const declared = new Set([...snapshots.values()].flatMap((snapshot) => Object.keys(snapshot?.events ?? {})));
92
+ const { tables, findings } = readTables(ts, repoRoot, files);
93
+ for (const table of tables) {
94
+ findings.push(...(table.kind === 'events' ? providerFindings(table, snapshots, declared) : consumerFindings(table, snapshots)));
95
+ }
96
+ return findings;
97
+ }
98
+
99
+ export const eventTableServices = (files) => files.filter((file) => TABLE.test(file) && TABLE.exec(file)[2] === 'events')
100
+ .map((file) => TABLE.exec(file)[1]);