@starci/hfs 4.0.9 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/LICENSE +21 -0
  3. package/emit/operations.mjs +1 -1
  4. package/emit/type-schema.mjs +2 -1
  5. package/lint/run.mjs +82 -8
  6. package/package.json +3 -2
  7. package/report/order.mjs +2 -0
  8. package/runtime/config.example.yaml +187 -0
  9. package/runtime/engine/by-code-unit.mjs +12 -0
  10. package/runtime/engine/config.mjs +247 -273
  11. package/runtime/engine/invalid-config.mjs +5 -5
  12. package/runtime/engine/model-config.mjs +90 -0
  13. package/runtime/engine/orca-config.mjs +3 -1
  14. package/runtime/engine/removed-vocabulary.mjs +28 -0
  15. package/runtime/engine/resources-config.mjs +52 -0
  16. package/runtime/engine/runtime-root.mjs +17 -0
  17. package/runtime/engine/secrets.mjs +134 -0
  18. package/runtime/engine/sonar-config.mjs +21 -0
  19. package/runtime/engine/temp-root.mjs +32 -0
  20. package/runtime/knowledge/hfs/canon-pins.yaml +18 -18
  21. package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
  22. package/runtime/knowledge/hfs/rules.yaml +102 -69
  23. package/runtime/knowledge/hfs/slots.yaml +11 -14
  24. package/runtime/knowledge/patterns/be/api.yaml +7 -24
  25. package/runtime/knowledge/patterns/be/cli.yaml +3 -12
  26. package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
  27. package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
  28. package/runtime/modules/kernel/failure-codes.yaml +1 -1
  29. package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
  30. package/runtime/modules/models/registry.yaml +25 -397
  31. package/runtime/modules/models/runtimes.yaml +92 -262
  32. package/runtime/modules/models/tiers.yaml +65 -0
  33. package/runtime/scripts/api/fs/claim-file.mjs +30 -0
  34. package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
  35. package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
  36. package/runtime/scripts/api/fs/lib.mjs +6 -0
  37. package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
  38. package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
  39. package/runtime/scripts/api/git/lib.mjs +36 -1
  40. package/runtime/scripts/api/process/resolve-real-tool.mjs +52 -0
  41. package/runtime/scripts/api/process/run-program.mjs +8 -0
  42. package/runtime/scripts/api/sops/decrypt.mjs +9 -16
  43. package/runtime/scripts/api/sops/lib.mjs +309 -11
  44. package/runtime/scripts/api/sops/seal.mjs +8 -4
  45. package/runtime/scripts/hfs/allows.mjs +3 -3
  46. package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
  47. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
  48. package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
  49. package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
  50. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
  51. package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
  52. package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
  53. package/runtime/scripts/hfs/architecture/config.mjs +95 -80
  54. package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
  55. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
  56. package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
  57. package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
  58. package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
  59. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
  60. package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
  61. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
  62. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
  63. package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
  64. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
  65. package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
  66. package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
  67. package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
  68. package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
  69. package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
  70. package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
  71. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +3 -2
  72. package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
  73. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
  74. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
  75. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
  76. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
  77. package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
  78. package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
  79. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
  80. package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
  81. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
  82. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
  83. package/runtime/scripts/hfs/architecture/index.mjs +158 -102
  84. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
  85. package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
  86. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
  87. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
  88. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
  89. package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
  90. package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
  91. package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
  92. package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
  93. package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
  94. package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
  95. package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
  96. package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
  97. package/runtime/scripts/hfs/architecture/required-files.mjs +100 -68
  98. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
  99. package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
  100. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
  101. package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
  102. package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
  103. package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
  104. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
  105. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
  106. package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
  107. package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
  108. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
  109. package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
  110. package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
  111. package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
  112. package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
  113. package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
  114. package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
  115. package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
  116. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
  117. package/runtime/scripts/hfs/architecture.mjs +3 -4
  118. package/runtime/scripts/hfs/check.mjs +61 -70
  119. package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
  120. package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
  121. package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
  122. package/runtime/scripts/hfs/edition-slots.mjs +5 -3
  123. package/runtime/scripts/hfs/linear-text.mjs +30 -0
  124. package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
  125. package/runtime/scripts/hfs/path-findings.mjs +58 -38
  126. package/runtime/scripts/hfs/pin-findings.mjs +31 -0
  127. package/runtime/scripts/hfs/repo-identity.mjs +5 -2
  128. package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
  129. package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
  130. package/runtime/scripts/hfs/rules/cli.mjs +5 -1
  131. package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
  132. package/runtime/scripts/hfs/rules/contract.mjs +35 -27
  133. package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
  134. package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
  135. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
  136. package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
  137. package/runtime/scripts/hfs/rules/database.mjs +36 -23
  138. package/runtime/scripts/hfs/rules/deps.mjs +64 -32
  139. package/runtime/scripts/hfs/rules/docker.mjs +113 -65
  140. package/runtime/scripts/hfs/rules/edition.mjs +118 -111
  141. package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
  142. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
  143. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
  144. package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
  145. package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
  146. package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
  147. package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
  148. package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
  149. package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
  150. package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
  151. package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
  152. package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
  153. package/runtime/scripts/hfs/rules/saga.mjs +128 -61
  154. package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
  155. package/runtime/scripts/hfs/rules/services.mjs +34 -16
  156. package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
  157. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
  158. package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
  159. package/runtime/scripts/hfs/secret.mjs +90 -45
  160. package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
  161. package/runtime/scripts/hfs/slot-classify.mjs +65 -0
  162. package/runtime/scripts/hfs/slot-errors.mjs +11 -0
  163. package/runtime/scripts/hfs/slot-imports.mjs +76 -0
  164. package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
  165. package/runtime/scripts/hfs/slot-match.mjs +153 -0
  166. package/runtime/scripts/hfs/slot-path.mjs +8 -0
  167. package/runtime/scripts/hfs/slot-required.mjs +76 -0
  168. package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
  169. package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
  170. package/runtime/scripts/hfs/slots.mjs +72 -693
  171. package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
  172. package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
  173. package/runtime/scripts/hfs/tree.mjs +19 -14
  174. package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
  175. package/runtime/scripts/hfs/view.mjs +1 -1
  176. package/runtime/scripts/lib/dockerfile.mjs +56 -33
  177. package/runtime/scripts/lib/event-contract.mjs +17 -13
  178. package/runtime/scripts/lib/fs-kind.mjs +14 -4
  179. package/runtime/scripts/lib/git.mjs +2 -2
  180. package/runtime/scripts/lib/graphql-contract.mjs +158 -322
  181. package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
  182. package/runtime/scripts/lib/i18n.mjs +3 -2
  183. package/runtime/scripts/lib/in-order.mjs +71 -0
  184. package/runtime/scripts/lib/language.mjs +3 -3
  185. package/runtime/scripts/lib/list.mjs +8 -1
  186. package/runtime/scripts/lib/mutation-fence.mjs +15 -0
  187. package/runtime/scripts/lib/path-key.mjs +42 -9
  188. package/runtime/scripts/lib/pid-alive.mjs +7 -0
  189. package/runtime/scripts/lib/regex.mjs +1 -1
  190. package/runtime/scripts/lib/same-text.mjs +1 -1
  191. package/runtime/scripts/lib/secret-patterns.mjs +6 -2
  192. package/runtime/scripts/lib/sleep-sync.mjs +2 -2
  193. package/runtime/scripts/lib/sops-envelope.mjs +103 -2
  194. package/runtime/scripts/lib/stack-services.mjs +1 -2
  195. package/runtime/scripts/lib/ts-ast.mjs +9 -0
  196. package/runtime/scripts/lib/walk.mjs +6 -1
  197. package/scaffold/add-cli-lite.mjs +2 -1
  198. package/scaffold/add-table.mjs +2 -2
  199. package/scaffold/add.mjs +3 -2
  200. package/scaffold/app.mjs +4 -2
  201. package/scaffold/edition-gate.mjs +25 -25
  202. package/scaffold/lite-exports.mjs +2 -1
  203. package/scaffold/service.mjs +5 -4
  204. package/sync/hygiene.mjs +15 -9
  205. package/sync/index.mjs +7 -3
  206. package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
  207. package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
  208. package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
  209. package/templates/app/ci-workflows/sonar-steps.yml +2 -2
  210. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
  211. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
  212. package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
  213. package/templates/app/starciwork.gitignore +1 -1
  214. package/templates/be/image/api/Dockerfile +1 -1
  215. package/templates/be/image/cli/Dockerfile +1 -1
  216. package/templates/be/image/worker/Dockerfile +1 -1
  217. package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
  218. package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
  219. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
  220. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
  221. package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
  222. package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
  223. package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
  224. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
  225. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
  226. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
  227. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
  228. package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
  229. package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
  230. package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
  231. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
  232. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  233. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
  234. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
  235. package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
  236. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  237. package/templates/fe/image/next/Dockerfile +2 -1
  238. package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
  239. package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
  240. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
  241. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
  242. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
  243. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
  244. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
  245. package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
  246. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
  247. package/upgrade/index.mjs +9 -2
