@starci/hfs 4.0.10 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/emit/operations.mjs +1 -1
  3. package/emit/type-schema.mjs +2 -1
  4. package/lint/run.mjs +3 -2
  5. package/package.json +1 -1
  6. package/report/order.mjs +2 -0
  7. package/runtime/config.example.yaml +187 -0
  8. package/runtime/engine/by-code-unit.mjs +12 -0
  9. package/runtime/engine/config.mjs +221 -239
  10. package/runtime/engine/invalid-config.mjs +5 -5
  11. package/runtime/engine/model-config.mjs +90 -0
  12. package/runtime/engine/orca-config.mjs +3 -1
  13. package/runtime/engine/removed-vocabulary.mjs +28 -0
  14. package/runtime/engine/resources-config.mjs +52 -0
  15. package/runtime/engine/runtime-root.mjs +17 -0
  16. package/runtime/engine/secrets.mjs +35 -31
  17. package/runtime/engine/sonar-config.mjs +21 -0
  18. package/runtime/engine/temp-root.mjs +32 -0
  19. package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
  20. package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
  21. package/runtime/knowledge/hfs/rules.yaml +98 -65
  22. package/runtime/knowledge/hfs/slots.yaml +11 -14
  23. package/runtime/knowledge/patterns/be/api.yaml +7 -24
  24. package/runtime/knowledge/patterns/be/cli.yaml +3 -12
  25. package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
  26. package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
  27. package/runtime/modules/kernel/failure-codes.yaml +1 -41
  28. package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
  29. package/runtime/modules/models/registry.yaml +25 -359
  30. package/runtime/modules/models/runtimes.yaml +92 -256
  31. package/runtime/modules/models/tiers.yaml +65 -0
  32. package/runtime/scripts/api/fs/claim-file.mjs +30 -0
  33. package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
  34. package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
  35. package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
  36. package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
  37. package/runtime/scripts/api/git/lib.mjs +34 -1
  38. package/runtime/scripts/api/process/resolve-real-tool.mjs +14 -8
  39. package/runtime/scripts/api/process/run-program.mjs +2 -1
  40. package/runtime/scripts/api/sops/lib.mjs +198 -120
  41. package/runtime/scripts/hfs/allows.mjs +3 -3
  42. package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
  43. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
  44. package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
  45. package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
  46. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
  47. package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
  48. package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
  49. package/runtime/scripts/hfs/architecture/config.mjs +95 -80
  50. package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
  51. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
  52. package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
  53. package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
  54. package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
  55. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
  56. package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
  57. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
  58. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
  59. package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
  60. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
  61. package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
  62. package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
  63. package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
  64. package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
  65. package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
  66. package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
  67. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
  68. package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
  69. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
  70. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
  71. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
  72. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
  73. package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
  74. package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
  75. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
  76. package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
  77. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
  78. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
  79. package/runtime/scripts/hfs/architecture/index.mjs +158 -102
  80. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
  81. package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
  82. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
  83. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
  84. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
  85. package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
  86. package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
  87. package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
  88. package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
  89. package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
  90. package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
  91. package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
  92. package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
  93. package/runtime/scripts/hfs/architecture/required-files.mjs +100 -68
  94. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
  95. package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
  96. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
  97. package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
  98. package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
  99. package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
  100. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
  101. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
  102. package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
  103. package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
  104. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
  105. package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
  106. package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
  107. package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
  108. package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
  109. package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
  110. package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
  111. package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
  112. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
  113. package/runtime/scripts/hfs/architecture.mjs +3 -4
  114. package/runtime/scripts/hfs/check.mjs +61 -70
  115. package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
  116. package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
  117. package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
  118. package/runtime/scripts/hfs/edition-slots.mjs +5 -3
  119. package/runtime/scripts/hfs/linear-text.mjs +30 -0
  120. package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
  121. package/runtime/scripts/hfs/path-findings.mjs +58 -38
  122. package/runtime/scripts/hfs/pin-findings.mjs +31 -0
  123. package/runtime/scripts/hfs/repo-identity.mjs +5 -2
  124. package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
  125. package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
  126. package/runtime/scripts/hfs/rules/cli.mjs +5 -1
  127. package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
  128. package/runtime/scripts/hfs/rules/contract.mjs +35 -27
  129. package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
  130. package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
  131. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
  132. package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
  133. package/runtime/scripts/hfs/rules/database.mjs +36 -23
  134. package/runtime/scripts/hfs/rules/deps.mjs +64 -32
  135. package/runtime/scripts/hfs/rules/docker.mjs +113 -65
  136. package/runtime/scripts/hfs/rules/edition.mjs +118 -111
  137. package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
  138. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
  139. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
  140. package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
  141. package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
  142. package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
  143. package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
  144. package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
  145. package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
  146. package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
  147. package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
  148. package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
  149. package/runtime/scripts/hfs/rules/saga.mjs +128 -61
  150. package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
  151. package/runtime/scripts/hfs/rules/services.mjs +34 -16
  152. package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
  153. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
  154. package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
  155. package/runtime/scripts/hfs/secret.mjs +59 -48
  156. package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
  157. package/runtime/scripts/hfs/slot-classify.mjs +65 -0
  158. package/runtime/scripts/hfs/slot-errors.mjs +11 -0
  159. package/runtime/scripts/hfs/slot-imports.mjs +76 -0
  160. package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
  161. package/runtime/scripts/hfs/slot-match.mjs +153 -0
  162. package/runtime/scripts/hfs/slot-path.mjs +8 -0
  163. package/runtime/scripts/hfs/slot-required.mjs +76 -0
  164. package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
  165. package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
  166. package/runtime/scripts/hfs/slots.mjs +72 -693
  167. package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
  168. package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
  169. package/runtime/scripts/hfs/tree.mjs +19 -14
  170. package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
  171. package/runtime/scripts/hfs/view.mjs +1 -1
  172. package/runtime/scripts/lib/dockerfile.mjs +56 -33
  173. package/runtime/scripts/lib/event-contract.mjs +17 -13
  174. package/runtime/scripts/lib/git.mjs +2 -2
  175. package/runtime/scripts/lib/graphql-contract.mjs +158 -322
  176. package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
  177. package/runtime/scripts/lib/i18n.mjs +3 -2
  178. package/runtime/scripts/lib/in-order.mjs +71 -0
  179. package/runtime/scripts/lib/language.mjs +3 -3
  180. package/runtime/scripts/lib/list.mjs +8 -1
  181. package/runtime/scripts/lib/path-key.mjs +38 -5
  182. package/runtime/scripts/lib/pid-alive.mjs +7 -0
  183. package/runtime/scripts/lib/regex.mjs +1 -1
  184. package/runtime/scripts/lib/same-text.mjs +1 -1
  185. package/runtime/scripts/lib/secret-patterns.mjs +6 -2
  186. package/runtime/scripts/lib/sleep-sync.mjs +2 -2
  187. package/runtime/scripts/lib/sops-envelope.mjs +95 -48
  188. package/runtime/scripts/lib/stack-services.mjs +1 -2
  189. package/runtime/scripts/lib/ts-ast.mjs +9 -0
  190. package/runtime/scripts/lib/walk.mjs +6 -1
  191. package/scaffold/add-cli-lite.mjs +2 -1
  192. package/scaffold/add-table.mjs +2 -2
  193. package/scaffold/add.mjs +3 -2
  194. package/scaffold/app.mjs +4 -2
  195. package/scaffold/edition-gate.mjs +25 -25
  196. package/scaffold/lite-exports.mjs +2 -1
  197. package/scaffold/service.mjs +5 -4
  198. package/sync/hygiene.mjs +15 -9
  199. package/sync/index.mjs +7 -3
  200. package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
  201. package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
  202. package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
  203. package/templates/app/ci-workflows/sonar-steps.yml +2 -2
  204. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
  205. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
  206. package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
  207. package/templates/app/starciwork.gitignore +1 -1
  208. package/templates/be/image/api/Dockerfile +1 -1
  209. package/templates/be/image/cli/Dockerfile +1 -1
  210. package/templates/be/image/worker/Dockerfile +1 -1
  211. package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
  212. package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
  213. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
  214. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
  215. package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
  216. package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
  217. package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
  218. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
  219. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
  220. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
  221. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
  222. package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
  223. package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
  224. package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
  225. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
  226. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  227. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
  228. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
  229. package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
  230. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  231. package/templates/fe/image/next/Dockerfile +2 -1
  232. package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
  233. package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
  234. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
  235. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
  236. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
  237. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
  238. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
  239. package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
  240. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
  241. package/upgrade/index.mjs +9 -2
  242. package/runtime/engine/admission.mjs +0 -308
  243. package/runtime/engine/canonical-json.mjs +0 -10
  244. package/runtime/engine/db/blob.mjs +0 -315
  245. package/runtime/engine/db/ledger-paths.mjs +0 -83
  246. package/runtime/engine/db/ledger.mjs +0 -1118
  247. package/runtime/engine/db/machine-connection.mjs +0 -135
  248. package/runtime/engine/db/machine-schema.mjs +0 -84
  249. package/runtime/engine/db/machine.mjs +0 -1367
  250. package/runtime/engine/db/migrations/machine/0001-init.sql +0 -924
  251. package/runtime/engine/db/migrations/runtime/0001-init.sql +0 -1108
  252. package/runtime/engine/db/provider-reservations.mjs +0 -101
  253. package/runtime/engine/digest.mjs +0 -16
  254. package/runtime/engine/refuse.mjs +0 -11
  255. package/runtime/modules/ops/_labels.yaml +0 -53
  256. package/runtime/scripts/api/node/lib.mjs +0 -14
  257. package/runtime/scripts/api/node/spawn-node.mjs +0 -6
  258. package/runtime/scripts/api/process/lib.mjs +0 -111
  259. package/runtime/scripts/api/process/owned-process.mjs +0 -78
  260. package/runtime/scripts/api/process/stop-owned-process.mjs +0 -9
  261. package/runtime/scripts/connectors/lib.mjs +0 -488
  262. package/runtime/scripts/lib/clip.mjs +0 -19
  263. package/runtime/scripts/lib/display-names.mjs +0 -259
  264. package/runtime/scripts/lib/example-refs.mjs +0 -158
  265. package/runtime/scripts/lib/json-schema.mjs +0 -52
  266. package/runtime/scripts/lib/process-identity.mjs +0 -6
  267. package/runtime/scripts/lib/read-yaml.mjs +0 -18
  268. package/runtime/scripts/lib/redact.mjs +0 -161
  269. package/runtime/scripts/lib/sleep.mjs +0 -7
  270. package/runtime/scripts/lib/source-phrases.mjs +0 -34
  271. package/runtime/scripts/lib/sqlite.mjs +0 -21
