@starci/hfs 4.0.10 → 4.1.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 (271) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/emit/operations.mjs +1 -1
  3. package/emit/type-schema.mjs +2 -1
  4. package/lint/run.mjs +3 -2
  5. package/package.json +1 -1
  6. package/report/order.mjs +2 -0
  7. package/runtime/config.example.yaml +187 -0
  8. package/runtime/engine/by-code-unit.mjs +12 -0
  9. package/runtime/engine/config.mjs +221 -239
  10. package/runtime/engine/invalid-config.mjs +5 -5
  11. package/runtime/engine/model-config.mjs +90 -0
  12. package/runtime/engine/orca-config.mjs +3 -1
  13. package/runtime/engine/removed-vocabulary.mjs +28 -0
  14. package/runtime/engine/resources-config.mjs +52 -0
  15. package/runtime/engine/runtime-root.mjs +17 -0
  16. package/runtime/engine/secrets.mjs +35 -31
  17. package/runtime/engine/sonar-config.mjs +21 -0
  18. package/runtime/engine/temp-root.mjs +32 -0
  19. package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
  20. package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
  21. package/runtime/knowledge/hfs/rules.yaml +98 -65
  22. package/runtime/knowledge/hfs/slots.yaml +11 -14
  23. package/runtime/knowledge/patterns/be/api.yaml +7 -24
  24. package/runtime/knowledge/patterns/be/cli.yaml +3 -12
  25. package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
  26. package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
  27. package/runtime/modules/kernel/failure-codes.yaml +1 -41
  28. package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
  29. package/runtime/modules/models/registry.yaml +25 -359
  30. package/runtime/modules/models/runtimes.yaml +92 -256
  31. package/runtime/modules/models/tiers.yaml +65 -0
  32. package/runtime/scripts/api/fs/claim-file.mjs +30 -0
  33. package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
  34. package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
  35. package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
  36. package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
  37. package/runtime/scripts/api/git/lib.mjs +34 -1
  38. package/runtime/scripts/api/process/resolve-real-tool.mjs +14 -8
  39. package/runtime/scripts/api/process/run-program.mjs +2 -1
  40. package/runtime/scripts/api/sops/lib.mjs +198 -120
  41. package/runtime/scripts/hfs/allows.mjs +3 -3
  42. package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
  43. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
  44. package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
  45. package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
  46. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
  47. package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
  48. package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
  49. package/runtime/scripts/hfs/architecture/config.mjs +95 -80
  50. package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
  51. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
  52. package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
  53. package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
  54. package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
  55. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
  56. package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
  57. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
  58. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
  59. package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
  60. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
  61. package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
  62. package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
  63. package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
  64. package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
  65. package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
  66. package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
  67. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
  68. package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
  69. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
  70. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
  71. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
  72. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
  73. package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
  74. package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
  75. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
  76. package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
  77. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
  78. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
  79. package/runtime/scripts/hfs/architecture/index.mjs +158 -102
  80. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
  81. package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
  82. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
  83. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
  84. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
  85. package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
  86. package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
  87. package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
  88. package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
  89. package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
  90. package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
  91. package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
  92. package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
  93. package/runtime/scripts/hfs/architecture/required-files.mjs +100 -68
  94. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
  95. package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
  96. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
  97. package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
  98. package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
  99. package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
  100. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
  101. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
  102. package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
  103. package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
  104. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
  105. package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
  106. package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
  107. package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
  108. package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
  109. package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
  110. package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
  111. package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
  112. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
  113. package/runtime/scripts/hfs/architecture.mjs +3 -4
  114. package/runtime/scripts/hfs/check.mjs +61 -70
  115. package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
  116. package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
  117. package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
  118. package/runtime/scripts/hfs/edition-slots.mjs +5 -3
  119. package/runtime/scripts/hfs/linear-text.mjs +30 -0
  120. package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
  121. package/runtime/scripts/hfs/path-findings.mjs +58 -38
  122. package/runtime/scripts/hfs/pin-findings.mjs +31 -0
  123. package/runtime/scripts/hfs/repo-identity.mjs +5 -2
  124. package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
  125. package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
  126. package/runtime/scripts/hfs/rules/cli.mjs +5 -1
  127. package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
  128. package/runtime/scripts/hfs/rules/contract.mjs +35 -27
  129. package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
  130. package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
  131. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
  132. package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
  133. package/runtime/scripts/hfs/rules/database.mjs +36 -23
  134. package/runtime/scripts/hfs/rules/deps.mjs +64 -32
  135. package/runtime/scripts/hfs/rules/docker.mjs +113 -65
  136. package/runtime/scripts/hfs/rules/edition.mjs +118 -111
  137. package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
  138. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
  139. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
  140. package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
  141. package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
  142. package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
  143. package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
  144. package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
  145. package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
  146. package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
  147. package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
  148. package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
  149. package/runtime/scripts/hfs/rules/saga.mjs +128 -61
  150. package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
  151. package/runtime/scripts/hfs/rules/services.mjs +34 -16
  152. package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
  153. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
  154. package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
  155. package/runtime/scripts/hfs/secret.mjs +59 -48
  156. package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
  157. package/runtime/scripts/hfs/slot-classify.mjs +65 -0
  158. package/runtime/scripts/hfs/slot-errors.mjs +11 -0
  159. package/runtime/scripts/hfs/slot-imports.mjs +76 -0
  160. package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
  161. package/runtime/scripts/hfs/slot-match.mjs +153 -0
  162. package/runtime/scripts/hfs/slot-path.mjs +8 -0
  163. package/runtime/scripts/hfs/slot-required.mjs +76 -0
  164. package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
  165. package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
  166. package/runtime/scripts/hfs/slots.mjs +72 -693
  167. package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
  168. package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
  169. package/runtime/scripts/hfs/tree.mjs +19 -14
  170. package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
  171. package/runtime/scripts/hfs/view.mjs +1 -1
  172. package/runtime/scripts/lib/dockerfile.mjs +56 -33
  173. package/runtime/scripts/lib/event-contract.mjs +17 -13
  174. package/runtime/scripts/lib/git.mjs +2 -2
  175. package/runtime/scripts/lib/graphql-contract.mjs +158 -322
  176. package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
  177. package/runtime/scripts/lib/i18n.mjs +3 -2
  178. package/runtime/scripts/lib/in-order.mjs +71 -0
  179. package/runtime/scripts/lib/language.mjs +3 -3
  180. package/runtime/scripts/lib/list.mjs +8 -1
  181. package/runtime/scripts/lib/path-key.mjs +38 -5
  182. package/runtime/scripts/lib/pid-alive.mjs +7 -0
  183. package/runtime/scripts/lib/regex.mjs +1 -1
  184. package/runtime/scripts/lib/same-text.mjs +1 -1
  185. package/runtime/scripts/lib/secret-patterns.mjs +6 -2
  186. package/runtime/scripts/lib/sleep-sync.mjs +2 -2
  187. package/runtime/scripts/lib/sops-envelope.mjs +95 -48
  188. package/runtime/scripts/lib/stack-services.mjs +1 -2
  189. package/runtime/scripts/lib/ts-ast.mjs +9 -0
  190. package/runtime/scripts/lib/walk.mjs +6 -1
  191. package/scaffold/add-cli-lite.mjs +2 -1
  192. package/scaffold/add-table.mjs +2 -2
  193. package/scaffold/add.mjs +3 -2
  194. package/scaffold/app.mjs +4 -2
  195. package/scaffold/edition-gate.mjs +25 -25
  196. package/scaffold/lite-exports.mjs +2 -1
  197. package/scaffold/service.mjs +5 -4
  198. package/sync/hygiene.mjs +15 -9
  199. package/sync/index.mjs +7 -3
  200. package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
  201. package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
  202. package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
  203. package/templates/app/ci-workflows/sonar-steps.yml +2 -2
  204. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
  205. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
  206. package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
  207. package/templates/app/starciwork.gitignore +1 -1
  208. package/templates/be/image/api/Dockerfile +1 -1
  209. package/templates/be/image/cli/Dockerfile +1 -1
  210. package/templates/be/image/worker/Dockerfile +1 -1
  211. package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
  212. package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
  213. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
  214. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
  215. package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
  216. package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
  217. package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
  218. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
  219. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
  220. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
  221. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
  222. package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
  223. package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
  224. package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
  225. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
  226. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  227. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
  228. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
  229. package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
  230. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  231. package/templates/fe/image/next/Dockerfile +2 -1
  232. package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
  233. package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
  234. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
  235. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
  236. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
  237. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
  238. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
  239. package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
  240. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
  241. package/upgrade/index.mjs +9 -2
  242. package/runtime/engine/admission.mjs +0 -308
  243. package/runtime/engine/canonical-json.mjs +0 -10
  244. package/runtime/engine/db/blob.mjs +0 -315
  245. package/runtime/engine/db/ledger-paths.mjs +0 -83
  246. package/runtime/engine/db/ledger.mjs +0 -1118
  247. package/runtime/engine/db/machine-connection.mjs +0 -135
  248. package/runtime/engine/db/machine-schema.mjs +0 -84
  249. package/runtime/engine/db/machine.mjs +0 -1367
  250. package/runtime/engine/db/migrations/machine/0001-init.sql +0 -924
  251. package/runtime/engine/db/migrations/runtime/0001-init.sql +0 -1108
  252. package/runtime/engine/db/provider-reservations.mjs +0 -101
  253. package/runtime/engine/digest.mjs +0 -16
  254. package/runtime/engine/refuse.mjs +0 -11
  255. package/runtime/modules/ops/_labels.yaml +0 -53
  256. package/runtime/scripts/api/node/lib.mjs +0 -14
  257. package/runtime/scripts/api/node/spawn-node.mjs +0 -6
  258. package/runtime/scripts/api/process/lib.mjs +0 -111
  259. package/runtime/scripts/api/process/owned-process.mjs +0 -78
  260. package/runtime/scripts/api/process/stop-owned-process.mjs +0 -9
  261. package/runtime/scripts/connectors/lib.mjs +0 -488
  262. package/runtime/scripts/lib/clip.mjs +0 -19
  263. package/runtime/scripts/lib/display-names.mjs +0 -259
  264. package/runtime/scripts/lib/example-refs.mjs +0 -158
  265. package/runtime/scripts/lib/json-schema.mjs +0 -52
  266. package/runtime/scripts/lib/process-identity.mjs +0 -6
  267. package/runtime/scripts/lib/read-yaml.mjs +0 -18
  268. package/runtime/scripts/lib/redact.mjs +0 -161
  269. package/runtime/scripts/lib/sleep.mjs +0 -7
  270. package/runtime/scripts/lib/source-phrases.mjs +0 -34
  271. package/runtime/scripts/lib/sqlite.mjs +0 -21