@@ -14,6 +14,7 @@
14
14
  * Specs and tests are not in the graph. The public entry of an owner is found the way dead-exports.mjs finds it.
15
15
  */
16
16
  import { canonical } from './config.mjs';
17
+ import { hasAnyFlag } from '../../lib/ts-ast.mjs';
17
18
  import { entryOf } from './dead-exports.mjs';
18
19
 
19
20
  export const SYMBOL_RULE_IDS = ['HFS_DUPLICATE_SYMBOL', 'HFS_ALIAS_REEXPORT'];
@@ -21,41 +22,58 @@ export const SYMBOL_RULE_IDS = ['HFS_DUPLICATE_SYMBOL', 'HFS_ALIAS_REEXPORT'];
21
22
  const lineOf = (sourceFile, node) => sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
22
23
 
23
24
  /** The names a source file declares at top level and exports: Map<name, line>. Re-exports from another module declare nothing. */
25
+ function addBindingNames(ts, sourceFile, name, into, node) {
26
+ if (name.kind === ts.SyntaxKind.Identifier) {
27
+ into.set(name.text, lineOf(sourceFile, node));
28
+ return;
29
+ }
30
+ for (const element of name.elements) {
31
+ if (element.kind === ts.SyntaxKind.BindingElement) addBindingNames(ts, sourceFile, element.name, into, node);
32
+ }
33
+ }
34
+
35
+ function recordStatementDeclarations(ts, sourceFile, statement, declared, exported) {
36
+ const kind = ts.SyntaxKind;
37
+ const modifiers = statement.modifiers ?? [];
38
+ const publish = modifiers.some(modifier => modifier.kind === kind.ExportKeyword) && !modifiers.some(modifier => modifier.kind === kind.DefaultKeyword);
39
+ const into = publish ? exported : declared;
40
+ if (statement.kind === kind.VariableStatement) {
41
+ for (const declaration of statement.declarationList.declarations) addBindingNames(ts, sourceFile, declaration.name, into, declaration);
42
+ return;
43
+ }
44
+ if (statement.name?.kind === kind.Identifier
45
+ && [kind.FunctionDeclaration, kind.ClassDeclaration, kind.EnumDeclaration, kind.TypeAliasDeclaration, kind.InterfaceDeclaration].includes(statement.kind)) {
46
+ into.set(statement.name.text, lineOf(sourceFile, statement));
47
+ }
48
+ }
49
+
50
+ function exposeLocalExports(localLists, declared, exported) {
51
+ for (const element of localLists) {
52
+ const local = (element.propertyName ?? element.name).text;
53
+ if (declared.has(local)) exported.set(element.name.text, declared.get(local));
54
+ }
55
+ }
56
+
24
57
  function declaredExports(ts, sourceFile) {
25
58
  const kind = ts.SyntaxKind;
26
59
  const declared = new Map();
27
60
  const exported = new Map();
28
61
  const localLists = [];
29
- const bindingNames = (name, into, node) => {
30
- if (name.kind === kind.Identifier) into.set(name.text, lineOf(sourceFile, node));
31
- else for (const element of name.elements) if (element.kind === kind.BindingElement) bindingNames(element.name, into, node);
32
- };
33
62
  for (const statement of sourceFile.statements) {
34
63
  if (statement.kind === kind.ExportDeclaration) {
35
64
  if (!statement.moduleSpecifier && statement.exportClause?.kind === kind.NamedExports) localLists.push(...statement.exportClause.elements);
36
65
  continue;
37
66
  }
38
- const modifiers = statement.modifiers ?? [];
39
- const publish = modifiers.some(modifier => modifier.kind === kind.ExportKeyword) && !modifiers.some(modifier => modifier.kind === kind.DefaultKeyword);
40
- const into = publish ? exported : declared;
41
- if (statement.kind === kind.VariableStatement) {
42
- for (const declaration of statement.declarationList.declarations) bindingNames(declaration.name, into, declaration);
43
- } else if (statement.name?.kind === kind.Identifier
44
- && [kind.FunctionDeclaration, kind.ClassDeclaration, kind.EnumDeclaration, kind.TypeAliasDeclaration, kind.InterfaceDeclaration].includes(statement.kind)) {
45
- into.set(statement.name.text, lineOf(sourceFile, statement));
46
- }
67
+ recordStatementDeclarations(ts, sourceFile, statement, declared, exported);
47
68
  }
48
69
  // `export { local }` of a name declared in this file makes that declaration exported.
49
- for (const element of localLists) {
50
- const local = (element.propertyName ?? element.name).text;
51
- if (declared.has(local)) exported.set(element.name.text, declared.get(local));
52
- }
70
+ exposeLocalExports(localLists, declared, exported);
53
71
  return exported;
54
72
  }