@@ -17,57 +17,66 @@ export const CROSS_APP_DUPLICATE_RULE_IDS = ['FE_CROSS_APP_DUPLICATE'];
17
17
  const RULE = 'FE_CROSS_APP_DUPLICATE';
18
18
  const SOURCE = /\.[cm]?tsx?$/u;
19
19
 
20
- export function checkCrossAppDuplicate({ graph, context }) {
21
- const ts = context.ts;
22
- const { SyntaxKind } = ts;
23
- const resolver = graph.resolver;
24
- const declaredApps = resolver.repo.apps.map(app => app.name);
25
- /** The app a file belongs to: the slot binding when a slot owns it, else the declared app whose directory holds it. */
26
- const appOf = rel => resolver.classifyPath(rel).bindings?.app ?? declaredApps.find(name => rel.startsWith(`apps/${name}/`)) ?? null;
27
- const onlyModuleEdges = file => file.sourceFile.statements.every(statement => ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement)
28
- || (ts.isExportAssignment(statement) && ts.isIdentifier(statement.expression)));
20
+ /** The app a file belongs to: the slot binding when a slot owns it, else the declared app whose directory holds it. */
21
+ const appOf = (resolver, declaredApps, rel) => resolver.classifyPath(rel).bindings?.app ?? declaredApps.find(name => rel.startsWith(`apps/${name}/`)) ?? null;
29
22
 
