@starci/hfs 1.0.0 → 2.0.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 (246) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +116 -12
  3. package/bin/hfs.mjs +119 -15
  4. package/emit/compiler.mjs +35 -0
  5. package/emit/contracts.mjs +97 -0
  6. package/emit/operations-worker.mjs +24 -0
  7. package/emit/operations.mjs +126 -0
  8. package/emit/schema-worker.mjs +117 -0
  9. package/emit/static-graph.mjs +670 -0
  10. package/emit/type-schema.mjs +145 -0
  11. package/package.json +10 -2
  12. package/report/sonar.mjs +180 -0
  13. package/runtime/engine/admission.mjs +284 -0
  14. package/runtime/engine/digest.mjs +10 -0
  15. package/runtime/engine/ledger-db.mjs +1245 -0
  16. package/runtime/engine/machine-db.mjs +1484 -0
  17. package/runtime/engine/migrations/machine/0001-init.sql +887 -0
  18. package/runtime/engine/migrations/runtime/0001-init.sql +1072 -0
  19. package/runtime/engine/migrations/runtime/0003-usage-unavailable.sql +13 -0
  20. package/runtime/engine/migrations/runtime/0004-attempt-why.sql +43 -0
  21. package/runtime/engine/plain-object.mjs +5 -0
  22. package/runtime/knowledge/hfs/canon-pins.yaml +16 -58
  23. package/runtime/knowledge/hfs/slots.yaml +409 -140
  24. package/runtime/knowledge/patterns/fe/folder.yaml +309 -0
  25. package/runtime/knowledge/sonar-gate.yaml +85 -0
  26. package/runtime/modules/kernel/failure-codes.yaml +1480 -16
  27. package/runtime/scripts/checks/architecture/backend.mjs +350 -0
  28. package/runtime/scripts/checks/architecture/background-unowned.mjs +107 -0
  29. package/runtime/scripts/checks/architecture/client-reaches-server.mjs +94 -0
  30. package/runtime/scripts/checks/architecture/clones.mjs +200 -0
  31. package/runtime/scripts/checks/architecture/config-unread.mjs +35 -0
  32. package/runtime/scripts/checks/architecture/config.mjs +310 -0
  33. package/runtime/scripts/checks/architecture/connection-map.mjs +208 -0
  34. package/runtime/scripts/checks/architecture/constructor-deps.mjs +100 -0
  35. package/runtime/scripts/checks/architecture/contract-fixture-guard.mjs +128 -0
  36. package/runtime/scripts/checks/architecture/contracts.mjs +792 -0
  37. package/runtime/scripts/checks/architecture/cross-app-duplicate.mjs +73 -0
  38. package/runtime/scripts/checks/architecture/dead-exports.mjs +265 -0
  39. package/runtime/scripts/checks/architecture/default-deny.mjs +129 -0
  40. package/runtime/scripts/checks/architecture/doc-language.mjs +39 -0
  41. package/runtime/scripts/checks/architecture/entrypoint.mjs +57 -0
  42. package/runtime/scripts/checks/architecture/error-codes.mjs +45 -0
  43. package/runtime/scripts/checks/architecture/error-masked.mjs +60 -0
  44. package/runtime/scripts/checks/architecture/fe-slot-allows.mjs +38 -0
  45. package/runtime/scripts/checks/architecture/feature-shape.mjs +49 -0
  46. package/runtime/scripts/checks/architecture/framework-pinned.mjs +83 -0
  47. package/runtime/scripts/checks/architecture/frontend.mjs +995 -0
  48. package/runtime/scripts/checks/architecture/hfs-graph.mjs +61 -0
  49. package/runtime/scripts/checks/architecture/hfs.mjs +521 -0
  50. package/runtime/scripts/checks/architecture/hooks-are-hooks.mjs +88 -0
  51. package/runtime/scripts/checks/architecture/i18n-keys.mjs +146 -0
  52. package/runtime/scripts/checks/architecture/index.mjs +316 -0
  53. package/runtime/scripts/checks/architecture/injection-token-exported.mjs +62 -0
  54. package/runtime/scripts/checks/architecture/machine-ast.mjs +138 -0
  55. package/runtime/scripts/checks/architecture/module-per-transport.mjs +130 -0
  56. package/runtime/scripts/checks/architecture/next-data.mjs +775 -0
  57. package/runtime/scripts/checks/architecture/owners.mjs +89 -0
  58. package/runtime/scripts/checks/architecture/package-shape.mjs +63 -0
  59. package/runtime/scripts/checks/architecture/reachability.mjs +233 -0
  60. package/runtime/scripts/checks/architecture/register-once.mjs +141 -0
  61. package/runtime/scripts/checks/architecture/registration.mjs +319 -0
  62. package/runtime/scripts/checks/architecture/required-files.mjs +172 -0
  63. package/runtime/scripts/checks/architecture/route-files-thin.mjs +97 -0
  64. package/runtime/scripts/checks/architecture/schema-owner.mjs +261 -0
  65. package/runtime/scripts/checks/architecture/size-growth.mjs +73 -0
  66. package/runtime/scripts/checks/architecture/source-names.mjs +607 -0
  67. package/runtime/scripts/checks/architecture/sql-owner.mjs +142 -0
  68. package/runtime/scripts/checks/architecture/sql-tokens.mjs +327 -0
  69. package/runtime/scripts/checks/architecture/symbols.mjs +193 -0
  70. package/runtime/scripts/checks/architecture/test-world-files.mjs +163 -0
  71. package/runtime/scripts/checks/architecture/tiers.mjs +130 -0
  72. package/runtime/scripts/checks/architecture/transport-owner.mjs +112 -0
  73. package/runtime/scripts/checks/architecture/typescript.mjs +500 -0
  74. package/runtime/scripts/checks/architecture/unit-spec-providers.mjs +122 -0
  75. package/runtime/scripts/checks/architecture.mjs +41 -0
  76. package/runtime/scripts/checks/common.mjs +37 -0
  77. package/runtime/scripts/checks/typescript-programs.mjs +82 -0
  78. package/runtime/scripts/lib/artifact-hold.mjs +89 -0
  79. package/runtime/scripts/lib/artifact-store.mjs +103 -0
  80. package/runtime/scripts/lib/fs-kind.mjs +10 -0
  81. package/runtime/scripts/lib/git.mjs +53 -0
  82. package/runtime/scripts/lib/hfs-allows.mjs +57 -0
  83. package/runtime/scripts/lib/hfs-check.mjs +254 -28
  84. package/runtime/scripts/lib/hfs-rules/contract.mjs +126 -0
  85. package/runtime/scripts/lib/hfs-rules/deps.mjs +63 -0
  86. package/runtime/scripts/lib/hfs-rules/fe-no-tests.mjs +47 -0
  87. package/runtime/scripts/lib/hfs-rules/frontend.mjs +124 -0
  88. package/runtime/scripts/lib/hfs-rules/lint-suppression.mjs +34 -0
  89. package/runtime/scripts/lib/hfs-rules/pipeline.mjs +51 -0
  90. package/runtime/scripts/lib/hfs-rules/proof-commands.mjs +64 -0
  91. package/runtime/scripts/lib/hfs-rules/read.mjs +28 -0
  92. package/runtime/scripts/lib/hfs-rules/repo-local-checks.mjs +32 -0
  93. package/runtime/scripts/lib/hfs-rules/secrets.mjs +54 -0
  94. package/runtime/scripts/lib/hfs-rules/spec-placement.mjs +31 -0
  95. package/runtime/scripts/lib/hfs-rules/stacks.mjs +54 -0
  96. package/runtime/scripts/lib/hfs-rules/test-topology.mjs +31 -0
  97. package/runtime/scripts/lib/hfs-slots.mjs +95 -41
  98. package/runtime/scripts/lib/hfs-tree.mjs +80 -0
  99. package/runtime/scripts/lib/hfs-view.mjs +68 -0
  100. package/runtime/scripts/lib/json.mjs +22 -0
  101. package/runtime/scripts/lib/language.mjs +107 -0
  102. package/runtime/scripts/lib/path-key.mjs +2 -0
  103. package/runtime/scripts/lib/redact.mjs +148 -0
  104. package/runtime/scripts/lib/repo-identity.mjs +50 -0
  105. package/runtime/scripts/lib/safe-remove.mjs +179 -0
  106. package/runtime/scripts/lib/secret-patterns.mjs +44 -0
  107. package/runtime/scripts/lib/sleep-sync.mjs +17 -0
  108. package/runtime/scripts/lib/stack-declaration.mjs +52 -0
  109. package/runtime/scripts/lib/stack-services.mjs +217 -0
  110. package/runtime/scripts/lib/test-secrets.mjs +120 -0
  111. package/scaffold/service.mjs +333 -0
  112. package/sync/format.mjs +46 -0
  113. package/sync/hygiene.mjs +56 -24
  114. package/sync/index.mjs +126 -41
  115. package/sync/managed.mjs +170 -0
  116. package/sync/skeleton.mjs +32 -10
  117. package/sync/sonar-key.mjs +13 -0
  118. package/sync/ts-strict.mjs +48 -0
  119. package/templates/{common → be/ci-workflows/github/workflows}/ci.yml +13 -13
  120. package/templates/be/{e2e.yml → ci-workflows/github/workflows/e2e.yml} +1 -0
  121. package/templates/be/hooks/husky/pre-commit +13 -0
  122. package/templates/be/hooks/husky/pre-push +7 -0
  123. package/templates/be/package-scripts/package.json +21 -0
  124. package/templates/be/{sonar-project.properties → quality-config/sonar-project.properties} +2 -3
  125. package/templates/be/skeleton/apps/__app__/src/app.module.ts +14 -4
  126. package/templates/be/skeleton/apps/__app__/src/main.ts +9 -6
  127. package/templates/be/skeleton/scripts/.gitkeep +0 -0
  128. package/templates/be/skeleton/src/features/system-health/application/check-liveness.contracts.ts +2 -0
  129. package/templates/be/skeleton/src/features/system-health/application/check-liveness.handler.ts +22 -0
  130. package/templates/be/skeleton/src/features/system-health/application/check-liveness.query.ts +11 -0
  131. package/templates/be/skeleton/src/features/system-health/index.ts +1 -1
  132. package/templates/be/skeleton/src/features/system-health/system-health.module.ts +3 -3
  133. package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.ts +8 -9
  134. package/templates/be/skeleton/src/features/system-health/transport/http/system-health-http.module.ts +3 -6
  135. package/templates/be/skeleton/src/modules/domain/liveness/index.ts +3 -0
  136. package/templates/be/skeleton/src/modules/domain/liveness/liveness.contracts.ts +9 -0
  137. package/templates/be/skeleton/src/modules/domain/liveness/liveness.module-definition.ts +7 -0
  138. package/templates/be/skeleton/src/modules/domain/liveness/liveness.module.ts +14 -0
  139. package/templates/be/skeleton/src/modules/domain/liveness/liveness.options.ts +2 -0
  140. package/templates/be/skeleton/src/modules/domain/liveness/liveness.service.spec.ts +40 -0
  141. package/templates/be/skeleton/src/modules/domain/liveness/liveness.service.ts +26 -0
  142. package/templates/be/skeleton/src/modules/platform/clock/clock.decorators.ts +9 -0
  143. package/templates/be/skeleton/src/modules/platform/clock/clock.module-definition.ts +7 -0
  144. package/templates/be/skeleton/src/modules/platform/clock/clock.module.ts +19 -0
  145. package/templates/be/skeleton/src/modules/platform/clock/clock.options.ts +2 -0
  146. package/templates/be/skeleton/src/modules/platform/clock/clock.port.ts +5 -0
  147. package/templates/be/skeleton/src/modules/platform/clock/index.ts +4 -0
  148. package/templates/be/skeleton/src/modules/platform/clock/system-clock.service.spec.ts +20 -0
  149. package/templates/be/skeleton/src/modules/platform/clock/system-clock.service.ts +9 -0
  150. package/templates/be/skeleton/src/modules/platform/composition/composition.decorators.ts +8 -0
  151. package/templates/be/skeleton/src/modules/platform/composition/index.ts +2 -0
  152. package/templates/be/skeleton/src/modules/platform/config/config.decorators.ts +9 -0
  153. package/templates/be/skeleton/src/modules/platform/config/index.ts +2 -3
  154. package/templates/be/skeleton/src/modules/platform/config/server.config.ts +1 -1
  155. package/templates/be/skeleton/src/modules/platform/config/server.options.ts +0 -3
  156. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.contracts.ts +5 -0
  157. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.decorators.ts +9 -0
  158. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.handler.ts +27 -0
  159. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.log-events.ts +5 -0
  160. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module-definition.ts +7 -0
  161. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module.ts +18 -0
  162. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.options.ts +2 -0
  163. package/templates/be/skeleton/src/modules/platform/cqrs/index.ts +4 -0
  164. package/templates/be/skeleton/src/modules/platform/errors/error.filter.ts +8 -12
  165. package/templates/be/skeleton/src/modules/platform/errors/errors.log-events.ts +5 -0
  166. package/templates/be/skeleton/src/modules/platform/errors/index.ts +1 -1
  167. package/templates/be/skeleton/src/modules/platform/logging/index.ts +4 -4
  168. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +99 -0
  169. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +38 -0
  170. package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +9 -0
  171. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +7 -0
  172. package/templates/be/skeleton/src/modules/platform/logging/logging.module-definition.ts +7 -0
  173. package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +23 -10
  174. package/templates/be/skeleton/src/modules/platform/logging/logging.options.ts +2 -0
  175. package/templates/be/skeleton/src/modules/platform/logging/logging.port.ts +15 -0
  176. package/templates/be/tool-config/eslint.config.mjs +3 -0
  177. package/templates/be/tool-config/jest.config.js +1 -0
  178. package/templates/be/tool-config/prettierignore +8 -0
  179. package/templates/be/tool-config/prettierrc +1 -0
  180. package/templates/be/tool-config/src/tests/tsconfig.json +5 -0
  181. package/templates/be/tool-config/tsconfig.build.json +5 -0
  182. package/templates/be/tool-config/tsconfig.json +11 -0
  183. package/templates/common/gitignore.base +1 -1
  184. package/templates/fe/ci-workflows/github/workflows/ci.yml +54 -0
  185. package/templates/fe/hooks/husky/pre-commit +16 -0
  186. package/templates/fe/hooks/husky/pre-push +6 -0
  187. package/templates/fe/package-scripts/package.json +17 -0
  188. package/templates/fe/parts/api-client.ts +44 -0
  189. package/templates/fe/parts/api-outcome.ts +7 -0
  190. package/templates/fe/quality-config/sonar-project.properties +8 -0
  191. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/layout.tsx +1 -1
  192. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/not-found.tsx +1 -1
  193. package/templates/fe/skeleton/apps/__app__/src/app/global-error.tsx +1 -1
  194. package/templates/fe/skeleton/scripts/.gitkeep +0 -0
  195. package/templates/fe/skeleton-app/apps/__app__/src/modules/api/client.ts +1 -0
  196. package/templates/fe/skeleton-app/apps/__app__/src/modules/api/index.ts +3 -0
  197. package/templates/fe/skeleton-app/apps/__app__/src/modules/api/outcome.ts +1 -0
  198. package/templates/fe/skeleton-app/apps/__app__/src/modules/i18n/index.ts +4 -0
  199. package/templates/fe/skeleton-shared/apps/__app__/next.config.ts +12 -0
  200. package/templates/fe/skeleton-shared/apps/__app__/src/modules/api/index.ts +2 -0
  201. package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/index.ts +9 -0
  202. package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/request.ts +5 -0
  203. package/templates/fe/skeleton-shared/apps/__app__/src/proxy.ts +5 -0
  204. package/templates/fe/skeleton-shared/packages/__family__-api/package.json +12 -0
  205. package/templates/fe/skeleton-shared/packages/__family__-api/src/client.ts +1 -0
  206. package/templates/fe/skeleton-shared/packages/__family__-api/src/index.ts +3 -0
  207. package/templates/fe/skeleton-shared/packages/__family__-api/src/outcome.ts +1 -0
  208. package/templates/fe/skeleton-shared/packages/__family__-api/tsconfig.json +5 -0
  209. package/templates/fe/skeleton-shared/packages/__family__-i18n/package.json +18 -0
  210. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/app.ts +19 -0
  211. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/index.ts +2 -0
  212. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/proxy.ts +12 -0
  213. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/request.ts +15 -0
  214. package/templates/fe/skeleton-shared/packages/__family__-i18n/tsconfig.json +5 -0
  215. package/templates/fe/tool-config/eslint.config.mjs +3 -0
  216. package/templates/fe/tool-config/prettierignore +10 -0
  217. package/templates/fe/tool-config/prettierrc +1 -0
  218. package/templates/fe/tool-config/stylelint.config.mjs +3 -0
  219. package/templates/fe/tool-config/tsconfig.json +4 -0
  220. package/templates/be/pre-commit +0 -8
  221. package/templates/be/skeleton/apps/__app__/src/__app__.composition.spec.ts +0 -31
  222. package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.spec.ts +0 -13
  223. package/templates/be/skeleton/src/modules/platform/config/env-source.spec.ts +0 -36
  224. package/templates/be/skeleton/src/modules/platform/config/server.config.spec.ts +0 -19
  225. package/templates/be/skeleton/src/modules/platform/errors/domain-error.spec.ts +0 -15
  226. package/templates/be/skeleton/src/modules/platform/errors/error.filter.spec.ts +0 -38
  227. package/templates/be/skeleton/src/modules/platform/logging/json-logger.spec.ts +0 -33
  228. package/templates/be/skeleton/src/modules/platform/logging/json-logger.ts +0 -35
  229. package/templates/be/skeleton/src/modules/platform/logging/log-id.ts +0 -9
  230. package/templates/be/skeleton/src/modules/platform/logging/logger.port.ts +0 -19
  231. package/templates/common/codecov.yml +0 -13
  232. package/templates/common/pre-push +0 -5
  233. package/templates/fe/e2e.yml +0 -22
  234. package/templates/fe/pre-commit +0 -7
  235. package/templates/fe/skeleton/apps/__app__/src/app/health/live/route.spec.ts +0 -10
  236. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages.spec.ts +0 -27
  237. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/routing.spec.ts +0 -10
  238. package/templates/fe/sonar-project.properties +0 -11
  239. /package/templates/be/skeleton/src/modules/platform/config/{env-source.ts → env-source.config.ts} +0 -0
  240. /package/templates/be/skeleton/src/modules/platform/errors/{domain-error.ts → domain.error.ts} +0 -0
  241. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/next.config.ts +0 -0
  242. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/config.ts +0 -0
  243. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/navigation.ts +0 -0
  244. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/request.ts +0 -0
  245. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/routing.ts +0 -0
  246. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/proxy.ts +0 -0