55
73
 
56
74
  /** The declaration an export of an entry resolves to: its repository-relative file and its own name, or null. */
57
75
  function declaringFile(ts, checker, graph, symbol) {
58
- const resolved = symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
76
+ const resolved = hasAnyFlag(symbol.flags, ts.SymbolFlags.Alias) ? checker.getAliasedSymbol(symbol) : symbol;
59
77
  for (const declaration of resolved?.declarations ?? []) {
60
78
  const rel = graph.abs(canonical(declaration.getSourceFile().fileName));
61
79
  if (rel) return { rel, name: resolved.getName() };
@@ -63,8 +81,7 @@ function declaringFile(ts, checker, graph, symbol) {
63
81
  return null;
64
82
  }
65
83
 
66
- function duplicateSymbols({ context, graph, config }) {
67
- const { ts } = context;
84
+ function declarationIndex(ts, graph) {
68
85
  const declarations = new Map(); // name -> Map<rel, line>
69
86
  for (const [rel, node] of graph.files) {
70
87
  for (const [name, line] of declaredExports(ts, node.sourceFile)) {
@@ -73,47 +90,72 @@ function duplicateSymbols({ context, graph, config }) {
73
90
  declarations.get(name).set(rel, line);
74
91
  }
75
92
  }
93
+ return declarations;
94
+ }
95
+
96
+ function duplicateViolation(scan, symbol, owner, entry) {
97
+ const { ts, checker, graph, declarations, seen } = scan;
98
+ const declaration = declaringFile(ts, checker, graph, symbol);
99
+ if (!declaration || declaration.name === 'default') return null;
100
+ const { rel: home, name } = declaration;
101
+ const others = [...(declarations.get(name) ?? [])].filter(([rel]) => rel !== home);
102
+ if (!others.length || seen.has(`${home}\0${name}`)) return null;
103
+ seen.add(`${home}\0${name}`);
104
+ const where = others.map(([rel, at]) => `${rel}:${at}`);
105
+ return {
106
+ ruleId: 'HFS_DUPLICATE_SYMBOL',
107
+ path: home, line: declarations.get(name).get(home) ?? 1, column: 1, name, owner: owner.root, entry,
108
+ otherDeclarations: where,
109
+ message: `${name} is exported through the public entry ${entry} (declared in ${home}) and also declared and exported in ${where.join(', ')}; one name, one declaration: rename or delete the copy.`,
110
+ };
111
+ }
112
+
113
+ function inspectPublicOwner(ts, context, graph, config, declarations, seen, [key, owner]) {
114
+ if (owner.tier === 'app') return { violations: [], surface: 0 };
115
+ const entry = entryOf(graph, config, key, owner);
116
+ if (!entry) return { violations: [], surface: 0 };
117
+ const entryFile = graph.files.get(entry);
118
+ const checker = context.checkerFor(entryFile.abs);
119
+ const moduleSymbol = checker?.getSymbolAtLocation(entryFile.sourceFile);
120
+ if (!moduleSymbol) return { violations: [], surface: 0 };
121
+ const violations = [];
122
+ const scan = { ts, checker, graph, declarations, seen };
123
+ let surface = 0;
124
+ for (const symbol of checker.getExportsOfModule(moduleSymbol)) {
125
+ surface += 1;
126
+ const violation = duplicateViolation(scan, symbol, owner, entry);
127
+ if (violation) violations.push(violation);
128
+ }
129
+ return { violations, surface };
130
+ }
131
+
132
+ function duplicateSymbols({ context, graph, config }) {
133
+ const declarations = declarationIndex(context.ts, graph);
76
134
  const violations = [];
77
135
  const seen = new Set();
78
136
  let surface = 0;
79
- for (const [key, owner] of graph.ownerRoots) {
80
- if (owner.tier === 'app') continue;
81
- const entry = entryOf(graph, config, key, owner);
82
- if (!entry) continue;
83
- const entryFile = graph.files.get(entry);
84
- const checker = context.checkerFor(entryFile.abs);
85
- const moduleSymbol = checker?.getSymbolAtLocation(entryFile.sourceFile);
86
- if (!moduleSymbol) continue;
87
- for (const symbol of checker.getExportsOfModule(moduleSymbol)) {
88
- surface += 1;
89
- const declaration = declaringFile(ts, checker, graph, symbol);
90
- if (!declaration || declaration.name === 'default') continue;
91
- const { rel: home, name } = declaration;
92
- const others = [...(declarations.get(name) ?? [])].filter(([rel]) => rel !== home);
93
- if (!others.length || seen.has(`${home}\0${name}`)) continue;
94
- seen.add(`${home}\0${name}`);
95
- const where = others.map(([rel, at]) => `${rel}:${at}`);
96
- violations.push({
97
- ruleId: 'HFS_DUPLICATE_SYMBOL',
98
- path: home, line: declarations.get(name).get(home) ?? 1, column: 1, name, owner: owner.root, entry,
99
- otherDeclarations: where,
100
- message: `${name} is exported through the public entry ${entry} (declared in ${home}) and also declared and exported in ${where.join(', ')}; one name, one declaration: rename or delete the copy.`,
101
- });
102
- }
137
+ for (const item of graph.ownerRoots) {
138
+ const result = inspectPublicOwner(context.ts, context, graph, config, declarations, seen, item);
139
+ violations.push(...result.violations);
140
+ surface += result.surface;
103
141
  }
104
142
  return { violations, surface };
105
143
  }
106
144
 
107
145
  const isExported = (ts, statement) => (statement.modifiers ?? []).some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)
108
146
  && !(statement.modifiers ?? []).some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword);
147
+ const bareAliasExpression = (ts, expression) => ts.isIdentifier(expression)
148
+ || (ts.isPropertyAccessExpression(expression) && ts.isIdentifier(expression.name) && (ts.isIdentifier(expression.expression) || ts.isPropertyAccessExpression(expression.expression)));
109
149
 
110
150
  /** The declaration `node` (an identifier, a member or a qualified name) resolves to when a repository file declares it as one of `kinds`, else null. */
111
151
  function repositoryDeclaration(ts, checker, graph, node, kinds) {
112
- const target = ts.isPropertyAccessExpression(node) ? node.name : ts.isQualifiedName(node) ? node.right : node;
152
+ let target = node;
153
+ if (ts.isPropertyAccessExpression(node)) target = node.name;
154
+ else if (ts.isQualifiedName(node)) target = node.right;
113
155
  const symbol = checker?.getSymbolAtLocation(target);
114
156
  if (!symbol) return null;
115
- const resolved = symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
116
- const home = (resolved?.declarations ?? []).find(declaration => kinds.includes(declaration.kind) && graph.abs(canonical(declaration.getSourceFile().fileName)));
157
+ const resolved = hasAnyFlag(symbol.flags, ts.SymbolFlags.Alias) ? checker.getAliasedSymbol(symbol) : symbol;
158
+ const home = (resolved?.declarations ?? []).some(declaration => kinds.includes(declaration.kind) && graph.abs(canonical(declaration.getSourceFile().fileName)));
117
159
  return home ? resolved.getName() : null;
118
160
  }
119
161
 
@@ -122,31 +164,44 @@ function repositoryDeclaration(ts, checker, graph, node, kinds) {
122
164
  * `export const Y = X` and `export const Y = X.y` (X or y a function, class, const or enum), `export type Y = X` and
123
165
  * `export interface Y extends X {}` with no body, no type arguments and no type parameters.
124
166
  */
125
- function declarationAliases(ts, checker, graph, statement) {
126
- const kind = ts.SyntaxKind;
127
- if (!checker || !isExported(ts, statement)) return [];
128
- const bare = expression => ts.isIdentifier(expression) || (ts.isPropertyAccessExpression(expression) && ts.isIdentifier(expression.name) && (ts.isIdentifier(expression.expression) || ts.isPropertyAccessExpression(expression.expression)));
167
+ function variableAliases(ts, checker, graph, statement, kind) {
129
168
  const found = [];
130
- if (statement.kind === kind.VariableStatement) {
131
- for (const declaration of statement.declarationList.declarations) {
132
- if (!ts.isIdentifier(declaration.name) || !declaration.initializer || !bare(declaration.initializer)) continue;
133
- const of = repositoryDeclaration(ts, checker, graph, declaration.initializer, [kind.FunctionDeclaration, kind.ClassDeclaration, kind.VariableDeclaration, kind.EnumDeclaration]);
134
- if (of && of !== declaration.name.text) found.push({ node: declaration, name: declaration.name.text, of, form: `export const ${declaration.name.text} = ${declaration.initializer.getText()}` });
135
- }
136
- } else if (statement.kind === kind.TypeAliasDeclaration && !statement.typeParameters?.length && ts.isTypeReferenceNode(statement.type) && !statement.type.typeArguments?.length) {
137
- const of = repositoryDeclaration(ts, checker, graph, statement.type.typeName, [kind.InterfaceDeclaration, kind.TypeAliasDeclaration, kind.ClassDeclaration, kind.EnumDeclaration]);
138
- if (of && of !== statement.name.text) found.push({ node: statement, name: statement.name.text, of, form: `export type ${statement.name.text} = ${statement.type.getText()}` });
139
- } else if (statement.kind === kind.InterfaceDeclaration && !statement.members.length && !statement.typeParameters?.length) {
140
- const heritage = (statement.heritageClauses ?? []).flatMap(clause => clause.types);
141
- const [only] = heritage;
142
- if (heritage.length === 1 && !only.typeArguments?.length && bare(only.expression)) {
143
- const of = repositoryDeclaration(ts, checker, graph, only.expression, [kind.InterfaceDeclaration, kind.TypeAliasDeclaration, kind.ClassDeclaration]);
144
- if (of && of !== statement.name.text) found.push({ node: statement, name: statement.name.text, of, form: `export interface ${statement.name.text} extends ${only.expression.getText()} {}` });
145
- }
169
+ for (const declaration of statement.declarationList.declarations) {
170
+ if (!ts.isIdentifier(declaration.name) || !declaration.initializer || !bareAliasExpression(ts, declaration.initializer)) continue;
171
+ const of = repositoryDeclaration(ts, checker, graph, declaration.initializer, [kind.FunctionDeclaration, kind.ClassDeclaration, kind.VariableDeclaration, kind.EnumDeclaration]);
172
+ if (of && of !== declaration.name.text) found.push({ node: declaration, name: declaration.name.text, of, form: `export const ${declaration.name.text} = ${declaration.initializer.getText()}` });
146
173
  }
147
174
  return found;
148
175
  }
149
176
 
177
+ function typeAliasOf(ts, checker, graph, statement, kind) {
178
+ if (statement.typeParameters?.length || !ts.isTypeReferenceNode(statement.type) || statement.type.typeArguments?.length) return [];
179
+ const of = repositoryDeclaration(ts, checker, graph, statement.type.typeName, [kind.InterfaceDeclaration, kind.TypeAliasDeclaration, kind.ClassDeclaration, kind.EnumDeclaration]);
180
+ return of && of !== statement.name.text
181
+ ? [{ node: statement, name: statement.name.text, of, form: `export type ${statement.name.text} = ${statement.type.getText()}` }]
182
+ : [];
183
+ }
184
+
185
+ function interfaceAliasOf(ts, checker, graph, statement, kind) {
186
+ if (statement.members.length || statement.typeParameters?.length) return [];
187
+ const heritage = (statement.heritageClauses ?? []).flatMap(clause => clause.types);
188
+ const [only] = heritage;
189
+ if (heritage.length !== 1 || only.typeArguments?.length || !bareAliasExpression(ts, only.expression)) return [];
190
+ const of = repositoryDeclaration(ts, checker, graph, only.expression, [kind.InterfaceDeclaration, kind.TypeAliasDeclaration, kind.ClassDeclaration]);
191
+ return of && of !== statement.name.text
192
+ ? [{ node: statement, name: statement.name.text, of, form: `export interface ${statement.name.text} extends ${only.expression.getText()} {}` }]
193
+ : [];
194
+ }
195
+
196
+ function declarationAliases(ts, checker, graph, statement) {
197
+ const kind = ts.SyntaxKind;
198
+ if (!checker || !isExported(ts, statement)) return [];
199
+ if (statement.kind === kind.VariableStatement) return variableAliases(ts, checker, graph, statement, kind);
200
+ if (statement.kind === kind.TypeAliasDeclaration) return typeAliasOf(ts, checker, graph, statement, kind);
201
+ if (statement.kind === kind.InterfaceDeclaration) return interfaceAliasOf(ts, checker, graph, statement, kind);
202
+ return [];
203
+ }
204
+
150
205
  /** The names Next.js reads from a route segment file (layout, page, route, ...): the framework fixes them. */
151
206
  const NEXT_SEGMENT_EXPORTS = new Set([
152
207
  'generateMetadata', 'metadata', 'generateViewport', 'viewport', 'generateStaticParams', 'generateImageMetadata', 'generateSitemaps',
@@ -154,42 +209,63 @@ const NEXT_SEGMENT_EXPORTS = new Set([
154
209
  ]);
155
210
  const ROUTE_SLOT = 'fe.route';
156
211
 
212
+ function isFrameworkName(node, name) {
213
+ return node.slot === ROUTE_SLOT && NEXT_SEGMENT_EXPORTS.has(name);
214
+ }
215
+
216
+ function addDeclarationAliasViolations(env, rel, node, statement, point, violations) {
217
+ const { ts, checker, graph } = env;
218
+ for (const alias of declarationAliases(ts, checker, graph, statement)) {
219
+ if (isFrameworkName(node, alias.name)) continue;
220
+ const at = point(alias.node);
221
+ violations.push({
222
+ ruleId: 'HFS_ALIAS_REEXPORT', path: rel, line: at.line + 1, column: at.character + 1, name: alias.name, aliasOf: alias.of,
223
+ message: `${alias.form} in ${rel} gives the declaration ${alias.of} a second name: use ${alias.of} where ${alias.name} is used and delete ${alias.name}.`,
224
+ });
225
+ }
226
+ }
227
+
228
+ function addNamespaceAliasViolation(rel, statement, clause, point, violations) {
229
+ const at = point(statement);
230
+ violations.push({
231
+ ruleId: 'HFS_ALIAS_REEXPORT', path: rel, line: at.line + 1, column: at.character + 1, name: clause.name.text,
232
+ message: `export * as ${clause.name.text} in ${rel} publishes a whole module under a second name: export the declarations it holds by their own names.`,
233
+ });
234
+ }
235
+
236
+ function addNamedAliasViolations(rel, node, clause, point, violations) {
237
+ for (const element of clause.elements) {
238
+ if (!element.propertyName || element.propertyName.text === element.name.text || isFrameworkName(node, element.name.text)) continue;
239
+ const at = point(element);
240
+ violations.push({
241
+ ruleId: 'HFS_ALIAS_REEXPORT', path: rel, line: at.line + 1, column: at.character + 1,
242
+ name: element.name.text, aliasOf: element.propertyName.text,
243
+ message: `export { ${element.propertyName.text} as ${element.name.text} } in ${rel} gives one declaration a second name: rename the declaration to ${element.name.text}, or import and export it as ${element.propertyName.text}.`,
244
+ });
245
+ }
246
+ }
247
+
248
+ function addExportAliasViolations(ts, rel, node, statement, point, violations) {
249
+ if (statement.kind !== ts.SyntaxKind.ExportDeclaration || !statement.exportClause) return;
250
+ const clause = statement.exportClause;
251
+ if (clause.kind === ts.SyntaxKind.NamespaceExport) {
252
+ addNamespaceAliasViolation(rel, statement, clause, point, violations);
253
+ return;
254
+ }
255
+ addNamedAliasViolations(rel, node, clause, point, violations);
256
+ }
257
+
258
+ function addStatementAliasViolations(context, graph, rel, node, statement, checker, violations) {
259
+ const point = target => node.sourceFile.getLineAndCharacterOfPosition(target.getStart(node.sourceFile));
260
+ addDeclarationAliasViolations({ ts: context.ts, checker, graph }, rel, node, statement, point, violations);
261
+ addExportAliasViolations(context.ts, rel, node, statement, point, violations);
262
+ }
263
+
157
264
  function aliasReexports({ context, graph }) {
158
- const kind = context.ts.SyntaxKind;
159
265
  const violations = [];
160
266
  for (const [rel, node] of graph.files) {
161
- const frameworkName = name => node.slot === ROUTE_SLOT && NEXT_SEGMENT_EXPORTS.has(name);
162
- const point = target => node.sourceFile.getLineAndCharacterOfPosition(target.getStart(node.sourceFile));
163
267
  const checker = context.checkerFor(node.abs);
164
- for (const statement of node.sourceFile.statements) {
165
- for (const alias of declarationAliases(context.ts, checker, graph, statement)) {
166
- if (frameworkName(alias.name)) continue;
167
- const at = point(alias.node);
168
- violations.push({
169
- ruleId: 'HFS_ALIAS_REEXPORT', path: rel, line: at.line + 1, column: at.character + 1, name: alias.name, aliasOf: alias.of,
170
- message: `${alias.form} in ${rel} gives the declaration ${alias.of} a second name: use ${alias.of} where ${alias.name} is used and delete ${alias.name}.`,
171
- });
172
- }
173
- if (statement.kind !== kind.ExportDeclaration || !statement.exportClause) continue;
174
- const clause = statement.exportClause;
175
- if (clause.kind === kind.NamespaceExport) {
176
- const at = point(statement);
177
- violations.push({
178
- ruleId: 'HFS_ALIAS_REEXPORT', path: rel, line: at.line + 1, column: at.character + 1, name: clause.name.text,
179
- message: `export * as ${clause.name.text} in ${rel} publishes a whole module under a second name: export the declarations it holds by their own names.`,
180
- });
181
- continue;
182
- }
183
- for (const element of clause.elements) {
184
- if (!element.propertyName || element.propertyName.text === element.name.text || frameworkName(element.name.text)) continue;
185
- const at = point(element);
186
- violations.push({
187
- ruleId: 'HFS_ALIAS_REEXPORT', path: rel, line: at.line + 1, column: at.character + 1,
188
- name: element.name.text, aliasOf: element.propertyName.text,
189
- message: `export { ${element.propertyName.text} as ${element.name.text} } in ${rel} gives one declaration a second name: rename the declaration to ${element.name.text}, or import and export it as ${element.propertyName.text}.`,
190
- });
191
- }
192
- }
268
+ for (const statement of node.sourceFile.statements) addStatementAliasViolations(context, graph, rel, node, statement, checker, violations);
193
269
  }
194
270
  return { violations };
195
271
  }
@@ -3,7 +3,8 @@ import { allowsFile } from '../allows.mjs';
3
3
  import { locateDeclaration } from '../slots.mjs';
4
4
  import { DEFAULT_ENVIRONMENT, STACKS_DIRECTORY, STATEFUL_KINDS, namesOfService, readStack } from '../../lib/stack-services.mjs';
5
5
  import { unwrapEach } from './ast-walks.mjs';
6
- import { nameText, sourceLocation } from '../../lib/ts-ast.mjs';
6
+ import { hasAnyFlag, nameText, sourceLocation } from '../../lib/ts-ast.mjs';
7
+ import { byCodeUnit } from '../../lib/list.mjs';
7
8
 
8
9
  /**
9
10
  * R47 `test-world-files` (BE_TEST_TOPOLOGY). Judgements over the test world, read through slots, the repository's own
@@ -59,7 +60,7 @@ function defineLiteral(ts, expression) {
59
60
  function configLiteral(ts, sourceFile) {
60
61
  for (const statement of sourceFile.statements) {
61
62
  if (!ts.isVariableStatement(statement) || !isExported(ts, statement)) continue;
62
- if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) continue;
63
+ if (!hasAnyFlag(statement.declarationList.flags, ts.NodeFlags.Const)) continue;
63
64
  for (const declaration of statement.declarationList.declarations) {
64
65
  const literal = declaration.initializer ? defineLiteral(ts, declaration.initializer) : null;
65
66
  if (literal) return literal;
@@ -73,7 +74,7 @@ function environmentOf(ts, literal) {
73
74
  const node = literal ? propertyOf(ts, literal, 'stack') : null;
74
75
  const value = node ? unwrap(ts, node) : null;
75
76
  if (!value || !ts.isStringLiteralLike(value)) return null;
76
- return value.text.split(/[\\/]+/u).filter(Boolean).at(-1) ?? null;
77
+ return value.text.split(/[\\/]+/u).filter(Boolean).findLast(Boolean) ?? null;
77
78
  }
78
79
 
79
80
  /** The keys of the declaration's `fakes` object literal, with their positions. */
@@ -86,57 +87,63 @@ function fakeEntriesOf(ts, sourceFile, literal) {
86
87
  .filter(entry => entry.name);
87
88
  }
88
89
 
90
+ /** The `fakedBy` entry of one `stacks` property, or null when the property does not declare one. */
91
+ function fakedByEntry(ts, sourceFile, property) {
92
+ if (!ts.isPropertyAssignment(property)) return null;
93
+ const body = unwrap(ts, property.initializer);
94
+ if (!body || !ts.isObjectLiteralExpression(body)) return null;
95
+ const fake = propertyOf(ts, body, 'fakedBy');
96
+ if (!fake) return null;
97
+ const reason = propertyOf(ts, body, 'reason');
98
+ return {
99
+ service: nameText(ts, property.name),
100
+ fake: ts.isStringLiteralLike(fake) ? fake.text : '',
101
+ reason: reason && ts.isStringLiteralLike(reason) ? reason.text.trim() : '',
102
+ ...sourceLocation(sourceFile, property),
103
+ };
104
+ }
105
+
89
106
  /** The `stacks` entries that declare `fakedBy`: [{service, fake, reason, line, column}]; a value the reader cannot read statically has an empty fake and reason. */
90
107
  function fakedByOf(ts, sourceFile, literal) {
91
108
  const node = literal ? propertyOf(ts, literal, 'stacks') : null;
92
109
  const value = node ? unwrap(ts, node) : null;
93
110
  if (!value || !ts.isObjectLiteralExpression(value)) return [];
94
- const entries = [];
95
- for (const property of value.properties) {
96
- if (!ts.isPropertyAssignment(property)) continue;
97
- const body = unwrap(ts, property.initializer);
98
- if (!body || !ts.isObjectLiteralExpression(body)) continue;
99
- const fake = propertyOf(ts, body, 'fakedBy');
100
- if (!fake) continue;
101
- const reason = propertyOf(ts, body, 'reason');
102
- entries.push({
103
- service: nameText(ts, property.name),
104
- fake: ts.isStringLiteralLike(fake) ? fake.text : '',
105
- reason: reason && ts.isStringLiteralLike(reason) ? reason.text.trim() : '',
106
- ...sourceLocation(sourceFile, property),
107
- });
108
- }
109
- return entries;
111
+ return value.properties.map(property => fakedByEntry(ts, sourceFile, property)).filter(Boolean);
110
112
  }
111
113
 
112
- export function checkTestWorldFiles(input) {
113
- const { config, graph, ts, resolver, tree } = checkerScope(input);
114
- const { suffixes } = resolver.ruleParams();
115
- const violations = [];
114
+ function registerFakeProvider(fakeProviders, relative, file) {
115
+ if (!relative.startsWith(FAKES_DIRECTORY)) return;
116
+ const [provider, ...rest] = relative.slice(FAKES_DIRECTORY.length).split('/');
117
+ if (rest.length > 0 && !fakeProviders.has(provider)) fakeProviders.set(provider, file);
118
+ }
119
+
120
+ function isRoleFileAtRoot(relative, suffixes) {
121
+ const parts = relative.slice(0, -'.ts'.length).split('.');
122
+ return !relative.includes('/') && parts.length >= 2 && parts.every(part => KEBAB.test(part)) && suffixes.includes(parts.at(-1));
123
+ }
124
+
125
+ function collectWorldFiles(resolver, tree, suffixes, violations) {
116
126
  const fakeProviders = new Map();
117
127
  let worldRoot = null;
118
128
  let files = 0;
119
- for (const file of [...tree.files].sort()) {
129
+ for (const file of [...tree.files].sort(byCodeUnit)) {
120
130
  if (!file.endsWith('.ts')) continue;
121
131
  const verdict = allowsFile(resolver, file);
122
- if (!verdict || verdict.slot !== WORLD_SLOT) continue;
132
+ if (verdict?.slot !== WORLD_SLOT) continue;
123
133
  files += 1;
124
134
  worldRoot ??= file.slice(0, file.length - verdict.relative.length);
125
- if (verdict.relative.startsWith(FAKES_DIRECTORY)) {
126
- const [provider, ...rest] = verdict.relative.slice(FAKES_DIRECTORY.length).split('/');
127
- if (rest.length > 0 && !fakeProviders.has(provider)) fakeProviders.set(provider, file);
128
- }
129
- if (verdict.allowed) continue;
130
- const parts = verdict.relative.slice(0, -'.ts'.length).split('.');
131
- const roleFileAtRoot = !verdict.relative.includes('/') && parts.length >= 2 && parts.every(part => KEBAB.test(part)) && suffixes.includes(parts.at(-1));
132
- if (roleFileAtRoot) continue;
135
+ registerFakeProvider(fakeProviders, verdict.relative, file);
136
+ if (verdict.allowed || isRoleFileAtRoot(verdict.relative, suffixes)) continue;
133
137
  violations.push({
134
138
  ruleId: RULE, path: file, line: 1, column: 1,
135
139
  message: `${file} is not allowed in the test world; src/tests/world/ holds only ${verdict.allows.join(', ')} and role-suffixed files (<name>.<role>.ts) at its root. Move it to fakes/ or kit/ (be.tests.world.kit), give it a role suffix, or delete it.`,
136
140
  slot: verdict.slot,
137
141
  });
138
142
  }
143
+ return { fakeProviders, worldRoot, files };
144
+ }
139
145
 
146
+ function worldDeclaration(config, graph, ts, worldRoot, violations) {
140
147
  const configPath = worldRoot === null ? null : `${worldRoot}${CONFIG_FILE}`;
141
148
  const configFile = configPath === null ? null : graph.files.get(configPath) ?? null;
142
149
  const literal = configFile ? configLiteral(ts, configFile.sourceFile) : null;
@@ -146,7 +153,10 @@ export function checkTestWorldFiles(input) {
146
153
  message: `${configPath} declares no world the machine can read. Declare it in the one named form \`${NAMED_FORM}\` (an exported const of a ${DEFINE} call with an object literal, imported from @starci/test-world); a default export is refused by R89 and is not read.`,
147
154
  });
148
155
  }
149
- const environments = [(literal ? environmentOf(ts, literal) : null) ?? defaultEnvironment];
156
+ return { configPath, configFile, literal };
157
+ }
158
+
159
+ function stackServices(config, environments) {
150
160
  const services = [];
151
161
  for (const environment of environments) {
152
162
  let stack = null;
@@ -154,11 +164,15 @@ export function checkTestWorldFiles(input) {
154
164
  try { stack = readStack({ root: locateDeclaration(config.root).appRoot, environment }); } catch { stack = null; }
155
165
  for (const service of stack?.services ?? []) if (service.role !== 'service' && !services.some(known => known.name === service.name)) services.push({ ...service, environment });
156
166
  }
157
- const stackHint = `${STACKS_DIRECTORY}/${environments.join(', ')}`;
158
- const statefulWhy = service => (STATEFUL_KINDS.has(service.kind) ? `a ${service.kind} holds data the app reads back` : service.persistent ? 'the stack gives it a persistent volume' : null);
159
- const declared = literal ? fakedByOf(ts, configFile.sourceFile, literal) : [];
160
- const fakeEntries = literal ? fakeEntriesOf(ts, configFile.sourceFile, literal) : [];
167
+ return services;
168
+ }
169
+
170
+ function statefulWhy(service) {
171
+ if (STATEFUL_KINDS.has(service.kind)) return `a ${service.kind} holds data the app reads back`;
172
+ return service.persistent ? 'the stack gives it a persistent volume' : null;
173
+ }
161
174
 
175
+ function reportDeclaredFakeProblems(declared, services, fakeProviders, fakeEntries, configPath, stackHint, violations) {
162
176
  for (const entry of declared) {
163
177
  const service = services.find(candidate => candidate.name === entry.service);
164
178
  const problems = [];
@@ -173,21 +187,48 @@ export function checkTestWorldFiles(input) {
173
187
  });
174
188
  }
175
189
  }
190
+ }
176
191
 
177
- // Every fake the world runs: the repository's fakes/<provider>/ folders and the fakes entries of the declaration.
178
- const fakes = [
192
+ function worldFakes(fakeProviders, fakeEntries, configPath) {
193
+ return [
179
194
  ...[...fakeProviders].map(([provider, file]) => ({ name: provider, path: file, line: 1, column: 1, what: `fakes/${provider}/` })),
180
195
  ...fakeEntries.filter(entry => !fakeProviders.has(entry.name)).map(entry => ({ name: entry.name, path: configPath, line: entry.line, column: entry.column, what: `the fakes entry ${entry.name}` })),
181
196
  ].sort((a, b) => a.name.localeCompare(b.name));
197
+ }
198
+
199
+ function reportServiceFakes(fakes, services, declared, configPath, stackHint, violations) {
182
200
  for (const fake of fakes) {
183
201
  const service = services.find(candidate => namesOfService(candidate).includes(fake.name));
184
202
  if (!service) continue;
185
203
  if (declared.some(entry => entry.service === service.name && entry.fake === fake.name)) continue; // judged on its stacks entry above
186
204
  const why = statefulWhy(service);
205
+ let reason = `${service.name} is stateful (${why}), so no exception applies. `;
206
+ if (!why) {
207
+ const worldConfig = configPath ?? `${CONFIG_FILE} in the world`;
208
+ reason = `Only stateless compute that needs special hardware or an external model may be faked, and only when ${worldConfig} declares it in its stacks entry ({ fakedBy, reason }). `;
209
+ }
187
210
  violations.push({
188
211
  ruleId: RULE, path: fake.path, line: fake.line, column: fake.column, slot: WORLD_SLOT,
189
- message: `${fake.what} fakes ${service.name} (${service.image}), which ${stackHint} declares: a service of the repository's own stack runs real in the test world. ${why ? `${service.name} is stateful (${why}), so no exception applies. ` : `Only stateless compute that needs special hardware or an external model may be faked, and only when ${configPath ?? `${CONFIG_FILE} in the world`} declares it in its stacks entry ({ fakedBy, reason }). `}Delete the fake and let the world run the real service; fakes are otherwise only for external SaaS the team does not operate.`,
212
+ message: `${fake.what} fakes ${service.name} (${service.image}), which ${stackHint} declares: a service of the repository's own stack runs real in the test world. ${reason}Delete the fake and let the world run the real service; fakes are otherwise only for external SaaS the team does not operate.`,
190
213
  });
191
214
  }
215
+ }
216
+
217
+ export function checkTestWorldFiles(input) {
218
+ const { config, graph, ts, resolver, tree } = checkerScope(input);
219
+ const { suffixes } = resolver.ruleParams();
220
+ const violations = [];
221
+ const { fakeProviders, worldRoot, files } = collectWorldFiles(resolver, tree, suffixes, violations);
222
+ const { configPath, configFile, literal } = worldDeclaration(config, graph, ts, worldRoot, violations);
223
+ const environments = [(literal ? environmentOf(ts, literal) : null) ?? defaultEnvironment];
224
+ const services = stackServices(config, environments);
225
+ const stackHint = `${STACKS_DIRECTORY}/${environments.join(', ')}`;
226
+ const declared = literal ? fakedByOf(ts, configFile.sourceFile, literal) : [];
227
+ const fakeEntries = literal ? fakeEntriesOf(ts, configFile.sourceFile, literal) : [];
228
+ reportDeclaredFakeProblems(declared, services, fakeProviders, fakeEntries, configPath, stackHint, violations);
229
+
230
+ // Every fake the world runs: the repository's fakes/<provider>/ folders and the fakes entries of the declaration.
231
+ const fakes = worldFakes(fakeProviders, fakeEntries, configPath);
232
+ reportServiceFakes(fakes, services, declared, configPath, stackHint, violations);
192
233
  return { violations, coverage: { status: 'checked', files } };
193
234
  }