@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
@@ -0,0 +1,20 @@
1
+ // R289: every declared back-end app owns a tracked typed environment boundary.
2
+ // ESLint's app-env-required rule checks its interface, reader and bootstrap delegation.
3
+ import { found, readText } from './read.mjs';
4
+
5
+ const REQUIRED_CODE = 'BE_APP_ENV_REQUIRED';
6
+
7
+ /** Missing files cannot be visited by ESLint, so the content check owns their presence. */
8
+ export function appEnvRequiredFindings({ repoRoot, files, all = files, repo, inScope = () => true }) {
9
+ if (repo.profile !== 'be' || !['lite', 'full'].includes(repo.edition ?? 'full')) return [];
10
+ const tracked = new Set(all);
11
+ const findings = [];
12
+ for (const app of repo.apps ?? []) {
13
+ const file = `apps/${app.name}/src/${app.name}.env.ts`;
14
+ if (!inScope(file)) continue;
15
+ if (tracked.has(file) && readText(repoRoot, file) !== null) continue;
16
+ findings.push(found(REQUIRED_CODE, file,
17
+ `${app.name} requires a tracked ${file} exporting its environment interface and read<App>Env(env: EnvSource); main.ts builds EnvSource once and delegates environment reads to that boundary`));
18
+ }
19
+ return findings;
20
+ }
@@ -27,7 +27,7 @@ import { testTopologyFindings } from './test-topology.mjs';
27
27
  * The findings of the app-root rules: `files` are the root's own tracked paths, `all` every tracked path of the app, `repo` the
28
28
  * resolved app declaration, `resolver` its slot resolver and `pins` the canon pins.
29
29
  */
