@starci/hfs 4.3.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (515) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/db/migrate.mjs +361 -0
  3. package/db/post-cutover.mjs +136 -0
  4. package/emit/db-types.mjs +10 -1
  5. package/lint/run.mjs +1 -1
  6. package/package.json +4 -1
  7. package/release/gate.mjs +298 -0
  8. package/release/index.mjs +103 -0
  9. package/release/push.mjs +110 -0
  10. package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
  11. package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
  12. package/runtime/knowledge/hfs/rules.yaml +3401 -152
  13. package/runtime/knowledge/hfs/slots.yaml +344 -62
  14. package/runtime/knowledge/patterns/be/api.yaml +32 -5
  15. package/runtime/knowledge/patterns/be/cli.yaml +14 -4
  16. package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
  17. package/runtime/knowledge/patterns/be/jobs.yaml +20 -2
  18. package/runtime/knowledge/patterns/be/projections.yaml +4 -2
  19. package/runtime/knowledge/patterns/be/queues.yaml +11 -3
  20. package/runtime/knowledge/patterns/be/reactors.yaml +20 -1
  21. package/runtime/knowledge/patterns/be/realtime.yaml +25 -3
  22. package/runtime/knowledge/patterns/be/saga.yaml +20 -3
  23. package/runtime/knowledge/patterns/be/webhooks.yaml +23 -4
  24. package/runtime/knowledge/patterns/fe/folder.yaml +1 -0
  25. package/runtime/knowledge/sonar-gate.yaml +1 -1
  26. package/runtime/modules/kernel/failure-codes.yaml +3113 -43
  27. package/runtime/modules/models/runtimes.yaml +1 -1
  28. package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
  29. package/runtime/packages/cli/src/catalog.generated.mjs +14104 -0
  30. package/runtime/packages/cli/src/validate-args.mjs +213 -0
  31. package/runtime/scripts/api/git/add.mjs +5 -0
  32. package/runtime/scripts/api/git/init.mjs +7 -0
  33. package/runtime/scripts/api/git/lib.mjs +3 -0
  34. package/runtime/scripts/api/git/tag.mjs +5 -0
  35. package/runtime/scripts/api/node/lib.mjs +20 -0
  36. package/runtime/scripts/api/node/run-node.mjs +6 -0
  37. package/runtime/scripts/api/sops/lib.mjs +41 -0
  38. package/runtime/scripts/api/sops/selected-age-recipient.mjs +7 -0
  39. package/runtime/scripts/api/supabase/database-dump.mjs +5 -0
  40. package/runtime/scripts/api/supabase/database-query.mjs +5 -0
  41. package/runtime/scripts/api/supabase/lib.mjs +44 -0
  42. package/runtime/scripts/api/supabase/migration-repair.mjs +5 -0
  43. package/runtime/scripts/hfs/app-wiring.mjs +201 -0
  44. package/runtime/scripts/hfs/architecture/backend-app-files.mjs +1 -8
  45. package/runtime/scripts/hfs/architecture/backend.mjs +2 -7
  46. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
  47. package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
  48. package/runtime/scripts/hfs/architecture/dead-exports.mjs +9 -41
  49. package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
  50. package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
  51. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
  52. package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
  53. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +28 -13
  54. package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
  55. package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
  56. package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
  57. package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
  58. package/runtime/scripts/hfs/architecture/register-once.mjs +2 -2
  59. package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
  60. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +211 -11
  61. package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -1
  62. package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
  63. package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
  64. package/runtime/scripts/hfs/architecture/tiers.mjs +3 -3
  65. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +8 -2
  66. package/runtime/scripts/hfs/check-results.mjs +28 -0
  67. package/runtime/scripts/hfs/check.mjs +46 -47
  68. package/runtime/scripts/hfs/coverage-from.mjs +70 -0
  69. package/runtime/scripts/hfs/db-baseline-verb.mjs +276 -0
  70. package/runtime/scripts/hfs/db-plan-verb.mjs +254 -0
  71. package/runtime/scripts/hfs/db-restamp-verb.mjs +60 -0
  72. package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
  73. package/runtime/scripts/hfs/doc-commands.mjs +367 -0
  74. package/runtime/scripts/hfs/edition-slots.mjs +2 -2
  75. package/runtime/scripts/hfs/example-secrets.mjs +158 -0
  76. package/runtime/scripts/hfs/manifest-shape.mjs +67 -7
  77. package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
  78. package/runtime/scripts/hfs/rule-catalog.mjs +156 -11
  79. package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
  80. package/runtime/scripts/hfs/rules/a15-findings.mjs +88 -0
  81. package/runtime/scripts/hfs/rules/app-env-required.mjs +20 -0
  82. package/runtime/scripts/hfs/rules/app-root.mjs +2 -2
  83. package/runtime/scripts/hfs/rules/assets.mjs +79 -0
  84. package/runtime/scripts/hfs/rules/boot-proof.mjs +16 -0
  85. package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +59 -0
  86. package/runtime/scripts/hfs/rules/contract.mjs +66 -5
  87. package/runtime/scripts/hfs/rules/coverage-below.mjs +230 -0
  88. package/runtime/scripts/hfs/rules/database-access.mjs +343 -0
  89. package/runtime/scripts/hfs/rules/database-baseline.mjs +142 -0
  90. package/runtime/scripts/hfs/rules/database-config.mjs +250 -2
  91. package/runtime/scripts/hfs/rules/database-constants.mjs +9 -0
  92. package/runtime/scripts/hfs/rules/database-migrations.mjs +286 -7
  93. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +119 -9
  94. package/runtime/scripts/hfs/rules/database-queue-sources.mjs +176 -0
  95. package/runtime/scripts/hfs/rules/database-queues.mjs +361 -0
  96. package/runtime/scripts/hfs/rules/database-sql.mjs +91 -9
  97. package/runtime/scripts/hfs/rules/database.mjs +9 -0
  98. package/runtime/scripts/hfs/rules/deploy-scan.mjs +192 -0
  99. package/runtime/scripts/hfs/rules/deploy-workflow.mjs +219 -0
  100. package/runtime/scripts/hfs/rules/doc-fresh.mjs +286 -0
  101. package/runtime/scripts/hfs/rules/docker-health.mjs +249 -0
  102. package/runtime/scripts/hfs/rules/docker.mjs +24 -7
  103. package/runtime/scripts/hfs/rules/edition.mjs +9 -8
  104. package/runtime/scripts/hfs/rules/env-parity.mjs +373 -0
  105. package/runtime/scripts/hfs/rules/env-value-shape.mjs +113 -0
  106. package/runtime/scripts/hfs/rules/event-bus.mjs +3 -2
  107. package/runtime/scripts/hfs/rules/event-tables.mjs +100 -0
  108. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +115 -15
  109. package/runtime/scripts/hfs/rules/fe-stack.mjs +57 -0
  110. package/runtime/scripts/hfs/rules/feature-files.mjs +28 -0
  111. package/runtime/scripts/hfs/rules/frontend-tree.mjs +54 -35
  112. package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -2
  113. package/runtime/scripts/hfs/rules/lexicon.mjs +62 -0
  114. package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +89 -0
  115. package/runtime/scripts/hfs/rules/monorepo.mjs +14 -1
  116. package/runtime/scripts/hfs/rules/pipeline.mjs +8 -1
  117. package/runtime/scripts/hfs/rules/release-provenance.mjs +211 -0
  118. package/runtime/scripts/hfs/rules/saga-delivery.mjs +66 -0
  119. package/runtime/scripts/hfs/rules/saga.mjs +8 -16
  120. package/runtime/scripts/hfs/rules/script-hygiene.mjs +151 -0
  121. package/runtime/scripts/hfs/rules/secret-custody-only.mjs +65 -0
  122. package/runtime/scripts/hfs/rules/secrets.mjs +13 -0
  123. package/runtime/scripts/hfs/rules/services.mjs +4 -4
  124. package/runtime/scripts/hfs/rules/stack-tooling.mjs +93 -0
  125. package/runtime/scripts/hfs/rules/stacks-deploy.mjs +76 -0
  126. package/runtime/scripts/hfs/rules/stacks-ingress.mjs +87 -0
  127. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -0
  128. package/runtime/scripts/hfs/rules/stacks-release.mjs +366 -0
  129. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +41 -1
  130. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
  131. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +1 -1
  132. package/runtime/scripts/hfs/rules/supply-chain.mjs +167 -0
  133. package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +101 -0
  134. package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +77 -0
  135. package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +165 -0
  136. package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +103 -0
  137. package/runtime/scripts/hfs/rules/unused-symbol.mjs +320 -0
  138. package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +185 -0
  139. package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +19 -0
  140. package/runtime/scripts/hfs/sealed-members.mjs +101 -0
  141. package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +200 -0
  142. package/runtime/scripts/hfs/secret-edit.mjs +170 -0
  143. package/runtime/scripts/hfs/secret-init.mjs +127 -19
  144. package/runtime/scripts/hfs/secret-sops.mjs +38 -6
  145. package/runtime/scripts/hfs/secret-store.mjs +56 -54
  146. package/runtime/scripts/hfs/secret.mjs +34 -5
  147. package/runtime/scripts/hfs/slot-imports.mjs +18 -5
  148. package/runtime/scripts/hfs/slots.mjs +1 -0
  149. package/runtime/scripts/hfs/stack-config-kit.mjs +34 -0
  150. package/runtime/scripts/hfs/stack-deploy-config.mjs +313 -0
  151. package/runtime/scripts/hfs/stack-ingress-config.mjs +237 -0
  152. package/runtime/scripts/hfs/stacks-gate.mjs +414 -0
  153. package/runtime/scripts/hfs/starcistacks-runtime.mjs +61 -0
  154. package/runtime/scripts/lib/command-tokens.mjs +126 -0
  155. package/runtime/scripts/lib/compose-values.mjs +11 -0
  156. package/runtime/scripts/lib/deployment-findings.mjs +7 -0
  157. package/runtime/scripts/lib/json-clone.mjs +61 -0
  158. package/runtime/scripts/lib/list.mjs +4 -0
  159. package/runtime/scripts/lib/runtime-example.mjs +13 -0
  160. package/runtime/scripts/lib/secret-patterns.mjs +60 -5
  161. package/runtime/scripts/lib/sql-ast.mjs +32 -0
  162. package/runtime/scripts/lib/stack-services.mjs +1 -1
  163. package/runtime/scripts/lib/swarm-stack.mjs +5 -18
  164. package/runtime/scripts/lib/trim-end.mjs +8 -0
  165. package/runtime/scripts/lib/ts-analysis.mjs +5 -2
  166. package/runtime/scripts/lib/ts-ast.mjs +27 -2
  167. package/runtime/scripts/lib/workflow-events.mjs +8 -0
  168. package/scaffold/add-app.mjs +77 -59
  169. package/scaffold/add-cli-lite.mjs +14 -6
  170. package/scaffold/add-compose.mjs +79 -0
  171. package/scaffold/add-database-lite.mjs +14 -0
  172. package/scaffold/add-fe-overlay.mjs +122 -0
  173. package/scaffold/add-fe-page.mjs +269 -0
  174. package/scaffold/add-inbox-lite.mjs +74 -42
  175. package/scaffold/add-table.mjs +110 -87
  176. package/scaffold/add.mjs +159 -44
  177. package/scaffold/app.mjs +124 -39
  178. package/scaffold/edition-gate.mjs +3 -3
  179. package/scaffold/lite-exports.mjs +2 -1
  180. package/scaffold/service.mjs +2 -2
  181. package/src/main.mjs +229 -116
  182. package/stack/age-install.mjs +37 -0
  183. package/stack/backup.mjs +100 -0
  184. package/stack/build-args.mjs +42 -0
  185. package/stack/build.mjs +173 -0
  186. package/stack/cli.mjs +130 -0
  187. package/stack/config.mjs +57 -0
  188. package/stack/context.mjs +57 -0
  189. package/stack/converge.mjs +107 -0
  190. package/stack/deploy.mjs +301 -0
  191. package/stack/digest.mjs +16 -0
  192. package/stack/docker-transport.mjs +53 -0
  193. package/stack/edge-alias.mjs +118 -0
  194. package/stack/errors.mjs +21 -0
  195. package/stack/image-scan.mjs +152 -0
  196. package/stack/ingress-create.mjs +114 -0
  197. package/stack/ingress-publish.mjs +273 -0
  198. package/stack/ingress.mjs +706 -0
  199. package/stack/jobs.mjs +51 -0
  200. package/stack/live-restore.mjs +177 -0
  201. package/stack/prepare.mjs +266 -0
  202. package/stack/private-file.mjs +22 -0
  203. package/stack/provision.mjs +90 -0
  204. package/stack/prune.mjs +46 -0
  205. package/stack/release-store.mjs +87 -0
  206. package/stack/remote.mjs +205 -0
  207. package/stack/render.mjs +149 -0
  208. package/stack/retire.mjs +47 -0
  209. package/stack/sealed.mjs +149 -0
  210. package/stack/task.mjs +42 -0
  211. package/stack/verify.mjs +99 -0
  212. package/sync/ci-commands.mjs +13 -0
  213. package/sync/index.mjs +108 -52
  214. package/sync/stack-images.mjs +127 -0
  215. package/templates/app/ci-workflows/github/workflows/ci.yml +5 -1
  216. package/templates/app/ci-workflows/github/workflows/images.yml +15 -3
  217. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +148 -0
  218. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +7 -3
  219. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +38 -6
  220. package/templates/app/ci-workflows-lite/github/workflows/images.yml +14 -2
  221. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +148 -0
  222. package/templates/app/codegen-graphql.mjs.partial +13 -2
  223. package/templates/app/codegen-lite/scripts/codegen.mjs +6 -1
  224. package/templates/app/codegen-prelude.mjs.partial +8 -0
  225. package/templates/app/gitignore +3 -0
  226. package/templates/app/gitignore-lite +3 -0
  227. package/templates/app/package-scripts/package.json +1 -0
  228. package/templates/app/package-scripts/supabase.json +7 -0
  229. package/templates/app/package-scripts-lite/package.json +1 -7
  230. package/templates/app/skeleton/.editorconfig +1 -0
  231. package/templates/app/skeleton/.gitattributes +7 -1
  232. package/templates/app/skeleton/.github/dependabot.yml +17 -0
  233. package/templates/app/skeleton/.nvmrc +1 -1
  234. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +134 -1
  235. package/templates/app/skeleton/.starcistacks/dev/README.md +16 -0
  236. package/templates/app/skeleton/.starcistacks/dev/environment.json +5 -0
  237. package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +71 -0
  238. package/templates/app/skeleton/.starcistacks/dev/runtime/env/KEYS.md +14 -0
  239. package/templates/app/skeleton/.starcistacks/production/README.md +37 -0
  240. package/templates/app/skeleton/.starcistacks/production/environment.json +5 -0
  241. package/templates/app/skeleton/.starcistacks/production/infra/swarm/Caddyfile +15 -0
  242. package/templates/app/skeleton/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  243. package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  244. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  245. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +9 -0
  246. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  247. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +142 -0
  248. package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +14 -0
  249. package/templates/app/skeleton/README.md +6 -6
  250. package/templates/app/skeleton/scripts/codegen.mjs +1 -2
  251. package/templates/app/skeleton-lite/.editorconfig +16 -0
  252. package/templates/app/skeleton-lite/.gitattributes +8 -0
  253. package/templates/app/skeleton-lite/.github/dependabot.yml +17 -0
  254. package/templates/app/skeleton-lite/.nvmrc +1 -0
  255. package/templates/app/skeleton-lite/.sops.yaml +3 -2
  256. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +178 -1
  257. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +20 -0
  258. package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +2 -1
  259. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +16 -0
  260. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +11 -0
  261. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +7 -0
  262. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +6 -0
  263. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +8 -0
  264. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +6 -0
  265. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +120 -0
  266. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -0
  267. package/templates/app/skeleton-lite/.starcistacks/production/README.md +38 -0
  268. package/templates/app/skeleton-lite/.starcistacks/production/environment.json +6 -0
  269. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/Caddyfile +11 -0
  270. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  271. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  272. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  273. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/images.lock.yaml +8 -0
  274. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  275. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +121 -0
  276. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +21 -0
  277. package/templates/app/skeleton-lite/.starciwork/brand/index.yaml +12 -0
  278. package/templates/app/skeleton-lite/README.md +5 -3
  279. package/templates/app/skeleton-lite/supabase/config.toml +8 -1
  280. package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +15 -6
  281. package/templates/app/upgrade-full/.starcistacks/dev/README.md +13 -7
  282. package/templates/be/image/worker/Dockerfile +1 -1
  283. package/templates/be/inbox/database-client.spec.ts.tpl +17 -18
  284. package/templates/be/inbox/database-client.ts.tpl +12 -11
  285. package/templates/be/inbox/delivery-recorded.event.ts.tpl +36 -0
  286. package/templates/be/inbox/event-index.ts.tpl +1 -0
  287. package/templates/be/inbox/events.json.tpl +10 -0
  288. package/templates/be/inbox/migration.sql.tpl +1 -0
  289. package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +42 -0
  290. package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +9 -0
  291. package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +6 -0
  292. package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +19 -0
  293. package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +2 -0
  294. package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +16 -0
  295. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +45 -0
  296. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +15 -0
  297. package/templates/be/inbox/platform/event-bus/index.ts.tpl +5 -0
  298. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +5 -0
  299. package/templates/be/inbox/platform/inbox/inbox.decorators.ts.tpl +9 -0
  300. package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +6 -0
  301. package/templates/be/inbox/platform/inbox/inbox.module.ts.tpl +19 -0
  302. package/templates/be/inbox/platform/inbox/inbox.options.ts.tpl +2 -0
  303. package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +10 -0
  304. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +41 -0
  305. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +19 -0
  306. package/templates/be/inbox/platform/inbox/index.ts.tpl +3 -0
  307. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +7 -0
  308. package/templates/be/inbox/platform/migration.sql.tpl +48 -0
  309. package/templates/be/inbox/service.spec.ts.tpl +42 -9
  310. package/templates/be/inbox/service.ts.tpl +23 -8
  311. package/templates/be/inbox/webhook-door.spec.ts.tpl +1 -1
  312. package/templates/be/inbox/webhook-door.ts.tpl +2 -2
  313. package/templates/be/patterns/api/feature.module-definition.ts.tpl +6 -0
  314. package/templates/be/patterns/api/feature.module.ts.tpl +2 -1
  315. package/templates/be/patterns/api/feature.options.ts.tpl +2 -0
  316. package/templates/be/patterns/api/graphql.module-definition.ts.tpl +6 -0
  317. package/templates/be/patterns/api/graphql.module.ts.tpl +3 -2
  318. package/templates/be/patterns/api/graphql.options.ts.tpl +2 -0
  319. package/templates/be/patterns/cli/group.module-definition.ts.tpl +6 -0
  320. package/templates/be/patterns/cli/group.module.ts.tpl +2 -1
  321. package/templates/be/patterns/cli/group.options.ts.tpl +2 -0
  322. package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +0 -3
  323. package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +0 -2
  324. package/templates/be/patterns/jobs/job.module-definition.ts.tpl +6 -0
  325. package/templates/be/patterns/jobs/job.module.ts.tpl +2 -1
  326. package/templates/be/patterns/jobs/job.options.ts.tpl +2 -0
  327. package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +6 -0
  328. package/templates/be/patterns/jobs/job.queue.module.ts.tpl +6 -3
  329. package/templates/be/patterns/jobs/job.queue.options.ts.tpl +2 -0
  330. package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +3 -1
  331. package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +25 -9
  332. package/templates/be/patterns/queues/platform/queue.config.ts.tpl +1 -1
  333. package/templates/be/patterns/queues/platform/queue.index.ts.tpl +1 -1
  334. package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +2 -2
  335. package/templates/be/patterns/queues/platform/queue.module.ts.tpl +11 -0
  336. package/templates/be/patterns/queues/platform/queue.options.ts.tpl +10 -0
  337. package/templates/be/patterns/queues/queue.module-definition.ts.tpl +6 -0
  338. package/templates/be/patterns/queues/queue.module.ts.tpl +2 -1
  339. package/templates/be/patterns/queues/queue.options.ts.tpl +2 -0
  340. package/templates/be/patterns/reactors/message.module-definition.ts.tpl +6 -0
  341. package/templates/be/patterns/reactors/message.module.ts.tpl +6 -3
  342. package/templates/be/patterns/reactors/message.options.ts.tpl +2 -0
  343. package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +6 -0
  344. package/templates/be/patterns/reactors/reactor.module.ts.tpl +2 -1
  345. package/templates/be/patterns/reactors/reactor.options.ts.tpl +2 -0
  346. package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +188 -0
  347. package/templates/be/patterns/realtime/channel.gateway.ts.tpl +22 -0
  348. package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +6 -0
  349. package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +2 -1
  350. package/templates/be/patterns/realtime/channel.graphql.options.ts.tpl +2 -0
  351. package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +6 -0
  352. package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +110 -0
  353. package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +7 -0
  354. package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +6 -0
  355. package/templates/be/patterns/saga/saga.message.module.ts.tpl +6 -3
  356. package/templates/be/patterns/saga/saga.message.options.ts.tpl +2 -0
  357. package/templates/be/patterns/saga/saga.module-definition.ts.tpl +6 -0
  358. package/templates/be/patterns/saga/saga.module.ts.tpl +2 -1
  359. package/templates/be/patterns/saga/saga.options.ts.tpl +2 -0
  360. package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +6 -0
  361. package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +2 -1
  362. package/templates/be/patterns/webhooks/webhook.http.options.ts.tpl +2 -0
  363. package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +80 -26
  364. package/templates/be/platform/graphql/localize-errors.mapper.ts +2 -5
  365. package/templates/be/skeleton/apps/__app__/src/app.module.ts +11 -9
  366. package/templates/be/skeleton/apps/__app__/src/compose/api.ts +4 -0
  367. package/templates/be/skeleton/apps/__app__/src/compose/domain.ts +11 -0
  368. package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +2 -0
  369. package/templates/be/skeleton/apps/__app__/src/compose/system.ts +5 -0
  370. package/templates/be/skeleton/apps/__app__/src/compose/webhooks.ts +4 -0
  371. package/templates/be/skeleton/apps/__app__/src/main.ts +15 -1
  372. package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
  373. package/templates/be/skeleton/apps/cli/src/cli.options.ts +6 -14
  374. package/templates/be/skeleton/apps/cli/src/compose/cli.ts +5 -0
  375. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +24 -0
  376. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.contracts.ts +2 -0
  377. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.handler.ts +22 -0
  378. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.query.ts +11 -0
  379. package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +6 -0
  380. package/templates/be/skeleton/src/features/api/system-health/system-health.module.ts +5 -3
  381. package/templates/be/skeleton/src/features/api/system-health/system-health.options.ts +2 -0
  382. package/templates/be/skeleton/src/features/api/system-health/transport/http/ready.controller.ts +19 -0
  383. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +6 -0
  384. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module.ts +8 -3
  385. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.options.ts +2 -0
  386. package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +6 -0
  387. package/templates/be/skeleton/src/features/cli/cli.module.ts +3 -2
  388. package/templates/be/skeleton/src/features/cli/cli.options.ts +2 -0
  389. package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +6 -0
  390. package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +2 -1
  391. package/templates/be/skeleton/src/features/cli/migrate/migrate.options.ts +2 -0
  392. package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +6 -0
  393. package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +2 -1
  394. package/templates/be/skeleton/src/features/cli/seed/seed.options.ts +2 -0
  395. package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +5 -6
  396. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +56 -4
  397. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +36 -5
  398. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +39 -8
  399. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +14 -8
  400. package/templates/be/skeleton/src/modules/domain/identity/credential.contracts.ts +26 -0
  401. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +119 -0
  402. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +43 -0
  403. package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +16 -0
  404. package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -1
  405. package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +22 -4
  406. package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +8 -2
  407. package/templates/be/skeleton/src/modules/domain/identity/index.ts +1 -0
  408. package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +11 -4
  409. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.spec.ts +32 -0
  410. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.ts +14 -0
  411. package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
  412. package/templates/be/skeleton/src/modules/platform/database/index.ts +2 -0
  413. package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +8 -0
  414. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +25 -6
  415. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +16 -6
  416. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +130 -0
  417. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +58 -0
  418. package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +3 -0
  419. package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -4
  420. package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -10
  421. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +3 -1
  422. package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +2 -0
  423. package/templates/be/skeleton/src/modules/platform/http-security/index.ts +5 -0
  424. package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +12 -5
  425. package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +13 -8
  426. package/templates/be/skeleton/src/modules/platform/http-security/readiness.contracts.ts +5 -0
  427. package/templates/be/skeleton/src/modules/platform/http-security/readiness.decorators.ts +7 -0
  428. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +86 -0
  429. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +39 -0
  430. package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +139 -131
  431. package/templates/be/skeleton/src/modules/platform/i18n/index.ts +1 -1
  432. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +91 -67
  433. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +5 -4
  434. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +0 -2
  435. package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +20 -6
  436. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +16 -4
  437. package/templates/be/skeleton/src/modules/platform/retry/index.ts +3 -0
  438. package/templates/be/skeleton/src/modules/platform/retry/retry.decorators.ts +9 -0
  439. package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +7 -0
  440. package/templates/be/skeleton/src/modules/platform/retry/retry.module.ts +19 -0
  441. package/templates/be/skeleton/src/modules/platform/retry/retry.options.ts +2 -0
  442. package/templates/be/skeleton/src/modules/platform/retry/retry.port.ts +11 -0
  443. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.spec.ts +36 -0
  444. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.ts +10 -0
  445. package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +54 -0
  446. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +14 -18
  447. package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +4 -0
  448. package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +10 -0
  449. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +16 -0
  450. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +2 -0
  451. package/templates/be/skeleton-lite/apps/api/src/compose/system.ts +5 -0
  452. package/templates/be/skeleton-lite/apps/api/src/compose/webhooks.ts +4 -0
  453. package/templates/be/skeleton-lite/apps/api/src/main.ts +2 -3
  454. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +12 -0
  455. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +71 -5
  456. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +29 -2
  457. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +68 -15
  458. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +14 -9
  459. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +9 -2
  460. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +16 -1
  461. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +19 -4
  462. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +5 -1
  463. package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -1
  464. package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -3
  465. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +17 -5
  466. package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +24 -13
  467. package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +5 -0
  468. package/templates/be/skeleton-lite/src/modules/platform/database/errors/database.error.ts +16 -0
  469. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +3 -0
  470. package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +5 -2
  471. package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +4 -1
  472. package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
  473. package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +7 -0
  474. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +29 -0
  475. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +16 -0
  476. package/templates/be/table/api/feature.module-definition.ts.tpl +6 -0
  477. package/templates/be/table/api/feature.module.ts.tpl +2 -1
  478. package/templates/be/table/api/feature.options.ts.tpl +2 -0
  479. package/templates/be/table/api/graphql.module-definition.ts.tpl +6 -0
  480. package/templates/be/table/api/graphql.module.ts.tpl +3 -2
  481. package/templates/be/table/api/graphql.options.ts.tpl +2 -0
  482. package/templates/be/table/migration.sql.tpl +1 -0
  483. package/templates/be/upgrade-full/apps/cli/src/app.module.ts +4 -6
  484. package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +7 -0
  485. package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +5 -0
  486. package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +9 -0
  487. package/templates/be/upgrade-full/apps/cli/src/main.ts +3 -0
  488. package/templates/be/upgrade-full/src/features/cli/cli.module.ts +3 -2
  489. package/templates/fe/auth/supabase-client.ts.tpl +34 -0
  490. package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +11 -0
  491. package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/index.tsx +6 -1
  492. package/templates/fe/skeleton/apps/__app__/src/components/leaves/Icon/index.tsx +4 -0
  493. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/component.tsx +31 -0
  494. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/index.tsx +24 -0
  495. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -1
  496. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +3 -4
  497. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/index.ts +1 -0
  498. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +15 -0
  499. package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +18 -0
  500. package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +19 -0
  501. package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +7 -1
  502. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/en.json +57 -0
  503. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +6 -0
  504. package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/en.json +31 -0
  505. package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +1 -1
  506. package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -0
  507. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +1 -34
  508. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +7 -1
  509. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +57 -0
  510. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +30 -24
  511. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +1 -1
  512. package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
  513. package/templates/fe/transport/graphql/clients/links/locale.ts +2 -2
  514. package/upgrade/check.mjs +3 -1
  515. package/upgrade/index.mjs +53 -40