@@ -0,0 +1,607 @@
1
+ import path from 'node:path';
2
+ import { isInside } from './config.mjs';
3
+ import { isUnshadowedCommonJsRequire, relativePath, sourceLocation, UNPROVEN_FRAMEWORK, unwrapExpression } from './typescript.mjs';
4
+
5
+ const SOURCE_LAYOUT_RULE_ID = 'BE_FEATURE_LAYOUT_INVALID';
6
+ const SOURCE_NAME_RULE_ID = 'BE_SOURCE_FORM';
7
+ const FRAMEWORK_EXPORTS = new Map([
8
+ ['@nestjs/graphql', new Set(['Args', 'ArgsType', 'InputType', 'Mutation', 'ObjectType', 'Query', 'registerEnumType'])],
9
+ ['typeorm', new Set(['Entity', 'EntitySchema', 'MigrationInterface', 'ViewEntity'])],
10
+ ]);
11
+ const APPLICATION_ROLES = new Set(['command', 'contracts', 'handler', 'query']);
12
+ const TRANSPORT_ROLES = new Set(['args', 'cli', 'consumer', 'controller', 'gateway', 'input', 'job', 'request', 'resolver', 'response', 'type']);
13
+ const APPLICATION_LAYER_ROLES = new Set([...APPLICATION_ROLES, 'mapper']);
14
+ const TRANSPORT_LAYER_ROLES = new Set([...TRANSPORT_ROLES, 'enum', 'filter', 'guard', 'interceptor', 'mapper']);
15
+ // Class-bearing roles of the CLOSED suffix list of BE-CONVENTION 1.15 (class name = PascalCase of file + role). A role that is
16
+ // not in that list (adapter, exception, processor, provider, repository, strategy, use-case) is never an allowed role here.
17
+ const CLASS_ROLE_SUFFIX = new Map([
18
+ ['args', 'Args'], ['cli', 'Cli'], ['client', 'Client'], ['command', 'Command'], ['consumer', 'Consumer'],
19
+ ['controller', 'Controller'], ['entity', 'Entity'], ['error', 'Error'], ['filter', 'Filter'], ['gateway', 'Gateway'],
20
+ ['guard', 'Guard'], ['handler', 'Handler'], ['input', 'Input'], ['interceptor', 'Interceptor'], ['job', 'Job'],
21
+ ['mapper', 'Mapper'], ['module', 'Module'], ['module-definition', 'Module'], ['policy', 'Policy'], ['query', 'Query'],
22
+ ['request', 'Request'], ['resolver', 'Resolver'], ['response', 'Response'], ['service', 'Service'], ['type', 'Type'],
23
+ ]);
24
+ // Transport object contracts (interfaces and object aliases) named by their file role: <Action>Input in *.input.ts,
25
+ // <Action>Request in *.request.ts, <Name>Row in *.rows.ts ... Domain values in *.contracts.ts and *.options.ts carry no suffix.
26
+ const TRANSPORT_OBJECT_SUFFIX = new Map([
27
+ ['args', 'Args'], ['input', 'Input'], ['request', 'Request'], ['response', 'Response'], ['rows', 'Row'], ['type', 'Type'],
28
+ ]);
29
+ // A class that implements an interface declared in a file with one of these roles may be named <Qualifier><InterfaceName>.
30
+ const PORT_DECLARATION_ROLES = new Set(['contracts', 'port']);
31
+ const NON_CLASS_ROLES = new Set(['constants', 'contracts', 'decorators', 'enum', 'providers', 'types']);
32
+ const KNOWN_HYPHEN_ROLES = [...CLASS_ROLE_SUFFIX.keys()].sort((a, b) => b.length - a.length);
33
+ const SPECIAL_BASENAMES = new Set(['config', 'configuration', 'constants', 'decorators', 'env', 'environment', 'index', 'main', 'types']);
34
+ const SOURCE_EXTENSION = /\.[cm]?[jt]sx?$/i;
35
+
36
+ function absoluteRoots(root, relatives) {
37
+ return relatives.map(relative => path.resolve(root, ...relative.split('/')));
38
+ }
39
+
40
+ function canonical(file) {
41
+ return path.resolve(file);
42
+ }
43
+
44
+ function normalizedSymbolValue(ts, checker, value) {
45
+ let symbol = value ?? null;
46
+ const seen = new Set();
47
+ while (symbol && (symbol.flags & ts.SymbolFlags.Alias) && !seen.has(symbol)) {
48
+ seen.add(symbol);
49
+ const target = checker.getAliasedSymbol(symbol);
50
+ if (!target || target === symbol) break;
51
+ symbol = target;
52
+ }
53
+ return symbol;
54
+ }
55
+
56
+ function normalizedSymbol(ts, checker, node) {
57
+ return normalizedSymbolValue(ts, checker, checker?.getSymbolAtLocation(node) ?? null);
58
+ }
59
+
60
+ function selectedNode(ts, expression) {
61
+ expression = unwrapExpression(ts, expression);
62
+ if (ts.isIdentifier(expression)) return expression;
63
+ if (ts.isPropertyAccessExpression(expression)) return expression.name;
64
+ if (ts.isElementAccessExpression(expression) && ts.isStringLiteralLike(expression.argumentExpression)) return expression.argumentExpression;
65
+ return null;
66
+ }
67
+
68
+ function valueSymbol(ts, checker, node, seen = new Set()) {
69
+ const selected = selectedNode(ts, node) ?? node;
70
+ const symbol = normalizedSymbol(ts, checker, selected);
71
+ if (!symbol || seen.has(symbol)) return symbol;
72
+ seen.add(symbol);
73
+ const declarations = symbol.getDeclarations?.() ?? [];
74
+ if (declarations.length === 1 && ts.isVariableDeclaration(declarations[0]) && declarations[0].initializer
75
+ && (ts.getCombinedNodeFlags(declarations[0].parent) & ts.NodeFlags.Const)) {
76
+ return valueSymbol(ts, checker, declarations[0].initializer, seen);
77
+ }
78
+ return symbol;
79
+ }
80
+
81
+ function frameworkTargets(config, context, checker, localFiles) {
82
+ const bySymbol = new Map();
83
+ const reasons = [];
84
+ const program = context.programs.find(candidate => candidate.getTypeChecker() === checker);
85
+ if (!program) return { bySymbol, reasons: ['a TypeScript program checker could not be associated with its source'] };
86
+ for (const sourceFile of program.getSourceFiles()) {
87
+ if (!localFiles.has(canonical(sourceFile.fileName))) continue;
88
+ for (const statement of sourceFile.statements) {
89
+ const specifierNode = (context.ts.isImportDeclaration(statement) || context.ts.isExportDeclaration(statement))
90
+ && statement.moduleSpecifier && context.ts.isStringLiteralLike(statement.moduleSpecifier) ? statement.moduleSpecifier : null;
91
+ const specifier = specifierNode?.text;
92
+ if (!specifier || !FRAMEWORK_EXPORTS.has(specifier)) continue;
93
+ const moduleSymbol = checker.getSymbolAtLocation(specifierNode);
94
+ if (!moduleSymbol) {
95
+ reasons.push(`${relativePath(config.root, sourceFile.fileName)} cannot resolve ${specifier}`);
96
+ continue;
97
+ }
98
+ const selected = FRAMEWORK_EXPORTS.get(specifier);
99
+ for (const exported of checker.getExportsOfModule(moduleSymbol)) if (selected.has(exported.getName())) {
100
+ const target = normalizedSymbolValue(context.ts, checker, exported);
101
+ if (target) bySymbol.set(target, exported.getName());
102
+ }
103
+ }
104
+ const visit = node => {
105
+ if (context.ts.isCallExpression(node) && isUnshadowedCommonJsRequire(context.ts, checker, node.expression)
106
+ && node.arguments.length === 1 && context.ts.isStringLiteralLike(node.arguments[0]) && FRAMEWORK_EXPORTS.has(node.arguments[0].text)) {
107
+ reasons.push(`${relativePath(config.root, sourceFile.fileName)} uses a CommonJS ${node.arguments[0].text} binding whose source role cannot be proved`);
108
+ }
109
+ context.ts.forEachChild(node, visit);
110
+ };
111
+ visit(sourceFile);
112
+ }
113
+ return { bySymbol, reasons };
114
+ }
115
+
116
+ function calledExpression(ts, decorator) {
117
+ return ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression;
118
+ }
119
+
120
+ function decoratorKind(ts, checker, decorator, targets) {
121
+ const selected = selectedNode(ts, calledExpression(ts, decorator));
122
+ return selected ? targets.get(valueSymbol(ts, checker, selected)) ?? null : null;
123
+ }
124
+
125
+ function mutableDecoratorKind(ts, checker, decorator, targets) {
126
+ const selected = selectedNode(ts, calledExpression(ts, decorator));
127
+ const symbol = selected ? normalizedSymbol(ts, checker, selected) : null;
128
+ const declarations = symbol?.getDeclarations?.() ?? [];
129
+ if (declarations.length !== 1 || !ts.isVariableDeclaration(declarations[0]) || !declarations[0].initializer
130
+ || (ts.getCombinedNodeFlags(declarations[0].parent) & ts.NodeFlags.Const)) return null;
131
+ return targets.get(valueSymbol(ts, checker, declarations[0].initializer)) ?? null;
132
+ }
133
+
134
+ function returnedExpressions(ts, declaration) {
135
+ if ((ts.isArrowFunction(declaration) || ts.isFunctionExpression(declaration)) && !ts.isBlock(declaration.body)) return [declaration.body];
136
+ const body = ts.isFunctionDeclaration(declaration) || ts.isMethodDeclaration(declaration)
137
+ || ts.isArrowFunction(declaration) || ts.isFunctionExpression(declaration) ? declaration.body : null;
138
+ if (!body || !ts.isBlock(body)) return [];
139
+ const returned = [];
140
+ const visit = node => {
141
+ if (node !== body && (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node))) return;
142
+ if (ts.isReturnStatement(node) && node.expression) returned.push(node.expression);
143
+ else ts.forEachChild(node, visit);
144
+ };
145
+ visit(body);
146
+ return returned;
147
+ }
148
+
149
+ function tracedFrameworkKinds(ts, checker, expression, targets, seen = new Set(), depth = 0) {
150
+ if (!expression) return new Set();
151
+ if (depth > 10) return new Set([UNPROVEN_FRAMEWORK]);
152
+ expression = unwrapExpression(ts, expression);
153
+ const selected = selectedNode(ts, expression);
154
+ const symbol = selected ? normalizedSymbol(ts, checker, selected) : null;
155
+ const direct = symbol ? targets.get(symbol) : null;
156
+ if (direct) return new Set([direct]);
157
+ if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)) {
158
+ const kinds = new Set();
159
+ for (const returned of returnedExpressions(ts, expression)) {
160
+ for (const kind of tracedFrameworkKinds(ts, checker, returned, targets, seen, depth + 1)) kinds.add(kind);
161
+ }
162
+ return kinds;
163
+ }
164
+ if (ts.isCallExpression(expression)) {
165
+ const kinds = tracedFrameworkKinds(ts, checker, expression.expression, targets, seen, depth + 1);
166
+ if (kinds.size) return kinds;
167
+ }
168
+ if (ts.isNewExpression(expression)) return tracedFrameworkKinds(ts, checker, expression.expression, targets, seen, depth + 1);
169
+ if (!symbol || seen.has(symbol)) return new Set();
170
+ const nextSeen = new Set(seen).add(symbol);
171
+ const kinds = new Set();
172
+ for (const declaration of symbol.getDeclarations?.() ?? []) {
173
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
174
+ for (const kind of tracedFrameworkKinds(ts, checker, declaration.initializer, targets, nextSeen, depth + 1)) kinds.add(kind);
175
+ }
176
+ for (const returned of returnedExpressions(ts, declaration)) {
177
+ for (const kind of tracedFrameworkKinds(ts, checker, returned, targets, nextSeen, depth + 1)) kinds.add(kind);
178
+ }
179
+ }
180
+ return kinds;
181
+ }
182
+
183
+ function constructedDecoratorKind(ts, checker, decorator, targets) {
184
+ const kinds = tracedFrameworkKinds(ts, checker, calledExpression(ts, decorator), targets);
185
+ if (kinds.has(UNPROVEN_FRAMEWORK)) return 'unproven framework';
186
+ return kinds.size === 1 ? [...kinds][0] : kinds.size ? 'multiple framework' : null;
187
+ }
188
+
189
+ function decorators(ts, node) {
190
+ return ts.canHaveDecorators(node) ? ts.getDecorators(node) ?? [] : node.decorators ?? [];
191
+ }
192
+
193
+ function hasModifier(ts, node, kind) {
194
+ return (ts.canHaveModifiers(node) ? ts.getModifiers(node) ?? [] : node.modifiers ?? []).some(modifier => modifier.kind === kind);
195
+ }
196
+
197
+ function violation(config, sourceFile, node, ruleId, message, extra = {}) {
198
+ return { ruleId, path: relativePath(config.root, sourceFile.fileName), ...sourceLocation(sourceFile, node), message, ...extra };
199
+ }
200
+
201
+ function sourceRole(ts, sourceFile) {
202
+ const file = path.basename(sourceFile.fileName).replace(SOURCE_EXTENSION, '');
203
+ const base = file.replace(/\.(?:int-)?spec$/i, '');
204
+ const segments = base.split('.');
205
+ if (segments.length > 1) return { base, role: segments.at(-1).toLowerCase(), spec: base !== file };
206
+ const lower = base.toLowerCase();
207
+ const known = KNOWN_HYPHEN_ROLES.find(role => lower === role || lower.endsWith(`-${role}`));
208
+ if (known) return { base, role: known, spec: base !== file };
209
+ const topLevel = sourceFile.statements;
210
+ if (topLevel.some(statement => ts.isEnumDeclaration(statement))) return { base, role: 'enum', spec: base !== file };
211
+ return { base, role: null, spec: base !== file };
212
+ }
213
+
214
+ function kebabSourceBase(base) {
215
+ return /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\.[a-z0-9]+(?:-[a-z0-9]+)*)*$/.test(base);
216
+ }
217
+
218
+ function exportedDeclarations(ts, checker, sourceFile) {
219
+ const exported = new Set();
220
+ const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
221
+ for (const symbol of moduleSymbol ? checker.getExportsOfModule(moduleSymbol) : []) {
222
+ const target = normalizedSymbolValue(ts, checker, symbol);
223
+ for (const declaration of target?.getDeclarations?.() ?? []) if (declaration.getSourceFile() === sourceFile) exported.add(declaration);
224
+ }
225
+ return exported;
226
+ }
227
+
228
+ function combineClassValue(results) {
229
+ if (results.some(result => result.status === 'unavailable')) return { status: 'unavailable', names: [] };
230
+ const classes = results.filter(result => result.status === 'class');
231
+ if (!classes.length) return { status: 'not-class', names: [] };
232
+ if (classes.length !== results.length) return { status: 'unavailable', names: [] };
233
+ return { status: 'class', names: [...new Set(classes.flatMap(result => result.names))] };
234
+ }
235
+
236
+ function classValueStatus(ts, checker, expression, seen = new Set(), depth = 0) {
237
+ if (!expression) return { status: 'not-class', names: [] };
238
+ if (depth > 10) return { status: 'unavailable', names: [] };
239
+ expression = unwrapExpression(ts, expression);
240
+ if (ts.isClassExpression(expression)) return { status: 'class', names: expression.name ? [expression.name.text] : [] };
241
+ if (ts.isConditionalExpression(expression)) return combineClassValue([
242
+ classValueStatus(ts, checker, expression.whenTrue, seen, depth + 1),
243
+ classValueStatus(ts, checker, expression.whenFalse, seen, depth + 1),
244
+ ]);
245
+ const selected = selectedNode(ts, ts.isCallExpression(expression) ? expression.expression : expression);
246
+ const symbol = selected ? normalizedSymbol(ts, checker, selected) : null;
247
+ if (!symbol || seen.has(symbol)) return { status: 'not-class', names: [] };
248
+ const nextSeen = new Set(seen).add(symbol);
249
+ const results = [];
250
+ for (const declaration of symbol.getDeclarations?.() ?? []) {
251
+ if (ts.isClassDeclaration(declaration)) results.push({ status: 'class', names: declaration.name ? [declaration.name.text] : [] });
252
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
253
+ if (ts.isCallExpression(expression)
254
+ && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) {
255
+ for (const returned of returnedExpressions(ts, declaration.initializer)) {
256
+ results.push(classValueStatus(ts, checker, returned, nextSeen, depth + 1));
257
+ }
258
+ } else results.push(classValueStatus(ts, checker, declaration.initializer, nextSeen, depth + 1));
259
+ }
260
+ if (ts.isCallExpression(expression)) for (const returned of returnedExpressions(ts, declaration)) {
261
+ results.push(classValueStatus(ts, checker, returned, nextSeen, depth + 1));
262
+ }
263
+ }
264
+ return results.length ? combineClassValue(results) : { status: 'not-class', names: [] };
265
+ }
266
+
267
+ function combineObjectStatus(statuses) {
268
+ if (statuses.includes('object')) return 'object';
269
+ if (statuses.includes('unavailable')) return 'unavailable';
270
+ return 'not-object';
271
+ }
272
+
273
+ function objectTypeStatus(ts, checker, node, seen = new Set(), depth = 0) {
274
+ if (!node || depth > 12) return 'unavailable';
275
+ if (ts.isTypeLiteralNode(node) || ts.isMappedTypeNode(node)) return 'object';
276
+ if (ts.isParenthesizedTypeNode(node)) return objectTypeStatus(ts, checker, node.type, seen, depth + 1);
277
+ if (ts.isIntersectionTypeNode(node) || ts.isUnionTypeNode(node)) {
278
+ return combineObjectStatus(node.types.map(type => objectTypeStatus(ts, checker, type, seen, depth + 1)));
279
+ }
280
+ if (ts.isTypeReferenceNode(node)) {
281
+ const symbol = normalizedSymbol(ts, checker, node.typeName);
282
+ if (!symbol || seen.has(symbol)) return 'unavailable';
283
+ const nextSeen = new Set(seen).add(symbol);
284
+ const declarations = symbol.getDeclarations?.() ?? [];
285
+ if (!declarations.length) return 'unavailable';
286
+ return combineObjectStatus(declarations.map(declaration => {
287
+ if (ts.isInterfaceDeclaration(declaration)) return 'object';
288
+ if (ts.isTypeAliasDeclaration(declaration)) return objectTypeStatus(ts, checker, declaration.type, nextSeen, depth + 1);
289
+ if (ts.isTypeParameterDeclaration(declaration)) return 'unavailable';
290
+ if (ts.isClassDeclaration(declaration)) return 'unavailable';
291
+ return 'not-object';
292
+ }));
293
+ }
294
+ if (ts.isConditionalTypeNode(node) || ts.isIndexedAccessTypeNode(node) || ts.isInferTypeNode(node) || ts.isTypeQueryNode(node)) return 'unavailable';
295
+ return 'not-object';
296
+ }
297
+
298
+ function objectContractStatus(ts, checker, node) {
299
+ if (ts.isInterfaceDeclaration(node)) return 'object';
300
+ return ts.isTypeAliasDeclaration(node) ? objectTypeStatus(ts, checker, node.type) : 'not-object';
301
+ }
302
+
303
+ function companionInterfaceAllowed(ts, sourceFile, name) {
304
+ if (!/^I[A-Z]/.test(name)) return false;
305
+ const companion = name.slice(1);
306
+ return sourceFile.statements.some(statement => ts.isClassDeclaration(statement) && statement.name?.text === companion
307
+ && decorators(ts, statement).length > 0);
308
+ }
309
+
310
+ function camelCase(value) {
311
+ return /^[a-z][A-Za-z0-9]*$/.test(value);
312
+ }
313
+
314
+ function staticProperty(ts, object, name) {
315
+ if (!ts.isObjectLiteralExpression(object)) return { status: 'dynamic' };
316
+ const matching = object.properties.filter(property => {
317
+ if (!property.name) return false;
318
+ if (ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name)) return property.name.text === name;
319
+ return false;
320
+ });
321
+ if (!matching.length) return { status: 'missing' };
322
+ if (matching.length !== 1 || !ts.isPropertyAssignment(matching[0])) return { status: 'dynamic', node: matching[0] };
323
+ const initializer = unwrapExpression(ts, matching[0].initializer);
324
+ return ts.isStringLiteralLike(initializer)
325
+ ? { status: 'literal', value: initializer.text, node: initializer }
326
+ : { status: 'dynamic', node: matching[0].initializer };
327
+ }
328
+
329
+ function graphqlField(config, context, sourceFile, node, decorator, kind, namingReasons, violations) {
330
+ const call = decorator.expression;
331
+ if (!context.ts.isCallExpression(call)) {
332
+ namingReasons.push(`${relativePath(config.root, sourceFile.fileName)} has a non-call @${kind} decorator`);
333
+ return;
334
+ }
335
+ const options = call.arguments.find(argument => context.ts.isObjectLiteralExpression(unwrapExpression(context.ts, argument)));
336
+ const selected = options ? staticProperty(context.ts, unwrapExpression(context.ts, options), 'name') : { status: 'missing' };
337
+ if (selected.status === 'dynamic') {
338
+ namingReasons.push(`${relativePath(config.root, sourceFile.fileName)} has a dynamic @${kind} field name`);
339
+ return;
340
+ }
341
+ const methodName = node.name && (context.ts.isIdentifier(node.name) || context.ts.isStringLiteralLike(node.name)) ? node.name.text : null;
342
+ const name = selected.status === 'literal' ? selected.value : methodName;
343
+ if (!name) namingReasons.push(`${relativePath(config.root, sourceFile.fileName)} cannot prove the @${kind} field name`);
344
+ else if (!camelCase(name)) violations.push(violation(config, sourceFile, selected.node ?? node.name ?? node, SOURCE_NAME_RULE_ID,
345
+ `GraphQL field name ${name} must be camelCase.`));
346
+ }
347
+
348
+ function graphqlArgument(config, context, sourceFile, node, decorator, namingReasons, violations) {
349
+ const call = decorator.expression;
350
+ if (!context.ts.isCallExpression(call) || call.arguments.length === 0) {
351
+ namingReasons.push(`${relativePath(config.root, sourceFile.fileName)} cannot prove the @Args input name`);
352
+ return;
353
+ }
354
+ const selected = unwrapExpression(context.ts, call.arguments[0]);
355
+ if (!context.ts.isStringLiteralLike(selected)) {
356
+ namingReasons.push(`${relativePath(config.root, sourceFile.fileName)} has a dynamic @Args input name`);
357
+ } else if (selected.text !== 'input') violations.push(violation(config, sourceFile, selected, SOURCE_NAME_RULE_ID,
358
+ 'GraphQL request arguments use the literal name input.'));
359
+ }
360
+
361
+ function graphqlEnumRegistration(config, context, sourceFile, call, namingReasons, violations) {
362
+ if (call.arguments.length < 2) {
363
+ namingReasons.push(`${relativePath(config.root, sourceFile.fileName)} cannot prove the registered GraphQL enum name`);
364
+ return;
365
+ }
366
+ const selected = staticProperty(context.ts, unwrapExpression(context.ts, call.arguments[1]), 'name');
367
+ if (selected.status !== 'literal') namingReasons.push(`${relativePath(config.root, sourceFile.fileName)} has a dynamic registered GraphQL enum name`);
368
+ else if (!/^[A-Z][A-Za-z0-9]*$/.test(selected.value)) violations.push(violation(config, sourceFile, selected.node, SOURCE_NAME_RULE_ID,
369
+ `Registered GraphQL enum name ${selected.value} must be PascalCase.`));
370
+ }
371
+
372
+ function selectedCallKind(ts, checker, call, targets) {
373
+ const selected = selectedNode(ts, call.expression);
374
+ return selected ? targets.get(valueSymbol(ts, checker, selected)) ?? null : null;
375
+ }
376
+
377
+ function callsNamedHelper(ts, checker, call, name) {
378
+ const selected = selectedNode(ts, call.expression);
379
+ const symbol = selected ? normalizedSymbol(ts, checker, selected) : null;
380
+ return (symbol?.getDeclarations?.() ?? []).some(declaration => {
381
+ const declarationName = declaration.name;
382
+ return declarationName && (ts.isIdentifier(declarationName) || ts.isStringLiteralLike(declarationName)) && declarationName.text === name;
383
+ });
384
+ }
385
+
386
+ function implementsFramework(ts, checker, declaration, targets, name) {
387
+ return (declaration.heritageClauses ?? []).some(clause => clause.token === ts.SyntaxKind.ImplementsKeyword
388
+ && clause.types.some(type => targets.get(valueSymbol(ts, checker, type.expression)) === name));
389
+ }
390
+
391
+ /** Names of the interfaces a class implements that are declared in a *.port.ts or *.contracts.ts file (resolved by the type checker). */
392
+ function implementedPortNames(ts, checker, declaration) {
393
+ const names = [];
394
+ for (const clause of declaration.heritageClauses ?? []) if (clause.token === ts.SyntaxKind.ImplementsKeyword) {
395
+ for (const type of clause.types) {
396
+ const symbol = normalizedSymbol(ts, checker, type.expression);
397
+ for (const target of symbol?.getDeclarations?.() ?? []) {
398
+ if (!ts.isInterfaceDeclaration(target) && !ts.isTypeAliasDeclaration(target)) continue;
399
+ const declaredRole = path.basename(target.getSourceFile().fileName).replace(SOURCE_EXTENSION, '').split('.').at(-1);
400
+ if (PORT_DECLARATION_ROLES.has(declaredRole)) names.push(target.name.text);
401
+ }
402
+ }
403
+ }
404
+ return names;
405
+ }
406
+
407
+ function locatedRoot(roots, fileName) {
408
+ return roots.find(root => isInside(root, fileName)) ?? null;
409
+ }
410
+
411
+ /** Check the adopted domain-first backend source layout without inferring semantic ownership from folder names alone. */
412
+ export function checkBackendSourceShape(config, context) {
413
+ const ts = context.ts;
414
+ const featureRoots = absoluteRoots(config.root, config.backend.features);
415
+ const moduleRoots = absoluteRoots(config.root, config.backend.modules);
416
+ const localFiles = new Set(context.files.map(file => canonical(file.fileName)));
417
+ const frameworkByChecker = new Map();
418
+ const violations = [];
419
+ const layoutReasons = [];
420
+ const namingReasons = [];
421
+ let checkedFiles = 0;
422
+
423
+ for (const sourceFile of context.files) {
424
+ const fileName = canonical(sourceFile.fileName);
425
+ const featureRoot = locatedRoot(featureRoots, fileName);
426
+ const moduleRoot = locatedRoot(moduleRoots, fileName);
427
+ if (!featureRoot && !moduleRoot) continue;
428
+ checkedFiles += 1;
429
+ const checker = context.checkerFor(fileName);
430
+ if (!frameworkByChecker.has(checker)) frameworkByChecker.set(checker, frameworkTargets(config, context, checker, localFiles));
431
+ const framework = frameworkByChecker.get(checker);
432
+ const relativeToOwner = relativePath(featureRoot ?? moduleRoot, fileName);
433
+ const parts = relativeToOwner.split('/');
434
+ const directories = parts.slice(0, -1).map(part => part.toLowerCase());
435
+ const applicationIndex = directories.indexOf('application');
436
+ const transportIndex = directories.indexOf('transport');
437
+ const inApplication = applicationIndex >= 0;
438
+ const inTransport = transportIndex >= 0;
439
+ const role = sourceRole(ts, sourceFile);
440
+ const relative = relativePath(config.root, fileName);
441
+ const migrationName = /^\d{10,}-[A-Z][A-Za-z0-9]*$/.test(role.base);
442
+ const persistencePath = !role.spec && (migrationName || directories.includes('migrations'));
443
+ const special = SPECIAL_BASENAMES.has(role.base.toLowerCase()) || migrationName
444
+ || ['constants', 'enums', 'errors', 'migrations'].some(folder => directories.includes(folder));
445
+ const publicDeclarations = exportedDeclarations(ts, checker, sourceFile);
446
+
447
+ if (!migrationName && !kebabSourceBase(role.base)) violations.push(violation(config, sourceFile, sourceFile, SOURCE_NAME_RULE_ID,
448
+ `Source basename ${role.base} must use kebab-case segments plus an explicit role suffix.`));
449
+ if (!role.role && !special) namingReasons.push(`${relative} has no statically identifiable source role`);
450
+
451
+ if (featureRoot) {
452
+ if (inApplication && inTransport) violations.push(violation(config, sourceFile, sourceFile, SOURCE_LAYOUT_RULE_ID,
453
+ 'A feature source cannot belong to both application and transport layers.'));
454
+ if (role.role && APPLICATION_ROLES.has(role.role) && !inApplication) violations.push(violation(config, sourceFile, sourceFile,
455
+ SOURCE_LAYOUT_RULE_ID, `${role.role} source belongs under the feature application/ layer.`));
456
+ if (role.role && TRANSPORT_ROLES.has(role.role) && !inTransport) violations.push(violation(config, sourceFile, sourceFile,
457
+ SOURCE_LAYOUT_RULE_ID, `${role.role} source belongs under the feature transport/<protocol>/ layer.`));
458
+ if (inApplication && role.role && TRANSPORT_ROLES.has(role.role)) violations.push(violation(config, sourceFile, sourceFile,
459
+ SOURCE_LAYOUT_RULE_ID, `Transport role ${role.role} cannot live in application/.`));
460
+ if (inTransport && role.role && APPLICATION_ROLES.has(role.role)) violations.push(violation(config, sourceFile, sourceFile,
461
+ SOURCE_LAYOUT_RULE_ID, `Application role ${role.role} cannot live in transport/.`));
462
+ if (inTransport && transportIndex >= directories.length - 1) violations.push(violation(config, sourceFile, sourceFile,
463
+ SOURCE_LAYOUT_RULE_ID, 'Feature transport source must name a protocol below transport/.'));
464
+ const topLevelFeatureFile = directories.length === 1 && (role.base === 'index' || role.role === 'module' || role.spec);
465
+ if (inApplication && role.base !== 'index' && (!role.role || !APPLICATION_LAYER_ROLES.has(role.role))
466
+ && !(role.role && (TRANSPORT_ROLES.has(role.role) || role.role === 'entity'))) {
467
+ layoutReasons.push(`${relative} role ${role.role ?? '(unclassified)'} is not a selected application-layer role`);
468
+ }
469
+ if (inTransport && role.base !== 'index' && (!role.role || !TRANSPORT_LAYER_ROLES.has(role.role))
470
+ && !(role.role && (APPLICATION_ROLES.has(role.role) || role.role === 'entity'))) {
471
+ layoutReasons.push(`${relative} role ${role.role ?? '(unclassified)'} is not a selected transport-layer role`);
472
+ }
473
+ if (!inApplication && !inTransport && !topLevelFeatureFile && role.role && role.role !== 'entity'
474
+ && !APPLICATION_ROLES.has(role.role) && !TRANSPORT_ROLES.has(role.role)) {
475
+ layoutReasons.push(`${relative} role ${role.role} has no statically selected feature layer`);
476
+ }
477
+ if (!inApplication && !inTransport && !topLevelFeatureFile && !role.role && !persistencePath) {
478
+ layoutReasons.push(`${relative} has no statically selected feature layer`);
479
+ }
480
+ if (persistencePath) violations.push(violation(config, sourceFile, sourceFile, SOURCE_LAYOUT_RULE_ID,
481
+ 'Migration source cannot be owned by a feature; place it under the declared persistence module.'));
482
+ if (role.role === 'entity') violations.push(violation(config, sourceFile, sourceFile, SOURCE_LAYOUT_RULE_ID,
483
+ 'Entity source cannot be owned by a feature; place schema under the declared persistence module.'));
484
+ }
485
+
486
+ const classDeclarations = sourceFile.statements.filter(statement => ts.isClassDeclaration(statement));
487
+ for (const declaration of classDeclarations) {
488
+ const declarationDecorators = decorators(ts, declaration);
489
+ const kinds = declarationDecorators.map(decorator => decoratorKind(ts, checker, decorator, framework.bySymbol)).filter(Boolean);
490
+ const graphQlDto = kinds.some(kind => kind === 'ArgsType' || kind === 'InputType' || kind === 'ObjectType');
491
+ if (graphQlDto && (!featureRoot || !inTransport || directories[transportIndex + 1] !== 'graphql')) {
492
+ violations.push(violation(config, sourceFile, declaration.name ?? declaration, SOURCE_LAYOUT_RULE_ID,
493
+ 'GraphQL DTO classes belong under a feature transport/graphql/ boundary.'));
494
+ }
495
+ const persistenceSchema = kinds.includes('Entity') || kinds.includes('ViewEntity')
496
+ || implementsFramework(ts, checker, declaration, framework.bySymbol, 'MigrationInterface');
497
+ if (persistenceSchema && featureRoot) violations.push(violation(config, sourceFile, declaration.name ?? declaration, SOURCE_LAYOUT_RULE_ID,
498
+ 'TypeORM entities and migrations cannot be owned by a feature; place schema under its declared persistence module.'));
499
+ const classSuffix = role.role && !NON_CLASS_ROLES.has(role.role) ? CLASS_ROLE_SUFFIX.get(role.role) : null;
500
+ if (publicDeclarations.has(declaration) && !declaration.name) namingReasons.push(`${relative} exports an anonymous class whose role name cannot be proved`);
501
+ else if (publicDeclarations.has(declaration) && declaration.name && classSuffix && !declaration.name.text.endsWith(classSuffix)
502
+ && !implementedPortNames(ts, checker, declaration).some(port => declaration.name.text.endsWith(port))) {
503
+ violations.push(violation(config, sourceFile, declaration.name, SOURCE_NAME_RULE_ID,
504
+ `Exported class ${declaration.name.text} must end in ${classSuffix} for a ${role.role} source file, or in the name of the port it implements.`));
505
+ } else if (publicDeclarations.has(declaration) && declaration.name && role.role && !NON_CLASS_ROLES.has(role.role) && !classSuffix) {
506
+ namingReasons.push(`${relative} declares exported class ${declaration.name.text} with unclassified file role ${role.role}`);
507
+ }
508
+ }
509
+
510
+ for (const statement of sourceFile.statements) if (ts.isVariableStatement(statement)) for (const declaration of statement.declarationList.declarations) {
511
+ if (!publicDeclarations.has(declaration) || !declaration.initializer) continue;
512
+ const classValue = classValueStatus(ts, checker, declaration.initializer);
513
+ if (classValue.status === 'unavailable') {
514
+ namingReasons.push(`${relative} exports a class-valued declaration whose role identity is not statically proved`);
515
+ continue;
516
+ }
517
+ if (classValue.status !== 'class') continue;
518
+ const classSuffix = role.role && !NON_CLASS_ROLES.has(role.role) ? CLASS_ROLE_SUFFIX.get(role.role) : null;
519
+ if (!classSuffix || !ts.isIdentifier(declaration.name)) {
520
+ namingReasons.push(`${relative} exports a class-valued declaration without a statically selected class role`);
521
+ continue;
522
+ }
523
+ const names = [declaration.name.text, ...classValue.names];
524
+ const invalid = names.find(name => !name.endsWith(classSuffix));
525
+ if (invalid) violations.push(violation(config, sourceFile, declaration.name, SOURCE_NAME_RULE_ID,
526
+ `Exported class value ${invalid} must end in ${classSuffix} for a ${role.role} source file.`));
527
+ }
528
+
529
+ for (const statement of sourceFile.statements) {
530
+ if (ts.isEnumDeclaration(statement)) {
531
+ if (hasModifier(ts, statement, ts.SyntaxKind.ConstKeyword) || !/^[A-Z][A-Za-z0-9]*$/.test(statement.name.text)) {
532
+ violations.push(violation(config, sourceFile, statement.name, SOURCE_NAME_RULE_ID,
533
+ 'Enums must be non-const declarations with PascalCase names.'));
534
+ }
535
+ for (const member of statement.members) {
536
+ const name = ts.isIdentifier(member.name) || ts.isStringLiteralLike(member.name) ? member.name.text : null;
537
+ const initializer = member.initializer && unwrapExpression(ts, member.initializer);
538
+ if (!name || !/^[A-Z][A-Za-z0-9]*$/.test(name) || !initializer || !ts.isStringLiteralLike(initializer)) {
539
+ violations.push(violation(config, sourceFile, member.name, SOURCE_NAME_RULE_ID,
540
+ 'Enum members must use PascalCase names with explicit static string values.'));
541
+ }
542
+ }
543
+ }
544
+ const contractStatus = objectContractStatus(ts, checker, statement);
545
+ const transportSuffix = TRANSPORT_OBJECT_SUFFIX.get(role.role);
546
+ if (contractStatus === 'object' && publicDeclarations.has(statement) && transportSuffix && !role.spec
547
+ && !statement.name.text.endsWith(transportSuffix) && !companionInterfaceAllowed(ts, sourceFile, statement.name.text)) {
548
+ violations.push(violation(config, sourceFile, statement.name, SOURCE_NAME_RULE_ID,
549
+ `Public object contract ${statement.name.text} in a ${role.role} source file must end in ${transportSuffix}.`));
550
+ }
551
+ }
552
+
553
+ const visit = node => {
554
+ for (const decorator of decorators(ts, node)) {
555
+ const kind = decoratorKind(ts, checker, decorator, framework.bySymbol);
556
+ if (!kind) {
557
+ const dynamic = mutableDecoratorKind(ts, checker, decorator, framework.bySymbol)
558
+ ?? constructedDecoratorKind(ts, checker, decorator, framework.bySymbol);
559
+ if (dynamic) {
560
+ const detail = `${relative} uses a mutable or constructed ${dynamic} decorator identity`;
561
+ if (['Entity', 'ViewEntity', 'ArgsType', 'InputType', 'ObjectType', 'multiple framework', 'unproven framework'].includes(dynamic)) layoutReasons.push(detail);
562
+ if (!['Entity', 'ViewEntity'].includes(dynamic)) namingReasons.push(detail);
563
+ }
564
+ }
565
+ if (kind === 'Mutation' || kind === 'Query') graphqlField(config, context, sourceFile, node, decorator, kind, namingReasons, violations);
566
+ if (kind === 'Args') graphqlArgument(config, context, sourceFile, node, decorator, namingReasons, violations);
567
+ }
568
+ if (ts.isCallExpression(node)) {
569
+ if (selectedCallKind(ts, checker, node, framework.bySymbol) === 'registerEnumType') {
570
+ graphqlEnumRegistration(config, context, sourceFile, node, namingReasons, violations);
571
+ } else if (callsNamedHelper(ts, checker, node, 'createEnumType')) {
572
+ namingReasons.push(`${relative} calls a project GraphQL enum adapter whose emitted type name is not statically proved`);
573
+ }
574
+ }
575
+ const exportedFactoryCall = ts.isCallExpression(node) && ts.isVariableDeclaration(node.parent) && publicDeclarations.has(node.parent);
576
+ if ((ts.isNewExpression(node) || exportedFactoryCall) && featureRoot) {
577
+ const constructed = tracedFrameworkKinds(ts, checker, node, framework.bySymbol);
578
+ if (constructed.has('EntitySchema')) violations.push(violation(config, sourceFile, node.expression, SOURCE_LAYOUT_RULE_ID,
579
+ 'TypeORM EntitySchema cannot be owned by a feature; place schema under its declared persistence module.'));
580
+ if (constructed.has(UNPROVEN_FRAMEWORK)) layoutReasons.push(`${relative} has a constructed provider identity beyond the bounded static trace`);
581
+ }
582
+ ts.forEachChild(node, visit);
583
+ };
584
+ visit(sourceFile);
585
+ }
586
+
587
+ for (const framework of frameworkByChecker.values()) {
588
+ layoutReasons.push(...framework.reasons);
589
+ namingReasons.push(...framework.reasons.filter(reason => reason.includes('@nestjs/graphql')));
590
+ }
591
+ if (checkedFiles === 0) {
592
+ layoutReasons.push('the checked TypeScript program contains no configured backend feature or module source');
593
+ namingReasons.push('the checked TypeScript program contains no configured backend feature or module source');
594
+ }
595
+ const coverage = {
596
+ files: checkedFiles,
597
+ layout: layoutReasons.length
598
+ ? { status: 'unavailable', reason: 'one or more backend source placement relations are not statically proved', details: [...new Set(layoutReasons)].sort() }
599
+ : { status: 'checked' },
600
+ naming: namingReasons.length
601
+ ? { status: 'unavailable', reason: 'one or more backend source naming relations are not statically proved', details: [...new Set(namingReasons)].sort() }
602
+ : { status: 'checked' },
603
+ };
604
+ return { violations, coverage };
605
+ }
606
+
607
+ export { SOURCE_LAYOUT_RULE_ID, SOURCE_NAME_RULE_ID };