30
- const tokens = sourceFile => {
31
- const parts = [];
32
- const visit = node => {
33
- if (node.kind >= SyntaxKind.FirstJSDocNode && node.kind <= SyntaxKind.LastJSDocNode) return;
34
- if (node.kind === SyntaxKind.JsxText && node.containsOnlyTriviaWhiteSpaces) return;
35
- const children = node.getChildren(sourceFile);
36
- if (!children.length) {
37
- if (node.kind !== SyntaxKind.EndOfFileToken) parts.push(node.kind === SyntaxKind.JsxText ? node.getText(sourceFile).replace(/\s+/gu, ' ').trim() : node.getText(sourceFile));
38
- return;
39
- }
40
- for (const child of children) visit(child);
41
- };
42
- visit(sourceFile);
43
- return parts.join('\u0000');
44
- };
23
+ const onlyModuleEdges = (ts, file) => file.sourceFile.statements.every(statement => ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement)
24
+ || (ts.isExportAssignment(statement) && ts.isIdentifier(statement.expression)));
25
+
26
+ const leafText = (SyntaxKind, node, sourceFile) => (node.kind === SyntaxKind.JsxText ? node.getText(sourceFile).replace(/\s+/gu, ' ').trim() : node.getText(sourceFile));
27
+
28
+ function collectTokens(SyntaxKind, sourceFile, node, parts) {
29
+ if (node.kind >= SyntaxKind.FirstJSDocNode && node.kind <= SyntaxKind.LastJSDocNode) return;
30
+ if (node.kind === SyntaxKind.JsxText && node.containsOnlyTriviaWhiteSpaces) return;
31
+ const children = node.getChildren(sourceFile);
32
+ if (!children.length) {
33
+ if (node.kind !== SyntaxKind.EndOfFileToken) parts.push(leafText(SyntaxKind, node, sourceFile));
34
+ return;
35
+ }
36
+ for (const child of children) collectTokens(SyntaxKind, sourceFile, child, parts);
37
+ }
45
38
 
39
+ const tokenText = (SyntaxKind, sourceFile) => {
40
+ const parts = [];
41
+ collectTokens(SyntaxKind, sourceFile, sourceFile, parts);
42
+ return parts.join('\u0000');
43
+ };
44
+
45
+ /** Groups the comparable files by the hash of their token text; returns the groups and the number of files compared. */
46
+ function groupByContent(graph, ts, declaredApps) {
46
47
  const groups = new Map();
47
48
  let files = 0;
48
49
  for (const file of graph.files.values()) {
49
50
  if (!SOURCE.test(file.rel) || /\.d\.[cm]?tsx?$/u.test(file.rel)) continue;
50
- const app = appOf(file.rel);
51
- if (!app || onlyModuleEdges(file) || !file.sourceFile.statements.length) continue;
51
+ const app = appOf(graph.resolver, declaredApps, file.rel);
52
+ if (!app || onlyModuleEdges(ts, file) || !file.sourceFile.statements.length) continue;
52
53
  files += 1;
53
- const key = createHash('sha256').update(tokens(file.sourceFile)).digest('hex');
54
+ const key = createHash('sha256').update(tokenText(ts.SyntaxKind, file.sourceFile)).digest('hex');
54
55
  if (!groups.has(key)) groups.set(key, []);
55
56
  groups.get(key).push({ rel: file.rel, app });
56
57
  }
58
+ return { groups, files };
59
+ }
60
+
61
+ const duplicateViolation = (group, item) => {
62
+ const twins = group.filter(other => other.app !== item.app).map(other => other.rel);
63
+ return {
64
+ ruleId: RULE, path: item.rel, line: 1, column: 1, twins,
65
+ message: `${item.rel} is token-for-token the same file as ${twins.join(', ')} in another app (comments and whitespace ignored); shared code is a packages/<pkg> slot. Move it to a package (packages/<pkg>) and let each app re-export it (${path.posix.basename(item.rel)} keeps only an import or export statement).`,
66
+ };
67
+ };
57
68
 