@@ -4,7 +4,7 @@ import { sourceLocation } from '../../lib/ts-ast.mjs';
4
4
  /**
5
5
  * R55 `client-reaches-server` (FE_CLIENT_REACHES_SERVER), the repository half of the eslint rule `client-no-server-import`,
6
6
  * which sees one hop from a file that carries the directive. The machine walks the resolved import graph from every module
7
- * whose first statement is the `"use client"` directive, over runtime edges only (a type-only import vanishes at build), and
7
+ * whose directive prologue contains `"use client"`, over runtime edges only (a type-only import vanishes at build), and
8
8
  * reports any reachable module (the entry itself included) that imports a server-only surface:
9
9
  *
10
10
  * - `server-only`, `next/headers`, `next/server`, `next-intl/server`;
@@ -22,8 +22,11 @@ const NODE_BUILTINS = new Set(builtinModules.map(name => name.replace(/^node:/u,
22
22
 
23
23
  const isServerSpecifier = specifier => SERVER_MODULES.has(specifier) || specifier.startsWith('node:') || NODE_BUILTINS.has(specifier.split('/')[0]);
24
24
  const isClientEntry = (ts, file) => {
25
- const first = file.sourceFile.statements[0];
26
- return Boolean(first) && ts.isExpressionStatement(first) && ts.isStringLiteral(first.expression) && first.expression.text === 'use client';
25
+ for (const statement of file.sourceFile.statements) {
26
+ if (!ts.isExpressionStatement(statement) || !ts.isStringLiteral(statement.expression)) return false;
27
+ if (statement.expression.text === 'use client') return true;
28
+ }
29
+ return false;
27
30
  };
28
31
 
29
32
  const reachesBundle = (ts, declaration) => {
@@ -0,0 +1,149 @@
1
+ // Import and namespace consumption analysis shared by dead-export edges.
2
+ import path from 'node:path';
3
+
4
+ /** Disk/staged test consumers have no production checker; bind their local names without resolving dependencies. */
5
+ const namespaceChecker = (context, sourceFile, namespace) => {
6
+ if (context.namespaceCheckers.has(sourceFile)) return context.namespaceCheckers.get(sourceFile);
7
+ const { ts } = context;
8
+ let checker = context.checkerFor?.(sourceFile.fileName);
9
+ if (!checker?.getSymbolAtLocation(namespace.name)) {
10
+ const options = { noResolve: true, noLib: true, types: [], allowJs: true, target: ts.ScriptTarget.Latest };
11
+ const host = ts.createCompilerHost(options);
12
+ const filename = path.resolve(sourceFile.fileName);
13
+ host.getSourceFile = file => path.resolve(file) === filename ? sourceFile : undefined;
14
+ host.fileExists = file => path.resolve(file) === filename;
15
+ host.readFile = file => path.resolve(file) === filename ? sourceFile.text : undefined;
16
+ checker = ts.createProgram([filename], options, host).getTypeChecker();
17
+ }
18
+ context.namespaceCheckers.set(sourceFile, checker);
19
+ return checker;
20
+ };
21
+
22
+ const transparentExpression = (ts, node) => node && (ts.isParenthesizedExpression(node) || ts.isAsExpression(node)
23
+ || ts.isTypeAssertionExpression(node) || ts.isNonNullExpression(node) || ts.isSatisfiesExpression(node));
24
+
25
+ const literalMember = (ts, node) => {
26
+ while (transparentExpression(ts, node)) node = node.expression;
27
+ return node && (ts.isStringLiteralLike(node) || ts.isNumericLiteral(node)) ? node.text : null;
28
+ };
29
+
30
+ const destructuredMember = (ts, name) => {
31
+ if (ts.isIdentifier(name)) return name.text;
32
+ if (ts.isComputedPropertyName(name)) return literalMember(ts, name.expression);
33
+ return literalMember(ts, name);
34
+ };
35
+
36
+ /** A destructuring alias consumes the property's name, while a rest/computed binding may consume every export. */
37
+ const destructuredBindings = (ts, pattern) => {
38
+ const names = [];
39
+ const elements = ts.isObjectBindingPattern(pattern) ? pattern.elements : pattern.properties;
40
+ for (const element of elements) {
41
+ if (element.dotDotDotToken || ts.isSpreadAssignment(element)) return { all: true, pairs: [] };
42
+ const name = destructuredMember(ts, element.propertyName ?? element.name);
43
+ if (name === null) return { all: true, pairs: [] };
44
+ names.push(name);
45
+ }
46
+ return { all: false, pairs: names.map(imported => ({ imported, exported: null })) };
47
+ };
48
+
49
+ const namespaceReference = (ts, checker, symbol, node) => {
50
+ if (checker.getSymbolAtLocation(node) === symbol) return true;
51
+ const parent = node.parent;
52
+ if (ts.isShorthandPropertyAssignment(parent) && parent.name === node) return checker.getShorthandAssignmentValueSymbol(parent) === symbol;
53
+ return ts.isExportSpecifier(parent) && node === (parent.propertyName ?? parent.name)
54
+ && checker.getExportSpecifierLocalTargetSymbol(parent) === symbol;
55
+ };
56
+
57
+ const unwrappedNamespace = (ts, node) => {
58
+ while (transparentExpression(ts, node.parent)) node = node.parent;
59
+ return node;
60
+ };
61
+
62
+ const indexedNamespaceMember = (ts, node) => {
63
+ if (!ts.isTypeQueryNode(node.parent) || node.parent.exprName !== node) return null;
64
+ let query = node.parent;
65
+ while (ts.isParenthesizedTypeNode(query.parent)) query = query.parent;
66
+ const indexed = query.parent;
67
+ return ts.isIndexedAccessTypeNode(indexed) && indexed.objectType === query && ts.isLiteralTypeNode(indexed.indexType)
68
+ ? literalMember(ts, indexed.indexType.literal) : null;
69
+ };
70
+
71
+ /** The first member is the entry export even in a chain such as namespace.Enum.Member or namespace.Type.Inner. */
72
+ const namespaceUse = (ts, reference) => {
73
+ const node = unwrappedNamespace(ts, reference);
74
+ const parent = node.parent;
75
+ let name = null;
76
+ if (ts.isPropertyAccessExpression(parent) && parent.expression === node) name = parent.name.text;
77
+ else if (ts.isQualifiedName(parent) && parent.left === node) name = parent.right.text;
78
+ else if (ts.isElementAccessExpression(parent) && parent.expression === node) name = literalMember(ts, parent.argumentExpression);
79
+ else if (ts.isVariableDeclaration(parent) && parent.initializer === node && ts.isObjectBindingPattern(parent.name)) return destructuredBindings(ts, parent.name);
80
+ else if (ts.isBinaryExpression(parent) && parent.right === node && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken
81
+ && ts.isObjectLiteralExpression(parent.left)) return destructuredBindings(ts, parent.left);
82
+ else name = indexedNamespaceMember(ts, node);
83
+ return name === null ? { all: true, pairs: [] } : { all: false, pairs: [{ imported: name, exported: null }] };
84
+ };
85
+
86
+ const namespaceBindings = (context, declaration, namespace) => {
87
+ if (context.namespaceUses.has(namespace)) return context.namespaceUses.get(namespace);
88
+ const { ts } = context;
89
+ const sourceFile = declaration.getSourceFile();
90
+ const checker = namespaceChecker(context, sourceFile, namespace);
91
+ const symbol = checker.getSymbolAtLocation(namespace.name);
92
+ const result = { all: false, pairs: [] };
93
+ const visit = node => {
94
+ if (ts.isJSDocSeeTag(node) || ts.isJSDocLink(node) || ts.isJSDocLinkCode(node) || ts.isJSDocLinkPlain(node)) return;
95
+ if (ts.isIdentifier(node) && node !== namespace.name && namespaceReference(ts, checker, symbol, node)) {
96
+ const used = namespaceUse(ts, node);
97
+ result.all ||= used.all;
98
+ result.pairs.push(...used.pairs);
99
+ }
100
+ if (!result.all) {
101
+ ts.forEachChild(node, visit);
102
+ for (const doc of node.jsDoc ?? []) visit(doc);
103
+ }
104
+ };
105
+ // An unavailable binding must never turn an unknown consumer into a false unused-export finding.
106
+ if (symbol) visit(sourceFile);
107
+ else result.all = true;
108
+ context.namespaceUses.set(namespace, result);
109
+ return result;
110
+ };
111
+
112
+ const importBindings = (context, declaration) => {
113
+ const { ts } = context;
114
+ const clause = declaration.importClause;
115
+ if (!clause) return { all: false, pairs: [] }; // side-effect import: uses nothing
116
+ const pairs = [];
117
+ if (clause.name) pairs.push({ imported: 'default', exported: null });
118
+ const named = clause.namedBindings;
119
+ if (named && named.kind === ts.SyntaxKind.NamespaceImport) {
120
+ const namespace = namespaceBindings(context, declaration, named);
121
+ return { all: namespace.all, pairs: [...pairs, ...namespace.pairs] };
122
+ }
123
+ if (named) for (const element of named.elements) pairs.push({ imported: (element.propertyName ?? element.name).text, exported: null });
124
+ return { all: false, pairs };
125
+ };
126
+
127
+ const exportBindings = (ts, declaration) => {
128
+ const clause = declaration.exportClause;
129
+ if (!clause || clause.kind === ts.SyntaxKind.NamespaceExport) return { all: true, pairs: [] };
130
+ return { all: false, pairs: clause.elements.map(element => ({ imported: (element.propertyName ?? element.name).text, exported: element.name.text })) };
131
+ };
132
+
133
+ const importTypeBindings = (ts, declaration) => {
134
+ let qualifier = declaration.qualifier;
135
+ if (!qualifier) return { all: true, pairs: [] };
136
+ while (qualifier.kind === ts.SyntaxKind.QualifiedName) qualifier = qualifier.left;
137
+ return { all: false, pairs: [{ imported: qualifier.text, exported: null }] };
138
+ };
139
+
140
+ /** Names an import/export declaration takes from its target: {all} or {pairs: [{imported, exported}]}. */
141
+ export function bindingsOf(context, declaration) {
142
+ const { ts } = context;
143
+ if (!declaration) return { all: true, pairs: [] };
144
+ if (declaration.kind === ts.SyntaxKind.ImportDeclaration) return importBindings(context, declaration);
145
+ if (declaration.kind === ts.SyntaxKind.ExportDeclaration) return exportBindings(ts, declaration);
146
+ if (declaration.kind === ts.SyntaxKind.ImportTypeNode) return importTypeBindings(ts, declaration);
147
+ // ImportEqualsDeclaration, dynamic import(), require(): the whole module is taken.
148
+ return { all: true, pairs: [] };
149
+ }
@@ -17,8 +17,9 @@
17
17
  * and contributes no name here.
