@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,319 @@
1
+ import path from 'node:path';
2
+ import { canonical } from './config.mjs';
3
+ import { isUnshadowedCommonJsRequire, referencedExports, relativePath, sourceLocation, unwrapExpression } from './typescript.mjs';
4
+
5
+ const REGISTRATION_RULE_IDS = ['BE_MODULE_HANDLER_REGISTRATION', 'BE_MODULE_PROVIDER_REREGISTRATION'];
6
+ const FRAMEWORK = new Map([
7
+ ['Module', '@nestjs/common'],
8
+ ['CommandHandler', '@nestjs/cqrs'],
9
+ ['QueryHandler', '@nestjs/cqrs'],
10
+ ]);
11
+
12
+ function normalizedSymbolValue(ts, checker, value) {
13
+ let symbol = value ?? null;
14
+ const seen = new Set();
15
+ while (symbol && (symbol.flags & ts.SymbolFlags.Alias) && !seen.has(symbol)) {
16
+ seen.add(symbol);
17
+ const target = checker.getAliasedSymbol(symbol);
18
+ if (!target || target === symbol) break;
19
+ symbol = target;
20
+ }
21
+ return symbol;
22
+ }
23
+
24
+ function normalizedSymbol(ts, checker, node) {
25
+ return normalizedSymbolValue(ts, checker, checker?.getSymbolAtLocation(node) ?? null);
26
+ }
27
+
28
+ function valueSymbol(ts, checker, node, seen = new Set()) {
29
+ const selected = selectedNode(ts, node) ?? node;
30
+ const symbol = normalizedSymbol(ts, checker, selected);
31
+ if (!symbol || seen.has(symbol)) return symbol;
32
+ seen.add(symbol);
33
+ const declarations = symbol.getDeclarations?.() ?? [];
34
+ if (declarations.length === 1 && ts.isVariableDeclaration(declarations[0]) && declarations[0].initializer
35
+ && (ts.getCombinedNodeFlags(declarations[0].parent) & ts.NodeFlags.Const)) {
36
+ return valueSymbol(ts, checker, declarations[0].initializer, seen);
37
+ }
38
+ return symbol;
39
+ }
40
+
41
+ function calledExpression(ts, decorator) {
42
+ const expression = decorator.expression;
43
+ return ts.isCallExpression(expression) ? expression.expression : expression;
44
+ }
45
+
46
+ function selectedNode(ts, expression) {
47
+ expression = unwrapExpression(ts, expression);
48
+ if (ts.isIdentifier(expression)) return expression;
49
+ if (ts.isPropertyAccessExpression(expression)) return expression.name;
50
+ if (ts.isElementAccessExpression(expression) && ts.isStringLiteralLike(expression.argumentExpression)) return expression.argumentExpression;
51
+ return null;
52
+ }
53
+
54
+ function frameworkDecorator(ts, checker, decorator, targets) {
55
+ const selected = selectedNode(ts, calledExpression(ts, decorator));
56
+ if (!selected) return null;
57
+ const symbol = valueSymbol(ts, checker, selected);
58
+ for (const [name, target] of targets) if (symbol === target) return name;
59
+ return null;
60
+ }
61
+
62
+ function mutableFrameworkAlias(ts, checker, decorator, targets) {
63
+ const selected = selectedNode(ts, calledExpression(ts, decorator));
64
+ const symbol = selected ? normalizedSymbol(ts, checker, selected) : null;
65
+ const declarations = symbol?.getDeclarations?.() ?? [];
66
+ if (declarations.length !== 1 || !ts.isVariableDeclaration(declarations[0]) || !declarations[0].initializer
67
+ || (ts.getCombinedNodeFlags(declarations[0].parent) & ts.NodeFlags.Const)) return null;
68
+ const target = valueSymbol(ts, checker, declarations[0].initializer);
69
+ for (const [name, expected] of targets) if (target === expected) return name;
70
+ return null;
71
+ }
72
+
73
+ function frameworkTargets(config, context, checker, localFiles) {
74
+ const targets = new Map();
75
+ const reasons = [];
76
+ const program = context.programs.find(candidate => candidate.getTypeChecker() === checker);
77
+ if (!program) return { targets, reasons: ['a TypeScript program checker could not be associated with its source'] };
78
+ for (const sourceFile of program.getSourceFiles()) {
79
+ if (!localFiles.has(canonical(sourceFile.fileName))) continue;
80
+ for (const statement of sourceFile.statements) {
81
+ const moduleSpecifier = (context.ts.isImportDeclaration(statement) || context.ts.isExportDeclaration(statement))
82
+ && statement.moduleSpecifier && context.ts.isStringLiteralLike(statement.moduleSpecifier) ? statement.moduleSpecifier : null;
83
+ const importEquals = context.ts.isImportEqualsDeclaration(statement) && context.ts.isExternalModuleReference(statement.moduleReference)
84
+ && statement.moduleReference.expression && context.ts.isStringLiteralLike(statement.moduleReference.expression)
85
+ ? statement.moduleReference.expression : null;
86
+ const specifier = (moduleSpecifier ?? importEquals)?.text;
87
+ if (!specifier || ![...FRAMEWORK.values()].includes(specifier)) continue;
88
+ const expected = new Set([...FRAMEWORK].filter(([, packageName]) => packageName === specifier).map(([name]) => name));
89
+ const selected = referencedExports(context.ts, statement, expected);
90
+ if (!selected.length) continue;
91
+ const moduleSymbol = checker.getSymbolAtLocation(moduleSpecifier ?? importEquals);
92
+ if (!moduleSymbol) {
93
+ reasons.push(`${relativePath(config.root, sourceFile.fileName)} cannot resolve ${specifier}`);
94
+ continue;
95
+ }
96
+ const exports = new Map(checker.getExportsOfModule(moduleSymbol).map(symbol => [symbol.getName(), normalizedSymbolValue(context.ts, checker, symbol)]));
97
+ for (const name of selected) {
98
+ const target = exports.get(name);
99
+ if (target) targets.set(name, target);
100
+ else reasons.push(`${relativePath(config.root, sourceFile.fileName)} cannot resolve ${name} from ${specifier}`);
101
+ }
102
+ }
103
+ const visitRequire = node => {
104
+ if (context.ts.isCallExpression(node) && isUnshadowedCommonJsRequire(context.ts, checker, node.expression)
105
+ && node.arguments.length === 1 && context.ts.isStringLiteralLike(node.arguments[0])
106
+ && [...FRAMEWORK.values()].includes(node.arguments[0].text)) {
107
+ reasons.push(`${relativePath(config.root, sourceFile.fileName)} uses a CommonJS Nest framework binding whose decorator identity cannot be proved`);
108
+ }
109
+ context.ts.forEachChild(node, visitRequire);
110
+ };
111
+ visitRequire(sourceFile);
112
+ }
113
+ return { targets, reasons };
114
+ }
115
+
116
+ function classToken(ts, checker, node, localFiles) {
117
+ const symbol = valueSymbol(ts, checker, node);
118
+ const declarations = symbol?.getDeclarations?.().filter(declaration => ts.isClassDeclaration(declaration)) ?? [];
119
+ const local = declarations.filter(declaration => localFiles.has(canonical(declaration.getSourceFile().fileName)));
120
+ const keys = [...new Set(local.map(declaration => `${canonical(declaration.getSourceFile().fileName)}#${declaration.getStart(declaration.getSourceFile())}`))];
121
+ return { key: keys.length === 1 ? keys[0] : null, external: declarations.length > 0 && local.length === 0 };
122
+ }
123
+
124
+ function localClassKey(ts, checker, node, localFiles) {
125
+ return classToken(ts, checker, node, localFiles).key;
126
+ }
127
+
128
+ function propertyName(ts, property) {
129
+ if (!property.name) return null;
130
+ if (ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name)) return property.name.text;
131
+ return null;
132
+ }
133
+
134
+ function tokenEntries(ts, checker, initializer, localFiles, field) {
135
+ initializer = unwrapExpression(ts, initializer);
136
+ if (!ts.isArrayLiteralExpression(initializer)) return { tokens: new Map(), unknown: true };
137
+ const tokens = new Map();
138
+ let unknown = false;
139
+ const add = (key, node) => {
140
+ if (!tokens.has(key)) tokens.set(key, []);
141
+ tokens.get(key).push(node);
142
+ };
143
+ for (const element of initializer.elements) {
144
+ if (ts.isSpreadElement(element)) { unknown = true; continue; }
145
+ if (ts.isObjectLiteralExpression(element)) {
146
+ if (element.properties.some(property => ts.isSpreadAssignment(property))) { unknown = true; continue; }
147
+ const providers = element.properties.filter(property => propertyName(ts, property) === 'provide');
148
+ if (providers.length !== 1 || !ts.isPropertyAssignment(providers[0])) { unknown = true; continue; }
149
+ const key = localClassKey(ts, checker, providers[0].initializer, localFiles);
150
+ if (key) add(key, providers[0].initializer);
151
+ continue;
152
+ }
153
+ const token = classToken(ts, checker, element, localFiles);
154
+ if (token.key) add(token.key, element);
155
+ else if (field === 'providers' && !token.external) unknown = true;
156
+ }
157
+ return { tokens, unknown };
158
+ }
159
+
160
+ function metadataTokens(ts, checker, metadata, field, localFiles) {
161
+ metadata = unwrapExpression(ts, metadata);
162
+ if (!ts.isObjectLiteralExpression(metadata)) return { tokens: new Map(), unknown: true };
163
+ if (metadata.properties.some(property => ts.isSpreadAssignment(property))) return { tokens: new Map(), unknown: true };
164
+ const matches = metadata.properties.filter(property => propertyName(ts, property) === field);
165
+ if (!matches.length) return { tokens: new Map(), unknown: false };
166
+ if (matches.length !== 1 || !ts.isPropertyAssignment(matches[0])) return { tokens: new Map(), unknown: true };
167
+ return tokenEntries(ts, checker, matches[0].initializer, localFiles, field);
168
+ }
169
+
170
+ function moduleRecord(config, context, sourceFile, declaration, decorator, checker, localFiles) {
171
+ const call = decorator.expression;
172
+ const location = declaration.name ?? declaration;
173
+ const base = {
174
+ key: declaration.name ? localClassKey(context.ts, checker, declaration.name, localFiles) : null,
175
+ name: declaration.name?.text ?? '(anonymous)',
176
+ path: relativePath(config.root, sourceFile.fileName),
177
+ ...sourceLocation(sourceFile, location),
178
+ providers: new Map(),
179
+ exports: new Map(),
180
+ unknown: false,
181
+ };
182
+ if (!context.ts.isCallExpression(call) || call.arguments.length !== 1 || !base.key) return { ...base, unknown: true };
183
+ const providers = metadataTokens(context.ts, checker, call.arguments[0], 'providers', localFiles);
184
+ const exports = metadataTokens(context.ts, checker, call.arguments[0], 'exports', localFiles);
185
+ return { ...base, providers: providers.tokens, exports: exports.tokens, unknown: providers.unknown || exports.unknown };
186
+ }
187
+
188
+ function registrationFinding(module, node, ruleId, message, extra = {}) {
189
+ return { ruleId, path: module.path, ...sourceLocation(node.getSourceFile(), node), module: module.name, message, ...extra };
190
+ }
191
+
192
+ /** A module belongs to an app only when its runtime source is reachable from that app's composition root. */
193
+ function applicationGraphs(config, context, modules) {
194
+ const roots = modules.filter(module => /^apps\/[^/]+\/src\/app\.module\.ts$/.test(module.path))
195
+ .sort((a, b) => a.path.localeCompare(b.path));
196
+ // Small architecture fixtures without app composition retain the original single-graph relation.
197
+ if (!roots.length) return [modules];
198
+ const byFile = new Map();
199
+ for (const module of modules) {
200
+ const file = canonical(path.resolve(config.root, ...module.path.split('/')));
201
+ if (!byFile.has(file)) byFile.set(file, []);
202
+ byFile.get(file).push(module);
203
+ }
204
+ const bound = new Set();
205
+ const graphs = roots.map(root => {
206
+ const queue = [canonical(path.resolve(config.root, ...root.path.split('/')))];
207
+ const seen = new Set();
208
+ const reachable = [];
209
+ for (let index = 0; index < queue.length; index += 1) {
210
+ const file = queue[index];
211
+ if (seen.has(file)) continue;
212
+ seen.add(file);
213
+ for (const module of byFile.get(file) ?? []) {
214
+ reachable.push(module);
215
+ bound.add(module);
216
+ }
217
+ for (const edge of context.edges.get(file) ?? []) if (edge.runtime && !seen.has(edge.to)) queue.push(edge.to);
218
+ }
219
+ return reachable;
220
+ });
221
+ // A module outside every proven app graph can still contain a duplicate in its own metadata.
222
+ return [...graphs, ...modules.filter(module => !bound.has(module)).map(module => [module])];
223
+ }
224
+
225
+ /** Prove only static exported class-token identity and selected CQRS handler registration. */
226
+ export function checkModuleRegistration(config, context) {
227
+ const selected = config.backend.moduleRegistration;
228
+ if (!selected) return { violations: [], coverage: { status: 'unavailable', reason: 'architecture.json does not select Nest module-registration identity checks' } };
229
+ const ts = context.ts;
230
+ const localFiles = new Set(context.files.map(file => canonical(file.fileName)));
231
+ const modules = [];
232
+ const handlers = [];
233
+ const reasons = [];
234
+ const frameworkByChecker = new Map();
235
+ for (const sourceFile of context.files) {
236
+ const checker = context.checkerFor(sourceFile.fileName);
237
+ if (!frameworkByChecker.has(checker)) frameworkByChecker.set(checker, frameworkTargets(config, context, checker, localFiles));
238
+ const framework = frameworkByChecker.get(checker);
239
+ reasons.push(...framework.reasons);
240
+ const visit = node => {
241
+ if (ts.isClassDeclaration(node)) {
242
+ const decorators = ts.canHaveDecorators(node) ? ts.getDecorators(node) ?? [] : node.decorators ?? [];
243
+ const kinds = decorators.map(decorator => ({ decorator, kind: frameworkDecorator(ts, checker, decorator, framework.targets) }));
244
+ const recognized = kinds.filter(item => item.kind);
245
+ for (const { decorator, kind } of kinds) {
246
+ const dynamic = kind ? null : mutableFrameworkAlias(ts, checker, decorator, framework.targets);
247
+ if (dynamic) reasons.push(`${relativePath(config.root, sourceFile.fileName)} uses mutable ${dynamic} decorator identity`);
248
+ }
249
+ const moduleDecorators = recognized.filter(item => item.kind === 'Module');
250
+ if (moduleDecorators.length === 1) modules.push(moduleRecord(config, context, sourceFile, node, moduleDecorators[0].decorator, checker, localFiles));
251
+ else if (moduleDecorators.length > 1) reasons.push(`${relativePath(config.root, sourceFile.fileName)} has multiple resolved @Module decorators`);
252
+ const handlerDecorators = recognized.filter(item => item.kind === 'CommandHandler' || item.kind === 'QueryHandler');
253
+ if (handlerDecorators.length === 1) handlers.push({
254
+ key: node.name ? localClassKey(ts, checker, node.name, localFiles) : null,
255
+ name: node.name?.text ?? '(anonymous)',
256
+ kind: handlerDecorators[0].kind,
257
+ path: relativePath(config.root, sourceFile.fileName),
258
+ ...sourceLocation(sourceFile, node.name ?? node),
259
+ });
260
+ else if (handlerDecorators.length > 1) reasons.push(`${relativePath(config.root, sourceFile.fileName)} has multiple resolved CQRS handler decorators`);
261
+ }
262
+ ts.forEachChild(node, visit);
263
+ };
264
+ visit(sourceFile);
265
+ }
266
+ for (const module of modules) if (module.unknown) reasons.push(`${module.path} has dynamic or unresolvable @Module providers/exports metadata`);
267
+ for (const handler of handlers) {
268
+ if (!handler.key) reasons.push(`${handler.path} handler class identity is not resolvable`);
269
+ if (!selected.handlerDecorators.includes(handler.kind)) reasons.push(`${handler.path} uses unselected ${handler.kind}`);
270
+ }
271
+ const violations = [];
272
+ const providerOwners = new Map();
273
+ for (const module of modules) for (const key of module.providers.keys()) if (module.exports.has(key)) {
274
+ if (!providerOwners.has(key)) providerOwners.set(key, []);
275
+ providerOwners.get(key).push(module);
276
+ }
277
+ const reportedRegistrations = new Set();
278
+ for (const graph of applicationGraphs(config, context, modules)) for (const [key] of providerOwners) {
279
+ const owners = graph.filter(module => module.providers.has(key) && module.exports.has(key))
280
+ .sort((a, b) => a.path.localeCompare(b.path));
281
+ if (!owners.length) continue;
282
+ const owner = owners[0];
283
+ const registrations = graph.flatMap(module => (module.providers.get(key) ?? []).map(node => ({ module, node })))
284
+ .sort((a, b) => a.module.path.localeCompare(b.module.path) || a.node.getStart() - b.node.getStart());
285
+ let keptOwnerRegistration = false;
286
+ for (const duplicate of registrations.filter(registration => {
287
+ if (registration.module !== owner || keptOwnerRegistration) return true;
288
+ keptOwnerRegistration = true;
289
+ return false;
290
+ })) {
291
+ const identity = `${key}\0${duplicate.module.path}\0${duplicate.node.getStart()}`;
292
+ if (reportedRegistrations.has(identity)) continue;
293
+ reportedRegistrations.add(identity);
294
+ violations.push(registrationFinding(duplicate.module, duplicate.node,
295
+ 'BE_MODULE_PROVIDER_REREGISTRATION',
296
+ `Class-token provider exported by ${owner.name} is registered again by ${duplicate.module.name} in one application graph; import the owning module and preserve one class-token registration.`,
297
+ { ownerModule: owner.name, ownerPath: owner.path }));
298
+ }
299
+ }
300
+ for (const handler of handlers.filter(item => item.key && selected.handlerDecorators.includes(item.kind))) {
301
+ const registrations = modules.flatMap(module => (module.providers.get(handler.key) ?? []).map(node => ({ module, node })));
302
+ if (registrations.length !== 1) violations.push({
303
+ ruleId: 'BE_MODULE_HANDLER_REGISTRATION', path: handler.path, line: handler.line, column: handler.column,
304
+ handler: handler.name, decorator: handler.kind,
305
+ message: registrations.length === 0
306
+ ? `${handler.kind} ${handler.name} is not registered as a direct class-token provider in a resolved Nest module.`
307
+ : `${handler.kind} ${handler.name} has multiple direct module registrations; one owner must preserve its handler identity.`,
308
+ modules: registrations.map(registration => ({ name: registration.module.name, path: registration.module.path,
309
+ ...sourceLocation(registration.node.getSourceFile(), registration.node) })),
310
+ });
311
+ }
312
+ const coverage = reasons.length
313
+ ? { status: 'unavailable', reason: 'one or more Nest module-registration relations are not statically provable', details: [...new Set(reasons)].sort() }
314
+ : { status: 'checked', modules: modules.length, exportedClassTokenProviders: providerOwners.size, handlers: handlers.length,
315
+ selectedHandlerDecorators: selected.handlerDecorators };
316
+ return { violations, coverage };
317
+ }
318
+
319
+ export { REGISTRATION_RULE_IDS };
@@ -0,0 +1,172 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { gitOutput } from '../../lib/git.mjs';
4
+
5
+ /**
6
+ * HFS check 5: the files and directories the slot manifest requires (knowledge/hfs/slots.yaml `requires`,
7
+ * `requiredInstances`, `minInstances`, required app kinds) must exist in the repository tree. Everything is read through the
8
+ * resolver of scripts/lib/hfs-slots.mjs; nothing here names a path.
9
+ *
10
+ * BE_REQUIRED_MODULE_MISSING a backend app, feature, domain, integrations or platform instance lacks a required file or
11
+ * directory, or a required platform instance (config, logging, errors, primitives) is absent
12
+ * FE_ERROR_BOUNDARY_MISSING a fe.app.next app lacks global-error, error, not-found or loading under src/app
13
+ * HFS_REQUIRED_FILE_MISSING any other required file or directory (packages, fe modules, i18n, api ...), and a minimum
14
+ * (no api app, no feature, no next app) which is reported once at hfs.json
15
+ *
16
+ * Slots of tier none (README, root config, .starciwork, .starcistacks) belong to the slot check; fe.app.next is the one
17
+ * tier-none slot judged here. The tree is the tracked one (`git ls-files` from the repository root, minus files deleted
18
+ * on disk), else the filesystem without node_modules, .git, dist and .next.
19
+ */
20
+ export const REQUIRED_FILE_RULE_IDS = ['BE_REQUIRED_MODULE_MISSING', 'FE_ERROR_BOUNDARY_MISSING', 'HFS_REQUIRED_FILE_MISSING'];
21
+
22
+ const SKIPPED_DIRECTORIES = new Set(['node_modules', '.git', 'dist', '.next']);
23
+ const ERROR_BOUNDARY_FILE = /^(?:global-error|error|not-found|loading)\.tsx$/u;
24
+ const BACKEND_MODULE_SLOT = /^be\.(?:app|feature|transport|domain|integrations|platform)(?:\.|$)/u;
25
+ const ALSO_JUDGED_TIER_NONE = new Set(['fe.app.next']);
26
+
27
+ function gitFiles(root) {
28
+ try {
29
+ const run = args => gitOutput(args, { cwd: root, maxBuffer: 256 * 1024 * 1024 });
30
+ run(['rev-parse', '--is-inside-work-tree']);
31
+ const deleted = new Set(run(['ls-files', '--deleted', '-z', '--', '.']).split('\0').filter(Boolean));
32
+ return run(['ls-files', '--cached', '-z', '--', '.']).split('\0').filter(file => file && !deleted.has(file));
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ function diskFiles(root, relative = '') {
39
+ const out = [];
40
+ let entries;
41
+ try { entries = fs.readdirSync(path.join(root, ...relative.split('/').filter(Boolean)), { withFileTypes: true }); } catch { return out; }
42
+ for (const entry of entries) {
43
+ if (SKIPPED_DIRECTORIES.has(entry.name)) continue;
44
+ const child = relative ? `${relative}/${entry.name}` : entry.name;
45
+ if (entry.isDirectory()) out.push(...diskFiles(root, child));
46
+ else out.push(child);
47
+ }
48
+ return out;
49
+ }
50
+
51
+ /** The repository tree as file and directory sets over posix relatives. */
52
+ export function treeOf(root) {
53
+ const list = gitFiles(root) ?? diskFiles(root);
54
+ const files = new Set(list);
55
+ const directories = new Set();
56
+ for (const file of list) {
57
+ const parts = file.split('/');
58
+ for (let i = 1; i < parts.length; i += 1) directories.add(parts.slice(0, i).join('/'));
59
+ }
60
+ return { files, directories };
61
+ }
62
+
63
+ const strip = entry => entry.replace(/\/+$/u, '');
64
+ const rootOfTarget = target => (target.endsWith('/') ? strip(target) : (path.posix.dirname(target) === '.' ? '' : path.posix.dirname(target)));
65
+
66
+ function ruleFor(slotId, target, rootExists) {
67
+ if (slotId === 'fe.app.next' && rootExists && ERROR_BOUNDARY_FILE.test(path.posix.basename(strip(target)))) return 'FE_ERROR_BOUNDARY_MISSING';
68
+ return BACKEND_MODULE_SLOT.test(slotId) ? 'BE_REQUIRED_MODULE_MISSING' : 'HFS_REQUIRED_FILE_MISSING';
69
+ }
70
+
71
+ export function checkRequiredFiles({ config, graph }) {
72
+ const resolver = graph?.resolver ?? config.hfs;
73
+ const tree = treeOf(config.root);
74
+ const judged = slotId => {
75
+ const slot = resolver.slot(slotId);
76
+ return Boolean(slot) && (slot.tier !== 'none' || ALSO_JUDGED_TIER_NONE.has(slotId));
77
+ };
78
+
79
+ const requirements = new Map(); // repository-relative target (as written, dirs end with /) -> {slot, target, root}
80
+ const instances = new Set();
81
+ const addRequirement = (slot, target, root) => { if (!requirements.has(target)) requirements.set(target, { slot, target, root }); };
82
+
83
+ // 1. What the repository must hold whatever it contains: fixed instances (requiredInstances, one per declared app).
84
+ const declared = resolver.requiredPaths();
85
+ const currentRoot = new Map();
86
+ for (const item of declared.paths) {
87
+ if (!judged(item.slot)) continue;
88
+ if (item.via !== 'requires') {
89
+ const root = rootOfTarget(item.path);
90
+ currentRoot.set(item.slot, root);
91
+ instances.add(`${item.slot}|${root}`);
92
+ addRequirement(item.slot, item.path, root);
93
+ } else {
94
+ addRequirement(item.slot, item.path, currentRoot.get(item.slot) ?? '');
95
+ }
96
+ }
97
+
98
+ // 2. What each instance found by walking the tree requires (features, domains, integrations, packages, transports, ...).
99
+ const seenInstances = new Set();
100
+ const instanceRequires = (slotId, root, representative) => {
101
+ const key = `${slotId}|${root}`;
102
+ if (seenInstances.has(key) || !judged(slotId)) return;
103
+ seenInstances.add(key);
104
+ instances.add(key);
105
+ for (const target of resolver.requiredFiles(representative)) addRequirement(slotId, target, root);
106
+ };
107
+ const ownerRequires = owner => {
108
+ const key = `${owner.slot}|${owner.root}`;
109
+ if (seenInstances.has(key) || !judged(owner.slot) || !resolver.slotEnabled(resolver.slot(owner.slot))) return;
110
+ seenInstances.add(key);
111
+ instances.add(key);
112
+ for (const entry of resolver.slot(owner.slot).requires ?? []) {
113
+ const rooted = entry.startsWith('/');
114
+ const filled = (rooted ? entry.slice(1) : entry).replace(/<([A-Za-z][A-Za-z0-9-]*)>/g, (whole, name) => owner.bindings[name] ?? whole);
115
+ addRequirement(owner.slot, rooted || !owner.root ? filled : `${owner.root}/${filled}`, owner.root);
116
+ }
117
+ };
118
+ const found = new Map(); // slot -> Set(root) of instances seen while walking
119
+ const note = (slotId, root) => {
120
+ if (!found.has(slotId)) found.set(slotId, new Set());
121
+ found.get(slotId).add(root);
122
+ };
123
+ for (const file of tree.files) {
124
+ const classified = resolver.classifyPath(file);
125
+ if (classified.status === 'owned' && classified.slot) {
126
+ note(classified.slot, classified.root);
127
+ instanceRequires(classified.slot, classified.root, file);
128
+ }
129
+ const owner = resolver.ownerOf(file);
130
+ if (owner) {
131
+ note(owner.slot, owner.root);
132
+ // an owner root may classify to a more specific slot (modules/i18n) or a nested one, so expand the owner slot's own requires
133
+ ownerRequires(owner);
134
+ }
135
+ }
136
+
137
+ const violations = [];
138
+ const reported = new Set();
139
+ const report = (ruleId, target, slotId, root, extra, message) => {
140
+ const key = `${ruleId}|${target}`;
141
+ if (reported.has(key)) return;
142
+ reported.add(key);
143
+ violations.push({ ruleId, path: target, slot: slotId, ...(root ? { root } : {}), ...extra, message });
144
+ };
145
+ const exists = target => (target.endsWith('/') ? tree.directories.has(strip(target)) : tree.files.has(target));
146
+
147
+ for (const requirement of requirements.values()) {
148
+ if (exists(requirement.target)) continue;
149
+ const rootExists = !requirement.root || tree.directories.has(requirement.root);
150
+ if (!rootExists) {
151
+ report(ruleFor(requirement.slot, requirement.target, false), requirement.root, requirement.slot, requirement.root, { kind: 'directory' },
152
+ `Required directory ${requirement.root} (slot ${requirement.slot}) does not exist; the slot manifest requires it, along with what it holds.`);
153
+ continue;
154
+ }
155
+ const kind = requirement.target.endsWith('/') ? 'directory' : 'file';
156
+ report(ruleFor(requirement.slot, requirement.target, true), strip(requirement.target), requirement.slot, requirement.root, { kind },
157
+ `Required ${kind} ${strip(requirement.target)} is missing; slot ${requirement.slot} requires it in every instance${requirement.root ? ` (${requirement.root})` : ''}.`);
158
+ }
159
+
160
+ // 3. Minimums: at least minInstances instances of a required slot.
161
+ for (const minimum of declared.minimums) {
162
+ if (!judged(minimum.slot)) continue;
163
+ const count = minimum.appKind !== undefined
164
+ ? resolver.repo.apps.filter(app => app.kind === minimum.appKind).length
165
+ : (found.get(minimum.slot)?.size ?? 0);
166
+ if (count >= minimum.min) continue;
167
+ report('HFS_REQUIRED_FILE_MISSING', 'hfs.json', minimum.slot, '', { minimum: minimum.min, found: count },
168
+ `Slot ${minimum.slot} needs at least ${minimum.min} instance${minimum.min === 1 ? '' : 's'}${minimum.appKind ? ` (an app of kind ${minimum.appKind} declared in hfs.json)` : ''}; the repository has ${count}.`);
169
+ }
170
+
171
+ return { violations, coverage: { status: 'checked', instances: instances.size, requirements: requirements.size, missing: violations.length } };
172
+ }
@@ -0,0 +1,97 @@
1
+ import path from 'node:path';
2
+ import { machineKit } from './machine-ast.mjs';
3
+
4
+ /**
5
+ * R54 `route-files-thin` (FE_ROUTE_FILES_THIN, knowledge/hfs/README.md 6.2). A route file only mounts an owner: it holds no
6
+ * inline component, no drawing and no hook. For every file of the route slot (fe.route, apps/<app>/src/app) named
7
+ * `layout`, `template`, `loading` or `not-found`:
8
+ *
9
+ * - no host element (`<div>`, `<main>`): drawing belongs to the feature the file mounts;
10
+ * - no function other than the default export that returns JSX: an inline component is a feature in the wrong folder;
11
+ * - JSX, when present, mounts exactly one feature owner (a component resolved to a file of tier `feature`); a file with no
12
+ * JSX re-exports or delegates and needs none. Package components (a locale shell) may wrap the owner.
13
+ *
14
+ * and, for every such file and every `page`: no hook call. A hook is an imported `use<Name>` function or any function of
15
+ * hooks/. The page's own mount, redirect and decision rules are the existing FE_ROUTE_ONE_PAGE, FE_ROUTE_DRAWING_DECISION,
16
+ * FE_ROUTE_CLIENT_BOUNDARY and FE_ROUTE_CLIENT_HOOK checks of frontend.mjs and are not judged a second time here.
17
+ */
18
+ export const ROUTE_FILES_THIN_RULE_IDS = ['FE_ROUTE_FILES_THIN'];
19
+
20
+ const RULE = 'FE_ROUTE_FILES_THIN';
21
+ const ROUTE_SLOT = 'fe.route';
22
+ const MOUNTING_ROUTES = new Set(['layout', 'template', 'loading', 'not-found']);
23
+ const HOOK_ROUTES = new Set([...MOUNTING_ROUTES, 'page']);
24
+ const HOOK_NAME = /^use[A-Z0-9]/u;
25
+ const HOOKS_SLOT = 'fe.hooks';
26
+
27
+ export function checkRouteFilesThin(input) {
28
+ const { graph } = input;
29
+ const kit = machineKit(input);
30
+ const { ts } = kit;
31
+ const violations = [];
32
+ let routes = 0;
33
+
34
+ for (const file of graph.files.values()) {
35
+ if (file.slot !== ROUTE_SLOT) continue;
36
+ const stem = path.posix.basename(file.rel).replace(/\.[cm]?[jt]sx?$/u, '');
37
+ if (!HOOK_ROUTES.has(stem)) continue;
38
+ routes += 1;
39
+ const mounting = MOUNTING_ROUTES.has(stem);
40
+ const checker = kit.checkerOf(file.sourceFile);
41
+ const report = (node, message, extra = {}) => violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message, route: stem, ...extra });
42
+
43
+ // The default export function: `export default function X`, `export default X` (X a function), `export default () => ...`.
44
+ const defaults = new Set();
45
+ for (const statement of file.sourceFile.statements) {
46
+ if (ts.isFunctionDeclaration(statement) && statement.modifiers?.some(item => item.kind === ts.SyntaxKind.DefaultKeyword)) defaults.add(statement);
47
+ if (ts.isExportAssignment(statement) && !statement.isExportEquals) {
48
+ const target = statement.expression;
49
+ if (ts.isIdentifier(target)) for (const declaration of kit.declarationsOf(checker, target)) defaults.add(ts.isVariableDeclaration(declaration) ? declaration.initializer ?? declaration : declaration);
50
+ else defaults.add(target);
51
+ }
52
+ }
53
+ const isFunction = node => ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node);
54
+ const jsxOf = node => ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node);
55
+ const tagOf = node => (ts.isJsxElement(node) ? node.openingElement.tagName : ts.isJsxSelfClosingElement(node) ? node.tagName : null);
56
+ /** True when the function contains JSX that is not inside a nested function. */
57
+ const drawsDirectly = fn => {
58
+ let found = false;
59
+ const visit = node => {
60
+ if (found) return;
61
+ if (node !== fn && isFunction(node)) return;
62
+ if (jsxOf(node)) { found = true; return; }
63
+ ts.forEachChild(node, visit);
64
+ };
65
+ visit(fn);
66
+ return found;
67
+ };
68
+
69
+ let hasJsx = false;
70
+ let owners = 0;
71
+ kit.walk(file.sourceFile, node => {
72
+ if (jsxOf(node)) hasJsx = true;
73
+ const tag = tagOf(node);
74
+ if (mounting && tag) {
75
+ if (ts.isIdentifier(tag) && /^[a-z]/u.test(tag.text)) report(tag, `<${tag.text}> is drawing in a ${stem} route file; a route file mounts one feature and draws nothing. Move the markup into the feature.`);
76
+ else if (kit.declarationsOf(checker, ts.isPropertyAccessExpression(tag) ? tag.name : tag).some(declaration => graph.files.get(kit.graphPath(declaration) ?? '')?.tier === 'feature')) owners += 1;
77
+ }
78
+ if (mounting && isFunction(node) && !defaults.has(node) && drawsDirectly(node)) {
79
+ report(node, `A function other than the default export draws JSX in a ${stem} route file; that is an inline component. Move it into the feature the route mounts.`);
80
+ }
81
+ if (ts.isCallExpression(node)) {
82
+ const callee = ts.isIdentifier(node.expression) ? node.expression : null;
83
+ if (callee && HOOK_NAME.test(callee.text)) {
84
+ const binding = kit.importBinding(checker, callee);
85
+ const inHooks = kit.declarationsOf(checker, callee).some(declaration => graph.files.get(kit.graphPath(declaration) ?? '')?.slot === HOOKS_SLOT);
86
+ if (binding || inHooks) report(callee, `${callee.text} is a hook called in a ${stem} route file; a route file is a server adapter and holds no hook. Read the data in the feature it mounts.`);
87
+ }
88
+ }
89
+ return true;
90
+ });
91
+ if (mounting && hasJsx && owners !== 1) {
92
+ violations.push({ ruleId: RULE, path: file.rel, line: 1, column: 1, route: stem, owners,
93
+ message: `${file.rel} mounts ${owners} feature owners; a ${stem} route file mounts exactly one feature (a layouts- or pages-tier component of the app), optionally inside a package shell.` });
94
+ }
95
+ }
96
+ return { violations, coverage: { status: 'checked', routes } };
97
+ }