30
- export function appRootFindings({ repoRoot, files, all, repo, resolver, pins }) {
30
+ export function appRootFindings({ repoRoot, files, all, repo, resolver, pins, phase }) {
31
31
  return [
32
32
  ...depFindings({ repoRoot, files: all }),
33
33
  ...peerIntegrationFindings({ repoRoot, files: all }),
@@ -45,7 +45,7 @@ export function appRootFindings({ repoRoot, files, all, repo, resolver, pins })
45
45
  ...pipelineFindings({ repoRoot, files, pins }),
46
46
  ...testTopologyFindings({ repoRoot, files }),
47
47
  ...proofCommandFindings({ repoRoot, files: all, resolver, sides: Object.keys(repo.sides ?? {}) }),
48
- ...appFrontendFindings({ repoRoot, files: all, repo }),
48
+ ...appFrontendFindings({ repoRoot, files: all, repo, phase }),
49
49
  ...stacksFindings({ repoRoot, files, resolver }),
50
50
  ];
51
51
  }
@@ -0,0 +1,79 @@
1
+ // assets.mjs - R282: small build-time binaries belong in be.assets; user media, model weights and datasets do not belong in git.
2
+ // The owner assets policy has no numeric cap yet. 512000 bytes follows the existing repo.docs-media budget in slots.yaml;
3
+ // ruleParams.be.assets {bytes (or maxBytes), extensions} supplies a manifest's explicit cap and build-time extension allowlist.
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { editionRuleParams } from '../edition-slots.mjs';
7
+ import { found } from './read.mjs';
8
+
9
+ export const HFS_ASSET_OVERSIZE = 'HFS_ASSET_OVERSIZE';
10
+ export const HFS_ASSET_KIND = 'HFS_ASSET_KIND';
11
+ export const DEFAULT_ASSET_BYTES = 512000;
12
+ const BUILD_EXTENSIONS = ['.ttf', '.otf', '.woff', '.woff2', '.eot', '.png', '.svg', '.webp', '.jpg', '.jpeg', '.gif', '.ico', '.avif', '.bmp', '.tif', '.tiff'];
13
+ const MEDIA_EXTENSIONS = new Set(['.mp3', '.mp4', '.wav', '.ogg', '.oga', '.opus', '.flac', '.aac', '.m4a', '.wma', '.aiff', '.aif', '.mid', '.midi', '.mov', '.m4v', '.webm', '.avi', '.mkv', '.mpeg', '.mpg', '.wmv', '.flv', '.3gp']);
14
+ const IMAGE_EXTENSIONS = new Set(BUILD_EXTENSIONS.filter((ext) => !['.ttf', '.otf', '.woff', '.woff2', '.eot'].includes(ext)));
15
+ const MODEL_EXTENSIONS = new Set(['.onnx', '.pt', '.pth', '.ckpt', '.safetensors', '.gguf', '.ggml', '.tflite', '.keras', '.caffemodel', '.h5', '.hdf5', '.npy', '.npz', '.pkl', '.pickle', '.joblib', '.weights']);
16
+ const DATASET_EXTENSIONS = new Set(['.parquet', '.arrow', '.feather', '.avro', '.tfrecord', '.tfrecords', '.jsonl', '.ndjson']);
17
+ const COMPRESSED = /\.(?:gz|bz2|xz|zst|zip|tar|tgz)$/i;
18
+ const SOURCE_OR_DOC = /\.(?:[cm]?[jt]sx?|md|mdx|rst)$/i;
19
+ const DATA_DIRECTORY = /(?:^|\/)(?:datasets?|training-data|training-set|weights|checkpoints)(?:\/|$)/i;
20
+ const MODEL_DIRECTORY = /(?:^|\/)models?(?:\/|$)/i;
21
+ const DOCS_MEDIA = /^docs\/.+\.(?:png|svg|webp)$/i;
22
+ const USER_ASSET_DIRECTORY = /^src\/assets\/(?:media|uploads?|music|audio|videos?)(?:\/|$)/i;
23
+ const MODEL_BINARY = /(?:^|\/)(?:[^/]*[-_.])?(?:model|weights|checkpoint)(?:[-_.][^/]*)?\.bin$/i;
24
+
25
+ function assetPolicy(manifest, edition) {
26
+ const config = editionRuleParams(manifest?.ruleParams?.be ?? {}, edition).assets ?? {};
27
+ const cap = config.bytes ?? config.maxBytes;
28
+ const bytes = Number.isSafeInteger(cap) && cap > 0 ? cap : DEFAULT_ASSET_BYTES;
29
+ const extensions = Array.isArray(config.extensions) ? config.extensions : BUILD_EXTENSIONS;
30
+ return { bytes, extensions: new Set(extensions.filter((ext) => typeof ext === 'string').map((ext) => `.${ext.replace(/^\./, '').toLowerCase()}`)) };
31
+ }
32
+
33
+ function uncompressedName(file) {
34
+ let name = file;
35
+ while (COMPRESSED.test(name)) name = name.replace(COMPRESSED, '');
36
+ return name;
37
+ }
38
+
39
+ function refusedKind(file, extensions) {
40
+ const name = uncompressedName(file);
41
+ const ext = path.posix.extname(name).toLowerCase();
42
+ const asset = file.startsWith('src/assets/');
43
+ if (MODEL_EXTENSIONS.has(ext) || MODEL_BINARY.test(name) || (ext === '.bin' && MODEL_DIRECTORY.test(file))) return 'model weights';
44
+ if (DATASET_EXTENSIONS.has(ext)) return 'a dataset or model artifact';
45
+ // repo.docs-media owns these images even when their subject is a dataset, weights or checkpoints.
46
+ if (DOCS_MEDIA.test(file)) return null;
47
+ if (DATA_DIRECTORY.test(file) && !SOURCE_OR_DOC.test(file)) return 'a dataset or model artifact';
48
+ if (MEDIA_EXTENSIONS.has(ext) || USER_ASSET_DIRECTORY.test(file)) return 'user media';
49
+ if (asset) {
50
+ if (!/^src\/assets\/[^/]+\/.+/.test(file)) return 'an asset outside a per-kind folder';
51
+ if (!extensions.has(path.posix.extname(file).toLowerCase())) return 'an asset extension outside ruleParams.be.assets.extensions';
52
+ } else if (IMAGE_EXTENSIONS.has(ext)) return 'media outside be.assets';
53
+ return null;
54
+ }
55
+
56
+ function fileBytes(repoRoot, file) {
57
+ try {
58
+ const stat = fs.lstatSync(path.join(repoRoot, file));
59
+ return stat.isFile() ? stat.size : null;
60
+ } catch { return null; }
61
+ }
62
+
63
+ /** Side-relative tracked paths only: the app root and FE are left to their own rules, avoiding duplicate root/side findings. */
64
+ export function assetsFindings({ repoRoot, files = [], repo, manifest, isRoot = false, inScope = () => true }) {
65
+ const edition = repo?.edition ?? 'full';
66
+ if (isRoot || repo?.profile !== 'be' || !['lite', 'full'].includes(edition)) return [];
67
+ const policy = assetPolicy(manifest, edition);
68
+ const findings = [];
69
+ for (const raw of new Set(files)) {
70
+ if (!inScope(raw)) continue;
71
+ const file = raw.replaceAll('\\', '/');
72
+ const kind = refusedKind(file, policy.extensions);
73
+ if (kind) findings.push(found(HFS_ASSET_KIND, file, `${file} is ${kind}; keep only small build-time binaries in src/assets/<kind>/, put user media in object storage, and keep model weights and datasets outside git`));
74
+ if (!file.startsWith('src/assets/')) continue;
75
+ const bytes = fileBytes(repoRoot, file);
76
+ if (bytes !== null && bytes > policy.bytes) findings.push(found(HFS_ASSET_OVERSIZE, file, `${file} is ${bytes} bytes, above the ${policy.bytes}-byte per-file build-time asset budget; store it externally and fetch it at runtime`, { bytes, maxBytes: policy.bytes }));
77
+ }
78
+ return findings;
79
+ }
@@ -0,0 +1,16 @@
1
+ // R272: lite proves each app's real Nest composition with doubles; full keeps its e2e proof.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { appWiring } from '../app-wiring.mjs';
5
+
6
+ export const BE_APP_BOOT_PROOF_MISSING = 'BE_APP_BOOT_PROOF_MISSING';
7
+
8
+ /** Root scopes delegate to the be side, so each app is compiled once. */
9
+ export function bootProofFindings({ repoRoot, files, all = files, repo, isRoot = Boolean(repo?.sides), inScope = () => true }) {
10
+ if (repo?.edition !== 'lite' || isRoot || repo?.profile === 'fe' || repo?.kind === 'fe') return [];
11
+ // Dependencies not installed (a fresh scaffold, a lint-only checkout): the Nest graph cannot be compiled, so the proof is unavailable here; CI and every installed app judge it.
12
+ const installed = [repoRoot, path.dirname(repoRoot ?? '.')].some((dir) => dir && fs.existsSync(path.join(dir, 'node_modules')));
13
+ return appWiring({ repoRoot, files: all, repo }).findings
14
+ .filter(finding => installed || finding.reason !== 'module or proof could not be compiled')
15
+ .filter(finding => inScope(finding.path));
16
+ }
@@ -0,0 +1,59 @@
1
+ // canon-copy-drift.mjs - HFS_CANON_COPY_DRIFT (R253): a handful of scaffolded files are hash-sensitive canon,
2
+ // not files to edit. The drift judgement is one sha256 against the bytes of the be/skeleton template the file was
3
+ // scaffolded from - the file equals canon, or it has drifted. A finding carries the path and both hashes, never
4
+ // file content.
5
+ import crypto from 'node:crypto';
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { found } from './read.mjs';
9
+
10
+ const CANON_COPY = 'HFS_CANON_COPY_DRIFT';
11
+ const TEMPLATE_ROOTS = [
12
+ path.resolve(import.meta.dirname, '../../../packages/hfs/templates'),
13
+ path.resolve(import.meta.dirname, '../../../../templates'),
14
+ ];
15
+ const TEMPLATE_DIR = 'be/skeleton/';
16
+
17
+ // The three managed files the standard's secret-custody law pins byte-for-byte.
18
+ const CANON_PATHS = [
19
+ 'src/modules/platform/config/env-source.config.ts',
20
+ 'src/modules/platform/http-security/origin.guard.ts',
21
+ 'src/modules/platform/http-security/webhook-signature.service.ts',
22
+ ];
23
+ const REGEX_ESCAPE = String.raw`\$&`;
24
+ const CANON_SUFFIX = new RegExp(`(?:^|/)(${CANON_PATHS.map((rel) => rel.replace(/[.*+?^${}()|[\]\\]/g, REGEX_ESCAPE)).join('|')})$`);
25
+
26
+ const templateBytes = (rel) => {
27
+ for (const root of TEMPLATE_ROOTS) {
28
+ const file = path.join(root, TEMPLATE_DIR, rel);
29
+ try {
30
+ const bytes = fs.readFileSync(file);
31
+ if (bytes.length) return bytes;
32
+ } catch { /* next root */ }
33
+ }
34
+ return null;
35
+ };
36
+ const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
37
+
38
+ export function canonCopyDriftFindings({ repoRoot, files, inScope }) {
39
+ const findings = [];
40
+ for (const file of files.filter((f) => !inScope || inScope(f))) {
41
+ const hit = CANON_SUFFIX.exec(file.replaceAll('\\', '/'));
42
+ if (!hit) continue;
43
+ const expected = templateBytes(hit[1]);
44
+ if (!expected) continue;
45
+ let actual;
46
+ try {
47
+ actual = fs.readFileSync(path.join(repoRoot, file));
48
+ } catch {
49
+ continue;
50
+ }
51
+ const expectedHash = sha256(expected);
52
+ const actualHash = sha256(actual);
53
+ if (actualHash === expectedHash) continue;
54
+ findings.push(found(CANON_COPY, file,
55
+ `${file} differs from the canon copy it was scaffolded from (sha256 ${actualHash.slice(0, 12)} vs ${expectedHash.slice(0, 12)}); restore the managed file's bytes - it is hash-sensitive canon, not a file to edit`,
56
+ { expectedHash, actualHash }));
57
+ }
58
+ return findings;
59
+ }
@@ -9,13 +9,68 @@
9
9
  import { createHash } from 'node:crypto';
10
10
  import fs from 'node:fs';
11
11
  import path from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
12
13
  import { found, readText } from './read.mjs';
13
14
  import { safeRemove } from '../../api/fs/safe-remove.mjs';
14
15
  import { makeTempDir } from '../../api/fs/make-temp-dir.mjs';
16
+ import { loadTypescript } from '../../lib/package-at.mjs';
15
17
 
16
18
  export const CONTRACT_SNAPSHOT_DRIFT = 'HFS_CONTRACT_SNAPSHOT_DRIFT';
17
19
  const GRAPHQL_TRANSPORT_SLOT = 'be.transport.graphql';
18
20
  const SNAPSHOT_OF = { graphql: 'schema.graphql', json: 'openapi.json' };
21
+ const GRAPHQL_PLATFORM = /^src\/modules\/platform\/graphql(?:\/|\.ts$|$)/;
22
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
23
+
24
+ /** A side-relative source import through a relative path or the canonical backend aliases. */
25
+ function sourcePath(from, specifier) {
26
+ const alias = [['@modules/', 'src/modules/'], ['@features/', 'src/features/']].find(([prefix]) => specifier.startsWith(prefix));
27
+ let relative = null;
28
+ if (alias) relative = `${alias[1]}${specifier.slice(alias[0].length)}`;
29
+ else if (specifier.startsWith('.')) relative = path.posix.join(path.posix.dirname(from), specifier);
30
+ return relative === null ? null : path.posix.normalize(relative).replace(/\.js$/, '.ts');
31
+ }
32
+
33
+ /** A tracked source file of an import, including directory entries and JavaScript-extension TypeScript imports. */
34
+ function sourceOf(base, tracked) {
35
+ return [base, `${base}.ts`, `${base}/index.ts`].find(file => tracked.has(file)) ?? null;
36
+ }
37
+
38
+ /** Runtime imports and re-exports, parsed by the same TypeScript loader as the neighbouring content rules. */
39
+ function runtimeSpecifiers(ts, file, text) {
40
+ const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
41
+ const specifiers = [];
42
+ const visit = node => {
43
+ if (ts.isImportTypeNode(node)) return;
44
+ if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
45
+ const clause = node.importClause ?? node;
46
+ const bindings = ts.isImportDeclaration(node) ? clause.namedBindings : node.exportClause;
47
+ const onlyTypes = clause.isTypeOnly || (!clause.name && bindings?.elements?.length && bindings.elements.every(item => item.isTypeOnly));
48
+ if (!onlyTypes && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) specifiers.push(node.moduleSpecifier.text);
49
+ return;
50
+ }
51
+ if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword && node.arguments[0] && ts.isStringLiteral(node.arguments[0])) specifiers.push(node.arguments[0].text);
52
+ ts.forEachChild(node, visit);
53
+ };
54
+ visit(source);
55
+ return specifiers;
56
+ }
57
+
58
+ /** The import closure of one app's composition, including its groups and the feature transports it owns. */
59
+ function servesGraphql(app, tracked, classified, sources) {
60
+ if ([...classified.values()].some(file => file.slot === GRAPHQL_TRANSPORT_SLOT && file.bindings?.app === app.name)) return true;
61
+ const pending = [`apps/${app.name}/src/app.module.ts`];
62
+ const seen = new Set();
63
+ while (pending.length) {
64
+ const file = pending.pop();
65
+ if (seen.has(file) || !tracked.has(file)) continue;
66
+ seen.add(file);
67
+ if (classified.get(file)?.slot === GRAPHQL_TRANSPORT_SLOT) return true;
68
+ const bases = sources(file).map(specifier => sourcePath(file, specifier)).filter(base => base !== null);
69
+ if (bases.some(base => GRAPHQL_PLATFORM.test(base))) return true;
70
+ pending.push(...bases.map(base => sourceOf(base, tracked)).filter(Boolean));
71
+ }
72
+ return false;
73
+ }
19
74
 