69
+ export function checkCrossAppDuplicate({ graph, context }) {
70
+ const ts = context.ts;
71
+ const declaredApps = graph.resolver.repo.apps.map(app => app.name);
72
+ const { groups, files } = groupByContent(graph, ts, declaredApps);
58
73
  const violations = [];
59
74
  let duplicated = 0;
60
75
  for (const group of groups.values()) {
61
76
  if (new Set(group.map(item => item.app)).size < 2) continue;
62
77
  duplicated += 1;
63
78
  group.sort((a, b) => a.rel.localeCompare(b.rel));
64
- for (const item of group) {
65
- const twins = group.filter(other => other.app !== item.app).map(other => other.rel);
66
- violations.push({
67
- ruleId: RULE, path: item.rel, line: 1, column: 1, twins,
68
- message: `${item.rel} is token-for-token the same file as ${twins.join(', ')} in another app (comments and whitespace ignored); shared code is a packages/<pkg> slot. Move it to a package (packages/<pkg>) and let each app re-export it (${path.posix.basename(item.rel)} keeps only an import or export statement).`,
69
- });
70
- }
79
+ for (const item of group) violations.push(duplicateViolation(group, item));
71
80
  }
72
81
  return { violations, coverage: { status: 'checked', files, duplicatedGroups: duplicated } };
73
82
  }
@@ -62,65 +62,104 @@ function extraEntries(graph, entry) {
62
62
  return (found.slot ? graph.resolver.slot(found.slot)?.entries ?? [] : []).map(name => `${found.root}/${name}`).filter(rel => graph.files.has(rel));
63
63
  }
64
64
 
65
+ const importBindings = (ts, declaration) => {
66
+ const clause = declaration.importClause;
67
+ if (!clause) return { all: false, pairs: [] }; // side-effect import: uses nothing
68
+ const pairs = [];
69
+ if (clause.name) pairs.push({ imported: 'default', exported: null });
70
+ const named = clause.namedBindings;
71
+ if (named && named.kind === ts.SyntaxKind.NamespaceImport) return { all: true, pairs: [] };
72
+ if (named) for (const element of named.elements) pairs.push({ imported: (element.propertyName ?? element.name).text, exported: null });
73
+ return { all: false, pairs };
74
+ };
75
+
76
+ const exportBindings = (ts, declaration) => {
77
+ const clause = declaration.exportClause;
78
+ if (!clause || clause.kind === ts.SyntaxKind.NamespaceExport) return { all: true, pairs: [] };
79
+ return { all: false, pairs: clause.elements.map(element => ({ imported: (element.propertyName ?? element.name).text, exported: element.name.text })) };
80
+ };
81
+
82
+ const importTypeBindings = (ts, declaration) => {
83
+ let qualifier = declaration.qualifier;
84
+ if (!qualifier) return { all: true, pairs: [] };
85
+ while (qualifier.kind === ts.SyntaxKind.QualifiedName) qualifier = qualifier.left;
86
+ return { all: false, pairs: [{ imported: qualifier.text, exported: null }] };
87
+ };
88
+
65
89
  /** Names an import/export declaration takes from its target: {all} or {pairs: [{imported, exported}]}. */
66
90
  function bindingsOf(ts, declaration) {
67
- const kind = ts.SyntaxKind;
68
91
  if (!declaration) return { all: true, pairs: [] };
69
- if (declaration.kind === kind.ImportDeclaration) {
70
- const clause = declaration.importClause;
71
- if (!clause) return { all: false, pairs: [] }; // side-effect import: uses nothing
72
- const pairs = [];
73
- if (clause.name) pairs.push({ imported: 'default', exported: null });
74
- const named = clause.namedBindings;
75
- if (named && named.kind === kind.NamespaceImport) return { all: true, pairs: [] };
76
- if (named) for (const element of named.elements) pairs.push({ imported: (element.propertyName ?? element.name).text, exported: null });
77
- return { all: false, pairs };
78
- }
79
- if (declaration.kind === kind.ExportDeclaration) {
80
- const clause = declaration.exportClause;
81
- if (!clause || clause.kind === kind.NamespaceExport) return { all: true, pairs: [] };
82
- return { all: false, pairs: clause.elements.map(element => ({ imported: (element.propertyName ?? element.name).text, exported: element.name.text })) };
83
- }
84
- if (declaration.kind === kind.ImportTypeNode) {
85
- let qualifier = declaration.qualifier;
86
- if (!qualifier) return { all: true, pairs: [] };
87
- while (qualifier.kind === kind.QualifiedName) qualifier = qualifier.left;
88
- return { all: false, pairs: [{ imported: qualifier.text, exported: null }] };
89
- }
92
+ if (declaration.kind === ts.SyntaxKind.ImportDeclaration) return importBindings(ts, declaration);
93
+ if (declaration.kind === ts.SyntaxKind.ExportDeclaration) return exportBindings(ts, declaration);
94
+ if (declaration.kind === ts.SyntaxKind.ImportTypeNode) return importTypeBindings(ts, declaration);
90
95
  // ImportEqualsDeclaration, dynamic import(), require(): the whole module is taken.
91
96
  return { all: true, pairs: [] };
92
97
  }
93
98
 
