@starci/hfs 1.0.1 → 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 +46 -0
  2. package/README.md +110 -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 +4 -1
  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 +405 -137
  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 +88 -40
  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,138 @@
1
+ import path from 'node:path';
2
+ import { canonical } from './config.mjs';
3
+ import { sourceLocation } from './typescript.mjs';
4
+
5
+ /**
6
+ * The small TypeScript reading kit the R33/R38/R39/R41/R45/R84/R86 machine checks share (connection-map, sql-owner,
7
+ * register-once, error-masked, default-deny-app-guard, entrypoint-only-in-apps, error-home). Everything is decided by
8
+ * the checker and by where a declaration lives (the HFS graph), never by a variable name: a framework symbol is
9
+ * recognised by the package it is imported from, a capability class by the file that declares it.
10
+ */
11
+ export function machineKit({ config, context, graph }) {
12
+ const ts = context.ts;
13
+ const resolver = graph.resolver;
14
+
15
+ const checkerOf = sourceFile => context.checkerFor(sourceFile.fileName);
16
+
17
+ const aliased = (checker, symbol) => {
18
+ const seen = new Set();
19
+ let current = symbol;
20
+ while (current && (current.flags & ts.SymbolFlags.Alias) && !seen.has(current)) {
21
+ seen.add(current);
22
+ let target;
23
+ try { target = checker.getAliasedSymbol(current); } catch { break; }
24
+ if (!target || target === current || !(target.declarations?.length)) break;
25
+ current = target;
26
+ }
27
+ return current;
28
+ };
29
+
30
+ const symbolAt = (checker, node) => {
31
+ if (!node) return null;
32
+ if (ts.isShorthandPropertyAssignment(node.parent ?? {}) && node.parent.name === node) return checker.getShorthandAssignmentValueSymbol(node.parent) ?? null;
33
+ return checker.getSymbolAtLocation(node) ?? null;
34
+ };
35
+
36
+ /** {name, module} when `node` (identifier, or namespace.member) is bound by an import from a module specifier, else null. */
37
+ const importBinding = (checker, node) => {
38
+ const own = target => {
39
+ const symbol = symbolAt(checker, target);
40
+ const declaration = symbol?.declarations?.[0];
41
+ if (!declaration) return null;
42
+ if (ts.isImportSpecifier(declaration)) return { name: (declaration.propertyName ?? declaration.name).text, module: declaration.parent.parent.parent.moduleSpecifier.text };
43
+ if (ts.isImportClause(declaration)) return { name: 'default', module: declaration.parent.moduleSpecifier.text };
44
+ if (ts.isNamespaceImport(declaration)) return { name: '*', module: declaration.parent.parent.moduleSpecifier.text };
45
+ return null;
46
+ };
47
+ if (ts.isIdentifier(node)) return own(node);
48
+ if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression)) {
49
+ const namespace = own(node.expression);
50
+ return namespace?.name === '*' ? { name: node.name.text, module: namespace.module } : null;
51
+ }
52
+ return null;
53
+ };
54
+
55
+ const isImportOf = (checker, node, name, moduleName) => {
56
+ const binding = importBinding(checker, node);
57
+ return Boolean(binding) && binding.name === name && binding.module === moduleName;
58
+ };
59
+
60
+ /** The declarations `node` resolves to through aliases. */
61
+ const declarationsOf = (checker, node) => aliased(checker, symbolAt(checker, node))?.declarations ?? [];
62
+
63
+ /** The repository-relative graph path of the file declaring `declaration`, or null when it is outside the program. */
64
+ const graphPath = declaration => graph.abs(canonical(declaration.getSourceFile().fileName));
65
+
66
+ const graphFile = rel => graph.files.get(rel) ?? null;
67
+
68
+ /** The capability (owner) root, slot and tier of a declaration, or null. */
69
+ const ownerOfDeclaration = declaration => {
70
+ const rel = graphPath(declaration);
71
+ const file = rel ? graphFile(rel) : null;
72
+ return file?.owner ? { rel, root: file.owner.root, slot: file.owner.slot, tier: file.tier, name: path.posix.basename(file.owner.root) } : null;
73
+ };
74
+
75
+ /** The string a node evaluates to: a literal, or a const whose type is a string literal. */
76
+ const stringValue = (checker, node) => {
77
+ if (!node) return null;
78
+ if (ts.isStringLiteralLike(node)) return node.text;
79
+ let type;
80
+ try { type = checker.getTypeAtLocation(node); } catch { return null; }
81
+ if (type?.isStringLiteral?.()) return type.value;
82
+ const declaration = declarationsOf(checker, ts.isPropertyAccessExpression(node) ? node.name : node)[0];
83
+ if (declaration && ts.isVariableDeclaration(declaration) && declaration.initializer) return stringValue(checker, declaration.initializer);
84
+ return null;
85
+ };
86
+
87
+ const walk = (node, visit) => {
88
+ const step = child => {
89
+ if (visit(child) === false) return;
90
+ ts.forEachChild(child, step);
91
+ };
92
+ step(node);
93
+ };
94
+
95
+ const decorators = node => (ts.canHaveDecorators?.(node) ? ts.getDecorators(node) ?? [] : []);
96
+ const isExported = node => Boolean(ts.getCombinedModifierFlags?.(node) & ts.ModifierFlags.Export);
97
+
98
+ const propertyNameText = name => {
99
+ if (!name) return null;
100
+ if (ts.isIdentifier(name) || ts.isStringLiteralLike(name)) return name.text;
101
+ return null;
102
+ };
103
+
104
+ /** The object literal properties of `literal` by static name. */
105
+ const propertyOf = (literal, key) => literal.properties.find(property => (ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) && propertyNameText(property.name) === key);
106
+
107
+ const valueOfProperty = property => (ts.isPropertyAssignment(property) ? property.initializer : property.name);
108
+
109
+ const at = (rel, sourceFile, node, extra = {}) => ({ path: rel, ...sourceLocation(sourceFile, node), ...extra });
110
+
111
+ /** The app root file of an app (apps/<name>/src/app.module.ts), or null when the program does not hold it. */
112
+ const appRoot = name => {
113
+ const rel = `apps/${name}/src/app.module.ts`;
114
+ return graphFile(rel);
115
+ };
116
+
117
+ /** The `{ provide: <token>, ... }` provider literals of `file` whose token is `name` imported from `moduleName`, in source order. */
118
+ const providersOf = (file, name, moduleName) => {
119
+ const checker = checkerOf(file.sourceFile);
120
+ const found = [];
121
+ walk(file.sourceFile, node => {
122
+ if (!ts.isObjectLiteralExpression(node)) return true;
123
+ const provide = propertyOf(node, 'provide');
124
+ if (provide && isImportOf(checker, valueOfProperty(provide), name, moduleName)) {
125
+ const use = propertyOf(node, 'useClass');
126
+ found.push({ node, useClass: use ? valueOfProperty(use) : null, checker });
127
+ }
128
+ return true;
129
+ });
130
+ return found.sort((a, b) => a.node.getStart() - b.node.getStart());
131
+ };
132
+
133
+ return { ts, resolver, providersOf, checkerOf, aliased, symbolAt, importBinding, isImportOf, declarationsOf, graphPath, graphFile, ownerOfDeclaration,
134
+ stringValue, walk, decorators, isExported, propertyNameText, propertyOf, valueOfProperty, at, appRoot };
135
+ }
136
+
137
+ export const upperSnake = name => name.replace(/-/g, '_').toUpperCase();
138
+ export const pascal = name => name.split('-').filter(Boolean).map(part => part[0].toUpperCase() + part.slice(1)).join('');
@@ -0,0 +1,130 @@
1
+ import path from 'node:path';
2
+ import { machineKit } from './machine-ast.mjs';
3
+ import { treeOf } from './required-files.mjs';
4
+
5
+ /**
6
+ * R45 `module-per-transport` (BE_MODULE_SHAPE, BE-CONVENTION 1.2 and 1.6). A feature has one Nest module for its
7
+ * application (`<f>.module.ts` at the feature root) and exactly one module per transport folder
8
+ * (`transport/<protocol>/<f>-<protocol>.module.ts`). Never one module per operation, never a module-definition in a feature:
9
+ *
10
+ * - a `*.module.ts` file below a transport folder other than `<f>-<protocol>.module.ts`, a `*.module-definition.ts`
11
+ * anywhere in a feature, and an `@Module` class (or a `ConfigurableModuleBuilder`/`ConfigurableModuleClass` use) in a
12
+ * feature file that is neither the application module nor the transport module of its own folder are refused;
13
+ * - apps import only transport modules, of the kinds their app kind allows: the `composedBy` list of each transport slot
14
+ * in knowledge/hfs/slots.yaml (api: graphql, http, websocket; worker: schedule, message; cli: cli). An app root that
15
+ * lists a feature's application module, or the transport module of another kind, is refused.
16
+ */
17
+ export const MODULE_PER_TRANSPORT_RULE_IDS = ['BE_MODULE_SHAPE'];
18
+
19
+ const RULE = 'BE_MODULE_SHAPE';
20
+ const APP_ROOT_FILE = 'app.module.ts';
21
+
22
+ export function checkModulePerTransport(input) {
23
+ const { config, graph } = input;
24
+ const kit = machineKit(input);
25
+ const { ts, resolver } = kit;
26
+ const violations = [];
27
+ const report = (file, node, message, extra = {}) => violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
28
+ const plain = (rel, message, extra = {}) => violations.push({ ruleId: RULE, path: rel, line: 1, column: 1, message, ...extra });
29
+
30
+ const featureOf = rel => {
31
+ const owner = resolver.ownerOf(rel);
32
+ return owner && resolver.slot(owner.slot)?.tier === 'feature' ? owner : null;
33
+ };
34
+ /** {feature, root, protocol} of a file inside a feature: protocol is the transport folder, null for the application side. */
35
+ const placeOf = rel => {
36
+ const owner = featureOf(rel);
37
+ if (!owner) return null;
38
+ const below = rel.slice(owner.root.length + 1).split('/');
39
+ return { root: owner.root, feature: path.posix.basename(owner.root), protocol: below[0] === 'transport' && below.length > 2 ? below[1] : null, below };
40
+ };
41
+
42
+ // The transport slots and the app kinds that compose them, by protocol folder name.
43
+ const composedBy = new Map();
44
+ for (const slot of resolver.slots()) {
45
+ if (!slot.composedBy) continue;
46
+ composedBy.set(path.posix.basename(slot.path.replace(/\/+$/u, '')), slot.composedBy);
47
+ }
48
+
49
+ let transportModules = 0;
50
+ const tree = treeOf(config.root);
51
+ for (const rel of [...tree.files].sort()) {
52
+ const place = placeOf(rel);
53
+ if (!place) continue;
54
+ const base = path.posix.basename(rel);
55
+ if (/\.module-definition\.[cm]?tsx?$/u.test(base)) plain(rel, `${rel} is a module-definition inside feature ${place.feature}; a feature module is a static \`@Module\`, and only capabilities under src/modules build their module from a definition. Delete it and list the providers in the module.`, { feature: place.feature });
56
+ if (place.protocol && /\.module\.[cm]?tsx?$/u.test(base)) {
57
+ const expected = `${place.feature}-${place.protocol}.module.ts`;
58
+ if (place.below.length !== 3 || base !== expected) plain(rel, `${rel} is a second module of the ${place.protocol} transport of ${place.feature}; a transport folder has exactly one module, transport/${place.protocol}/${expected}, never one module per operation. List the operation's providers in that module and delete this file.`, { feature: place.feature, protocol: place.protocol });
59
+ else transportModules += 1;
60
+ }
61
+ }
62
+
63
+ // @Module classes of the program: where they live, and the feature classes among them.
64
+ const modules = new Map(); // class declaration -> {name, file}
65
+ for (const file of graph.files.values()) {
66
+ const checker = kit.checkerOf(file.sourceFile);
67
+ const place = placeOf(file.rel);
68
+ for (const statement of file.sourceFile.statements) {
69
+ if (!ts.isClassDeclaration(statement) || !statement.name) continue;
70
+ const isModule = kit.decorators(statement).some(decorator => {
71
+ const call = ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression;
72
+ return kit.isImportOf(checker, call, 'Module', '@nestjs/common');
73
+ });
74
+ if (!isModule) continue;
75
+ modules.set(statement, { name: statement.name.text, file });
76
+ if (!place) continue;
77
+ const own = place.protocol === null
78
+ ? `${place.root}/${place.feature}.module.ts`
79
+ : `${place.root}/transport/${place.protocol}/${place.feature}-${place.protocol}.module.ts`;
80
+ if (file.rel !== own) report(file, statement.name, `${statement.name.text} is a Nest module declared in ${file.rel}; a feature declares exactly its application module (${place.feature}.module.ts) and one module per transport folder (${place.feature}-<protocol>.module.ts), never a module per operation.`, { feature: place.feature });
81
+ }
82
+ if (place) {
83
+ kit.walk(file.sourceFile, node => {
84
+ if (ts.isNewExpression(node) && kit.isImportOf(checker, node.expression, 'ConfigurableModuleBuilder', '@nestjs/common')) {
85
+ report(file, node, `ConfigurableModuleBuilder builds a capability's module definition; feature ${place.feature} has static modules only, so declare the providers in its @Module.`, { feature: place.feature });
86
+ } else if (ts.isHeritageClause(node)) {
87
+ for (const type of node.types) if (kit.isImportOf(checker, type.expression, 'ConfigurableModuleClass', '@nestjs/common')) {
88
+ report(file, type, `A feature module never extends ConfigurableModuleClass; feature ${place.feature} has static modules only.`, { feature: place.feature });
89
+ }
90
+ }
91
+ return true;
92
+ });
93
+ }
94
+ }
95
+
96
+ // What each app root lists: a feature module must be a transport module of a kind the app kind composes.
97
+ let references = 0;
98
+ for (const app of resolver.repo.apps) {
99
+ const root = graph.files.get(`apps/${app.name}/src/${APP_ROOT_FILE}`);
100
+ if (!root) continue;
101
+ const checker = kit.checkerOf(root.sourceFile);
102
+ const seen = new Set();
103
+ const judge = node => {
104
+ const call = ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'register';
105
+ if (ts.isCallExpression(node) && !call) return;
106
+ const target = call ? node.expression.expression : node;
107
+ for (const declaration of kit.declarationsOf(checker, target)) {
108
+ const module = modules.get(declaration);
109
+ if (!module) continue;
110
+ const place = placeOf(module.file.rel);
111
+ if (!place || seen.has(node)) continue;
112
+ seen.add(node);
113
+ references += 1;
114
+ if (place.protocol === null) {
115
+ report(root, node, `App ${app.name} lists ${module.name}, the application module of feature ${place.feature}; apps import only transport modules (${place.feature}-<protocol>.module.ts), which import the application module themselves.`, { app: app.name, module: module.name });
116
+ } else if (!(composedBy.get(place.protocol) ?? []).includes(app.kind)) {
117
+ const allowed = [...composedBy].filter(([, kinds]) => kinds.includes(app.kind)).map(([protocol]) => protocol).sort();
118
+ report(root, node, `App ${app.name} is of kind ${app.kind} and lists ${module.name}, a ${place.protocol} transport module; an app of kind ${app.kind} composes ${allowed.length ? `${allowed.join(', ')} transports only` : 'no transport module'}.`, { app: app.name, module: module.name, protocol: place.protocol });
119
+ }
120
+ }
121
+ };
122
+ kit.walk(root.sourceFile, node => {
123
+ if (ts.isPropertyAssignment(node) && kit.propertyNameText(node.name) === 'imports' && ts.isArrayLiteralExpression(node.initializer)) {
124
+ for (const element of node.initializer.elements) judge(element);
125
+ } else if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'register') judge(node);
126
+ return true;
127
+ });
128
+ }
129
+ return { violations, coverage: { status: 'checked', modules: modules.size, transportModules, appReferences: references } };
130
+ }