20
75
  /** sha256 of a file's text with line endings folded, or null when it cannot be read. */
21
76
  function contractHash(file) {
@@ -26,13 +81,19 @@ function contractHash(file) {
26
81
  export function contractFindings({ repoRoot, files, repo, resolver }) {
27
82
  const findings = [];
28
83
  if (repo.profile !== 'be') return findings;
29
- const serves = files.some((file) => resolver.classifyPath(file).slot === GRAPHQL_TRANSPORT_SLOT);
30
84
  const tracked = new Set(files);
85
+ const classified = new Map(files.map(file => [file, resolver.classifyPath(file)]));
86
+ const ts = loadTypescript(repoRoot, HERE);
87
+ const imports = new Map();
88
+ const sources = file => {
89
+ if (!imports.has(file)) imports.set(file, ts ? runtimeSpecifiers(ts, file, readText(repoRoot, file) ?? '') : []);
90
+ return imports.get(file);
91
+ };
31
92
  for (const app of repo.apps.filter((a) => a.kind === 'api')) {
32
93
  const operations = `contracts/${app.name}/${SNAPSHOT_OF.json}`;
33
94
  if (tracked.has(`apps/${app.name}/src/operations.ts`) && !tracked.has(operations)) findings.push(found(CONTRACT_SNAPSHOT_DRIFT, operations, `${app.name} declares an operation table (apps/${app.name}/src/operations.ts) but ${operations} is not committed; declare be.contract.openapi in hfs.json sides.be.optionalSlots, run \`npm run contract:emit\` and commit the snapshot`, { app: app.name }));
34
- // An app serves GraphQL when its root module composes the platform graphql capability; a repository with a GraphQL transport slot does not make every api app one.
35
- if (!serves || !(readText(repoRoot, `apps/${app.name}/src/app.module.ts`) ?? '').includes('platform/graphql')) continue;
95
+ // Composition groups and transport ownership are app-specific: another service's GraphQL files do not apply here.
96
+ if (!servesGraphql(app, tracked, classified, sources)) continue;
36
97
  const snapshot = `contracts/${app.name}/${SNAPSHOT_OF.graphql}`;
37
98
  if (!tracked.has(snapshot)) findings.push(found(CONTRACT_SNAPSHOT_DRIFT, snapshot, `${app.name} serves GraphQL but ${snapshot} is not committed; declare be.contract.graphql in hfs.json sides.be.optionalSlots, run \`npm run contract:emit\` and commit the snapshot`, { app: app.name }));
38
99
  }
@@ -62,8 +123,8 @@ function artifactFindings({ repoRoot, tracked, scratch, app, artifact, what, emi
62
123
  }
63
124
  const fresh = contractHash(path.join(scratch, snapshot));
64
125
  if (committed === null) {
65
- // the uncommitted snapshot itself is reported by contractFindings above; here it only counts as not judged
66
126
  apps.push({ app: app.name, artifact, status: 'not-committed' });
127
+ findings.push(found(CONTRACT_SNAPSHOT_DRIFT, snapshot, `${app.name} emits ${snapshot} but it is not committed or readable; run \`npm run contract:emit\` and commit the snapshot`, { app: app.name, drift: 'not-committed' }));
67
128
  } else if (committed === fresh) {
68
129
  apps.push({ app: app.name, artifact, status: 'fresh' });
69
130
  } else {
@@ -76,7 +137,7 @@ function artifactFindings({ repoRoot, tracked, scratch, app, artifact, what, emi
76
137
  * R23, full pass: each committed `contracts/<app>/schema.graphql` and `contracts/<app>/openapi.json` equals what `emit` writes for
77
138
  * the app now. `emit({ repoRoot, declaration, outDir })` is `emitContracts` of packages/hfs/emit and answers `{ written, skipped }`;
78
139
  * it is injected because the runtime scripts do not import the hfs package. Every api app is emitted on its own into a temp
79
- * directory, so one app that cannot emit does not hide the others; an uncommitted snapshot is contractFindings' finding. Answers
140
+ * directory, so one app that cannot emit does not hide the others; an uncommitted emitted snapshot is a finding here too. Answers
80
141
  * `{ findings, apps }`, `apps` being what was judged: `[{ app, artifact, status }]`, `artifact` being `schema.graphql` or
81
142
  * `openapi.json` and `status` one of `fresh`, `stale`, `not-committed`, `none` (nothing emitted, nothing committed; GraphQL only),
82
143
  * `left-behind` (committed, nothing emitted) or `emit-failed`.
@@ -0,0 +1,230 @@
1
+ // R273: an explicitly supplied lcov report proves 100% per measured logic file in either edition.
2
+ // The denominator is the existing slot-derived coverage scope, never the source records present in the report.
3
+ // lcov carries lines, functions and branches; the unit runner also enforces its statement threshold.
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { coverageScope } from '../coverage-scope.mjs';
7
+ import { editionRuleParams, effectiveSlot, slotInEdition } from '../edition-slots.mjs';
8
+ import { globExpression } from '../../lib/glob.mjs';
9
+ import { byCodeUnit } from '../../lib/list.mjs';
10
+ import { found } from './read.mjs';
11
+
12
+ export const HFS_COVERAGE_BELOW = 'HFS_COVERAGE_BELOW';
13
+ const METRICS = ['lines', 'functions', 'branches'];
14
+ const SUMMARY = { LF: ['lines', 'found'], LH: ['lines', 'hit'], FNF: ['functions', 'found'], FNH: ['functions', 'hit'], BRF: ['branches', 'found'], BRH: ['branches', 'hit'] };
15
+ const SOURCE = /\.ts$/;
16
+ const SPEC = /(?:\.(?:spec|test)|-spec)\.ts$/;
17
+ const countOf = (value) => /^\d+$/.test(value) && Number.isSafeInteger(Number(value)) ? Number(value) : null;
18
+ const slash = (value) => path.posix.normalize(String(value).replaceAll('\\', '/'));
19
+
20
+ const emptyRecord = (source) => ({ source, lines: new Map(), functions: new Map(), branches: new Map(), summary: {}, problems: [] });
21
+ const addHits = (map, key, hits) => map.set(key, Math.min(Number.MAX_SAFE_INTEGER, (map.get(key) ?? 0) + hits));
22
+ const parseProblem = (record, line, tag) => record.problems.push(`invalid ${tag} at lcov line ${line}`);
23
+
24
+ function lineData(record, value, line) {
25
+ const [number, count] = value.split(',');
26
+ const hits = countOf(count ?? '');
27
+ if (countOf(number) === null || Number(number) < 1 || hits === null) parseProblem(record, line, 'DA');
28
+ else addHits(record.lines, String(Number(number)), hits);
29
+ }
30
+
31
+ function functionData(record, tag, value, line) {
32
+ const at = value.indexOf(',');
33
+ const count = countOf(value.slice(0, at));
34
+ let name = value.slice(at + 1);
35
+ // FN permits an optional end line before the function name; FNDA's names may themselves contain commas.
36
+ if (tag === 'FN' && /^\d+,/.test(name)) name = name.slice(name.indexOf(',') + 1);
37
+ if (at < 0 || count === null || !name || (tag === 'FN' && count < 1)) parseProblem(record, line, tag);
38
+ else if (tag === 'FN') { if (!record.functions.has(name)) record.functions.set(name, 0); }
39
+ else addHits(record.functions, name, count);
40
+ }
41
+
42
+ function branchData(record, value, line) {
43
+ const parts = value.split(',');
44
+ const hits = parts[3] === '-' ? 0 : countOf(parts[3] ?? '');
45
+ if (parts.length !== 4 || parts.slice(0, 3).some((part) => countOf(part) === null) || Number(parts[0]) < 1 || hits === null) parseProblem(record, line, 'BRDA');
46
+ else addHits(record.branches, parts.slice(0, 3).map(Number).join(','), hits);
47
+ }
48
+
49
+ function summaryData(record, tag, value, line) {
50
+ const count = countOf(value);
51
+ if (count === null || Object.hasOwn(record.summary, tag)) parseProblem(record, line, tag);
52
+ else record.summary[tag] = count;
53
+ }
54
+
55
+ function recordData(record, tag, value, line, problems) {
56
+ if (!['DA', 'FN', 'FNDA', 'BRDA', ...Object.keys(SUMMARY)].includes(tag)) return;
57
+ if (!record) { problems.push(`${tag} outside a source record at lcov line ${line}`); return; }
58
+ if (tag === 'DA') lineData(record, value, line);
59
+ else if (tag === 'FN' || tag === 'FNDA') functionData(record, tag, value, line);
60
+ else if (tag === 'BRDA') branchData(record, value, line);
61
+ else summaryData(record, tag, value, line);
62
+ }
63
+
64
+ /** Parse conventional lcov records without executing or reading any referenced source path. */
65
+ function parseLcov(text) {
66
+ const records = [];
67
+ const problems = [];
68
+ let current = null;
69
+ const flush = () => { if (current) records.push(current); current = null; };
70
+ for (const [index, raw] of String(text).replace(/^\uFEFF/, '').split(/\r?\n/).entries()) {
71
+ const line = raw.trim();
72
+ if (!line) continue;
73
+ if (line === 'end_of_record') { flush(); continue; }
74
+ const colon = line.indexOf(':');
75
+ const tag = line.slice(0, colon);
76
+ const value = line.slice(colon + 1);
77
+ if (tag === 'SF') {
78
+ flush();
79
+ if (!value || value.includes('\0')) problems.push(`invalid SF at lcov line ${index + 1}`);
80
+ else current = emptyRecord(value);
81
+ continue;
82
+ }
83
+ recordData(current, tag, value, index + 1, problems);
84
+ }
85
+ flush();
86
+ if (!records.length) problems.push('lcov has no source records');
87
+ return { records, problems };
88
+ }
89
+
90
+ /** Apply the same edition data view as the slot resolver before deriving the coverage scope. */
91
+ function manifestOfEdition(manifest, edition) {
92
+ return {
93
+ ...manifest,
94
+ slots: manifest.slots.filter((slot) => slotInEdition(manifest, slot, edition)).map((slot) => effectiveSlot(slot, 'be', edition)),
95
+ ruleParams: { ...manifest.ruleParams, be: editionRuleParams(manifest.ruleParams.be, edition) },
96
+ };
97
+ }
98
+
99
+ function inventory({ files, all, repo, manifest }) {
100
+ const prefix = repo.profile === 'app' || repo.sides ? 'be/' : '';
101
+ const effective = manifestOfEdition(manifest, repo.edition ?? 'full');
102
+ const scope = coverageScope(effective, repo.providers ?? repo.sides?.be?.providers ?? []);
103
+ const roots = scope.roots.map((dir) => globExpression(`${dir}**`));
104
+ const excludes = scope.excludes.map((dir) => globExpression(`${dir}**`));
105
+ const candidates = [...new Set((all ?? files ?? []).map(slash))].filter((file) => file.startsWith(prefix) && SOURCE.test(file) && !SPEC.test(file)).sort(byCodeUnit);
106
+ const measured = [];
107
+ const unmeasured = [];
108
+ for (const file of candidates) {
109
+ const relative = file.slice(prefix.length);
110
+ const role = /\.([a-z][a-z0-9-]*)\.ts$/.exec(relative)?.[1];
111
+ const measuredFile = scope.roles.includes(role) && roots.some((root) => root.test(relative)) && !excludes.some((exclude) => exclude.test(relative));
112
+ (measuredFile ? measured : unmeasured).push(file);
113
+ }
114
+ return { measured, unmeasured, unmeasuredScopes: scope.none.map((entry) => `${prefix}${entry}`), scope, prefix };
115
+ }
116
+
117
+ function sourcePath(source) {
118
+ if (source.startsWith('file:')) {
119
+ try {
120
+ const uri = new URL(source);
121
+ source = decodeURIComponent(uri.pathname).replace(/^\/([a-z]:\/)/i, '$1');
122
+ } catch { return null; }
123
+ }
124
+ return slash(source);
125
+ }
126
+
127
+ function matchesSource(source, file, prefix) {
128
+ const relative = prefix && file.startsWith(prefix) ? file.slice(prefix.length) : file;
129
+ const choices = [file, relative, ...(prefix ? [] : [`be/${file}`])];
130
+ const windows = /^[a-z]:\//i.test(source) || source.startsWith('//');
131
+ const absolute = source.startsWith('/') || windows;
132
+ const key = windows ? source.toLowerCase() : source;
133
+ return choices.some((choice) => {
134
+ const candidate = windows ? choice.toLowerCase() : choice;
135
+ return key === candidate || (absolute && key.endsWith(`/${candidate}`));
136
+ });
137
+ }
138
+
139
+ function recordsByFile(records, measured, prefix) {
140
+ const mapped = new Map(measured.map((file) => [file, []]));
141
+ const problems = [];
142
+ for (const record of records) {
143
+ const source = sourcePath(record.source);
144
+ const matches = source === null ? [] : measured.filter((file) => matchesSource(source, file, prefix));
145
+ if (matches.length > 1) problems.push(`lcov source ${record.source} matches more than one measured file`);
146
+ else if (matches.length === 1) mapped.get(matches[0]).push(record);
147
+ }
148
+ return { mapped, problems };
149
+ }
150
+
151
+ function metricOf(record, metric) {
152
+ const tags = Object.keys(SUMMARY).filter((tag) => SUMMARY[tag][0] === metric);
153
+ const [foundTag, hitTag] = tags;
154
+ const details = record[metric];
155
+ const found = (details.size > 0 || record.summary[foundTag] === 0) ? details.size : record.summary[foundTag];
156
+ const hit = details.size ? [...details.values()].filter((count) => count > 0).length : record.summary[hitTag];
157
+ const problems = [];
158
+ if (found === undefined || hit === undefined) problems.push(`${metric} coverage is unavailable`);
159
+ else if (hit > found) problems.push(`${metric} hits exceed the denominator`);
160
+ if (details.size) {
161
+ if (record.summary[foundTag] !== undefined && record.summary[foundTag] !== found) problems.push(`${foundTag} disagrees with its records`);
162
+ if (record.summary[hitTag] !== undefined && record.summary[hitTag] !== hit) problems.push(`${hitTag} disagrees with its records`);
163
+ }
164
+ return { found, hit, problems, detailed: details.size > 0 };
165
+ }
166
+
167
+ function measuredMetric(records, metric) {
168
+ const values = records.map((record) => metricOf(record, metric));
169
+ const problems = values.flatMap((value) => value.problems);
170
+ const details = new Map();
171
+ for (const record of records) for (const [key, hits] of record[metric]) addHits(details, key, hits);
172
+ // Detailed test-name records merge by line/function/branch identity. Without identities, every summary must prove 100%.
173
+ const hasOnlyDetails = values.every((value) => value.detailed || value.found === 0);
174
+ const valid = values.every((value) => !value.problems.length);
175
+ const worst = values.reduce((lowest, value) => {
176
+ const rate = (entry) => entry.found === 0 ? 1 : (entry.hit ?? 0) / (entry.found ?? 1);
177
+ return rate(value) < rate(lowest) || (rate(value) === rate(lowest) && value.found > lowest.found) ? value : lowest;
178
+ });
179
+ const found = hasOnlyDetails ? details.size : (worst.found ?? 0);
180
+ const hit = hasOnlyDetails ? [...details.values()].filter((count) => count > 0).length : (worst.hit ?? 0);
181
+ let percent = null;
182
+ if (valid) percent = found === 0 ? 100 : 100 * hit / found;
183
+ if (valid && hit !== found) problems.push(`${metric} ${hit}/${found} (${(100 * hit / found).toFixed(2)}%) is below 100%`);
184
+ return { metric: { found, hit, percent }, problems };
185
+ }
186
+
187
+ function measuredResult(file, records) {
188
+ const problems = records.flatMap((record) => record.problems);
189
+ const metrics = {};
190
+ if (!records.length) return { path: file, metrics, problems: ['no lcov source record for this measured logic file'], ok: false };
191
+ for (const metric of METRICS) {
192
+ const result = measuredMetric(records, metric);
193
+ metrics[metric] = result.metric;
194
+ problems.push(...result.problems);
195
+ }
196
+ return { path: file, metrics, problems: [...new Set(problems)], ok: problems.length === 0 };
197
+ }
198
+
199
+ /**
200
+ * Read-only R273 report for an app or be side. `lcov` injects report text; `coverageFrom` selects a path relative to repoRoot.
201
+ * Only an explicit report enables the check; ordinary app check does not infer a stale report from coverage/lcov.info.
202
+ */
203
+ export function coverageBelowReport({ repoRoot, files, all, repo, manifest, coverageFrom, lcov } = {}) {
204
+ const enabled = coverageFrom !== undefined || lcov !== undefined;
205
+ const reportPath = typeof coverageFrom === 'string' && coverageFrom ? slash(coverageFrom) : '<lcov>';
206
+ const empty = { ok: true, enabled, reportPath, metrics: METRICS, measured: [], unmeasured: [], unmeasuredScopes: [], results: [], findings: [] };
207
+ if (!enabled || !repo || !['app', 'be'].includes(repo.profile)) return empty;
208
+ const boundary = inventory({ files, all, repo, manifest });
209
+ const report = { ...empty, ...boundary };
210
+ const reportFailure = (message) => { report.findings.push(found(HFS_COVERAGE_BELOW, reportPath, message)); };
211
+ if (lcov === undefined) {
212
+ if (typeof coverageFrom !== 'string' || !coverageFrom) reportFailure('--coverage-from needs an lcov file path');
213
+ else {
214
+ try { lcov = fs.readFileSync(path.resolve(repoRoot, coverageFrom), 'utf8'); }
215
+ catch (error) { reportFailure(`lcov report cannot be read (${error.code ?? 'read failure'}); provide a fresh unit coverage report`); }
216
+ }
217
+ }
218
+ if (lcov !== undefined) {
219
+ const parsed = parseLcov(lcov);
220
+ const { mapped, problems } = recordsByFile(parsed.records, boundary.measured, boundary.prefix);
221
+ for (const problem of [...parsed.problems, ...problems]) reportFailure(problem);
222
+ report.results = boundary.measured.map((file) => measuredResult(file, mapped.get(file)));
223
+ for (const result of report.results.filter((entry) => !entry.ok)) report.findings.push(found(HFS_COVERAGE_BELOW, result.path, `${result.path}: ${result.problems.join('; ')}; measured logic files require 100% lines, functions and branches`, { coverage: result.metrics }));
224
+ }
225
+ report.ok = report.findings.length === 0;
226
+ return report;
227
+ }
228
+
229
+ /** Synchronous finder for the alpha.15 registry; explicit coverage reports may also be passed through extraFindings. */
230
+ export const coverageBelowFindings = (context) => coverageBelowReport(context).findings;