@@ -3,11 +3,21 @@ import { isInside } from './config.mjs';
3
3
  import { slotAdmitsFile } from '../allows.mjs';
4
4
  import { reachableViolation, relativePath } from './typescript.mjs';
5
5
  import { sourceLocation } from '../../lib/ts-ast.mjs';
6
+ import { byCodeUnit } from '../../lib/list.mjs';
6
7
 
7
8
  const FORBIDDEN_APP_ROLE = /(?:^|\.)(?:service|provider|providers|resolver|controller|handler|repository|entity|use-case|command|query|listener|consumer|processor)\.[cm]?[jt]sx?$/i;
8
9
  const FORBIDDEN_DECLARATION = /(?:Service|Provider|Resolver|Controller|Handler|Repository|Entity|UseCase|Command|Query|Listener|Consumer|Processor)$/;
9
10
  const FORBIDDEN_DECORATORS = new Set(['Controller', 'Resolver', 'Injectable', 'Processor', 'WebSocketGateway']);
10
- const TRANSPORT_PACKAGES = /^(?:@nestjs\/(?:graphql|microservices|platform-[^/]+|websockets)(?:\/|$)|@apollo\/|apollo-server(?:\/|$)|express(?:\/|$)|fastify(?:\/|$)|graphql(?:\/|$)|class-validator(?:\/|$)|class-transformer(?:\/|$))/;
11
+ const TRANSPORT_PACKAGES = new RegExp(`^(?:${[
12
+ '@nestjs/(?:graphql|microservices|platform-[^/]+|websockets)(?:/|$)',
13
+ '@apollo/',
14
+ 'apollo-server(?:/|$)',
15
+ 'express(?:/|$)',
16
+ 'fastify(?:/|$)',
17
+ 'graphql(?:/|$)',
18
+ 'class-validator(?:/|$)',
19
+ 'class-transformer(?:/|$)',
20
+ ].join('|')})`);
11
21
  const NEST_COMMON_TRANSPORT = new Set(['Controller', 'Get', 'Post', 'Put', 'Patch', 'Delete', 'Options', 'Head', 'Body', 'Param', 'Query', 'Req', 'Request', 'Res', 'Response', 'Headers', 'Header', 'HttpCode', 'Redirect', 'Render', 'Sse', 'UploadedFile', 'UploadedFiles', 'UseGuards', 'UseInterceptors', 'UsePipes']);