99
+ const addExportName = (found, sourceFile, name, node) => {
100
+ if (found.has(name)) return;
101
+ found.set(name, sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1);
102
+ };
103
+
104
+ const addBindingNames = (ts, found, sourceFile, name, node) => {
105
+ if (name.kind === ts.SyntaxKind.Identifier) addExportName(found, sourceFile, name.text, node);
106
+ else for (const element of name.elements) if (element.kind === ts.SyntaxKind.BindingElement) addBindingNames(ts, found, sourceFile, element.name, node);
107
+ };
108
+
109
+ const addExportDeclarationNames = (ts, statement, add) => {
110
+ if (statement.exportClause?.kind === ts.SyntaxKind.NamedExports) {
111
+ for (const element of statement.exportClause.elements) add(element.name.text, element);
112
+ } else if (statement.exportClause?.kind === ts.SyntaxKind.NamespaceExport) add(statement.exportClause.name.text, statement);
113
+ };
114
+
115
+ const addStatementNames = (ts, found, sourceFile, statement) => {
116
+ const kind = ts.SyntaxKind;
117
+ const add = (name, node) => addExportName(found, sourceFile, name, node);
118
+ if (statement.kind === kind.ExportDeclaration) return addExportDeclarationNames(ts, statement, add);
119
+ if (statement.kind === kind.ExportAssignment) return add('default', statement);
120
+ const modifiers = statement.modifiers ?? [];
121
+ if (!modifiers.some(modifier => modifier.kind === kind.ExportKeyword)) return undefined;
122
+ if (modifiers.some(modifier => modifier.kind === kind.DefaultKeyword)) return add('default', statement);
123
+ if (statement.kind === kind.VariableStatement) {
124
+ for (const declaration of statement.declarationList.declarations) addBindingNames(ts, found, sourceFile, declaration.name, declaration);
125
+ return undefined;
126
+ }
127
+ return statement.name && statement.name.kind === kind.Identifier ? add(statement.name.text, statement) : undefined;
128
+ };
129
+
94
130
  /** The exported names of an entry source file, each with the line of its declaration. */
95
131
  export function exportedNames(ts, sourceFile) {
96
- const kind = ts.SyntaxKind;
97
132
  const found = new Map();
98
- const add = (name, node) => {
99
- if (found.has(name)) return;
100
- found.set(name, sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1);
101
- };
102
- const bindingNames = (name, node) => {
103
- if (name.kind === kind.Identifier) add(name.text, node);
104
- else for (const element of name.elements) if (element.kind === kind.BindingElement) bindingNames(element.name, node);
105
- };
106
- for (const statement of sourceFile.statements) {
107
- if (statement.kind === kind.ExportDeclaration) {
108
- if (statement.exportClause?.kind === kind.NamedExports) for (const element of statement.exportClause.elements) add(element.name.text, element);
109
- else if (statement.exportClause?.kind === kind.NamespaceExport) add(statement.exportClause.name.text, statement);
110
- continue;
111
- }
112
- if (statement.kind === kind.ExportAssignment) { add('default', statement); continue; }
113
- const modifiers = statement.modifiers ?? [];
114
- if (!modifiers.some(modifier => modifier.kind === kind.ExportKeyword)) continue;
115
- if (modifiers.some(modifier => modifier.kind === kind.DefaultKeyword)) { add('default', statement); continue; }
116
- if (statement.kind === kind.VariableStatement) for (const declaration of statement.declarationList.declarations) bindingNames(declaration.name, declaration);
117
- else if (statement.name && statement.name.kind === kind.Identifier) add(statement.name.text, statement);
118
- }
133
+ for (const statement of sourceFile.statements) addStatementNames(ts, found, sourceFile, statement);
119
134
  return found;
120
135
  }
121
136
 
122
137
  const withoutSlash = value => value.replace(/\/$/, '');
123
138
 
139
+ const addConfigTextRoots = (graph, directory, text, roots) => {
140
+ for (const match of text.matchAll(CONFIG_STRING)) {
141
+ const target = path.posix.join(directory, match[1]);
142
+ const hit = CONFIG_EXTENSIONS.map(extension => `${target}${extension}`).find(candidate => graph.files.has(candidate));
143
+ if (hit) roots.add(hit);
144
+ }
145
+ };
146
+
147
+ const addAppConfigRoots = (graph, config, app, roots) => {
148
+ const directory = `apps/${app.name}`;
149
+ let names = [];
150
+ try { names = fs.readdirSync(path.join(config.root, ...directory.split('/'))); } catch { return; }
151
+ for (const name of names) {
152
+ if (graph.resolver.classifyPath(`${directory}/${name}`).slot !== 'fe.app.next' || !/\.[cm]?[jt]sx?$/.test(name)) continue;
153
+ let text = '';
154
+ try { text = fs.readFileSync(path.join(config.root, ...directory.split('/'), name), 'utf8'); } catch { continue; }
155
+ addConfigTextRoots(graph, directory, text, roots);
156
+ }
157
+ };
158
+
159
+ const addFrameworkConfigRoots = (graph, config, roots) => {
160
+ for (const app of config.apps ?? []) addAppConfigRoots(graph, config, app, roots);
161
+ };
162
+
124
163
  /** The files the graph serves from: what an app slot requires, every route file, every package entry, every config-named file. */