18
18
  *
19
19
  * A name is used when a production file outside the owner imports it from the entry (type-only imports count). A
20
- * namespace import, `import x = require`, a dynamic import()/require(), an `export *` or `export * as` of the entry make
21
- * every name used. A file outside the owner that re-exports the name (`export { n as m } from`) uses it when `m` is itself
20
+ * namespace import uses its statically accessed members; a computed read or an escape of the namespace uses every name.
21
+ * `import x = require`, a dynamic import()/require(), an `export *` or `export * as` of the entry make every name used.
22
+ * A file outside the owner that re-exports the name (`export { n as m } from`) uses it when `m` is itself
22
23
  * used by the importers of that file; a re-exporting file nobody imports is a framework entry (a route file) and uses it.
23
24
  * Specs and tests are not in the graph, so an export used only by a spec is dead, on purpose, with these exceptions (unit test
24
25
  * standard): the unit spec of every unit role (ruleParams.be.unitRoles: `<name>.service.spec.ts`, `<name>.cli.spec.ts`) and of every logic role
@@ -39,6 +40,7 @@ import path from 'node:path';
39
40
  import { treeOf } from './required-files.mjs';
40
41
  import { globExpression } from '../../lib/glob.mjs';
41
42
  import { stagedConsumerSources } from './staged-consumers.mjs';