12
22
 
13
23
  function absolute(root, relative) {
@@ -72,111 +82,142 @@ function roleEvidence(ts, sourceFile) {
72
82
  return found;
73
83
  }
74
84
 
75
- function transportFrameworkEvidence(ts, sourceFile, checker) {
76
- const found = [];
77
- // The destructured binding elements naming a transport export (or a computed binding, which cannot be
78
- // proved to exclude one): `const { Controller, [x]: y } = ...`. `rest` elements are always opaque.
79
- const destructuredBindings = (elements, detail) => elements.flatMap(element => {
80
- const selected = element.dotDotDotToken
81
- ? null
82
- : ts.isIdentifier(element.propertyName ?? element.name)
83
- ? (element.propertyName ?? element.name).text
84
- : ts.isStringLiteralLike(element.propertyName)
85
- ? element.propertyName.text
86
- : null;
85
+ // The destructured binding elements naming a transport export (or a computed binding, which cannot be
86
+ // proved to exclude one): `const { Controller, [x]: y } = ...`. `rest` elements are always opaque.
87
+ function destructuredTransportBindings(ts, elements, detail) {
88
+ return elements.flatMap(element => {
89
+ let selected = null;
90
+ if (!element.dotDotDotToken) {
91
+ const imported = element.propertyName ?? element.name;
92
+ if (ts.isIdentifier(imported) || ts.isStringLiteralLike(imported)) selected = imported.text;
93
+ }
87
94
  return selected === null || NEST_COMMON_TRANSPORT.has(selected) ? [{ node: element, detail: selected ?? detail }] : [];
88
95
  });
89
- const namespaceUsages = binding => {
90
- const usages = [];
91
- const alias = binding.text;
92
- const bindingSymbol = checker?.getSymbolAtLocation(binding);
93
- const isAlias = node => ts.isIdentifier(node) && node.text === alias
94
- && (!bindingSymbol || checker?.getSymbolAtLocation(node) === bindingSymbol);
95
- const visit = node => {
96
- if (ts.isPropertyAccessExpression(node) && isAlias(node.expression)
97
- && NEST_COMMON_TRANSPORT.has(node.name.text)) usages.push({ node: node.name, detail: node.name.text });
98
- if (ts.isElementAccessExpression(node) && isAlias(node.expression)) {
99
- const selected = ts.isStringLiteralLike(node.argumentExpression) ? node.argumentExpression.text : null;
100
- if (selected === null || NEST_COMMON_TRANSPORT.has(selected)) usages.push({ node: node.argumentExpression, detail: selected ?? 'computed @nestjs/common namespace access' });
101
- }
102
- if (ts.isVariableDeclaration(node) && isAlias(node.initializer)) {
103
- if (!ts.isObjectBindingPattern(node.name)) {
104
- usages.push({ node: node.name, detail: 'escaped @nestjs/common namespace access' });
105
- } else {
106
- usages.push(...destructuredBindings(node.name.elements, 'computed @nestjs/common namespace destructuring'));
107
- }
108
- }
109
- ts.forEachChild(node, visit);
110
- };
111
- visit(sourceFile);
112
- return usages;
96
+ }
97
+
98
+ const namespaceMemberUsages = (ts, node, isAlias) => {
99
+ if (ts.isPropertyAccessExpression(node) && isAlias(node.expression)
100
+ && NEST_COMMON_TRANSPORT.has(node.name.text)) return [{ node: node.name, detail: node.name.text }];
101
+ if (ts.isElementAccessExpression(node) && isAlias(node.expression)) {
102
+ const selected = ts.isStringLiteralLike(node.argumentExpression) ? node.argumentExpression.text : null;
103
+ if (selected === null || NEST_COMMON_TRANSPORT.has(selected)) return [{ node: node.argumentExpression, detail: selected ?? 'computed @nestjs/common namespace access' }];
104
+ }
105
+ return [];
106
+ };
107
+
108
+ const namespaceEscapeUsages = (ts, node, isAlias) => {
109
+ if (!ts.isVariableDeclaration(node) || !isAlias(node.initializer)) return [];
110
+ if (!ts.isObjectBindingPattern(node.name)) return [{ node: node.name, detail: 'escaped @nestjs/common namespace access' }];
111
+ return destructuredTransportBindings(ts, node.name.elements, 'computed @nestjs/common namespace destructuring');
112
+ };
113
+
114
+ function namespaceTransportUsages(ts, sourceFile, checker, binding) {
115
+ const usages = [];
116
+ const alias = binding.text;
117
+ const bindingSymbol = checker?.getSymbolAtLocation(binding);
118
+ const isAlias = node => ts.isIdentifier(node) && node.text === alias
119
+ && (!bindingSymbol || checker?.getSymbolAtLocation(node) === bindingSymbol);
120
+ const visit = node => {
121
+ usages.push(...namespaceMemberUsages(ts, node, isAlias), ...namespaceEscapeUsages(ts, node, isAlias));
122
+ ts.forEachChild(node, visit);
113
123
  };
124
+ visit(sourceFile);
125
+ return usages;
126
+ }
127
+
128
+ function importSpecifierOf(ts, statement) {
129
+ const importEqualsSpecifier = ts.isImportEqualsDeclaration(statement)
130
+ && ts.isExternalModuleReference(statement.moduleReference)
131
+ && statement.moduleReference.expression
132
+ && ts.isStringLiteralLike(statement.moduleReference.expression)
133
+ ? statement.moduleReference.expression
134
+ : null;
135
+ let importSpecifier = importEqualsSpecifier;
136
+ if (ts.isImportDeclaration(statement) && ts.isStringLiteralLike(statement.moduleSpecifier)) importSpecifier = statement.moduleSpecifier;
137
+ return importSpecifier;
138
+ }
139
+
140
+ function addCommonImportEvidence(ts, statement, importSpecifier, sourceFile, checker, found) {
141
+ if (ts.isImportEqualsDeclaration(statement)) {
142
+ found.push(...namespaceTransportUsages(ts, sourceFile, checker, statement.name).map(item => ({ ...item, specifier: importSpecifier.text })));
143
+ return;
144
+ }
145
+ if (!statement.importClause) return;
146
+ const bindings = statement.importClause.namedBindings;
147
+ if (bindings && ts.isNamespaceImport(bindings)) {
148
+ found.push(...namespaceTransportUsages(ts, sourceFile, checker, bindings.name).map(item => ({ ...item, specifier: importSpecifier.text })));
149
+ return;
150
+ }
151
+ if (!bindings || !ts.isNamedImports(bindings)) return;
152
+ for (const element of bindings.elements) {
153
+ const imported = element.propertyName?.text ?? element.name.text;
154
+ if (NEST_COMMON_TRANSPORT.has(imported)) found.push({ node: element, specifier: importSpecifier.text, detail: imported });
155
+ }
156
+ }
157
+
158
+ function importTransportEvidence(ts, sourceFile, checker, found) {
114
159
  for (const statement of sourceFile.statements) {
115
- const importEqualsSpecifier = ts.isImportEqualsDeclaration(statement)
116
- && ts.isExternalModuleReference(statement.moduleReference)
117
- && statement.moduleReference.expression
118
- && ts.isStringLiteralLike(statement.moduleReference.expression)
119
- ? statement.moduleReference.expression
120
- : null;
121
- const importSpecifier = ts.isImportDeclaration(statement) && ts.isStringLiteralLike(statement.moduleSpecifier)
122
- ? statement.moduleSpecifier
123
- : importEqualsSpecifier;
160
+ const importSpecifier = importSpecifierOf(ts, statement);
124
161
  if (!importSpecifier) continue;
125
162
  const specifier = importSpecifier.text;
126
163
  if (TRANSPORT_PACKAGES.test(specifier)) {
127
164
  found.push({ node: importSpecifier, specifier, detail: specifier });
128
165
  continue;
129
166
  }
130
- if (specifier !== '@nestjs/common') continue;
131
- if (ts.isImportEqualsDeclaration(statement)) {
132
- found.push(...namespaceUsages(statement.name).map(item => ({ ...item, specifier })));
133
- continue;
134
- }
135
- if (!statement.importClause) continue;
136
- const bindings = statement.importClause.namedBindings;
137
- if (bindings && ts.isNamespaceImport(bindings)) {
138
- found.push(...namespaceUsages(bindings.name).map(item => ({ ...item, specifier })));
139
- continue;
140
- }
141
- if (!bindings || !ts.isNamedImports(bindings)) continue;
142
- for (const element of bindings.elements) {
143
- const imported = element.propertyName?.text ?? element.name.text;
144
- if (NEST_COMMON_TRANSPORT.has(imported)) found.push({ node: element, specifier, detail: imported });
145
- }
167
+ if (specifier === '@nestjs/common') addCommonImportEvidence(ts, statement, importSpecifier, sourceFile, checker, found);
146
168
  }
147
- const visitRequire = node => {
148
- const requiredSpecifier = call => ts.isCallExpression(call)
149
- && ts.isIdentifier(call.expression) && call.expression.text === 'require'
150
- && call.arguments.length === 1 && ts.isStringLiteralLike(call.arguments[0])
151
- ? call.arguments[0].text
152
- : null;
153
- if ((ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) && ts.isCallExpression(node.expression)) {
154
- const specifier = requiredSpecifier(node.expression);
155
- const selected = ts.isPropertyAccessExpression(node)
156
- ? node.name.text
157
- : ts.isStringLiteralLike(node.argumentExpression) ? node.argumentExpression.text : null;
158
- if (specifier && TRANSPORT_PACKAGES.test(specifier)) {
159
- found.push({ node: ts.isPropertyAccessExpression(node) ? node.name : node.argumentExpression, specifier, detail: `${selected ?? 'computed member'} from ${specifier}` });
160
- } else if (specifier === '@nestjs/common' && (selected === null || NEST_COMMON_TRANSPORT.has(selected))) {
161
- found.push({ node: ts.isPropertyAccessExpression(node) ? node.name : node.argumentExpression, specifier, detail: selected ?? 'computed @nestjs/common require access' });
162
- }
163
- }
164
- if (ts.isVariableDeclaration(node) && node.initializer && ts.isCallExpression(node.initializer)
165
- && requiredSpecifier(node.initializer)) {
166
- const specifier = requiredSpecifier(node.initializer);
167
- if (TRANSPORT_PACKAGES.test(specifier)) {
168
- found.push({ node: node.initializer.arguments[0], specifier, detail: specifier });
169
- } else if (specifier === '@nestjs/common') {
170
- if (ts.isIdentifier(node.name)) {
171
- found.push(...namespaceUsages(node.name).map(item => ({ ...item, specifier })));
172
- } else if (ts.isObjectBindingPattern(node.name)) {
173
- found.push(...destructuredBindings(node.name.elements, 'computed @nestjs/common require destructuring').map(item => ({ ...item, specifier })));
174
- }
175
- }
169
+ }
170
+
171
+ function requiredSpecifier(ts, call) {
172
+ return ts.isCallExpression(call)
173
+ && ts.isIdentifier(call.expression) && call.expression.text === 'require'
174
+ && call.arguments.length === 1 && ts.isStringLiteralLike(call.arguments[0])
175
+ ? call.arguments[0].text
176
+ : null;
177
+ }
178
+
179
+ const requiredMemberSelection = (ts, node) => {
180
+ if (ts.isPropertyAccessExpression(node)) return node.name.text;
181
+ return ts.isStringLiteralLike(node.argumentExpression) ? node.argumentExpression.text : null;
182
+ };
183
+
184
+ function requiredMemberEvidence(ts, node, found) {
185
+ if (!(ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) || !ts.isCallExpression(node.expression)) return;
186
+ const specifier = requiredSpecifier(ts, node.expression);
187
+ const selected = requiredMemberSelection(ts, node);
188
+ const member = ts.isPropertyAccessExpression(node) ? node.name : node.argumentExpression;
189
+ if (specifier && TRANSPORT_PACKAGES.test(specifier)) {
190
+ found.push({ node: member, specifier, detail: `${selected ?? 'computed member'} from ${specifier}` });
191
+ } else if (specifier === '@nestjs/common' && (selected === null || NEST_COMMON_TRANSPORT.has(selected))) {
192
+ found.push({ node: member, specifier, detail: selected ?? 'computed @nestjs/common require access' });
193
+ }
194
+ }
195
+
196
+ function requiredVariableEvidence(ts, node, sourceFile, checker, found) {
197
+ if (!ts.isVariableDeclaration(node) || !node.initializer || !ts.isCallExpression(node.initializer)
198
+ || !requiredSpecifier(ts, node.initializer)) return;
199
+ const specifier = requiredSpecifier(ts, node.initializer);
200
+ if (TRANSPORT_PACKAGES.test(specifier)) {
201
+ found.push({ node: node.initializer.arguments[0], specifier, detail: specifier });
202
+ } else if (specifier === '@nestjs/common') {
203
+ if (ts.isIdentifier(node.name)) {
204
+ found.push(...namespaceTransportUsages(ts, sourceFile, checker, node.name).map(item => ({ ...item, specifier })));
205
+ } else if (ts.isObjectBindingPattern(node.name)) {
206
+ found.push(...destructuredTransportBindings(ts, node.name.elements, 'computed @nestjs/common require destructuring').map(item => ({ ...item, specifier })));
176
207
  }
177
- ts.forEachChild(node, visitRequire);
178
- };
179
- visitRequire(sourceFile);
208
+ }
209
+ }
210
+
211
+ function requiredTransportEvidence(ts, node, sourceFile, checker, found) {
212
+ requiredMemberEvidence(ts, node, found);
213
+ requiredVariableEvidence(ts, node, sourceFile, checker, found);
214
+ ts.forEachChild(node, child => requiredTransportEvidence(ts, child, sourceFile, checker, found));
215
+ }
216
+
217
+ function transportFrameworkEvidence(ts, sourceFile, checker) {
218
+ const found = [];
219
+ importTransportEvidence(ts, sourceFile, checker, found);
220
+ requiredTransportEvidence(ts, sourceFile, sourceFile, checker, found);
180
221
  return found;
181
222
  }
182
223
 
@@ -207,7 +248,7 @@ function selectedReexportSurface(ts, declaration, selected) {
207
248
  return next.size ? next : undefined;
208
249
  }
209
250
 
210
- function externalTransportReexport(ts, sourceFile, selected) {
251
+ function importedTransportBindings(ts, sourceFile) {
211
252
  const imported = new Map();
212
253
  for (const statement of sourceFile.statements) {
213
254
  if (!ts.isImportDeclaration(statement) || !ts.isStringLiteralLike(statement.moduleSpecifier) || !statement.importClause) continue;
@@ -217,60 +258,92 @@ function externalTransportReexport(ts, sourceFile, selected) {
217
258
  for (const element of bindings.elements) imported.set(element.name.text, { specifier, imported: element.propertyName?.text ?? element.name.text });
218
259
  } else if (bindings && ts.isNamespaceImport(bindings)) imported.set(bindings.name.text, { specifier, imported: null });
219
260
  }
261
+ return imported;
262
+ }
263
+
264
+ const unnamedReexport = (statement, specifier, selected) => {
265
+ if (specifier !== '@nestjs/common' || selected === null || [...selected].some(name => NEST_COMMON_TRANSPORT.has(name))) {
266
+ return { node: statement.moduleSpecifier, specifier, detail: selected === null ? `unbounded ${specifier} re-export` : `re-exported ${[...selected].join(', ')}` };
267
+ }
268
+ return null;
269
+ };
270
+
271
+ const namedReexport = (clause, specifier, selected) => {
272
+ for (const element of clause.elements) {
273
+ if (selected !== null && !selected.has(element.name.text)) continue;
274
+ const original = element.propertyName?.text ?? element.name.text;
275
+ if (specifier !== '@nestjs/common' || NEST_COMMON_TRANSPORT.has(original)) return { node: element, specifier, detail: `${element.name.text} re-exported from ${specifier}` };
276
+ }
277
+ return null;
278
+ };
279
+
280
+ function directTransportReexport(ts, statement, specifier, selected) {
281
+ const clause = statement.exportClause;
282
+ if (!clause) return unnamedReexport(statement, specifier, selected);
283
+ if (ts.isNamespaceExport(clause)) {
284
+ return selected === null || selected.has(clause.name.text) ? { node: clause.name, specifier, detail: `namespace re-export from ${specifier}` } : null;
285
+ }
286
+ return ts.isNamedExports(clause) ? namedReexport(clause, specifier, selected) : null;
287
+ }
288
+
289
+ /** True for an import binding of a transport package, or of a transport symbol of @nestjs/common. */
290
+ const isTransportBinding = binding => (binding.specifier === '@nestjs/common'
291
+ ? binding.imported !== null && NEST_COMMON_TRANSPORT.has(binding.imported)
292
+ : TRANSPORT_PACKAGES.test(binding.specifier));
293
+
294
+ function localTransportReexport(ts, statement, selected, imported) {
295
+ const clause = statement.exportClause;
296
+ if (!clause || !ts.isNamedExports(clause)) return null;
297
+ for (const element of clause.elements) {
298
+ if (selected !== null && !selected.has(element.name.text)) continue;
299
+ const binding = imported.get(element.propertyName?.text ?? element.name.text);
300
+ if (!binding || !isTransportBinding(binding)) continue;
301
+ return { node: element, specifier: binding.specifier, detail: `${element.name.text} re-exported from ${binding.specifier}` };
302
+ }
303
+ return null;
304
+ }
305
+
306
+ const statementTransportReexport = (ts, statement, selected, imported) => {
307
+ const specifier = statement.moduleSpecifier && ts.isStringLiteralLike(statement.moduleSpecifier) ? statement.moduleSpecifier.text : null;
308
+ if (specifier && (specifier === '@nestjs/common' || TRANSPORT_PACKAGES.test(specifier))) return directTransportReexport(ts, statement, specifier, selected);
309
+ return specifier ? null : localTransportReexport(ts, statement, selected, imported);
310
+ };
311
+
312
+ function externalTransportReexport(ts, sourceFile, selected) {
313
+ const imported = importedTransportBindings(ts, sourceFile);
220
314
  for (const statement of sourceFile.statements) {
221
315
  if (!ts.isExportDeclaration(statement)) continue;
222
- const clause = statement.exportClause;
223
- const specifier = statement.moduleSpecifier && ts.isStringLiteralLike(statement.moduleSpecifier) ? statement.moduleSpecifier.text : null;
224
- if (specifier && (specifier === '@nestjs/common' || TRANSPORT_PACKAGES.test(specifier))) {
225
- if (!clause) {
226
- if (specifier !== '@nestjs/common' || selected === null || [...selected].some(name => NEST_COMMON_TRANSPORT.has(name))) {
227
- return { node: statement.moduleSpecifier, specifier, detail: selected === null ? `unbounded ${specifier} re-export` : `re-exported ${[...selected].join(', ')}` };
228
- }
229
- continue;
230
- }
231
- if (ts.isNamespaceExport(clause)) {
232
- if (selected === null || selected.has(clause.name.text)) return { node: clause.name, specifier, detail: `namespace re-export from ${specifier}` };
233
- continue;
234
- }
235
- if (!ts.isNamedExports(clause)) continue;
236
- for (const element of clause.elements) {
237
- if (selected !== null && !selected.has(element.name.text)) continue;
238
- const original = element.propertyName?.text ?? element.name.text;
239
- if (specifier !== '@nestjs/common' || NEST_COMMON_TRANSPORT.has(original)) return { node: element, specifier, detail: `${element.name.text} re-exported from ${specifier}` };
240
- }
241
- continue;
242
- }
243
- if (specifier || !clause || !ts.isNamedExports(clause)) continue;
244
- for (const element of clause.elements) {
245
- if (selected !== null && !selected.has(element.name.text)) continue;
246
- const local = element.propertyName?.text ?? element.name.text;
247
- const binding = imported.get(local);
248
- if (!binding) continue;
249
- if (binding.specifier !== '@nestjs/common' && !TRANSPORT_PACKAGES.test(binding.specifier)) continue;
250
- if (binding.specifier === '@nestjs/common' && (binding.imported === null || !NEST_COMMON_TRANSPORT.has(binding.imported))) continue;
251
- return { node: element, specifier: binding.specifier, detail: `${element.name.text} re-exported from ${binding.specifier}` };
252
- }
316
+ const evidence = statementTransportReexport(ts, statement, selected, imported);
317
+ if (evidence) return evidence;
253
318
  }
254
319
  return null;
255
320
  }
256
321
 
322
+ function reexportQueueKey(current) {
323
+ return `${current.file}\0${current.selected === null ? '*' : [...current.selected].sort(byCodeUnit).join(',')}`;
324
+ }
325
+
326
+ function addReexportEdges(ts, context, current, queue) {
327
+ for (const edge of context.edges.get(current.file) ?? []) {
328
+ if (!edge.reexport) continue;
329
+ const selected = selectedReexportSurface(ts, edge.declaration, current.selected);
330
+ if (selected !== undefined) queue.push({ file: edge.to, selected, chain: [...current.chain, edge.to] });
331
+ }
332
+ }
333
+
257
334
  function reexportedTransportEvidence(ts, context, sourceFiles, firstEdge) {
258
335
  const queue = [{ file: firstEdge.to, selected: importedSurface(ts, firstEdge.declaration), chain: [firstEdge.from, firstEdge.to] }];
259
336
  const visited = new Set();
260
337
  while (queue.length) {
261
338
  const current = queue.shift();
262
- const key = `${current.file}\0${current.selected === null ? '*' : [...current.selected].sort().join(',')}`;
339
+ const key = reexportQueueKey(current);
263
340
  if (visited.has(key)) continue;
264
341
  visited.add(key);
265
342
  const sourceFile = sourceFiles.get(path.resolve(current.file));
266
343
  if (!sourceFile) continue;
267
344
  const evidence = externalTransportReexport(ts, sourceFile, current.selected);
268
345
  if (evidence) return { evidence, chain: current.chain };
269
- for (const edge of context.edges.get(current.file) ?? []) {
270
- if (!edge.reexport) continue;
271
- const selected = selectedReexportSurface(ts, edge.declaration, current.selected);
272
- if (selected !== undefined) queue.push({ file: edge.to, selected, chain: [...current.chain, edge.to] });
273
- }
346
+ addReexportEdges(ts, context, current, queue);
274
347
  }
275
348
  return null;
276
349
  }
@@ -288,56 +361,69 @@ function finding(config, edge, ruleId, message, chain) {
288
361
  };
289
362
  }
290
363
 
364
+ function checkApplicationComposition(config, ts, sourceFile, fileName, app, violations) {
365
+ const evidence = roleEvidence(ts, sourceFile);
366
+ if (evidence) {
367
+ violations.push({
368
+ ruleId: 'BE_APP_BUSINESS_ROLE',
369
+ path: relativePath(config.root, fileName),
370
+ ...sourceLocation(sourceFile, evidence.node),
371
+ message: `Application composition source contains measurable business/provider role ${evidence.detail}. Move that role under src/features or src/modules.`,
372
+ });
373
+ } else if (slotAdmitsFile(config.hfs, relativePath(config.root, fileName)) !== true) {
374
+ violations.push({
375
+ ruleId: FORBIDDEN_APP_ROLE.test(path.posix.basename(app.relative)) ? 'BE_APP_BUSINESS_ROLE' : 'BE_APP_COMPOSITION_ONLY',
376
+ path: relativePath(config.root, fileName),
377
+ line: 1,
378
+ column: 1,
379
+ message: `Application source ${app.relative} is not a file its app slot requires or allows (main.ts, app.module.ts, <app>.options.ts).`,
380
+ });
381
+ }
382
+ }
383
+
384
+ function checkApplicationTransport(config, context, sourceFile, fileName, violations) {
385
+ for (const evidence of transportFrameworkEvidence(context.ts, sourceFile, context.checkerFor(fileName))) {
386
+ violations.push({
387
+ ruleId: 'BE_APPLICATION_TRANSPORT_FRAMEWORK',
388
+ path: relativePath(config.root, fileName),
389
+ ...sourceLocation(sourceFile, evidence.node),
390
+ specifier: evidence.specifier,
391
+ message: `Feature application code imports transport framework surface ${evidence.detail}. Keep protocol decorators, request/response types, validation DTOs, and generated transport types under transport/.`,
392
+ });
393
+ }
394
+ }
395
+
396
+ function checkFeatureTransportEdges(config, context, sourceFiles, featureRoots, fileName, violations) {
397
+ for (const edge of context.edges.get(fileName) ?? []) {
398
+ const reexported = reexportedTransportEvidence(context.ts, context, sourceFiles, edge);
399
+ if (reexported) violations.push(finding(config, edge, 'BE_APPLICATION_TRANSPORT_FRAMEWORK', `Feature application code imports protocol surface ${reexported.evidence.detail} through an internal re-export. Keep protocol decorators and types under transport/.`, reexported.chain));
400
+ const transportChain = reachableViolation(context.edges, edge, target => insideFeatureLayer(featureRoots, target, 'transport'));
401
+ if (transportChain) violations.push(finding(config, edge, 'BE_APPLICATION_IMPORTS_TRANSPORT', 'Feature application code cannot depend on transport adapters or DTOs, including through a type import or barrel.', transportChain));
402
+ }
403
+ }
404
+
405
+ function checkBackendSource(config, context, sourceFiles, roots, sourceFile, violations) {
406
+ const { moduleRoots, featureRoots, nonAppRoots } = roots;
407
+ const fileName = path.resolve(sourceFile.fileName);
408
+ const fromModules = insideAny(moduleRoots, fileName);
409
+ const fromFeatures = insideAny(featureRoots, fileName);
410
+ const fromApplication = fromFeatures && insideFeatureLayer(featureRoots, fileName, 'application');
411
+ const app = !fromModules && !fromFeatures ? appSource(config, fileName, nonAppRoots) : null;
412
+ if (app) checkApplicationComposition(config, context.ts, sourceFile, fileName, app, violations);
413
+ if (!fromModules && !fromFeatures) return;
414
+ if (fromApplication) checkApplicationTransport(config, context, sourceFile, fileName, violations);
415
+ if (fromApplication) checkFeatureTransportEdges(config, context, sourceFiles, featureRoots, fileName, violations);
416
+ }
417
+
291
418
  /** Enforce backend direction and keep executable apps as measurable composition roots. */
292
419
  export function checkBackend(config, context) {
293
420
  const violations = [];
294
421
  const moduleRoots = roots(config.root, config.backend.modules);
295
422
  const featureRoots = roots(config.root, config.backend.features);
296
- const nonAppRoots = [...moduleRoots, ...featureRoots];
423
+ const sourceRoots = { moduleRoots, featureRoots, nonAppRoots: [...moduleRoots, ...featureRoots] };
297
424
  const sourceFiles = new Map(context.files.map(file => [path.resolve(file.fileName), file]));
298
425
  for (const sourceFile of context.files) {
299
- const fileName = path.resolve(sourceFile.fileName);
300
- const fromModules = insideAny(moduleRoots, fileName);
301
- const fromFeatures = insideAny(featureRoots, fileName);
302
- const fromApplication = fromFeatures && insideFeatureLayer(featureRoots, fileName, 'application');
303
- const app = !fromModules && !fromFeatures ? appSource(config, fileName, nonAppRoots) : null;
304
- if (app) {
305
- const evidence = roleEvidence(context.ts, sourceFile);
306
- if (evidence) {
307
- violations.push({
308
- ruleId: 'BE_APP_BUSINESS_ROLE',
309
- path: relativePath(config.root, fileName),
310
- ...sourceLocation(sourceFile, evidence.node),
311
- message: `Application composition source contains measurable business/provider role ${evidence.detail}. Move that role under src/features or src/modules.`,
312
- });
313
- } else if (slotAdmitsFile(config.hfs, relativePath(config.root, fileName)) !== true) {
314
- violations.push({
315
- ruleId: FORBIDDEN_APP_ROLE.test(path.posix.basename(app.relative)) ? 'BE_APP_BUSINESS_ROLE' : 'BE_APP_COMPOSITION_ONLY',
316
- path: relativePath(config.root, fileName),
317
- line: 1,
318
- column: 1,
319
- message: `Application source ${app.relative} is not a file its app slot requires or allows (main.ts, app.module.ts, <app>.options.ts).`,
320
- });
321
- }
322
- }
323
- if (!fromModules && !fromFeatures) continue;
324
- if (fromApplication) {
325
- for (const evidence of transportFrameworkEvidence(context.ts, sourceFile, context.checkerFor(fileName))) violations.push({
326
- ruleId: 'BE_APPLICATION_TRANSPORT_FRAMEWORK',
327
- path: relativePath(config.root, fileName),
328
- ...sourceLocation(sourceFile, evidence.node),
329
- specifier: evidence.specifier,
330
- message: `Feature application code imports transport framework surface ${evidence.detail}. Keep protocol decorators, request/response types, validation DTOs, and generated transport types under transport/.`,
331
- });
332
- }
333
- for (const edge of context.edges.get(fileName) ?? []) {
334
- if (fromApplication) {
335
- const reexported = reexportedTransportEvidence(context.ts, context, sourceFiles, edge);
336
- if (reexported) violations.push(finding(config, edge, 'BE_APPLICATION_TRANSPORT_FRAMEWORK', `Feature application code imports protocol surface ${reexported.evidence.detail} through an internal re-export. Keep protocol decorators and types under transport/.`, reexported.chain));
337
- const transportChain = reachableViolation(context.edges, edge, target => insideFeatureLayer(featureRoots, target, 'transport'));
338
- if (transportChain) violations.push(finding(config, edge, 'BE_APPLICATION_IMPORTS_TRANSPORT', 'Feature application code cannot depend on transport adapters or DTOs, including through a type import or barrel.', transportChain));
339
- }
340
- }
426
+ checkBackendSource(config, context, sourceFiles, sourceRoots, sourceFile, violations);
341
427
  }
342
428
  return violations;
343
429
  }