@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
@@ -4,7 +4,7 @@
4
4
  // (sides, app kinds) and refuses a manifest whole on any problem (HFS_MANIFEST_INVALID). Pure: every function takes a
5
5
  // parsed manifest or slot and returns a list of problems in the words of modules/schemas/hfs-slots.schema.yaml.
6
6
  import { isPlainObject } from '../../engine/plain-object.mjs';
7
- import { stringList, byCodeUnit } from '../lib/list.mjs';
7
+ import { stringList, byCodeUnit, nonemptyUniqueList } from '../lib/list.mjs';
8
8
 
9
9
  export const SEMVER = /^(\d+)\.(\d+)\.(\d+)$/;
10
10
  export const NAME = /^[a-z][a-z0-9-]*$/;
@@ -86,7 +86,7 @@ export const MANIFEST_KINDS = [APP_KIND, RUNTIME_KIND];
86
86
  export const manifestKind = (m) => (isPlainObject(m) && m.kind !== undefined ? m.kind : APP_KIND);
87
87
  const strList = (v) => stringList(v);
88
88
 
89
- const APP_SLOT_KEYS = ['id', 'profiles', 'path', 'presence', 'tracked', 'tier', 'tests', 'owner', 'appKind', 'minInstances', 'requiredWhen', 'requiredInstances', 'requires', 'pattern', 'trigger', 'allows', 'forbids', 'layers', 'kinds', 'roles', 'composedBy', 'budget', 'managedBy', 'liteManagedBy', 'rules', 'goesTo', 'why', 'since', 'perConnection', 'parent', 'editions', 'litePresence', 'lite', 'provider', 'coverage', 'entries', 'outcomeHome'];
89
+ const APP_SLOT_KEYS = ['id', 'profiles', 'path', 'presence', 'tracked', 'tier', 'tests', 'owner', 'appKind', 'minInstances', 'requiredWhen', 'requiredInstances', 'requires', 'pattern', 'trigger', 'allows', 'forbids', 'layers', 'kinds', 'roles', 'composedBy', 'budget', 'managedBy', 'liteManagedBy', 'rules', 'goesTo', 'why', 'since', 'perConnection', 'parent', 'editions', 'editionWhy', 'litePresence', 'lite', 'provider', 'coverage', 'entries', 'outcomeHome', 'templateNa', 'exemplifiedBy'];
90
90
  /** The fields a slot's `lite` overlay may hold: the same keys it would carry in the slot body, resolved under edition lite. */
91
91
  const LITE_OVERLAY_KEYS = ['path', 'requires', 'allows', 'forbids', 'minInstances', 'requiredInstances', 'why'];
92
92
  /** A runtime slot has no app kind, side composition, layer or managed template; it may name the generator of a generated copy. */