43
+ import { bindingsOf } from './dead-exports-bindings.mjs';
42
44
 
43
45
  export const DEAD_EXPORT_RULE_IDS = ['HFS_UNUSED_EXPORT', 'HFS_UNUSED_FILE'];
44
46
 
@@ -70,40 +72,6 @@ function extraEntries(graph, entry) {
70
72
  return names.flatMap(name => (name.includes('*') ? matches(name) : [`${found.root}/${name}`].filter(rel => graph.files.has(rel))));
71
73
  }
72
74
 
73
- const importBindings = (ts, declaration) => {
74
- const clause = declaration.importClause;
75
- if (!clause) return { all: false, pairs: [] }; // side-effect import: uses nothing
76
- const pairs = [];
77
- if (clause.name) pairs.push({ imported: 'default', exported: null });
78
- const named = clause.namedBindings;
79
- if (named && named.kind === ts.SyntaxKind.NamespaceImport) return { all: true, pairs: [] };
80
- if (named) for (const element of named.elements) pairs.push({ imported: (element.propertyName ?? element.name).text, exported: null });
81
- return { all: false, pairs };
82
- };
83
-
84
- const exportBindings = (ts, declaration) => {
85
- const clause = declaration.exportClause;
86
- if (!clause || clause.kind === ts.SyntaxKind.NamespaceExport) return { all: true, pairs: [] };
87
- return { all: false, pairs: clause.elements.map(element => ({ imported: (element.propertyName ?? element.name).text, exported: element.name.text })) };
88
- };
89
-
90
- const importTypeBindings = (ts, declaration) => {
91
- let qualifier = declaration.qualifier;
92
- if (!qualifier) return { all: true, pairs: [] };
93
- while (qualifier.kind === ts.SyntaxKind.QualifiedName) qualifier = qualifier.left;
94
- return { all: false, pairs: [{ imported: qualifier.text, exported: null }] };
95
- };
96
-
97
- /** Names an import/export declaration takes from its target: {all} or {pairs: [{imported, exported}]}. */
98
- function bindingsOf(ts, declaration) {
99
- if (!declaration) return { all: true, pairs: [] };
100
- if (declaration.kind === ts.SyntaxKind.ImportDeclaration) return importBindings(ts, declaration);
101
- if (declaration.kind === ts.SyntaxKind.ExportDeclaration) return exportBindings(ts, declaration);
102
- if (declaration.kind === ts.SyntaxKind.ImportTypeNode) return importTypeBindings(ts, declaration);
103
- // ImportEqualsDeclaration, dynamic import(), require(): the whole module is taken.
104
- return { all: true, pairs: [] };
105
- }
106
-
107
75
  const addExportName = (found, sourceFile, name, node) => {
108
76
  if (found.has(name)) return;
109
77
  found.set(name, sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1);
@@ -312,7 +280,7 @@ const consumedNames = (context, file, ownerKey, depth, visiting) => {
312
280
  const result = { all: false, names: new Set() };
313
281
  for (const edge of context.edgesTo.get(file) ?? []) {
314
282
  if (ownerKey && unitOfFile(context.graph, edge.from) === ownerKey) continue;
315
- const bindings = bindingsOf(context.ts, edge.edge.declaration);
283
+ const bindings = bindingsOf(context, edge.edge.declaration);
316
284
  if (bindings.all) { result.all = true; continue; }
317
285
  for (const pair of bindings.pairs) consumeReexportedName(context, result, edge, pair, depth, visiting);
318
286
  }
@@ -321,10 +289,10 @@ const consumedNames = (context, file, ownerKey, depth, visiting) => {
321
289
 
322
290
  /** Adds to `used` the entry exports reached by a consumer outside the owner that imports a file the entry re-exports by name. */
323
291
  const addReexportedUses = (context, key, publicEntry, used) => {
324
- const { ts, graph } = context;
292
+ const { graph } = context;
325
293
  for (const edge of graph.edges) {
326
294
  if (edge.from !== publicEntry || !edge.reexport || graph.unit(edge.to) !== key) continue;
327
- const reexport = bindingsOf(ts, edge.edge.declaration);
295
+ const reexport = bindingsOf(context, edge.edge.declaration);
328
296
  const inner = consumedNames(context, edge.to, key, 0, new Set([publicEntry, edge.to]));
329
297
  if (reexport.all) continue;
330
298
  for (const pair of reexport.pairs) if (inner.all || inner.names.has(pair.imported)) used.names.add(pair.exported);
@@ -342,7 +310,7 @@ const unusedExportViolation = (publicEntry, name, line, owner) => ({
342
310
  const addSiblingEntryUses = (context, publicEntry, declared, used) => {
343
311
  for (const edge of context.edgesTo.get(publicEntry) ?? []) {
344
312
  if (edge.reexport || edge.from === publicEntry || !declared.has(edge.from)) continue;
345
- const bindings = bindingsOf(context.ts, edge.edge.declaration);
313
+ const bindings = bindingsOf(context, edge.edge.declaration);
346
314
  if (bindings.all) used.all = true;
347
315
  for (const pair of bindings.pairs) used.names.add(pair.imported);
348
316
  }
@@ -382,7 +350,7 @@ const ownerExportFindings = (context, config) => {
382
350
 
383
351
  export function checkDeadExports({ context, graph, config }) {
384
352
  const edgesTo = edgeIndex([...graph.edges, ...testConsumerEdges({ context, graph, config })]);
385
- const analysis = { ...context, graph, edgesTo };
353
+ const analysis = { ...context, graph, edgesTo, namespaceCheckers: new WeakMap(), namespaceUses: new WeakMap() };
386
354
  const exports = ownerExportFindings(analysis, config);
387
355
  const files = deadFiles(graph, config);
388
356
  const dead = exports.violations.length;
@@ -2,22 +2,23 @@ import { machineKit } from './machine-ast.mjs';
2
2
 
3
3
  /**
4
4
  * R41 `default-deny-app-guard` (BE_DEFAULT_DENY). Every api app root provides its `APP_GUARD` entries in the order
5
- * throttler -> CSRF origin guard -> AuthGuard, each exactly once, so no request reaches a handler before it was
6
- * rate-limited, origin-checked and authenticated. A guard is classified by structure, never by name or folder alone
5
+ * throttler -> CSRF origin guard -> CredentialGuard -> AuthGuard, each exactly once, so no request reaches a handler before it was
6
+ * rate-limited, origin-checked, credential-authenticated and admitted. A guard is classified by structure, never by name or folder alone
7
7
  * (BE-CONVENTION 1.7 names the roles, not a library):
8
8
  * - throttler: a class of @nestjs/throttler or one that extends it, or a guard that reads through a Reflector the
9
9
  * per-door metadata (the `@RateLimit(tier)` decorator) that a SetMetadata call of its own capability writes;
10
10
  * - CSRF origin guard: a guard declared in platform/http-security that reads the request origin or referer header;
11
+ * - CredentialGuard: the `CredentialGuard` class declared in domain/identity;
11
12
  * - AuthGuard: the `AuthGuard` class declared in domain/identity.
12
13
  * Other guards are permitted between them; a missing, repeated or reordered one of the three is a finding.
13
14
  */
14
15
  export const DEFAULT_DENY_RULE_IDS = ['BE_DEFAULT_DENY'];
15
16
 
16
17
  const RULE = 'BE_DEFAULT_DENY';
17
- const ORDER = ['throttler', 'csrf', 'auth'];
18
+ const ORDER = ['throttler', 'csrf', 'credential', 'auth'];
18
19
  const ORIGIN_HEADERS = new Set(['origin', 'referer', 'referrer']);
19
20
  const METADATA_READERS = new Set(['get', 'getAll', 'getAllAndMerge', 'getAllAndOverride']);
20
- const LABEL = { throttler: 'the throttler guard', csrf: 'the CSRF origin guard of platform/http-security', auth: 'AuthGuard of domain/identity' };
21
+ const LABEL = { throttler: 'the throttler guard', csrf: 'the CSRF origin guard of platform/http-security', credential: 'CredentialGuard of domain/identity', auth: 'AuthGuard of domain/identity' };
21
22
 
22
23
  const keySymbol = (kit, checker, node) => (node ? kit.aliased(checker, kit.symbolAt(checker, node)) ?? null : null);
23
24
 
@@ -98,6 +99,7 @@ const classifyDeclaration = (kit, graph, keyCache, declaration) => {
98
99
  if (readsDoorMetadata(kit, graph, keyCache, declaration, owner)) return 'throttler';
99
100
  }
100
101
  if (owner?.tier === 'domain' && owner.name === 'identity' && ts.isClassDeclaration(declaration) && declaration.name?.text === 'AuthGuard') return 'auth';
102
+ if (owner?.tier === 'domain' && owner.name === 'identity' && ts.isClassDeclaration(declaration) && declaration.name?.text === 'CredentialGuard') return 'credential';
101
103
  return null;
102
104
  };
103
105
 
@@ -117,7 +119,7 @@ const roleMessages = (app, roles, anchor) => {
117
119
  const out = [];
118
120
  for (const role of ORDER) {
119
121
  const entries = roles.filter(item => item.role === role);
120
- if (entries.length === 0) out.push({ node: anchor, message: `App ${app.name} does not provide ${LABEL[role]} as an APP_GUARD; an api app is denied by default: throttler, then CSRF origin guard, then AuthGuard.` });
122
+ if (entries.length === 0) out.push({ node: anchor, message: `App ${app.name} does not provide ${LABEL[role]} as an APP_GUARD; an api app is denied by default: throttler, then CSRF origin guard, then CredentialGuard, then AuthGuard.` });
121
123
  else if (entries.length > 1) out.push({ node: entries[1].guard.node, message: `App ${app.name} provides ${LABEL[role]} more than once as an APP_GUARD.` });
122
124
  }
123
125
  return out;
@@ -128,7 +130,7 @@ const orderMessages = (app, roles) => {
128
130
  if (!indexes.every(index => index >= 0)) return [];
129
131
  const out = [];
130
132
  for (let i = 1; i < indexes.length; i += 1) {
131
- if (indexes[i] < indexes[i - 1]) out.push({ node: roles[indexes[i]].guard.node, message: `App ${app.name} provides ${LABEL[ORDER[i]]} before ${LABEL[ORDER[i - 1]]}; the APP_GUARD order is throttler, CSRF origin guard, AuthGuard.` });
133
+ if (indexes[i] < indexes[i - 1]) out.push({ node: roles[indexes[i]].guard.node, message: `App ${app.name} provides ${LABEL[ORDER[i]]} before ${LABEL[ORDER[i - 1]]}; the APP_GUARD order is throttler, CSRF origin guard, CredentialGuard, AuthGuard.` });
132
134
  }
133
135
  return out;
134
136
  };
@@ -74,7 +74,7 @@ class WorldAnalysis {
74
74
  let found = false;
75
75
  const visit = node => {
76
76
  if (!found && this.ts.isCallExpression(node) && this.ts.isIdentifier(node.expression) && node.expression.text === 'fetch'
77
- && !checker.getSymbolAtLocation(node.expression)) found = true;
77
+ && (checker.getSymbolAtLocation(node.expression)?.getDeclarations() ?? []).every(declaration => declaration.getSourceFile().isDeclarationFile)) found = true;
78
78
  if (!found) this.ts.forEachChild(node, visit);
79
79
  };
80
80
  visit(sourceFile);
@@ -0,0 +1,153 @@
1
+ import path from 'node:path';
2
+
3
+ export const keyOf = (ts, property) => (ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) && (ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name)) ? property.name.text : null;
4
+ const exported = (ts, statement) => statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false;
5
+
6
+ /** The directly defined functions among the declarations of an exported variable statement; other values are not callable. */
7
+ function callableVariables(ts, statement) {
8
+ const names = [];
9
+ for (const declaration of statement.declarationList.declarations) {
10
+ const initializer = declaration.initializer;
11
+ if (ts.isIdentifier(declaration.name) && initializer && (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer))) names.push(declaration.name.text);
12
+ }
13
+ return names;
14
+ }
15
+
16
+ /** The callable exports of one statement: an overload signature has no body and collapses into its implementation. */
17
+ function callableExports(ts, statement) {
18
+ if (!exported(ts, statement)) return [];
19
+ if (ts.isFunctionDeclaration(statement)) return statement.name && statement.body ? [statement.name.text] : [];
20
+ return ts.isVariableStatement(statement) ? callableVariables(ts, statement) : [];
21
+ }
22
+
23
+ /**
24
+ * The callable subset of a function subject's exports. Constants, enums, objects, types, `export ... from` re-exports and overload
25
+ * signatures do not change which functions the spec must provide; a class or a default-export assignment keeps the class-provider
26
+ * check (null), as does a module with no callable export.
27
+ */
28
+ export function functionSubjectExports(ts, sourceFile) {
29
+ const names = [];
30
+ for (const statement of sourceFile.statements) {
31
+ if (ts.isClassDeclaration(statement) || ts.isExportAssignment(statement)) return null;
32
+ for (const name of callableExports(ts, statement)) if (!names.includes(name)) names.push(name);
33
+ }
34
+ return names.length ? names : null;
35
+ }
36
+
37
+ const decoratorsOf = (ts, node) => (ts.canHaveDecorators(node) ? ts.getDecorators(node) ?? [] : []);
38
+
39
+ /** A value object: an undecorated class whose constructor parameters carry no injection token, so Nest never builds it. */
40
+ function isValueObjectClass(ts, declaration) {
41
+ if (decoratorsOf(ts, declaration).length > 0) return false;
42
+ const constructors = declaration.members.filter(member => ts.isConstructorDeclaration(member));
43
+ return constructors.every(constructor => constructor.parameters.every(parameter => decoratorsOf(ts, parameter).length === 0));
44
+ }
45
+
46
+ /** The names of the value-object classes a module exports, or null when it exports a class that is not one (or none). */
47
+ export function valueObjectSubjects(ts, sourceFile) {
48
+ const classes = sourceFile.statements.filter(statement => ts.isClassDeclaration(statement) && statement.name && exported(ts, statement));
49
+ if (!classes.length || !classes.every(declaration => isValueObjectClass(ts, declaration))) return null;
50
+ return classes.map(declaration => declaration.name.text);
51
+ }
52
+
53
+ /** The value of one literal object property. */
54
+ const propertyValue = (ts, object, key) => {
55
+ const property = object.properties.find(item => keyOf(ts, item) === key);
56
+ return property && ts.isPropertyAssignment(property) ? property.initializer : null;
57
+ };
58
+
59
+ /** A literal value object maps every exported function to its real sibling import. */
60
+ function realFunctions(ts, value, names, imports, rel, serviceRel) {
61
+ if (!value || !ts.isObjectLiteralExpression(value) || value.properties.length !== names.length) return false;
62
+ const remaining = new Set(names);
63
+ for (const property of value.properties) {
64
+ const key = keyOf(ts, property);
65
+ const identifier = ts.isShorthandPropertyAssignment(property) ? property.name : property.initializer;
66
+ if (!remaining.delete(key) || !identifier || !ts.isIdentifier(identifier)) return false;
67
+ const binding = imports.get(identifier.text);
68
+ if (!binding || binding.name !== key || !binding.module.startsWith('.')) return false;
69
+ const target = path.posix.normalize(path.posix.join(path.posix.dirname(rel), binding.module)).replace(/\.[cm]?[jt]s$/u, '');
70
+ if (target !== serviceRel.replace(/\.ts$/u, '')) return false;
71
+ }
72
+ return remaining.size === 0;
73
+ }
74
+
75
+ /** Every node satisfying a predicate, within one spec. */
76
+ function matchingNodes(ts, root, predicate) {
77
+ const found = [];
78
+ const visit = node => {
79
+ if (predicate(node)) found.push(node);
80
+ ts.forEachChild(node, visit);
81
+ };
82
+ visit(root);
83
+ return found;
84
+ }
85
+
86
+ /** The declared variable initialized by an expression, including its await and call chain. */
87
+ function initializedVariable(ts, expression) {
88
+ let cursor = expression;
89
+ let compiled = false;
90
+ let awaited = false;
91
+ while (cursor.parent && (ts.isAwaitExpression(cursor.parent) || ts.isPropertyAccessExpression(cursor.parent) || ts.isCallExpression(cursor.parent))) {
92
+ cursor = cursor.parent;
93
+ if (ts.isAwaitExpression(cursor)) awaited = true;
94
+ if (ts.isCallExpression(cursor) && ts.isPropertyAccessExpression(cursor.expression) && cursor.expression.name.text === 'compile') compiled = true;
95
+ }
96
+ if (!compiled || !awaited) return null;
97
+ const declaration = cursor.parent;
98
+ return declaration && ts.isVariableDeclaration(declaration) && ts.isIdentifier(declaration.name) ? declaration.name.text : null;
99
+ }
100
+
101
+ /** The subject variable receiving module.get, whether declared there or assigned in beforeEach. */
102
+ function retrievedVariable(ts, call) {
103
+ let cursor = call;
104
+ while (cursor.parent && (ts.isAwaitExpression(cursor.parent) || ts.isParenthesizedExpression(cursor.parent))) cursor = cursor.parent;
105
+ const parent = cursor.parent;
106
+ if (parent && ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) return parent.name.text;
107
+ if (parent && ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isIdentifier(parent.left)) return parent.left.text;
108
+ return null;
109
+ }
110
+
111
+ /** A helper returning the retrieved subject is followed to the variable receiving its result. */
112
+ function retrievedSubjects(ts, read, sourceFile) {
113
+ const direct = retrievedVariable(ts, read);
114
+ if (direct) return [direct];
115
+ if (!read.parent || !ts.isReturnStatement(read.parent)) return [];
116
+ let helper = read.parent;
117
+ while (helper.parent && !ts.isFunctionLike(helper)) helper = helper.parent;
118
+ const name = helper.name?.text ?? helper.parent?.name?.text;
119
+ if (!name) return [];
120
+ return matchingNodes(ts, sourceFile, node => ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === name)
121
+ .map(node => retrievedVariable(ts, node)).filter(Boolean);
122
+ }
123
+
124
+ /** The same compiled module retrieves the matching token and its result actually calls a subject function. */
125
+ function retrievedSubjectUsed(ts, call, token, names) {
126
+ const moduleName = initializedVariable(ts, call);
127
+ if (!moduleName) return false;
128
+ const sourceFile = call.getSourceFile();
129
+ const reads = matchingNodes(ts, sourceFile, node => {
130
+ if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) return false;
131
+ const member = node.expression;
132
+ return ts.isIdentifier(member.expression) && member.expression.text === moduleName && member.name.text === 'get'
133
+ && node.arguments.length === 1 && ts.isIdentifier(node.arguments[0]) && node.arguments[0].text === token.text;
134
+ });
135
+ return reads.some(read => {
136
+ const subjects = retrievedSubjects(ts, read, sourceFile);
137
+ return matchingNodes(ts, sourceFile, node => ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)
138
+ && ts.isIdentifier(node.expression.expression) && subjects.includes(node.expression.expression.text) && names.includes(node.expression.name.text)).length > 0;
139
+ });
140
+ }
141
+
142
+ /** A pure function service has exactly one real subject value provider, retrieved and exercised through Nest. */
143
+ export function checkFunctionSubject({ ts, rel, serviceRel, imports, call, list, names, report }) {
144
+ const provider = list.elements[0];
145
+ const shape = provider && ts.isObjectLiteralExpression(provider) && provider.properties.length === 2;
146
+ const token = shape ? propertyValue(ts, provider, 'provide') : null;
147
+ const value = shape ? propertyValue(ts, provider, 'useValue') : null;
148
+ if (list.elements.length !== 1 || !token || !ts.isIdentifier(token) || !realFunctions(ts, value, names, imports, rel, serviceRel)) {
149
+ report(provider ?? list, `${rel}: a function-only subject needs exactly one { provide: TOKEN, useValue: { ...realFunctions } } provider containing every callable export imported from ./${path.posix.basename(serviceRel, '.ts')}.`);
150
+ return;
151
+ }
152
+ if (!retrievedSubjectUsed(ts, call, token, names)) report(list, `${rel}: retrieve the function subject with moduleRef.get(TOKEN) from this compiled testing module and exercise a function on the returned subject.`);
153
+ }
@@ -297,11 +297,11 @@ function checkTestTopology(state) {
297
297
  // Test kinds and folders that do not exist: int-spec, harness-spec, src/tests/harness and live/;
298
298
  // a backend spec sits by its kind (beside its subject, or under src/tests/{integration,e2e,contract}/).
299
299
  for (const file of state.tree.files()) {
300
- if (UNSUPPORTED_TEST_SUFFIX.test(file))
300
+ if (state.backend && state.profile === 'be' && UNSUPPORTED_TEST_SUFFIX.test(file))
301
301
  state.finding('BE_TEST_TOPOLOGY', file, `${file} uses an unsupported test kind. Only unit *.spec.ts, *.integration-spec.ts, *.e2e-spec.ts and *.contract-spec.ts exist, each in its own folder under src/tests/.`);
302
- else if (state.backend && UNSUPPORTED_TEST_FOLDER.test(file))
302
+ else if (state.backend && state.profile === 'be' && UNSUPPORTED_TEST_FOLDER.test(file))
303
303
  state.finding('BE_TEST_TOPOLOGY', file, `${file} sits in an unsupported test folder. Unit specs sit beside their subject, flows go under src/tests/e2e/<area>/ and test infrastructure under src/tests/world/.`);
304
- else if (state.backend && file.startsWith('src/tests/') && isExtraTestConfig(file))
304
+ else if (state.backend && state.profile === 'be' && file.startsWith('src/tests/') && isExtraTestConfig(file))
305
305
  state.finding('BE_TEST_TOPOLOGY', file, `${file} is a per-lane test config. One root jest.config.js declares exactly the unit, integration, e2e and contract projects.`);
306
306
  }
307
307
  }
@@ -7,11 +7,13 @@ import { trimTrailingSlashes } from '../trailing-slashes.mjs';
7
7
  /**
8
8
  * R45 `module-per-transport` (BE_MODULE_SHAPE, BE-CONVENTION 1.2 and 1.6). A feature has one Nest module for its
9
9
  * application (`<f>.module.ts` at the feature root) and exactly one module per transport folder
10
- * (`transport/<protocol>/<f>-<protocol>.module.ts`). Never one module per operation, never a module-definition in a feature:
10
+ * (`transport/<protocol>/<f>-<protocol>.module.ts`). Never one module per operation. Every one of these modules has its own
11
+ * module-definition beside it (owner ruling module-definition-everywhere, R295):
11
12
  *
12
- * - a `*.module.ts` file below a transport folder other than `<f>-<protocol>.module.ts`, a `*.module-definition.ts`
13
- * anywhere in a feature, and an `@Module` class (or a `ConfigurableModuleBuilder`/`ConfigurableModuleClass` use) in a
14
- * feature file that is neither the application module nor the transport module of its own folder are refused;
13
+ * - a `*.module.ts` file below a transport folder other than `<f>-<protocol>.module.ts`, a `*.module-definition.ts` that is
14
+ * not the sibling of the application module or of the transport module of its own folder, and an `@Module` class (or a
15
+ * `ConfigurableModuleBuilder`/`ConfigurableModuleClass` use) in a feature file that is neither the application module, the
16
+ * transport module of its own folder nor that module's definition are refused;
15
17
  * - apps import only transport modules, of the kinds their app kind allows: the `composedBy` list of each transport slot
16
18
  * in knowledge/hfs/slots.yaml (api: graphql, http, websocket; worker: schedule, message). An app root (app.module.ts
17
19
  * or a composition group of its compose/ folder) that lists a feature's application module, or the transport module
@@ -53,13 +55,24 @@ function composedByProtocol(resolver) {
53
55
  return composedBy;
54
56
  }
55
57
 
56
- function reportTransportFileShapes(tree, placeOf, plain) {
58
+ /** The module a feature file may declare: the application module, the transport module of its folder, or a cli folder's module. */
59
+ function ownModuleOf(place, rel, composedModuleOf) {
60
+ if (place.composed) return composedModuleOf(rel);
61
+ return place.protocol === null
62
+ ? `${place.root}/${place.feature}.module.ts`
63
+ : `${place.root}/transport/${place.protocol}/${place.feature}-${place.protocol}.module.ts`;
64
+ }
65
+
66
+ const DEFINITION_FILE = /\.module-definition\.[cm]?tsx?$/u;
67
+ const definitionOf = modulePath => modulePath.replace(/\.module\.([cm]?tsx?)$/u, '.module-definition.$1');
68
+
69
+ function reportTransportFileShapes(tree, placeOf, plain, composedModuleOf) {
57
70
  let transportModules = 0;
58
71
  for (const rel of [...tree.files].sort(byCodeUnit)) {
59
72
  const place = placeOf(rel);
60
73
  if (!place) continue;
61
74
  const base = path.posix.basename(rel);
62
- if (/\.module-definition\.[cm]?tsx?$/u.test(base)) plain(rel, `${rel} is a module-definition inside feature ${place.feature}; a feature module is a static \`@Module\`, and only capabilities under src/modules build their module from a definition. Delete it and list the providers in the module.`, { feature: place.feature });
75
+ if (DEFINITION_FILE.test(base) && rel !== definitionOf(ownModuleOf(place, rel, composedModuleOf))) plain(rel, `${rel} is a module-definition that is not beside a module of feature ${place.feature}; the definition of a module is its sibling ${path.posix.basename(definitionOf(ownModuleOf(place, rel, composedModuleOf)))}, one per application module, transport module or cli folder.`, { feature: place.feature });
63
76
  if (place.protocol && /\.module\.[cm]?tsx?$/u.test(base)) {
64
77
  const expected = `${place.feature}-${place.protocol}.module.ts`;
65
78
  if (place.below.length !== 3 || base !== expected) plain(rel, `${rel} is a second module of the ${place.protocol} transport of ${place.feature}; a transport folder has exactly one module, transport/${place.protocol}/${expected}, never one module per operation. List the operation's providers in that module and delete this file.`, { feature: place.feature, protocol: place.protocol });
@@ -88,13 +101,15 @@ function reportModuleClassLocation(file, statement, place, composedModuleOf, rep
88
101
  if (file.rel !== own) report(file, statement.name, `${statement.name.text} is a Nest module declared in ${file.rel}; a feature declares exactly its application module (${place.feature}.module.ts) and one module per transport folder (${place.feature}-<protocol>.module.ts), never a module per operation.`, { feature: place.feature });
89
102
  }
90
103
 
91
- function reportConfigurableModuleUses(file, checker, kit, ts, place, report) {
104
+ function reportConfigurableModuleUses(file, checker, kit, ts, place, env) {
105
+ const own = ownModuleOf(place, file.rel, env.composedModuleOf);
106
+ const isDefinition = file.rel === definitionOf(own);
92
107
  kit.walk(file.sourceFile, node => {
93
- if (ts.isNewExpression(node) && kit.isImportOf(checker, node.expression, 'ConfigurableModuleBuilder', '@nestjs/common')) {
94
- report(file, node, `ConfigurableModuleBuilder builds a capability's module definition; feature ${place.feature} has static modules only, so declare the providers in its @Module.`, { feature: place.feature });
95
- } else if (ts.isHeritageClause(node)) {
108
+ if (ts.isNewExpression(node) && kit.isImportOf(checker, node.expression, 'ConfigurableModuleBuilder', '@nestjs/common') && !isDefinition) {
109
+ env.report(file, node, `ConfigurableModuleBuilder builds the module definition of one module; in feature ${place.feature} it is written only in ${path.posix.basename(definitionOf(own))}, beside ${path.posix.basename(own)}.`, { feature: place.feature });
110
+ } else if (ts.isHeritageClause(node) && file.rel !== own) {
96
111
  for (const type of node.types) if (kit.isImportOf(checker, type.expression, 'ConfigurableModuleClass', '@nestjs/common')) {
97
- report(file, type, `A feature module never extends ConfigurableModuleClass; feature ${place.feature} has static modules only.`, { feature: place.feature });
112
+ env.report(file, type, `ConfigurableModuleClass is extended by the module of its own definition only (${path.posix.basename(own)}); feature ${place.feature} declares no other configurable module.`, { feature: place.feature });
98
113
  }
99
114
  }
100
115
  return true;
@@ -118,7 +133,7 @@ function collectFeatureModules(graph, kit, ts, placeOf, composedModuleOf, report
118
133
  const checker = kit.checkerOf(file.sourceFile);
119
134
  const place = placeOf(file.rel);
120
135
  moduleClassesInFile(file, checker, place, modules, env);
121
- if (place) reportConfigurableModuleUses(file, checker, kit, ts, place, report);
136
+ if (place) reportConfigurableModuleUses(file, checker, kit, ts, place, env);
122
137
  }
123
138
  return modules;
124
139
  }
@@ -203,7 +218,7 @@ export function checkModulePerTransport(input) {
203
218
  const composedBy = composedByProtocol(resolver);
204
219
 
205
220
  const tree = treeOf(config.root);
206
- const transportModules = reportTransportFileShapes(tree, placeOf, plain);
221
+ const transportModules = reportTransportFileShapes(tree, placeOf, plain, composedModuleOf);
207
222
 
208
223
  const modules = collectFeatureModules(graph, kit, ts, placeOf, composedModuleOf, report);
209
224