125
164
  function rootFiles(graph, config) {
126
165
  const roots = new Set();
@@ -129,7 +168,7 @@ function rootFiles(graph, config) {
129
168
  if (!node.owner || node.tier !== 'app') continue;
130
169
  const slot = graph.resolver.slot(node.owner.slot);
131
170
  const inside = rel.slice(withoutSlash(node.owner.root).length + 1);
132
- if ((slot?.requires ?? []).some(name => name === inside)) roots.add(rel);
171
+ if ((slot?.requires ?? []).includes(inside)) roots.add(rel);
133
172
  }
134
173
  for (const [key, owner] of graph.ownerRoots) {
135
174
  if (owner.tier !== 'package') continue;
@@ -137,49 +176,29 @@ function rootFiles(graph, config) {
137
176
  if (entry) roots.add(entry);
138
177
  }
139
178
  // A framework config of an app (slot fe.app.next) names sources by string, never by import.
140
- for (const app of config.apps ?? []) {
141
- const directory = `apps/${app.name}`;
142
- let names = [];
143
- try { names = fs.readdirSync(path.join(config.root, ...directory.split('/'))); } catch { continue; }
144
- for (const name of names) {
145
- if (graph.resolver.classifyPath(`${directory}/${name}`).slot !== 'fe.app.next' || !/\.[cm]?[jt]sx?$/.test(name)) continue;
146
- let text = '';
147
- try { text = fs.readFileSync(path.join(config.root, ...directory.split('/'), name), 'utf8'); } catch { continue; }
148
- for (const match of text.matchAll(CONFIG_STRING)) {
149
- const target = path.posix.join(directory, match[1]);
150
- const hit = CONFIG_EXTENSIONS.map(extension => `${target}${extension}`).find(candidate => graph.files.has(candidate));
151
- if (hit) roots.add(hit);
152
- }
153
- }
154
- }
179
+ addFrameworkConfigRoots(graph, config, roots);
155
180
  return roots;
156
181
  }
157
182
 
183
+ const unusedFileViolation = (rel, node) => ({
184
+ ruleId: 'HFS_UNUSED_FILE',
185
+ path: rel, line: 1, column: 1,
186
+ slot: node.slot, owner: node.owner?.root ?? null,
187
+ message: `${rel} is not reached from any root (an app main.ts or app.module.ts, a route file, a package entry): nothing imports it, so no process runs it. Delete the file, or import it where it is used (a spec alone does not count).`,
188
+ });
189
+
158
190
  /** HFS_UNUSED_FILE: production files no root reaches through runtime or type-only imports and re-exports. */
159
191
  function deadFiles(graph, config) {
160
192
  const roots = rootFiles(graph, config);
161
- const forward = new Map();
162
- for (const edge of graph.edges) {
163
- if (!forward.has(edge.from)) forward.set(edge.from, []);
164
- forward.get(edge.from).push(edge.to);
165
- }
193
+ const forward = Map.groupBy(graph.edges, edge => edge.from);
166
194
  const reached = new Set(roots);
167
195
  const queue = [...roots];
168
- while (queue.length) for (const next of forward.get(queue.shift()) ?? []) if (!reached.has(next)) { reached.add(next); queue.push(next); }
169
- const violations = [];
170
- let judged = 0;
171
- for (const [rel, node] of [...graph.files].sort(([a], [b]) => a.localeCompare(b))) {
172
- if (node.tier === null || UNJUDGED_TIERS.has(node.tier) || node.tier === 'route') continue;
173
- judged += 1;
174
- if (reached.has(rel)) continue;
175
- violations.push({
176
- ruleId: 'HFS_UNUSED_FILE',
177
- path: rel, line: 1, column: 1,
178
- slot: node.slot, owner: node.owner?.root ?? null,
179
- message: `${rel} is not reached from any root (an app main.ts or app.module.ts, a route file, a package entry): nothing imports it, so no process runs it. Delete the file, or import it where it is used (a spec alone does not count).`,
180
- });
181
- }
182
- return { violations, judged, roots: roots.size };
196
+ while (queue.length) for (const { to } of forward.get(queue.shift()) ?? []) if (!reached.has(to)) { reached.add(to); queue.push(to); }
197
+ const judgedFiles = [...graph.files]
198
+ .sort(([a], [b]) => a.localeCompare(b))
199
+ .filter(([, node]) => !(node.tier === null || UNJUDGED_TIERS.has(node.tier) || node.tier === 'route'));
200
+ const violations = judgedFiles.filter(([rel]) => !reached.has(rel)).map(([rel, node]) => unusedFileViolation(rel, node));
201
+ return { violations, judged: judgedFiles.length, roots: roots.size };
183
202
  }
184
203
 
185
204
  /** The unit spec of a webhook, gateway or subscription door: a door has no service of its own, so its spec is the only unit spec of its kind. */
@@ -214,75 +233,113 @@ function inIntegrationOf(graph, to, provider) {
214
233
  return owner?.slot === INTEGRATION_SLOT && owner.bindings?.provider === provider;
215
234
  }
216
235
 
236
+ /** The pseudo edges of one test consumer file: its import/export declarations that resolve to graph files it may reach. */
237
+ function consumerFileEdges({ ts, options, graph, config }, rel, provider) {
238
+ const abs = path.join(config.root, ...rel.split('/'));
239
+ let text;
240
+ try { text = fs.readFileSync(abs, 'utf8'); } catch { return []; }
241
+ const sourceFile = ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, true);
242
+ const edges = [];
243
+ for (const statement of sourceFile.statements) {
244
+ if (!(ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement)) || !statement.moduleSpecifier || !ts.isStringLiteralLike(statement.moduleSpecifier)) continue;
245
+ const resolved = ts.resolveModuleName(statement.moduleSpecifier.text, abs, options, ts.sys).resolvedModule?.resolvedFileName;
246
+ const to = resolved ? graph.abs(canonical(resolved)) : null;
247
+ if (to && (provider === null || inIntegrationOf(graph, to, provider))) edges.push({ from: rel, to, edge: { declaration: statement }, reexport: false });
248
+ }
249
+ return edges;
250
+ }
251
+
217
252
  /**
218
253
  * Pseudo edges (read from disk, outside the production program) to graph files: from the unit role specs, the fixture builders and
219
254
  * the test world files to anything, and from an integration spec only to the integration of its own provider folder.
220
255
  */