@@ -184,6 +184,9 @@ function slotListProblems(slot, at) {
184
184
 
185
185
  function slotAppOptionProblems(slot, at) {
186
186
  const bad = [];
187
+ bad.push(...slotEditionWhyProblems(slot, at), ...slotCoverageEvidenceProblems(slot, at));
188
+ // Legacy coverage status derives only from an explicit `since`; a slot that omits it is refused, so a new slot can never be taken for an old one.
189
+ if (slot.since === undefined) bad.push(`${at}: since is required (the manifest version that added the slot)`);
187
190
  if (slot.editions !== undefined && !(Array.isArray(slot.editions) && slot.editions.length && slot.editions.every((e) => EDITIONS.includes(e)) && new Set(slot.editions).size === slot.editions.length)) bad.push(`${at}: editions must be a non-empty subset of ${EDITIONS.join(', ')}`);
188
191
  if (slot.litePresence !== undefined && !(PRESENCE.includes(slot.litePresence) || (isPlainObject(slot.litePresence) && Object.keys(slot.litePresence).length && Object.entries(slot.litePresence).every(([side, value]) => ['be', 'fe'].includes(side) && PRESENCE.includes(value))))) bad.push(`${at}: litePresence must be a presence value or map sides to presence values`);
189
192
  if (slot.provider !== undefined) {
@@ -193,6 +196,38 @@ function slotAppOptionProblems(slot, at) {
193
196
  return bad;
194
197
  }
195
198
 
199
+ function slotEditionWhyProblems(slot, at) {
200
+ if (slot.editionWhy === undefined) return [];
201
+ return typeof slot.editionWhy === 'string' && slot.editionWhy.trim()
202
+ ? [] : [`${at}: editionWhy must be non-empty text`];
203
+ }
204
+
205
+ const slotEvidencePathValid = (value) => relPath(value) && /^[^\s]+$/.test(value);
206
+
207
+ function slotEvidenceRefValid(ref) {
208
+ if (typeof ref === 'string') return slotEvidencePathValid(ref);
209
+ if (!isPlainObject(ref) || !slotEvidencePathValid(ref.path)) return false;
210
+ if (!Object.keys(ref).every((key) => ['path', 'edition', 'editions'].includes(key))) return false;
211
+ if (ref.edition !== undefined && !EDITIONS.includes(ref.edition)) return false;
212
+ if (ref.editions !== undefined && !nonemptyUniqueList(ref.editions, (edition) => EDITIONS.includes(edition))) return false;
213
+ return ref.edition === undefined || ref.editions === undefined;
214
+ }
215
+
216
+ const slotEvidenceRefKey = (ref) => typeof ref === 'string' ? ref
217
+ : JSON.stringify([ref.path, ref.edition, ref.editions]);
218
+
219
+ function slotCoverageEvidenceProblems(slot, at) {
220
+ const bad = [];
221
+ if (slot.templateNa !== undefined && !(typeof slot.templateNa === 'string' && slot.templateNa.trim()))
222
+ bad.push(`${at}: templateNa must be non-empty text`);
223
+ if (slot.exemplifiedBy === undefined) return bad;
224
+ const refs = slot.exemplifiedBy;
225
+ const valid = Array.isArray(refs) && refs.length > 0 && refs.every(slotEvidenceRefValid)
226
+ && new Set(refs.map(slotEvidenceRefKey)).size === refs.length;
227
+ if (!valid) bad.push(`${at}: exemplifiedBy must be unique repository-relative paths or {path, edition? | editions?} references`);
228
+ return bad;
229
+ }
230
+
196
231
  function slotLiteOverlayProblems(slot, at) {
197
232
  if (slot.lite === undefined) return [];
198
233
  const ok = isPlainObject(slot.lite) && Object.keys(slot.lite).length && Object.keys(slot.lite).every((key) => LITE_OVERLAY_KEYS.includes(key))
@@ -209,7 +244,7 @@ function slotEntryProblems(slot, at) {
209
244
  if (slot.entries !== undefined) {
210
245
  const listed = strList(slot.entries) && slot.entries.length > 0 && new Set(slot.entries).size === slot.entries.length && slot.entries.every(relPath);
211
246
  if (!listed) bad.push(`${at}: entries must be a non-empty list of unique relative file paths`);
212
- else if (typeof slot.path !== 'string' || !slot.path.endsWith('/') || slot.owner === true) bad.push(`${at}: entries belong to a directory slot that is not an owner (they name extra public entries of the owner that holds it)`);
247
+ else if (typeof slot.path !== 'string' || !slot.path.endsWith('/')) bad.push(`${at}: entries belong to a directory slot (they name its explicit public API files)`);
213
248
  else if (Array.isArray(slot.allows) && slot.entries.some((entry) => !slot.allows.includes(entry))) bad.push(`${at}: every entry must be one of the names the slot allows`);
214
249
  }
215
250
  if (slot.outcomeHome !== undefined && !(slot.outcomeHome === true && typeof slot.path === 'string' && slot.path && !slot.path.endsWith('/'))) bad.push(`${at}: outcomeHome is true and belongs to a file slot (the one file that declares the repository's Outcome union)`);
@@ -323,21 +358,71 @@ function runtimeParamProblems(rp) {
323
358
  ];
324
359
  }
325
360
 
361
+ const folderKindsValid = list => strList(list) && list.length > 0 && new Set(list).size === list.length && list.every(kind => NAME.test(kind));
362
+ const kindPairValid = pair => isPlainObject(pair) && Object.keys(pair).sort(byCodeUnit).join() === 'from,to'
363
+ && ['from', 'to'].every(side => folderKindsValid(pair[side]));
364
+ const kindRestrictionValid = kinds => isPlainObject(kinds) && Object.keys(kinds).length > 0
365
+ && Object.entries(kinds).every(([target, pair]) => NAME.test(target) && kindPairValid(pair));
366
+ const slotIdsValid = list => strList(list) && list.length > 0 && new Set(list).size === list.length
367
+ && list.every(id => SLOT_ID.test(id) || RUNTIME_SLOT_ID.test(id));
368
+ const slotRestrictionPairValid = pair => isPlainObject(pair) && Object.keys(pair).join() === 'from' && slotIdsValid(pair.from);
369
+ const slotRestrictionValid = slots => isPlainObject(slots) && Object.keys(slots).length > 0
370
+ && Object.entries(slots).every(([target, pair]) => NAME.test(target) && slotRestrictionPairValid(pair));
371
+
372
+ function tierDefinitionProblems(profile, tier, def) {
373
+ const bad = [];
374
+ const at = `tiers.${profile}.${tier}`;
375
+ if (!isPlainObject(def) || !Array.isArray(def.mayImport) || !def.mayImport.every((t) => NAME.test(String(t)))) bad.push(`${at}.mayImport must be a list of tier names`);
376
+ if (def?.mayImportKinds !== undefined && !kindRestrictionValid(def.mayImportKinds)) bad.push(`${at}.mayImportKinds must map tier names to {from, to} non-empty lists of unique folder kinds`);
377
+ if (def?.mayImportSlots !== undefined && !slotRestrictionValid(def.mayImportSlots)) bad.push(`${at}.mayImportSlots must map tier names to {from} non-empty lists of unique slot ids`);
378
+ for (const key of Object.keys(def ?? {})) if (!['mayImport', 'mayImportKinds', 'mayImportSlots', 'acyclic', 'lowerLayerOnly'].includes(key)) bad.push(`${at}.${key} is not a tier field`);
379
+ return bad;
380
+ }
381
+
326
382
  /** Shape problems of the tier map of one profile. */
327
383
  export function tierMapProblems(profile, tiers) {
328
384
  const bad = [];
329
385
  if (!isPlainObject(tiers) || !Object.keys(tiers).length) return [`tiers.${profile} must be a non-empty map`];
330
386
  for (const [tier, def] of Object.entries(tiers)) {
331
387
  if (!NAME.test(tier)) bad.push(`tiers.${profile}.${tier} is not a tier name`);
332
- if (!isPlainObject(def) || !Array.isArray(def.mayImport) || !def.mayImport.every((t) => NAME.test(String(t)))) bad.push(`tiers.${profile}.${tier}.mayImport must be a list of tier names`);
333
- for (const key of Object.keys(def ?? {})) if (!['mayImport', 'acyclic', 'lowerLayerOnly'].includes(key)) bad.push(`tiers.${profile}.${tier}.${key} is not a tier field`);
388
+ bad.push(...tierDefinitionProblems(profile, tier, def));
389
+ }
390
+ return bad;
391
+ }
392
+
393
+ function tierFolderKindProblems(manifest, profile, tier, kinds, at) {
394
+ const known = new Set(manifest.slots.filter((slot) => slot.profiles.includes(profile) && slot.tier === tier).flatMap((slot) => [...(slot.kinds ?? []), ...(slot.layers ?? [])]));
395
+ return kinds.filter(kind => !known.has(kind)).map(kind => `${at} names ${kind}, which is not a folder kind of the ${tier} tier`);
396
+ }
397
+
398
+ function tierSlotRestrictionProblems(manifest, profile, tier, def) {
399
+ const bad = [];
400
+ const known = new Set(manifest.slots.filter(slot => slot.profiles.includes(profile) && slot.tier === tier).map(slot => slot.id));
401
+ for (const [target, restriction] of Object.entries(def.mayImportSlots ?? {})) {
402
+ const at = `tiers.${profile}.${tier}.mayImportSlots.${target}`;
403
+ if (!def.mayImport.includes(target)) bad.push(`${at} restricts a tier that mayImport does not allow`);
404
+ for (const id of restriction.from) if (!known.has(id)) bad.push(`${at}.from names ${id}, which is not a slot of the ${profile} ${tier} tier`);
405
+ }
406
+ return bad;
407
+ }
408
+
409
+ /** Kind and slot restrictions narrow allowed tier edges and name declared source and target roles. */
410
+ export function tierKindProblems(manifest, profile) {
411
+ const bad = [];
412
+ for (const [tier, def] of Object.entries(manifest.tiers[profile])) {
413
+ bad.push(...tierSlotRestrictionProblems(manifest, profile, tier, def));
414
+ for (const [target, kinds] of Object.entries(def.mayImportKinds ?? {})) {
415
+ const at = `tiers.${profile}.${tier}.mayImportKinds.${target}`;
416
+ if (!def.mayImport.includes(target)) bad.push(`${at} restricts a tier that mayImport does not allow`);
417
+ bad.push(...tierFolderKindProblems(manifest, profile, tier, kinds.from, `${at}.from`), ...tierFolderKindProblems(manifest, profile, target, kinds.to, `${at}.to`));
418
+ }
334
419
  }
335
420
  return bad;
336
421
  }
337
422
 
338
423
  /** The semantic rules of a runtime manifest beyond the slot rules both kinds share: tier names and the schema major. */
339
424
  export function runtimeSemanticProblems(m) {
340
- const bad = [];
425
+ const bad = tierKindProblems(m, RUNTIME_KIND);
341
426
  const tiers = m.tiers.runtime;
342
427
  for (const [tier, def] of Object.entries(tiers)) {
343
428
  for (const target of def.mayImport) if (!(target in tiers)) bad.push(`tiers.runtime.${tier}.mayImport names ${target}, which is not a runtime tier`);
@@ -3,6 +3,7 @@
3
3
  // (scripts/hfs/project-graph.mjs) runs it over the same tree and serves the findings on a TypeScript file as ESLint reports.
4
4
  // HFS_SLOT_UNDECLARED / HFS_SLOT_AMBIGUOUS / HFS_SLOT_NOT_ENABLED / HFS_FORBIDDEN_PRESENT / HFS_TOOL_CONFIG_LOCAL / HFS_TRACKED_MUST_BE_IGNORED (R01, R16)
5
5
  // BE_SOURCE_FORM (R89, the file name), BE_SPEC_PLACEMENT (R102)
6
+ import fs from 'node:fs';
6
7
  import path from 'node:path';
7
8
  import { allowsFile } from './allows.mjs';
8
9
  import { isFeTestPath } from './rules/fe-no-tests.mjs';
@@ -30,8 +31,11 @@ function sourceFormContext(file, resolver, suffixes, bannedSuffixes) {
30
31
  const slot = resolver.slot(c.slot);
31
32
  if ([...(slot?.requires ?? []), ...(slot?.allows ?? [])].includes(base)) return null;
32
33
  const admitted = allowsFile(resolver, file);
34
+ // Environment parsing is an app slot role, not a source suffix admitted outside that matching app file.
35
+ if (c.slot.startsWith('be.app.') && admitted?.allowed && admitted.entry === '<app>.env.ts') return null;
33
36
  if (admitted?.allowed && admitted.entry?.includes('/') && path.posix.basename(admitted.entry) === base) return null;
34
- if (admitted?.allowed && PLAIN_ENTRY.test(admitted.entry ?? '') && KEBAB.test(base.slice(0, -'.ts'.length))) return null;
37
+ // A plain entry names its file by a variable only (`<x>.ts`, or `compose/<area>.ts` of an app slot): a kebab name is its form.
38
+ if (admitted?.allowed && PLAIN_ENTRY.test(path.posix.basename(admitted.entry ?? '')) && KEBAB.test(base.slice(0, -'.ts'.length))) return null;
35
39
  if (c.slot === 'be.persistence' && path.posix.basename(path.posix.dirname(file)) === 'migrations') return null;
36
40
  return { c, base, suffixes, bannedSuffixes };
37
41
  }
@@ -54,7 +58,7 @@ function sourceFormFinding(file, { c, base, suffixes, bannedSuffixes }, boundSuf
54
58
 
55
59
  /**
56
60
  * BE_SOURCE_FORM (R89): every tracked src/ or apps/ TypeScript file of a back end is index.ts, main.ts, a migration of
57
- * be.persistence, or <kebab-name>.<suffix>.ts with <suffix> in the closed vocabulary ruleParams.be.suffixes (a name such
61
+ * be.persistence, the matching <app>.env.ts explicitly admitted by its app slot, or <kebab-name>.<suffix>.ts with <suffix> in the closed vocabulary ruleParams.be.suffixes (a name such
58
62
  * as order.service.spec.ts keeps its inner words kebab-case). A suffix of ruleParams.be.bannedSuffixes anywhere in the
59
63
  * name is refused by name. Paths no slot owns are HFS_SLOT_UNDECLARED's, not this code's.
60
64
  */
@@ -86,6 +90,8 @@ function forbiddenPathFinding(file, c, resolver) {
86
90
  }
87
91
 
88
92
  function pathStatusFinding(file, resolver) {
93
+ const liteWork = liteStarciworkFinding(file, resolver);
94
+ if (liteWork) return liteWork;
89
95
  const c = resolver.classifyPath(file);
90
96
  if (c.status === 'no-slot') {
91
97
  const nearestNote = c.nearest ? `; nearest slot ${c.nearest.slot} (${c.nearest.pattern}), matched ${c.nearest.matchedPrefix || '.'} then expected ${c.nearest.expectedNext ?? 'nothing'}` : '';
@@ -98,6 +104,30 @@ function pathStatusFinding(file, resolver) {
98
104
  return null;
99
105
  }
100
106
 
107
+ /** The lite Work boundary, read from app.starciwork's edition-specific admission list, including paths a narrower slot owns. */
108
+ export function liteStarciworkFinding(file, resolver, { dir = false } = {}) {
109
+ if (resolver.repo?.edition !== 'lite' || !resolver.slot('app.starciwork')) return null;
110
+ const p = resolver.classifyPath(file).path;
111
+ if (!p.startsWith('.starciwork/')) return null;
112
+ const admitted = allowsFile(resolver, dir ? `${p}/.probe` : p);
113
+ if (admitted?.slot === 'app.starciwork' && admitted.allowed) return null;
114
+ return { code: 'HFS_STARCIWORK_LITE_FORBIDDEN', level: 'error', path: p, slot: 'app.starciwork', message: `${p}: lite .starciwork holds only workspace.yaml and brand/ (plus .gitignore); remove this path or use the full edition` };
115
+ }
116
+
117
+ /** Local lite Work leftovers, including metadata ignored by an old full-edition .gitignore. */
118
+ export function liteStarciworkTreeFindings(repoRoot, resolver, tracked) {
119
+ if (resolver.repo?.edition !== 'lite' || !resolver.slot('app.starciwork')) return [];
120
+ let entries = [];
121
+ try { entries = fs.readdirSync(path.join(repoRoot, '.starciwork'), { withFileTypes: true }); } catch { /* Work is optional. */ }
122
+ const trackedSet = new Set(tracked);
123
+ return entries.flatMap(entry => {
124
+ const file = `.starciwork/${entry.name}`;
125
+ if (trackedSet.has(file)) return [];
126
+ const finding = liteStarciworkFinding(file, resolver, { dir: entry.isDirectory() });
127
+ return finding ? [finding] : [];
128
+ });
129
+ }
130
+
101
131
  /** The findings of the slot manifest over `files` (repository-relative tracked paths) of a repository of `profile`. */
102
132
  export function pathFindings({ files, resolver, profile }) {
103
133
  const findings = [];
@@ -0,0 +1,6 @@
1
+ // Infrastructure refusals are shared with the dependency-free runtime copy generator.
2
+ // They describe checks that could not run, rather than catalogued obligations.
3
+ export const REFUSAL_CODES = Object.freeze([
4
+ 'HFS_REPO_UNREADABLE',
5
+ 'HFS_DECLARATION_INVALID', 'HFS_MANIFEST_MAJOR_MISMATCH', 'HFS_MANIFEST_INVALID', 'HFS_FORMAT_TOOL_MISSING', 'HFS_EDITION_INVALID',
6
+ ]);
@@ -3,6 +3,7 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { skillRoot } from '../../engine/runtime-root.mjs';
5
5
  import { parseYamlCached } from '../lib/yaml-cached.mjs';
6
+ import { jsonClone } from '../lib/json-clone.mjs';
6
7
  import { isPlainObject } from '../../engine/plain-object.mjs';
7
8
  import { SEMVER } from './manifest-shape.mjs';
8
9
  import { enforcerJudgedInEdition, judgedInEdition, ruleEditionProblems } from './edition-slots.mjs';
@@ -15,10 +16,129 @@ const RULE_KINDS = Object.freeze(['codemod', 'lint', 'check', 'design']);
15
16
  const FINDING_CODE = /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)+$/;
16
17
  const ENFORCER_ID = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
17
18
  const FILE_ENFORCERS = new Set(['machine', 'hfs', 'work-validate', 'sonar', 'runtime']);
18
- const RULE_KEYS = new Set(['id', 'code', 'law', 'scope', 'kinds', 'gates', 'failureCodes', 'editions', 'enforcers', 'cases']);
19
- const CASE_KEYS = new Set(['id', 'date', 'source', 'ruling', 'quote']);
19
+ const RULE_KEYS = new Set(['id', 'code', 'law', 'scope', 'kinds', 'gates', 'failureCodes', 'editions', 'codeEditions', 'enforcers', 'cases', 'docs', 'exemplifiedBy', 'proves', 'witness', 'editionWhy', 'amendedBy', 'templateNa']);
20
+ const CASE_KEYS = new Set(['id', 'date', 'source', 'ruling', 'quote', 'supersedes', 'status', 'staleWhen', 'amendedBy']);
20
21
  const CASE_DATE = /^\d{4}-\d{2}-\d{2}$/;
21
- const ENFORCER_KEYS = new Set(['kind', 'id', 'status', 'at', 'editions']);
22
+ const ENFORCER_KEYS = new Set(['kind', 'id', 'status', 'at', 'editions', 'editionWhy']);
23
+ const CASE_REFERENCE = /^(?:case-[1-9][0-9]*|(?:ruling:)?R[0-9]{2,3}(?:\/case-[1-9][0-9]*)?|owner:[a-z][a-z0-9]*(?:-[a-z0-9]+)*)$/;
24
+ const EVIDENCE_KEYS = new Set(['path', 'edition', 'editions']);
25
+ const PROOF_KEYS = new Set([...EVIDENCE_KEYS, 'codes', 'fixtures']);
26
+ const WITNESS_KEYS = new Set(['edition', 'base', 'mutate', 'expect']);
27
+ const MUTATION_KEYS = new Set(['op', 'path', 'content']);
28
+
29
+ function textProblem(value, label, bad) {
30
+ if (typeof value !== 'string' || !value.trim()) bad.push(`${label} must be non-empty text`);
31
+ }
32
+
33
+ function dateProblem(value, label, bad) {
34
+ const date = typeof value === 'string' && CASE_DATE.test(value) ? new Date(`${value}T00:00:00Z`) : null;
35
+ if (!date || !Number.isFinite(date.getTime()) || date.toISOString() !== `${value}T00:00:00.000Z`) bad.push(`${label} must be a valid YYYY-MM-DD date`);
36
+ }
37
+
38
+ function relativePathProblem(value, label, bad, anchor = false) {
39
+ const parts = typeof value === 'string' ? value.split('#') : [];
40
+ const file = parts[0];
41
+ const valid = typeof value === 'string' && value.trim() === value && value.length > 0
42
+ && !/[\r\n\u0000\\]/.test(value) && !/^(?:\/|[a-z][a-z0-9+.-]*:)/i.test(value) && !file.includes(':')
43
+ && !file.split('/').some((part) => ['', '.', '..'].includes(part))
44
+ && (parts.length === 1 || (anchor && parts.length === 2 && /^[A-Za-z0-9][A-Za-z0-9_.:-]*$/.test(parts[1])));
45
+ if (!valid) bad.push(`${label} must be a repository-relative path${anchor ? ' with an optional #anchor' : ''}`);
46
+ }
47
+
48
+ function unknownKeys(item, allowed, label, bad) {
49
+ for (const key of Object.keys(item)) if (!allowed.has(key)) bad.push(`${label} has unknown key ${key}`);
50
+ }
51
+
52
+ function nonEmptyList(value, label, bad) {
53
+ if (!Array.isArray(value) || !value.length) { bad.push(`${label} must be a non-empty list`); return false; }
54
+ const normalized = value.map((value) => JSON.stringify(value, (_key, item) => isPlainObject(item) ? Object.fromEntries(Object.keys(item).sort().map((key) => [key, item[key]])) : item));
55
+ if (new Set(normalized).size !== value.length) bad.push(`${label} repeats a value`);
56
+ return true;
57
+ }
58
+
59
+ function optionalText(item, key, label, bad) {
60
+ if (item[key] !== undefined) textProblem(item[key], `${label}.${key}`, bad);
61
+ }
62
+
63
+ function evidenceEditionProblems(item, label, bad) {
64
+ if (item.edition !== undefined && !['full', 'lite'].includes(item.edition)) bad.push(`${label}.edition must be full or lite`);
65
+ bad.push(...ruleEditionProblems(item, label));
66
+ if (item.edition !== undefined && item.editions !== undefined) bad.push(`${label} names edition or editions, not both`);
67
+ }
68
+
69
+ function codeListProblems(codes, label, bad) {
70
+ if (!nonEmptyList(codes, label, bad)) return;
71
+ for (const code of codes) if (typeof code !== 'string' || !FINDING_CODE.test(code)) bad.push(`${label} must contain UPPER_SNAKE finding codes`);
72
+ }
73
+
74
+ function evidenceMapProblems(item, key, label, bad) {
75
+ unknownKeys(item, key === 'docs' ? EVIDENCE_KEYS : PROOF_KEYS, label, bad);
76
+ relativePathProblem(item.path, `${label}.path`, bad, key === 'docs');
77
+ evidenceEditionProblems(item, label, bad);
78
+ if (item.codes !== undefined) codeListProblems(item.codes, `${label}.codes`, bad);
79
+ if (item.fixtures !== undefined && nonEmptyList(item.fixtures, `${label}.fixtures`, bad)) item.fixtures.forEach((value) => textProblem(value, `${label}.fixtures`, bad));
80
+ }
81
+
82
+ function evidenceProblems(rule, key, label, bad) {
83
+ if (rule[key] === undefined || !nonEmptyList(rule[key], `${label}.${key}`, bad)) return;
84
+ rule[key].forEach((item, index) => {
85
+ const at = `${label}.${key}[${index}]`;
86
+ if (typeof item === 'string') { relativePathProblem(item, at, bad, key === 'docs'); return; }
87
+ if (!isPlainObject(item)) { bad.push(`${at} must be a path or a map`); return; }
88
+ evidenceMapProblems(item, key, at, bad);
89
+ });
90
+ }
91
+
92
+ function mutationProblems(item, label, bad) {
93
+ if (!isPlainObject(item)) { bad.push(`${label} must be a map`); return; }
94
+ unknownKeys(item, MUTATION_KEYS, label, bad);
95
+ if (!['add', 'edit', 'delete'].includes(item.op)) bad.push(`${label}.op must be add, edit or delete`);
96
+ relativePathProblem(item.path, `${label}.path`, bad);
97
+ if (['add', 'edit'].includes(item.op) && typeof item.content !== 'string') bad.push(`${label}.content must be text for add or edit`);
98
+ if (item.op === 'delete' && item.content !== undefined) bad.push(`${label}.content is absent for delete`);
99
+ }
100
+
101
+ function witnessEntryProblems(item, label, bad) {
102
+ if (!isPlainObject(item)) { bad.push(`${label} must be a map`); return; }
103
+ if (Object.hasOwn(item, 'path')) { evidenceMapProblems(item, 'proves', label, bad); return; }
104
+ unknownKeys(item, WITNESS_KEYS, label, bad);
105
+ if (!['full', 'lite'].includes(item.edition)) bad.push(`${label}.edition must be full or lite`);
106
+ relativePathProblem(item.base, `${label}.base`, bad);
107
+ codeListProblems(item.expect, `${label}.expect`, bad);
108
+ if (nonEmptyList(item.mutate, `${label}.mutate`, bad)) item.mutate.forEach((mutation, index) => mutationProblems(mutation, `${label}.mutate[${index}]`, bad));
109
+ }
110
+
111
+ function witnessProblems(rule, label, bad) {
112
+ const value = rule.witness;
113
+ if (value === undefined) return;
114
+ const at = `${label}.witness`;
115
+ if (isPlainObject(value)) {
116
+ unknownKeys(value, new Set(['status', 'reason', 'date']), at, bad);
117
+ if (value.status !== 'unreachable') bad.push(`${at}.status must be unreachable`);
118
+ textProblem(value.reason, `${at}.reason`, bad);
119
+ dateProblem(value.date, `${at}.date`, bad);
120
+ return;
121
+ }
122
+ if (!nonEmptyList(value, at, bad)) return;
123
+ value.forEach((item, index) => witnessEntryProblems(item, `${at}[${index}]`, bad));
124
+ }
125
+
126
+ function caseReferenceProblems(value, label, bad) {
127
+ const values = Array.isArray(value) ? value : [value];
128
+ if (!nonEmptyList(values, label, bad)) return;
129
+ for (const ref of values) if (typeof ref !== 'string' || !CASE_REFERENCE.test(ref)) bad.push(`${label} must name case-N, R<id>[/case-N], ruling:R<id>[/case-N] or owner:<id>`);
130
+ }
131
+
132
+ function caseTransitionProblems(item, label, bad) {
133
+ if (item.status !== undefined && !['live', 'superseded', 'amended'].includes(item.status)) bad.push(`${label}.status must be live, superseded or amended`);
134
+ for (const key of ['supersedes', 'amendedBy']) if (item[key] !== undefined) caseReferenceProblems(item[key], `${label}.${key}`, bad);
135
+ if (item.staleWhen !== undefined && nonEmptyList(item.staleWhen, `${label}.staleWhen`, bad)) {
136
+ for (const pattern of item.staleWhen) {
137
+ if (typeof pattern !== 'string' || !pattern.trim()) { bad.push(`${label}.staleWhen must contain non-empty regex strings`); continue; }
138
+ try { new RegExp(pattern, 'i'); } catch { bad.push(`${label}.staleWhen has an invalid regex ${JSON.stringify(pattern)}`); }
139
+ }
140
+ }
141
+ }
22
142
 
23
143
  function vocabularyProblems(catalog, key, names, bad) {
24
144
  if (!isPlainObject(catalog[key])) { bad.push(`${key} must be a map`); return; }
@@ -75,13 +195,15 @@ function checkRuleCases(rule, label, bad) {
75
195
  if (typeof item.date !== 'string' || !CASE_DATE.test(item.date)) bad.push(`${at}.date must be YYYY-MM-DD`);
76
196
  if (item.source !== 'owner') bad.push(`${at}.source must be owner`);
77
197
  for (const key of ['ruling', ...(item.quote === undefined ? [] : ['quote'])]) if (typeof item[key] !== 'string' || !item[key].trim()) bad.push(`${at}.${key} must be text`);
198
+ caseTransitionProblems(item, at, bad);
78
199
  });
79
200
  }
80
201
 
81
- function checkRuleGates(label, gates, bad) {
202
+ function checkRuleGates(rule, label, gates, bad) {
82
203
  if (!gates.length) return;
83
- if (!gates.includes('land')) bad.push(`${label} must run at the land gate (every rule does)`);
84
- if (gates.includes('pre-commit') && !gates.includes('pre-push')) bad.push(`${label} runs at pre-commit, so it also runs at pre-push`);
204
+ const releaseGate = gates.includes('land') || gates.includes('ci');
205
+ const runtimeGate = rule.scope === 'runtime' && gates.includes('runtime');
206
+ if (!releaseGate && !runtimeGate) bad.push(`${label} must run at land or ci or, for runtime scope, the runtime gate`);
85
207
  }
86
208
 
87
209
  function checkRuleFailureCodes(rule, label, codeOwner, bad) {
@@ -97,6 +219,19 @@ function checkRuleFailureCodes(rule, label, codeOwner, bad) {
97
219
  }
98
220
  }
99
221
 
222
+ function checkCodeEditions(rule, label, bad) {
223
+ const value = rule.codeEditions;
224
+ if (value === undefined) return;
225
+ if (!isPlainObject(value) || !Object.keys(value).length) { bad.push(`${label}.codeEditions must be a non-empty map`); return; }
226
+ const editions = Array.isArray(rule.editions) ? rule.editions : ['full', 'lite'];
227
+ for (const [code, selected] of Object.entries(value)) {
228
+ const at = `${label}.codeEditions.${code}`;
229
+ if (!Array.isArray(rule.failureCodes) || !rule.failureCodes.includes(code)) bad.push(`${at} must name a code owned by this rule`);
230
+ enumListProblems({ editions: selected }, at, 'editions', ['full', 'lite'], bad);
231
+ if (Array.isArray(selected) && selected.some((edition) => !editions.includes(edition))) bad.push(`${at} must be a subset of the rule's editions`);
232
+ }
233
+ }
234
+
100
235
  function checkEnforcerIdentity(enforcer, label, seen, bad) {
101
236
  if (!ENFORCER_FAMILIES.includes(enforcer.kind)) bad.push(`${label}.kind must be one of ${ENFORCER_FAMILIES.join(', ')}`);
102
237
  if (!ENFORCER_ID.test(String(enforcer.id))) bad.push(`${label}.id must be kebab-case`);
@@ -119,6 +254,7 @@ function checkEnforcer(enforcer, index, ruleLabel, seen, bad) {
119
254
  if (!ENFORCER_KEYS.has(key)) bad.push(`${label} has unknown key ${key}`);
120
255
  }
121
256
  bad.push(...ruleEditionProblems(enforcer, label));
257
+ optionalText(enforcer, 'editionWhy', label, bad);
122
258
  checkEnforcerIdentity(enforcer, label, seen, bad);
123
259
  if (enforcer.status !== undefined && enforcer.status !== 'planned') bad.push(`${label}.status is either absent or planned`);
124
260
  checkEnforcerLocation(enforcer, label, bad);
@@ -140,11 +276,17 @@ function checkRule(rule, index, catalog, codeOwner, bad) {
140
276
  checkRuleIdentity(rule, index, catalog, at, label, bad);
141
277
  checkRuleText(rule, label, bad);
142
278
  checkRuleCases(rule, label, bad);
279
+ if (rule.amendedBy !== undefined) caseReferenceProblems(rule.amendedBy, `${label}.amendedBy`, bad);
280
+ optionalText(rule, 'editionWhy', label, bad);
281
+ optionalText(rule, 'templateNa', label, bad);
282
+ for (const key of ['docs', 'exemplifiedBy', 'proves']) evidenceProblems(rule, key, label, bad);
283
+ witnessProblems(rule, label, bad);
143
284
  enumListProblems(rule, label, 'kinds', RULE_KINDS, bad);
144
285
  bad.push(...ruleEditionProblems(rule, label));
145
286
  const gates = enumListProblems(rule, label, 'gates', RULE_GATES, bad);
146
- checkRuleGates(label, gates, bad);
287
+ checkRuleGates(rule, label, gates, bad);
147
288
  checkRuleFailureCodes(rule, label, codeOwner, bad);
289
+ checkCodeEditions(rule, label, bad);
148
290
  checkRuleEnforcers(rule, label, bad);
149
291
  if (Array.isArray(rule.gates)) {
150
292
  const hasSonar = rule.enforcers.some((enforcer) => isPlainObject(enforcer) && enforcer.kind === 'sonar');
@@ -183,15 +325,18 @@ export function loadRuleCatalog({ root = skillRoot, file = path.join(root, HFS_R
183
325
  const byCode = new Map(list.flatMap((rule) => rule.failureCodes.map((code) => [code, rule])));
184
326
  return Object.freeze({
185
327
  version: doc.version, major, minor, patch,
186
- gates: deepFreeze(structuredClone(doc.gates)),
187
- enforcerKinds: deepFreeze(structuredClone(doc.enforcerKinds)),
328
+ gates: deepFreeze(jsonClone(doc.gates)),
329
+ enforcerKinds: deepFreeze(jsonClone(doc.enforcerKinds)),
188
330
  rules: list,
189
331
  /** The rule with this id (R01..), or null. */
190
332
  rule: (id) => byId.get(id) ?? null,
191
333
  /** The rule that owns this failure code (its own or a sub-check code), or null. */
192
334
  byCode: (code) => byCode.get(code) ?? null,
193
- /** Whether a finding code is judged under `edition`: a code of a rule that names `editions` without it is not (a code outside the catalog always is). */
194
- judgedIn: (code, edition = 'full') => judgedInEdition(byCode.get(code), edition),
335
+ /** Whether a finding code is judged under `edition`: codeEditions narrows its owner's editions; uncatalogued codes remain judged. */
336
+ judgedIn: (code, edition = 'full') => {
337
+ const owner = byCode.get(code);
338
+ return judgedInEdition(owner, edition) && (!owner?.codeEditions?.[code] || owner.codeEditions[code].includes(edition));
339
+ },
195
340
  enforcerJudgedIn: (kind, id, edition = 'full') => enforcerJudgedInEdition(list, kind, id, edition),
196
341
  /** The rules that run at a gate. */
197
342
  forGate: (gate) => list.filter((rule) => rule.gates.includes(gate)),
@@ -4,22 +4,33 @@
4
4
  import { isPlainObject } from '../../engine/plain-object.mjs';
5
5
  import { paramNamesOk } from './param-names.mjs';
6
6
  import { kindParamProblems, scenarioProblem } from './declaration-slots.mjs';
7
- import { roleListProblems, SCHEMA_AUTHORITIES, unitRolesProblems } from './manifest-shape.mjs';
8
- import { byCodeUnit } from '../lib/list.mjs';
7
+ import { NAME, roleListProblems, SCHEMA_AUTHORITIES, unitRolesProblems } from './manifest-shape.mjs';
8
+ import { byCodeUnit, nonemptyUniqueList } from '../lib/list.mjs';
9
9
 
10
10
  const PROFILES = ['be', 'fe'];
11
11
  const BE_OWN = ['infraOwners', 'eventBus', 'specDoubles', 'paramNames', 'suffixes', 'bannedSuffixes', 'contractShape', 'unitRoles', 'logicRoles', 'thinRoles', 'patternScenarios', 'kindPatterns', 'addKinds'];
12
12
  const FORM_NAMES = new Set(['call', 'new', 'curried', 'object', 'primitive', 'array']);
13
13
  const blockOk = (v) => isPlainObject(v) && Number.isInteger(v.lines) && v.lines >= 2 && Number.isInteger(v.tokens) && v.tokens >= 1 && Object.keys(v).length === 2;
14
14
  const fileLinesOk = (v) => isPlainObject(v) && Number.isInteger(v.soft) && v.soft >= 1 && typeof v.hardGrowth === 'boolean' && Object.keys(v).length === 2;
15
- const sharedParam = (key, v) => (key === 'fileLines' ? fileLinesOk(v) : blockOk(v));
16
- const sideOk = (side, own, extra = []) => own.every((k) => side[k] !== undefined) && Object.entries(side).every(([k, v]) => own.includes(k) || extra.includes(k) || (['fileLines', 'duplicateBlock'].includes(k) && sharedParam(k, v)));
17
- const liteOk = (lite, keys) => lite === undefined || (isPlainObject(lite) && Object.keys(lite).length > 0 && Object.keys(lite).every((k) => k !== 'lite' && keys.includes(k)));
15
+ const SHARED_PARAMS = {
16
+ fileLines: fileLinesOk,
17
+ duplicateBlock: blockOk,
18
+ functionLines: v => Number.isInteger(v) && v >= 1,
19
+ featureFiles: v => Number.isInteger(v) && v >= 1,
20
+ };
21
+ const SHARED_KEYS = Object.keys(SHARED_PARAMS);
22
+ const sharedParam = (key, v) => Object.hasOwn(SHARED_PARAMS, key) && SHARED_PARAMS[key](v);
23
+ const sideOk = (side, own, extra = []) => own.every((k) => side[k] !== undefined) && Object.entries(side).every(([k, v]) => own.includes(k) || extra.includes(k) || sharedParam(k, v));
24
+ const liteKeyOk = (lite, key, keys) => key !== 'lite' && keys.includes(key)
25
+ && (!['functionLines', 'featureFiles'].includes(key) || sharedParam(key, lite[key]));
26
+ const liteOk = (lite, keys) => lite === undefined || (isPlainObject(lite) && Object.keys(lite).length > 0 && Object.keys(lite).every((k) => liteKeyOk(lite, k, keys)));
18
27
  const memberMap = (v, allowEmpty) => isPlainObject(v) && Object.keys(v).length > 0 && Object.values(v).every((list) => Array.isArray(list) && (allowEmpty || list.length > 0) && list.every((x) => /^[A-Z][A-Za-z0-9]*$/.test(String(x))) && new Set(list).size === list.length);
19
- const roleList = (v) => Array.isArray(v) && v.length > 0 && v.every((x) => /^[a-z][a-z0-9-]*$/.test(String(x))) && new Set(v).size === v.length;
28
+ const roleList = (v) => nonemptyUniqueList(v, (role) => NAME.test(String(role)));
20
29
  const formsOk = (v) => Array.isArray(v) && v.length > 0 && v.every((x) => FORM_NAMES.has(x)) && new Set(v).size === v.length;
21
30
  const doubleOk = (v) => isPlainObject(v) && /^[A-Za-z][A-Za-z0-9]*$/.test(String(v.double)) && formsOk(v.forms);
22
31
  const regexOk = (v) => { try { return typeof v === 'string' && v.length > 0 && Boolean(new RegExp(v)); } catch { return false; } };
32
+ const dbExtensionsOk = (value) => Array.isArray(value) && value.every((name) => typeof name === 'string' && name.length > 0) && new Set(value).size === value.length;
33
+ const dbParamsOk = (db) => db === undefined || (isPlainObject(db) && Object.entries(db).every(([key, value]) => key === 'extensions' && dbExtensionsOk(value)));
23
34
 
24
35
  function beSpecificProblems(be, manifest) {
25
36
  const bad = [];
@@ -43,14 +54,15 @@ function beSpecificProblems(be, manifest) {
43
54
  export function ruleParamsProblems(m) {
44
55
  const bad = [];
45
56
  const rp = m.ruleParams;
46
- if (!isPlainObject(rp) || Object.keys(rp).some((k) => ![...PROFILES, 'common'].includes(k)) || !isPlainObject(rp.common) || !isPlainObject(rp.be) || (rp.fe !== undefined && !isPlainObject(rp.fe))) return ['ruleParams must be a map with common, be and optionally fe'];
57
+ if (!isPlainObject(rp) || Object.keys(rp).some((k) => ![...PROFILES, 'common', 'db'].includes(k)) || !isPlainObject(rp.common) || !isPlainObject(rp.be) || (rp.fe !== undefined && !isPlainObject(rp.fe))) return ['ruleParams must be a map with common, be and optionally fe and db'];
47
58
  else {
48
59
  // common holds the parameters both sides share; a side may restate one of them (a valid value) as an override, nothing else.
49
60
  // Edition lite: a side's optional `lite` map overrides keys of the same side (never `lite` itself) when the app declares edition lite.
50
- if (Object.keys(rp.common).length !== 2 || !fileLinesOk(rp.common.fileLines) || !blockOk(rp.common.duplicateBlock)) bad.push('ruleParams.common needs exactly fileLines {soft, hardGrowth} and duplicateBlock {lines >= 2, tokens >= 1}');
51
- if (!sideOk(rp.be, BE_OWN, ['schemaAuthority', 'lite']) || (rp.be.schemaAuthority !== undefined && !SCHEMA_AUTHORITIES.includes(rp.be.schemaAuthority)) || !liteOk(rp.be.lite, [...BE_OWN, 'fileLines', 'duplicateBlock', 'schemaAuthority'])) bad.push('ruleParams.be needs infraOwners, eventBus, specDoubles, paramNames, suffixes, bannedSuffixes, contractShape {helper}, unitRoles, logicRoles, thinRoles, patternScenarios, kindPatterns and addKinds (fileLines and duplicateBlock live in ruleParams.common, a side may override them); optional: schemaAuthority (' + SCHEMA_AUTHORITIES.join(' | ') + ') and lite (overrides of the same keys)'); else bad.push(...unitRolesProblems(rp.be), ...roleListProblems(rp.be));
61
+ if (!sideOk(rp.common, []) || !fileLinesOk(rp.common.fileLines) || !blockOk(rp.common.duplicateBlock)) bad.push('ruleParams.common needs fileLines {soft, hardGrowth} and duplicateBlock {lines >= 2, tokens >= 1}; optional functionLines and featureFiles must be positive integers');
62
+ if (!sideOk(rp.be, BE_OWN, ['schemaAuthority', 'lite']) || (rp.be.schemaAuthority !== undefined && !SCHEMA_AUTHORITIES.includes(rp.be.schemaAuthority)) || !liteOk(rp.be.lite, [...BE_OWN, ...SHARED_KEYS, 'schemaAuthority'])) bad.push('ruleParams.be needs infraOwners, eventBus, specDoubles, paramNames, suffixes, bannedSuffixes, contractShape {helper}, unitRoles, logicRoles, thinRoles, patternScenarios, kindPatterns and addKinds (shared parameters live in ruleParams.common, a side may override them); optional: schemaAuthority (' + SCHEMA_AUTHORITIES.join(' | ') + ') and lite (overrides of the same keys)'); else bad.push(...unitRolesProblems(rp.be), ...roleListProblems(rp.be));
52
63
  bad.push(...beSpecificProblems(rp.be, m));
53
- if (rp.fe !== undefined && (!sideOk(rp.fe, [], ['lite']) || !liteOk(rp.fe.lite, ['fileLines', 'duplicateBlock']))) bad.push('ruleParams.fe may only restate fileLines and duplicateBlock of ruleParams.common, and carry a lite override of them');
64
+ if (rp.fe !== undefined && (!sideOk(rp.fe, [], ['lite']) || !liteOk(rp.fe.lite, SHARED_KEYS))) bad.push('ruleParams.fe may only restate the shared parameters of ruleParams.common, and carry a lite override of them');
65
+ if (!dbParamsOk(rp.db)) bad.push('ruleParams.db may contain only extensions: a list of unique non-empty extension names');
54
66
  }
55
67
  return bad;
56
68
  }
@@ -0,0 +1,88 @@
1
+ // a15-findings.mjs - the one place where the alpha.15 content rules (R247-R280) are wired into `starci app check`.
2
+ // Each rule module of a batch exports a synchronous finder `({ repoRoot, files, all, repo, resolver, manifest, isRoot, inScope }) => finding[]`
3
+ // and is wired here by appending ONE import line and ONE entry to FINDERS. Nothing else in this file changes; the file is union-merged.
4
+ // A finder judges its own edition (`repo.edition`): lite and full are both in scope unless the rule says otherwise.
5
+ // finders-import-begin
6
+ import { featureFileFindings } from './feature-files.mjs';
7
+ import { lexiconFindings } from './lexicon.mjs';
8
+ import { assetsFindings } from './assets.mjs';
9
+ import { unusedSymbolFindings } from './unused-symbol.mjs';
10
+ import { databaseBaselineFindings } from './database-baseline.mjs';
11
+ import { databaseQueuesFindings } from './database-queues.mjs';
12
+ import { databaseConfigFindings } from './database-config.mjs';
13
+ import { docFreshFindings } from './doc-fresh.mjs';
14
+ import { releaseProvenanceFindings } from './release-provenance.mjs';
15
+ import { feStackAllowlistFindings } from './fe-stack.mjs';
16
+ import { appEnvRequiredFindings } from './app-env-required.mjs';
17
+ import { ageKeyCustodyFindings } from './secrets.mjs';
18
+ import { canonCopyDriftFindings } from './canon-copy-drift.mjs';
19
+ import { secretCustodyOnlyFindings } from './secret-custody-only.mjs';
20
+ import { bootProofFindings } from './boot-proof.mjs';
21
+ import { coverageBelowFindings } from './coverage-below.mjs';
22
+ import { deployScanFindings } from './deploy-scan.mjs';
23
+ import { deployWorkflowFindings } from './deploy-workflow.mjs';
24
+ import { scriptHygieneFindings } from './script-hygiene.mjs';
25
+ import { supplyChainFindings } from './supply-chain.mjs';
26
+ import { envParityFindings } from './env-parity.mjs';
27
+ import { envValueShapeFindings } from './env-value-shape.mjs';
28
+ import { liteProofLaneFindings } from './lite-proof-lane.mjs';
29
+ import { stacksSwarmFindings } from './stacks-swarm.mjs';
30
+ import { stacksReleaseFindings } from './stacks-release.mjs';
31
+ import { stacksIngressFindings } from './stacks-ingress.mjs';
32
+ import { stacksLiteBackingFindings } from './stacks-lite-backing.mjs';
33
+ import { stackToolingFindings } from './stack-tooling.mjs';
34
+ import { stacksDeployFindings } from './stacks-deploy.mjs';
35
+ // finders-import-end
36
+
37
+ /** The failure codes the finders above emit, so `starci app check` ships their labels (check.mjs adds them to CHECK_CODES). Append-only; the coordinator regenerates it at merge time. */
38
+ export const A15_CODES = [
39
+ // codes-begin
40
+ 'BACKUP_PROOF_MISSING', 'BACKUP_UNDECLARED', 'BE_APP_BOOT_PROOF_MISSING', 'BE_APP_ENV_REQUIRED', 'BE_APP_MODULE_LIST_INLINE', 'BE_LITE_PROOF_LANE_MISSING', 'DB_API_EXPOSED', 'DB_ARCHIVE_MUTATED', 'DB_BASELINE_SHAPE', 'DB_CRON_OWNED', 'DB_EXTENSION_ALLOWED', 'DB_MIGRATION_DESTRUCTIVE', 'DB_PGMQ_DECLARED', 'DB_VAULT_WRITE', 'ENV_DUPLICATE', 'ENV_EXAMPLE_DRIFT', 'ENV_PUBLIC_SEALED', 'ENV_REQUIRED_UNSET', 'ENV_SEALED_UNREAD', 'ENV_VALUE_SHAPE', 'FE_STACK_CORE_MISSING', 'FE_STACK_UNLISTED', 'HFS_AGE_KEY_CUSTODY', 'HFS_APP_LEXICON', 'HFS_APP_WORKFLOW_TRIGGER', 'HFS_ASSET_KIND', 'HFS_ASSET_OVERSIZE', 'HFS_AUDIT_STEP_MISSING', 'HFS_CANON_COPY_DRIFT', 'HFS_CI_TOOL_INSTALL', 'HFS_COVERAGE_BELOW', 'HFS_DB_DEPLOY_UNGUARDED', 'HFS_DEPENDABOT_MISSING', 'HFS_DEPLOY_COMPOSE_ON_VPS', 'HFS_DEPLOY_NO_LOGIC', 'HFS_DEPLOY_SCHEDULER', 'HFS_DEPLOY_WORKFLOW', 'HFS_DOC_COMMAND_UNKNOWN', 'HFS_DOC_ENV_UNKNOWN', 'HFS_DOC_PATH_MISSING', 'HFS_DOC_RULE_UNKNOWN', 'HFS_FEATURE_FILE_BUDGET', 'HFS_HOST_CRON', 'HFS_RELEASE_TAG_MISMATCH', 'HFS_SBOM_MISSING', 'HFS_SCHEDULE_UNDECLARED', 'HFS_SCRIPT_PLAINTEXT_LOAD', 'HFS_SCRIPT_XTRACE', 'HFS_SECRET_OUTSIDE_ENC', 'HFS_SECRET_PLAINTEXT_FILE', 'HFS_SEED_PII', 'HFS_SERVICE_ROLE_SCOPE', 'HFS_UNUSED_SYMBOL', 'HFS_WORKFLOW_PIN', 'STACKS_DEPLOY_CONFIG_INVALID', 'STACKS_DNS_UNDECLARED', 'STACKS_HEALTHCHECK_MISSING', 'STACKS_HOST_UNROUTED', 'STACKS_INGRESS_UNDECLARED', 'STACKS_INGRESS_UNKNOWN', 'STACKS_LITE_SELFHOSTED_DB', 'STACKS_MODEL_UNAVAILABLE', 'STACKS_PLACEMENT_MISSING', 'STACKS_PORT_PUBLISHED', 'STACKS_REALTIME_SCALED', 'STACKS_RELEASE_UNDECLARED', 'STACKS_SECOND_INGRESS', 'STACKS_SECRET_ENV_LITERAL', 'STACKS_SECRET_NOT_EXTERNAL', 'STACKS_SECRET_UNVERSIONED', 'STACKS_SIDECAR_DB_UNSUPPORTED', 'STACKS_SIDECAR_VOLUME_UNDECLARED', 'STACKS_TLS_UNDECLARED', 'STACKS_UPDATE_POLICY_MISSING', 'STACK_TOOLING_REIMPLEMENTED',
41
+ // codes-end
42
+ ];
43
+
44
+ export const FINDERS = [
45
+ // finders-begin
46
+ featureFileFindings,
47
+ lexiconFindings,
48
+ assetsFindings,
49
+ unusedSymbolFindings,
50
+ databaseBaselineFindings,
51
+ databaseQueuesFindings,
52
+ databaseConfigFindings,
53
+ docFreshFindings,
54
+ releaseProvenanceFindings,
55
+ feStackAllowlistFindings,
56
+ appEnvRequiredFindings,
57
+ ageKeyCustodyFindings,
58
+ canonCopyDriftFindings,
59
+ secretCustodyOnlyFindings,
60
+ bootProofFindings,
61
+ coverageBelowFindings,
62
+ deployScanFindings,
63
+ deployWorkflowFindings,
64
+ scriptHygieneFindings,
65
+ supplyChainFindings,
66
+ envParityFindings,
67
+ envValueShapeFindings,
68
+ liteProofLaneFindings,
69
+ stacksSwarmFindings,
70
+ stacksReleaseFindings,
71
+ stacksIngressFindings,
72
+ stacksLiteBackingFindings,
73
+ stackToolingFindings,
74
+ stacksDeployFindings,
75
+ // finders-end
76
+ ];
77
+
78
+ /** Findings of every alpha.15 content rule over one repository. */
79
+ export function a15Findings(context) {
80
+ // `starci app check` always passes the full context; a partial one (a finder spec driving the registry with only the fields its rule reads) must not crash the finders of other rules.
81
+ const full = Boolean(context.repoRoot && context.repo && context.resolver && context.manifest);
82
+ return FINDERS.flatMap((finder) => {
83
+ if (full) return finder(context);
84
+ try { return finder(context); } catch { return []; }
85
+ });
86
+ }
87
+
88
+ // A02: include every deployment and supply-chain finding in the bundled app-check catalog.