221
256
  function testConsumerEdges({ context, graph, config }) {
222
- const { ts } = context;
223
- const options = context.projects?.[0]?.options ?? {};
224
- const edges = [];
257
+ const resolution = { ts: context.ts, options: context.projects?.[0]?.options ?? {}, graph, config };
225
258
  const consumers = [...treeOf(config.root).files]
226
259
  .map(file => ({ file, any: testConsumer(graph, file) }))
227
260
  .map(({ file, any }) => ({ file, provider: any ? null : integrationSpecProvider(graph, file), any }))
228
261
  .filter(({ any, provider }) => any || provider !== null)
229
262
  .sort((a, b) => a.file.localeCompare(b.file));
230
- for (const { file: rel, provider } of consumers) {
231
- const abs = path.join(config.root, ...rel.split('/'));
232
- let text;
233
- try { text = fs.readFileSync(abs, 'utf8'); } catch { continue; }
234
- const sourceFile = ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, true);
235
- for (const statement of sourceFile.statements) {
236
- if (!(ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement)) || !statement.moduleSpecifier || !ts.isStringLiteralLike(statement.moduleSpecifier)) continue;
237
- const resolved = ts.resolveModuleName(statement.moduleSpecifier.text, abs, options, ts.sys).resolvedModule?.resolvedFileName;
238
- const to = resolved ? graph.abs(canonical(resolved)) : null;
239
- if (to && (provider === null || inIntegrationOf(graph, to, provider))) edges.push({ from: rel, to, edge: { declaration: statement }, reexport: false });
240
- }
241
- }
242
- return edges;
263
+ return consumers.flatMap(({ file: rel, provider }) => consumerFileEdges(resolution, rel, provider));
243
264
  }
244
265
 
245
- export function checkDeadExports({ context, graph, config }) {
246
- const { ts } = context;
247
- const violations = [];
266
+ const edgeIndex = edges => {
248
267
  const edgesTo = new Map();
249
- const unitOf = rel => {
250
- const known = graph.unit(rel);
251
- if (known) return known;
252
- const classified = graph.resolver.classifyPath(rel);
253
- const owner = classified.slot ? graph.resolver.ownerOf(rel) : null;
254
- return owner ? `${owner.slot}:${owner.root}` : null;
255
- };
256
- for (const edge of [...graph.edges, ...testConsumerEdges({ context, graph, config })]) {
268
+ for (const edge of edges) {
257
269
  if (!edgesTo.has(edge.to)) edgesTo.set(edge.to, []);
258
270
  edgesTo.get(edge.to).push(edge);
259
271
  }
272
+ return edgesTo;
273
+ };
260
274
 
261
- /** What the importers of `file` take from it; edges from inside `ownerKey` are skipped when given. */
262
- const consumed = (file, ownerKey, depth, visiting) => {
263
- const result = { all: false, names: new Set() };
264
- for (const edge of edgesTo.get(file) ?? []) {
265
- if (ownerKey && unitOf(edge.from) === ownerKey) continue;
266
- const bindings = bindingsOf(ts, edge.edge.declaration);
267
- if (bindings.all) { result.all = true; continue; }
268
- for (const pair of bindings.pairs) {
269
- if (pair.exported === null) { result.names.add(pair.imported); continue; }
270
- // A re-export by name: the name is used when the re-exporting file's own export is used further on.
271
- const reexporter = edge.from;
272
- if (visiting.has(reexporter) || depth >= CHAIN_DEPTH) { result.names.add(pair.imported); continue; }
273
- if (!(edgesTo.get(reexporter)?.length)) {
274
- // nobody imports the re-exporting file: a framework entry (route file) uses it, an unused owner entry does not
275
- const holder = graph.files.get(reexporter);
276
- if (!holder?.owner || graph.resolver.slot(holder.owner.slot)?.tier === 'app') result.names.add(pair.imported);
277
- continue;
278
- }
279
- const next = consumed(reexporter, null, depth + 1, new Set([...visiting, reexporter]));
280
- if (next.all || next.names.has(pair.exported)) result.names.add(pair.imported);
281
- }
282
- }
283
- return result;
284
- };
275
+ const unitOfFile = (graph, rel) => {
276
+ const known = graph.unit(rel);
277
+ if (known) return known;
278
+ const classified = graph.resolver.classifyPath(rel);
279
+ const owner = classified.slot ? graph.resolver.ownerOf(rel) : null;
280
+ return owner ? `${owner.slot}:${owner.root}` : null;
281
+ };
282
+
283
+ const consumeReexportedName = (context, result, edge, pair, depth, visiting) => {
284
+ if (pair.exported === null) { result.names.add(pair.imported); return; }
285
+ // A re-export by name: the name is used when the re-exporting file's own export is used further on.
286
+ const reexporter = edge.from;
287
+ if (visiting.has(reexporter) || depth >= CHAIN_DEPTH) { result.names.add(pair.imported); return; }
288
+ if (!(context.edgesTo.get(reexporter)?.length)) {
289
+ // nobody imports the re-exporting file: a framework entry (route file) uses it, an unused owner entry does not
290
+ const holder = context.graph.files.get(reexporter);
291
+ if (!holder?.owner || context.graph.resolver.slot(holder.owner.slot)?.tier === 'app') result.names.add(pair.imported);
292
+ return;
293
+ }
294
+ const next = consumedNames(context, reexporter, null, depth + 1, new Set([...visiting, reexporter]));
295
+ if (next.all || next.names.has(pair.exported)) result.names.add(pair.imported);
296
+ };
285
297
 
298
+ /** What the importers of `file` take from it; edges from inside `ownerKey` are skipped when given. */
299
+ const consumedNames = (context, file, ownerKey, depth, visiting) => {
300
+ const result = { all: false, names: new Set() };
301
+ for (const edge of context.edgesTo.get(file) ?? []) {
302
+ if (ownerKey && unitOfFile(context.graph, edge.from) === ownerKey) continue;
303
+ const bindings = bindingsOf(context.ts, edge.edge.declaration);
304
+ if (bindings.all) { result.all = true; continue; }
305
+ for (const pair of bindings.pairs) consumeReexportedName(context, result, edge, pair, depth, visiting);
306
+ }
307
+ return result;
308
+ };
309
+
310
+ /** Adds to `used` the entry exports reached by a consumer outside the owner that imports a file the entry re-exports by name. */
311
+ const addReexportedUses = (context, key, publicEntry, used) => {
312
+ const { ts, graph } = context;
313
+ for (const edge of graph.edges) {
314
+ if (edge.from !== publicEntry || !edge.reexport || graph.unit(edge.to) !== key) continue;
315
+ const reexport = bindingsOf(ts, edge.edge.declaration);
316
+ const inner = consumedNames(context, edge.to, key, 0, new Set([publicEntry, edge.to]));
317
+ if (reexport.all) continue;
318
+ for (const pair of reexport.pairs) if (inner.all || inner.names.has(pair.imported)) used.names.add(pair.exported);
319
+ }
320
+ };
321
+
322
+ const unusedExportViolation = (publicEntry, name, line, owner) => ({
323
+ ruleId: 'HFS_UNUSED_EXPORT',
324
+ path: publicEntry, line, column: 1,
325
+ name, owner: owner.root, slot: owner.slot,
326
+ message: `${publicEntry} exports ${name}, but no production file outside ${owner.root || 'the repository root'} imports it; remove the export (only a unit role spec, a fixture builder, a test world file, or an integration spec of this integration's own provider folder counts besides production files).`,
327
+ });
328
+
329
+ /** The unused-export violations and the export count of one public entry of an owner. */
330
+ const publicEntryExports = (context, key, owner, publicEntry) => {
331
+ const names = exportedNames(context.ts, context.graph.files.get(publicEntry).sourceFile);
332
+ const used = consumedNames(context, publicEntry, key, 0, new Set([publicEntry]));
333
+ addReexportedUses(context, key, publicEntry, used);
334
+ const violations = [...names]
335
+ .filter(([name]) => !(used.all || used.names.has(name)))
336
+ .map(([name, line]) => unusedExportViolation(publicEntry, name, line, owner));
337
+ return { violations, exports: names.size };
338
+ };
339
+
340
+ const ownerExportFindings = (context, config) => {
341
+ const { graph } = context;
342
+ const violations = [];
286
343
  let owners = 0;
287
344
  let exports = 0;
288
345
  for (const [key, owner] of graph.ownerRoots) {
@@ -291,30 +348,20 @@ export function checkDeadExports({ context, graph, config }) {
291
348
  if (!entry) continue;
292
349
  owners += 1;
293
350
  for (const publicEntry of [entry, ...extraEntries(graph, entry)]) {
294
- const names = exportedNames(ts, graph.files.get(publicEntry).sourceFile);
295
- const used = consumed(publicEntry, key, 0, new Set([publicEntry]));
296
- // A consumer outside the owner that imports a file the entry re-exports by name reaches the entry's export of that name.
297
- for (const edge of graph.edges) {
298
- if (edge.from !== publicEntry || !edge.reexport || graph.unit(edge.to) !== key) continue;
299
- const reexport = bindingsOf(ts, edge.edge.declaration);
300
- const inner = consumed(edge.to, key, 0, new Set([publicEntry, edge.to]));
301
- if (reexport.all) continue;
302
- for (const pair of reexport.pairs) if (inner.all || inner.names.has(pair.imported)) used.names.add(pair.exported);
303
- }
304
- for (const [name, line] of names) {
305
- exports += 1;
306
- if (used.all || used.names.has(name)) continue;
307
- violations.push({
308
- ruleId: 'HFS_UNUSED_EXPORT',
309
- path: publicEntry, line, column: 1,
310
- name, owner: owner.root, slot: owner.slot,
311
- message: `${publicEntry} exports ${name}, but no production file outside ${owner.root || 'the repository root'} imports it; remove the export (only a unit role spec, a fixture builder, a test world file, or an integration spec of this integration's own provider folder counts besides production files).`,
312
- });
313
- }
351
+ const found = publicEntryExports(context, key, owner, publicEntry);
352
+ exports += found.exports;
353
+ violations.push(...found.violations);
314
354
  }
315
355
  }
356
+ return { violations, owners, exports };
357
+ };
358
+
359
+ export function checkDeadExports({ context, graph, config }) {
360
+ const edgesTo = edgeIndex([...graph.edges, ...testConsumerEdges({ context, graph, config })]);
361
+ const analysis = { ...context, graph, edgesTo };
362
+ const exports = ownerExportFindings(analysis, config);
316
363
  const files = deadFiles(graph, config);
317
- const deadExports = violations.length;
318
- violations.push(...files.violations);
319
- return { violations, coverage: { status: 'checked', owners, exports, dead: deadExports, files: files.judged, roots: files.roots, unusedFiles: files.violations.length } };
364
+ const dead = exports.violations.length;
365
+ exports.violations.push(...files.violations);
366
+ return { violations: exports.violations, coverage: { status: 'checked', owners: exports.owners, exports: exports.exports, dead, files: files.judged, roots: files.roots, unusedFiles: files.violations.length } };
320
367
  }