@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
@@ -0,0 +1,311 @@
1
+ import path from 'node:path';
2
+ import { normalizedSymbolValue } from './ast-walks.mjs';
3
+ import { relativePath, unwrapExpression } from './typescript.mjs';
4
+ function importDeclarationFor(ts, node) {
5
+ for (let current = node; current; current = current.parent) if (ts.isImportDeclaration(current)) return current;
6
+ return null;
7
+ }
8
+
9
+ function isReactCreateElement(ts, checker, call) {
10
+ const expression = unwrapExpression(ts, call.expression);
11
+ if (ts.isIdentifier(expression)) {
12
+ const symbol = checker.getSymbolAtLocation(expression);
13
+ return (symbol?.declarations ?? []).some(declaration => {
14
+ const imported = ts.isImportSpecifier(declaration) ? declaration.propertyName?.text ?? declaration.name.text : null;
15
+ const parent = importDeclarationFor(ts, declaration);
16
+ return imported === 'createElement' && parent && ts.isStringLiteralLike(parent.moduleSpecifier) && parent.moduleSpecifier.text === 'react';
17
+ });
18
+ }
19
+ if (!ts.isPropertyAccessExpression(expression) || expression.name.text !== 'createElement') return false;
20
+ const symbol = checker.getSymbolAtLocation(expression.expression);
21
+ return (symbol?.declarations ?? []).some(declaration => {
22
+ const parent = importDeclarationFor(ts, declaration);
23
+ return (ts.isNamespaceImport(declaration) || ts.isImportClause(declaration))
24
+ && parent && ts.isStringLiteralLike(parent.moduleSpecifier) && parent.moduleSpecifier.text === 'react';
25
+ });
26
+ }
27
+
28
+ function jsxTagName(ts, tagName) {
29
+ if (ts.isIdentifier(tagName)) return tagName.text;
30
+ if (ts.isPropertyAccessExpression(tagName)) return tagName.name.text;
31
+ return null;
32
+ }
33
+
34
+ function wrapperTag(ts, tagName) {
35
+ const name = jsxTagName(ts, tagName);
36
+ return name === 'Suspense' || name === 'SWRConfig' || name === 'Provider'
37
+ || Boolean(name && (name.endsWith('Provider') || name.endsWith('ErrorBoundary')));
38
+ }
39
+
40
+ export class WorldRenderAnalysis {
41
+ constructor(world, helpers) {
42
+ this.world = world;
43
+ this.helpers = helpers;
44
+ }
45
+
46
+ expressionHasRender(expression, checker, seen = new Set()) {
47
+ expression = unwrapExpression(this.world.ts, expression);
48
+ if (!expression) return false;
49
+ if (this.world.ts.isJsxElement(expression) || this.world.ts.isJsxSelfClosingElement(expression) || this.world.ts.isJsxFragment(expression)) return true;
50
+ if (this.world.ts.isCallExpression(expression) && isReactCreateElement(this.world.ts, checker, expression)) return true;
51
+ if (this.world.ts.isConditionalExpression(expression)) return this.conditionalExpressionHasRender(expression, checker, seen);
52
+ const logicalRender = this.logicalExpressionHasRender(expression, checker, seen);
53
+ if (logicalRender !== null) return logicalRender;
54
+ if (this.world.ts.isCallExpression(expression)) {
55
+ const mapped = this.mappedExpressionHasRender(expression, checker, seen);
56
+ if (mapped !== null) return mapped;
57
+ }
58
+ return this.referencedExpressionHasRender(expression, checker, seen);
59
+ }
60
+
61
+ conditionalExpressionHasRender(expression, checker, seen) {
62
+ return this.expressionHasRender(expression.whenTrue, checker, seen)
63
+ || this.expressionHasRender(expression.whenFalse, checker, seen);
64
+ }
65
+
66
+ logicalExpressionHasRender(expression, checker, seen) {
67
+ const ts = this.world.ts;
68
+ if (!ts.isBinaryExpression(expression) || ![ts.SyntaxKind.AmpersandAmpersandToken, ts.SyntaxKind.BarBarToken,
69
+ ts.SyntaxKind.QuestionQuestionToken].includes(expression.operatorToken.kind)) return null;
70
+ return this.expressionHasRender(expression.left, checker, seen) || this.expressionHasRender(expression.right, checker, seen);
71
+ }
72
+
73
+ mappedExpressionHasRender(expression, checker, seen) {
74
+ const ts = this.world.ts;
75
+ if (!ts.isPropertyAccessExpression(expression.expression) && !ts.isElementAccessExpression(expression.expression)) return null;
76
+ let name = ts.isPropertyAccessExpression(expression.expression) ? expression.expression.name.text : null;
77
+ if (name === null && ts.isStringLiteralLike(expression.expression.argumentExpression)) name = expression.expression.argumentExpression.text;
78
+ if (name !== 'map' || !expression.arguments[0]) return null;
79
+ return this.world.expressionFunctions(expression.arguments[0], checker).some(fn => this.functionHasRender(fn, checker, seen));
80
+ }
81
+
82
+ referencedExpressionHasRender(expression, checker, seen) {
83
+ const ts = this.world.ts;
84
+ const selected = ts.isCallExpression(expression) ? expression.expression : expression;
85
+ const symbol = normalizedSymbolValue(ts, checker, this.helpers.selectedSymbol(ts, checker, selected));
86
+ if (!symbol || seen.has(symbol)) return false;
87
+ const nextSeen = new Set(seen).add(symbol);
88
+ if (ts.isIdentifier(expression) && this.identifierInitializerHasRender(symbol, checker, nextSeen)) return true;
89
+ if (ts.isCallExpression(expression)) return this.world.expressionFunctions(expression.expression, checker, seen)
90
+ .some(fn => this.functionHasRender(fn, checker, nextSeen));
91
+ return false;
92
+ }
93
+
94
+ identifierInitializerHasRender(symbol, checker, seen) {
95
+ for (const declaration of symbol.getDeclarations?.() ?? []) {
96
+ if (this.world.ts.isVariableDeclaration(declaration) && declaration.initializer
97
+ && this.expressionHasRender(declaration.initializer, checker, seen)) return true;
98
+ }
99
+ return false;
100
+ }
101
+
102
+ functionHasRender(fn, checker, seen = new Set()) {
103
+ if (this.world.renderOutputCache.has(fn)) return this.world.renderOutputCache.get(fn);
104
+ this.world.renderOutputCache.set(fn, false);
105
+ const found = this.helpers.returnedExpressions(this.world.ts, fn).some(expression => this.expressionHasRender(expression, checker, seen));
106
+ this.world.renderOutputCache.set(fn, found);
107
+ return found;
108
+ }
109
+
110
+ capturesOuterFunction(fn, checker) {
111
+ const outers = [];
112
+ for (let parent = fn.parent; parent; parent = parent.parent) if (this.world.ts.isFunctionLike(parent)) outers.push(parent);
113
+ if (!outers.length) return false;
114
+ const capture = { found: false };
115
+ this.visitOuterReferences(fn.body ?? fn, fn, checker, outers, capture);
116
+ return capture.found;
117
+ }
118
+
119
+ visitOuterReferences(node, fn, checker, outers, capture) {
120
+ if (capture.found) return;
121
+ if (this.referencesOuterFunction(node, fn, checker, outers)) {
122
+ capture.found = true;
123
+ return;
124
+ }
125
+ this.world.ts.forEachChild(node, child => this.visitOuterReferences(child, fn, checker, outers, capture));
126
+ }
127
+
128
+ referencesOuterFunction(node, fn, checker, outers) {
129
+ if (!this.world.ts.isIdentifier(node)) return false;
130
+ const symbol = checker.getSymbolAtLocation(node);
131
+ for (const declaration of symbol?.getDeclarations?.() ?? []) {
132
+ if (declaration.pos >= fn.pos && declaration.end <= fn.end) continue;
133
+ if (outers.some(outer => declaration.pos >= outer.pos && declaration.end <= outer.end)) return true;
134
+ }
135
+ return false;
136
+ }
137
+
138
+ pureRenderTarget(expression, checker, expectedFile = null) {
139
+ const functions = this.world.expressionFunctions(expression, checker);
140
+ return functions.length > 0 && functions.every(fn => this.world.sourceSet.has(path.resolve(fn.getSourceFile().fileName))
141
+ && (this.world.insideAny(this.world.roots.components, fn.getSourceFile().fileName) || this.world.insideAny(this.world.roots.features, fn.getSourceFile().fileName))
142
+ && (!expectedFile || path.resolve(fn.getSourceFile().fileName) === expectedFile)
143
+ && this.functionHasRender(fn, checker) && !this.world.functionUsesWorld(fn, checker) && !this.capturesOuterFunction(fn, checker));
144
+ }
145
+
146
+ expressionSuppliesRender(expression, checker) {
147
+ return this.expressionHasRender(expression, checker)
148
+ || this.world.expressionFunctions(expression, checker).some(fn => this.functionHasRender(fn, checker));
149
+ }
150
+
151
+ routedLayoutChild(opening, child, checker) {
152
+ const ts = this.world.ts;
153
+ if (!ts.isJsxExpression(child) || !child.expression) return false;
154
+ const source = opening.getSourceFile().fileName;
155
+ if (!/^apps\/[^/]+\/src\/features\/layouts\/[^/]+\/index\.tsx$/.test(relativePath(this.world.config.root, source))) return false;
156
+ const sibling = path.resolve(path.dirname(source), 'component.tsx');
157
+ if (!this.pureRenderTarget(opening.tagName, checker, sibling)) return false;
158
+ const value = unwrapExpression(ts, child.expression);
159
+ if (!ts.isPropertyAccessExpression(value) || !['content', 'children'].includes(value.name.text)) return false;
160
+ const receiver = unwrapExpression(ts, value.expression);
161
+ if (!ts.isIdentifier(receiver)) return false;
162
+ const symbol = checker.getSymbolAtLocation(receiver);
163
+ return (symbol?.getDeclarations?.() ?? []).some(declaration => ts.isParameter(declaration));
164
+ }
165
+
166
+ renderBoundary(expression, checker, seen = new Set(), allowEmpty = false, expectedFile = null) {
167
+ const ts = this.world.ts;
168
+ expression = unwrapExpression(ts, expression);
169
+ if (!expression) return allowEmpty;
170
+ if (expression.kind === ts.SyntaxKind.NullKeyword || expression.kind === ts.SyntaxKind.FalseKeyword) return allowEmpty;
171
+ if (ts.isConditionalExpression(expression)) return this.conditionalRenderBoundary(expression, checker, seen, allowEmpty, expectedFile);
172
+ if (ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
173
+ return this.renderBoundary(expression.right, checker, seen, true, expectedFile);
174
+ }
175
+ if (ts.isArrayLiteralExpression(expression)) return this.arrayRenderBoundary(expression, checker, seen, allowEmpty, expectedFile);
176
+ if (ts.isIdentifier(expression)) return this.identifierRenderBoundary(expression, checker, seen, allowEmpty, expectedFile);
177
+ if (ts.isCallExpression(expression)) return this.callRenderBoundary(expression, checker, seen, expectedFile);
178
+ if (ts.isJsxFragment(expression)) return this.fragmentRenderBoundary(expression, checker, seen, expectedFile);
179
+ if (ts.isJsxElement(expression) || ts.isJsxSelfClosingElement(expression)) return this.jsxRenderBoundary(expression, checker, seen, expectedFile);
180
+ return false;
181
+ }
182
+
183
+ conditionalRenderBoundary(expression, checker, seen, allowEmpty, expectedFile) {
184
+ return this.renderBoundary(expression.whenTrue, checker, seen, allowEmpty, expectedFile)
185
+ && this.renderBoundary(expression.whenFalse, checker, seen, allowEmpty, expectedFile);
186
+ }
187
+
188
+ arrayRenderBoundary(expression, checker, seen, allowEmpty, expectedFile) {
189
+ return expression.elements.length > 0
190
+ && expression.elements.every(item => this.renderBoundary(item, checker, seen, allowEmpty, expectedFile));
191
+ }
192
+
193
+ identifierRenderBoundary(expression, checker, seen, allowEmpty, expectedFile) {
194
+ const symbol = normalizedSymbolValue(this.world.ts, checker, checker.getSymbolAtLocation(expression));
195
+ if (!symbol || seen.has(symbol)) return false;
196
+ const nextSeen = new Set(seen).add(symbol);
197
+ const values = (symbol.getDeclarations?.() ?? []).flatMap(declaration => this.world.ts.isVariableDeclaration(declaration) && declaration.initializer
198
+ ? [declaration.initializer] : []);
199
+ if (values.length) return values.every(value => this.renderBoundary(value, checker, nextSeen, allowEmpty, expectedFile));
200
+ return false;
201
+ }
202
+
203
+ callRenderBoundary(expression, checker, seen, expectedFile) {
204
+ if (isReactCreateElement(this.world.ts, checker, expression)) return this.createElementBoundary(expression, checker, seen, expectedFile);
205
+ const mapped = this.mappedCallBoundary(expression, checker, seen, expectedFile);
206
+ if (mapped !== null) return mapped;
207
+ return this.pureRenderTarget(expression.expression, checker, expectedFile);
208
+ }
209
+
210
+ createElementBoundary(expression, checker, seen, expectedFile) {
211
+ const ts = this.world.ts;
212
+ const target = expression.arguments[0] ? unwrapExpression(ts, expression.arguments[0]) : null;
213
+ if (!target || ts.isStringLiteralLike(target)) return false;
214
+ const pureTarget = this.pureRenderTarget(target, checker, expectedFile) && !wrapperTag(ts, target);
215
+ const wrapper = wrapperTag(ts, target);
216
+ if (!pureTarget && !wrapper) return false;
217
+ const boundary = { hasBoundary: pureTarget };
218
+ const props = expression.arguments[1] ? unwrapExpression(ts, expression.arguments[1]) : null;
219
+ if (!this.createElementPropsBoundary(props, checker, seen, expectedFile, boundary)) return false;
220
+ const children = expression.arguments.slice(2);
221
+ if (children.length) {
222
+ if (!children.every(child => this.renderBoundary(child, checker, seen, false, expectedFile))) return false;
223
+ boundary.hasBoundary = true;
224
+ }
225
+ return boundary.hasBoundary;
226
+ }
227
+
228
+ createElementPropsBoundary(props, checker, seen, expectedFile, boundary) {
229
+ const ts = this.world.ts;
230
+ if (!props || props.kind === ts.SyntaxKind.NullKeyword) return true;
231
+ if (!ts.isObjectLiteralExpression(props) || props.properties.some(property => ts.isSpreadAssignment(property))) return false;
232
+ return props.properties.every(property => this.propsPropertyBoundary(property, checker, seen, expectedFile, boundary));
233
+ }
234
+
235
+ propsPropertyBoundary(property, checker, seen, expectedFile, boundary) {
236
+ const ts = this.world.ts;
237
+ if (!ts.isPropertyAssignment(property)) return true;
238
+ const value = unwrapExpression(ts, property.initializer);
239
+ if (!this.expressionSuppliesRender(value, checker)) return true;
240
+ if (!this.pureRenderTarget(value, checker, expectedFile) && !this.renderBoundary(value, checker, seen, true, expectedFile)) return false;
241
+ const name = property.name && (ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name)) ? property.name.text : null;
242
+ if (['children', 'component', 'content', 'render', 'view'].includes(name)) boundary.hasBoundary = true;
243
+ return true;
244
+ }
245
+
246
+ mappedCallBoundary(expression, checker, seen, expectedFile) {
247
+ const ts = this.world.ts;
248
+ if (!ts.isPropertyAccessExpression(expression.expression) && !ts.isElementAccessExpression(expression.expression)) return null;
249
+ let name = ts.isPropertyAccessExpression(expression.expression) ? expression.expression.name.text : null;
250
+ if (name === null && ts.isStringLiteralLike(expression.expression.argumentExpression)) name = expression.expression.argumentExpression.text;
251
+ if (name !== 'map' || !expression.arguments[0]) return null;
252
+ const renderers = this.world.expressionFunctions(expression.arguments[0], checker);
253
+ return renderers.length > 0 && renderers.every(fn => !this.world.functionUsesWorld(fn, checker)
254
+ && this.helpers.returnedExpressions(ts, fn).length > 0
255
+ && this.helpers.returnedExpressions(ts, fn).every(value => this.renderBoundary(value, checker, seen, true, expectedFile)));
256
+ }
257
+
258
+ fragmentRenderBoundary(expression, checker, seen, expectedFile) {
259
+ const ts = this.world.ts;
260
+ const children = expression.children.filter(child => !ts.isJsxText(child) || child.text.trim());
261
+ return children.length > 0 && children.every(child => !ts.isJsxText(child)
262
+ && this.renderBoundary(ts.isJsxExpression(child) ? child.expression : child, checker, seen, false, expectedFile));
263
+ }
264
+
265
+ jsxRenderBoundary(expression, checker, seen, expectedFile) {
266
+ const ts = this.world.ts;
267
+ const opening = ts.isJsxElement(expression) ? expression.openingElement : expression;
268
+ const pureTarget = this.pureRenderTarget(opening.tagName, checker, expectedFile) && !wrapperTag(ts, opening.tagName);
269
+ const wrapper = wrapperTag(ts, opening.tagName);
270
+ if (!pureTarget && !wrapper) return false;
271
+ const boundary = { hasBoundary: pureTarget };
272
+ if (!this.jsxChildrenBoundary(expression, opening, checker, seen, expectedFile, pureTarget, boundary)) return false;
273
+ if (!this.jsxAttributesBoundary(opening, checker, seen, expectedFile, boundary)) return false;
274
+ return boundary.hasBoundary;
275
+ }
276
+
277
+ jsxChildrenBoundary(expression, opening, checker, seen, expectedFile, pureTarget, boundary) {
278
+ const ts = this.world.ts;
279
+ const children = ts.isJsxElement(expression)
280
+ ? expression.children.filter(child => !ts.isJsxText(child) || child.text.trim()) : [];
281
+ if (!children.length) return true;
282
+ const routedChild = pureTarget && children.length === 1 && this.routedLayoutChild(opening, children[0], checker);
283
+ if (!routedChild && !children.every(child => !ts.isJsxText(child)
284
+ && this.renderBoundary(ts.isJsxExpression(child) ? child.expression : child, checker, seen, false, expectedFile))) return false;
285
+ boundary.hasBoundary = true;
286
+ return true;
287
+ }
288
+
289
+ jsxAttributesBoundary(opening, checker, seen, expectedFile, boundary) {
290
+ const ts = this.world.ts;
291
+ return opening.attributes.properties.every(attribute => !ts.isJsxAttribute(attribute) || !attribute.initializer
292
+ || this.jsxAttributeBoundary(attribute, checker, seen, expectedFile, boundary));
293
+ }
294
+
295
+ jsxAttributeBoundary(attribute, checker, seen, expectedFile, boundary) {
296
+ const ts = this.world.ts;
297
+ const name = attribute.name.text;
298
+ if (['fallback', 'errorElement'].includes(name)) {
299
+ if (ts.isStringLiteral(attribute.initializer)) return false;
300
+ if (ts.isJsxExpression(attribute.initializer)
301
+ && !this.renderBoundary(attribute.initializer.expression, checker, seen, true, expectedFile)) return false;
302
+ }
303
+ if (ts.isJsxExpression(attribute.initializer) && attribute.initializer.expression
304
+ && this.expressionSuppliesRender(attribute.initializer.expression, checker)) {
305
+ if (!this.pureRenderTarget(attribute.initializer.expression, checker, expectedFile)
306
+ && !this.renderBoundary(attribute.initializer.expression, checker, seen, true, expectedFile)) return false;
307
+ if (['component', 'content', 'render', 'view'].includes(name)) boundary.hasBoundary = true;
308
+ }
309
+ return true;
310
+ }
311
+ }
@@ -0,0 +1,328 @@
1
+ import path from 'node:path';
2
+ import { normalizedSymbolValue, returnedExpressions as sharedReturnedExpressions } from './ast-walks.mjs';
3
+ import { reachableViolation, relativePath, unwrapExpression } from './typescript.mjs';
4
+ import { WorldRenderAnalysis } from './frontend-world-render.mjs';
5
+ const WORLD_NAVIGATION_CALLS = new Set(['useParams', 'usePathname', 'useRouter', 'useSearchParams', 'useSelectedLayoutSegment', 'useSelectedLayoutSegments']);
6
+ const WORLD_INTL_CALLS = new Set(['useLocale', 'useMessages', 'useNow', 'useTimeZone', 'useTranslations']);
7
+ const WORLD_SWR_CALLS = new Set(['default', 'useSWR', 'useSWRConfig', 'useSWRImmutable', 'useSWRMutation']);
8
+ const LOWER_COMPONENT_TIERS = new Set(['composites', 'branches', 'leaves']);
9
+ function selectedSymbol(ts, checker, expression) {
10
+ const selected = unwrapExpression(ts, expression);
11
+ if (!selected) return null;
12
+ if (ts.isIdentifier(selected)) return checker.getSymbolAtLocation(selected) ?? null;
13
+ if (ts.isPropertyAccessExpression(selected)) return checker.getSymbolAtLocation(selected.name) ?? null;
14
+ if (ts.isElementAccessExpression(selected) && ts.isStringLiteralLike(selected.argumentExpression)) {
15
+ return checker.getSymbolAtLocation(selected.argumentExpression)
16
+ ?? checker.getTypeAtLocation(selected.expression).getProperty(selected.argumentExpression.text)
17
+ ?? null;
18
+ }
19
+ return null;
20
+ }
21
+
22
+ /* The wider boundary set: `ts.isFunctionLike` (includes call signatures' declarations) rather than the
23
+ * four default callable kinds. */
24
+ const returnedExpressions = (ts, declaration) => sharedReturnedExpressions(ts, declaration, { functionLike: ts.isFunctionLike });
25
+
26
+ function knownWorldImport(specifier, imported) {
27
+ if (specifier === 'next-intl') return WORLD_INTL_CALLS.has(imported);
28
+ if (specifier === 'next/navigation' || /(?:^|\/)i18n\/navigation$/.test(specifier)) return WORLD_NAVIGATION_CALLS.has(imported);
29
+ if (specifier === 'swr' || specifier === 'swr/immutable' || specifier === 'swr/mutation') return WORLD_SWR_CALLS.has(imported);
30
+ if (specifier === 'next-auth/react') return imported === 'useSession';
31
+ return false;
32
+ }
33
+
34
+ class WorldAnalysis {
35
+ constructor(config, context, roots, helpers) {
36
+ this.config = config;
37
+ this.context = context;
38
+ this.roots = roots;
39
+ this.ts = context.ts;
40
+ Object.assign(this, helpers);
41
+ this.violations = [];
42
+ this.sourceSet = new Set(context.files.map(source => path.resolve(source.fileName)));
43
+ this.worldInfoCache = new Map();
44
+ this.functionWorldCache = new Map();
45
+ this.renderOutputCache = new Map();
46
+ this.intrinsicUiHookCache = new Map();
47
+ this.renderer = new WorldRenderAnalysis(this, { selectedSymbol, returnedExpressions });
48
+ }
49
+
50
+ forbiddenIntrinsicEdge(edge, nextVisiting) {
51
+ const forbidden = reachableViolation(this.context.edges, edge, target => this.insideAny(this.roots.transport, target)
52
+ || this.insideAny(this.roots.modules, target) || this.insideAny(this.roots.features, target) || this.insideAny(this.roots.routes, target)
53
+ || (this.insideAny(this.roots.hooks, target) && relativePath(this.rootFor(this.roots.hooks, target), target).split('/')[0] !== 'ui'),
54
+ { follow: candidate => candidate.runtime });
55
+ return Boolean(forbidden || (this.insideAny(this.roots.hooks, edge.to) && !this.intrinsicUiHookFile(edge.to, nextVisiting)));
56
+ }
57
+
58
+ forbiddenWorldImport(statement) {
59
+ if (!this.ts.isImportDeclaration(statement) || !statement.importClause || statement.importClause.isTypeOnly
60
+ || !this.ts.isStringLiteralLike(statement.moduleSpecifier)) return false;
61
+ const specifier = statement.moduleSpecifier.text;
62
+ const bindings = [];
63
+ if (statement.importClause.name) bindings.push('default');
64
+ const named = statement.importClause.namedBindings;
65
+ if (named && this.ts.isNamespaceImport(named)
66
+ && ['next/navigation', 'next-intl', 'swr', 'swr/immutable', 'swr/mutation', 'next-auth/react'].includes(specifier)) return true;
67
+ if (named && this.ts.isNamedImports(named)) {
68
+ for (const element of named.elements) if (!element.isTypeOnly) bindings.push(element.propertyName?.text ?? element.name.text);
69
+ }
70
+ return bindings.some(imported => knownWorldImport(specifier, imported));
71
+ }
72
+
73
+ containsUnshadowedFetch(sourceFile, checker) {
74
+ let found = false;
75
+ const visit = node => {
76
+ if (!found && this.ts.isCallExpression(node) && this.ts.isIdentifier(node.expression) && node.expression.text === 'fetch'
77
+ && !checker.getSymbolAtLocation(node.expression)) found = true;
78
+ if (!found) this.ts.forEachChild(node, visit);
79
+ };
80
+ visit(sourceFile);
81
+ return found;
82
+ }
83
+
84
+ intrinsicUiHookFile(fileName, visiting = new Set()) {
85
+ const resolved = path.resolve(fileName);
86
+ if (this.intrinsicUiHookCache.has(resolved)) return this.intrinsicUiHookCache.get(resolved);
87
+ const hookRoot = this.rootFor(this.roots.hooks, resolved);
88
+ if (!hookRoot || relativePath(hookRoot, resolved).split('/')[0] !== 'ui' || visiting.has(resolved)) return false;
89
+ const sourceFile = this.context.files.find(source => path.resolve(source.fileName) === resolved);
90
+ if (!sourceFile) return false;
91
+ this.intrinsicUiHookCache.set(resolved, false);
92
+ const nextVisiting = new Set(visiting).add(resolved);
93
+ const checker = this.context.checkerFor(resolved);
94
+ const edges = this.importsForSource(this.context, resolved);
95
+ let intrinsic = true;
96
+ for (const edge of edges.filter(candidate => candidate.runtime)) {
97
+ if (this.forbiddenIntrinsicEdge(edge, nextVisiting)) {
98
+ intrinsic = false;
99
+ break;
100
+ }
101
+ }
102
+ for (const statement of sourceFile.statements) {
103
+ if (!intrinsic) break;
104
+ if (this.forbiddenWorldImport(statement)) { intrinsic = false; break; }
105
+ }
106
+ if (intrinsic && this.containsUnshadowedFetch(sourceFile, checker)) intrinsic = false;
107
+ this.intrinsicUiHookCache.set(resolved, intrinsic);
108
+ return intrinsic;
109
+ }
110
+
111
+ intrinsicUiHookSymbol(symbol, checker) {
112
+ const identity = normalizedSymbolValue(this.ts, checker, symbol);
113
+ const declarations = identity?.getDeclarations?.() ?? [];
114
+ return declarations.length > 0 && declarations.every(declaration => this.intrinsicUiHookFile(declaration.getSourceFile().fileName));
115
+ }
116
+
117
+ worldEdgeOf(edge) {
118
+ return edge && Boolean(reachableViolation(this.context.edges, edge,
119
+ target => this.insideAny(this.roots.hooks, target) || this.insideAny(this.roots.transport, target),
120
+ { follow: candidate => candidate.reexport && candidate.runtime }));
121
+ }
122
+
123
+ collectNamedWorldImports(checker, named, specifier, worldEdge, symbols) {
124
+ for (const element of named.elements) {
125
+ if (element.isTypeOnly) continue;
126
+ const imported = element.propertyName?.text ?? element.name.text;
127
+ const symbol = checker.getSymbolAtLocation(element.name);
128
+ if (symbol && (worldEdge || knownWorldImport(specifier, imported))) symbols.add(symbol);
129
+ }
130
+ }
131
+
132
+ inspectWorldImportStatement(sourceFile, checker, byStart, symbols, namespaces, statement) {
133
+ if (!this.ts.isImportDeclaration(statement) || !statement.importClause || statement.importClause.isTypeOnly
134
+ || !this.ts.isStringLiteralLike(statement.moduleSpecifier)) return;
135
+ const specifier = statement.moduleSpecifier.text;
136
+ const worldEdge = this.worldEdgeOf(byStart.get(statement.moduleSpecifier.getStart(sourceFile)));
137
+ const clause = statement.importClause;
138
+ if (clause.name) {
139
+ const symbol = checker.getSymbolAtLocation(clause.name);
140
+ if (symbol && (worldEdge || knownWorldImport(specifier, 'default'))) symbols.add(symbol);
141
+ }
142
+ const named = clause.namedBindings;
143
+ if (named && this.ts.isNamedImports(named)) this.collectNamedWorldImports(checker, named, specifier, worldEdge, symbols);
144
+ if (named && this.ts.isNamespaceImport(named)) {
145
+ const symbol = checker.getSymbolAtLocation(named.name);
146
+ if (symbol) namespaces.set(symbol, { specifier, worldEdge });
147
+ }
148
+ }
149
+
150
+ importInfo(sourceFile) {
151
+ const key = path.resolve(sourceFile.fileName);
152
+ if (this.worldInfoCache.has(key)) return this.worldInfoCache.get(key);
153
+ const checker = this.context.checkerFor(sourceFile.fileName);
154
+ const edges = this.importsForSource(this.context, sourceFile.fileName);
155
+ const byStart = new Map(edges.map(edge => [edge.node.getStart(sourceFile), edge]));
156
+ const symbols = new Set();
157
+ const namespaces = new Map();
158
+ for (const statement of sourceFile.statements) this.inspectWorldImportStatement(sourceFile, checker, byStart, symbols, namespaces, statement);
159
+ const value = { checker, symbols, namespaces };
160
+ this.worldInfoCache.set(key, value);
161
+ return value;
162
+ }
163
+
164
+ collectExpressionFunctions(declaration, checker, seen, functions) {
165
+ if (this.ts.isFunctionDeclaration(declaration) || this.ts.isMethodDeclaration(declaration)) functions.push(declaration);
166
+ else if (this.ts.isVariableDeclaration(declaration) && declaration.initializer) {
167
+ functions.push(...this.expressionFunctions(declaration.initializer, checker, seen));
168
+ } else if (this.ts.isClassDeclaration(declaration)) {
169
+ functions.push(...declaration.members.filter(member => this.ts.isMethodDeclaration(member)
170
+ && member.name && (this.ts.isIdentifier(member.name) || this.ts.isStringLiteralLike(member.name)) && member.name.text === 'render'));
171
+ }
172
+ }
173
+
174
+ isComponentWrapperCall(call) {
175
+ const selected = unwrapExpression(this.ts, call.expression);
176
+ const name = (this.ts.isIdentifier(selected) && selected.text) || (this.ts.isPropertyAccessExpression(selected) && selected.name.text) || null;
177
+ return ['forwardRef', 'memo'].includes(name);
178
+ }
179
+
180
+ expressionFunctions(expression, checker, seen = new Set()) {
181
+ expression = unwrapExpression(this.ts, expression);
182
+ if (!expression) return [];
183
+ if (this.ts.isArrowFunction(expression) || this.ts.isFunctionExpression(expression) || this.ts.isMethodDeclaration(expression)
184
+ || this.ts.isFunctionDeclaration(expression)) return [expression];
185
+ if (this.ts.isConditionalExpression(expression)) {
186
+ return [...this.expressionFunctions(expression.whenTrue, checker, seen), ...this.expressionFunctions(expression.whenFalse, checker, seen)];
187
+ }
188
+ if (this.ts.isCallExpression(expression) && this.isComponentWrapperCall(expression) && expression.arguments[0]) {
189
+ return this.expressionFunctions(expression.arguments[0], checker, seen);
190
+ }
191
+ const symbol = normalizedSymbolValue(this.ts, checker, selectedSymbol(this.ts, checker, expression));
192
+ if (!symbol || seen.has(symbol)) return [];
193
+ const nextSeen = new Set(seen).add(symbol);
194
+ const functions = [];
195
+ for (const declaration of symbol.getDeclarations?.() ?? []) this.collectExpressionFunctions(declaration, checker, nextSeen, functions);
196
+ return [...new Set(functions.filter(fn => fn.body))];
197
+ }
198
+
199
+ symbolIsWorld(raw, sourceFile, propertyName = null) {
200
+ if (!raw) return false;
201
+ const info = this.importInfo(sourceFile);
202
+ if (this.intrinsicUiHookSymbol(raw, info.checker)) return false;
203
+ if (info.symbols.has(raw)) return true;
204
+ for (const [namespace, declaration] of info.namespaces) if (namespace === raw) {
205
+ return declaration.worldEdge || knownWorldImport(declaration.specifier, propertyName ?? '');
206
+ }
207
+ const identity = normalizedSymbolValue(this.ts, info.checker, raw);
208
+ return (identity?.getDeclarations?.() ?? []).some(declaration => {
209
+ const fileName = declaration.getSourceFile().fileName;
210
+ return this.insideAny(this.roots.hooks, fileName) || this.insideAny(this.roots.transport, fileName);
211
+ });
212
+ }
213
+
214
+ namespaceIsWorld(raw, sourceFile, propertyName) {
215
+ const info = this.importInfo(sourceFile);
216
+ const declaration = info.namespaces.get(raw);
217
+ return Boolean(declaration && (declaration.worldEdge || knownWorldImport(declaration.specifier, propertyName ?? '')));
218
+ }
219
+
220
+ worldMemberCallee(expression, checker, sourceFile) {
221
+ let property = this.ts.isPropertyAccessExpression(expression) ? expression.name.text : null;
222
+ if (property === null && this.ts.isStringLiteralLike(expression.argumentExpression)) property = expression.argumentExpression.text;
223
+ const namespace = checker.getSymbolAtLocation(expression.expression);
224
+ if (this.intrinsicUiHookSymbol(selectedSymbol(this.ts, checker, expression), checker)) return false;
225
+ if (this.namespaceIsWorld(namespace, sourceFile, property)) return true;
226
+ return this.symbolIsWorld(selectedSymbol(this.ts, checker, expression), sourceFile);
227
+ }
228
+
229
+ worldVariableInitializer(symbol, checker, nextSeen) {
230
+ for (const declaration of symbol.getDeclarations?.() ?? []) {
231
+ if (this.ts.isVariableDeclaration(declaration) && declaration.initializer
232
+ && this.calleeIsWorld(declaration.initializer, checker, declaration.getSourceFile(), nextSeen)) return true;
233
+ }
234
+ return false;
235
+ }
236
+
237
+ calleeIsWorld(input, checker, sourceFile, seen = new Set()) {
238
+ const expression = unwrapExpression(this.ts, input);
239
+ if (this.ts.isIdentifier(expression) && this.symbolIsWorld(checker.getSymbolAtLocation(expression), sourceFile)) return true;
240
+ if (this.ts.isPropertyAccessExpression(expression) || this.ts.isElementAccessExpression(expression))
241
+ if (this.worldMemberCallee(expression, checker, sourceFile)) return true;
242
+ const symbol = normalizedSymbolValue(this.ts, checker, selectedSymbol(this.ts, checker, expression));
243
+ if (!symbol || seen.has(symbol)) return false;
244
+ const nextSeen = new Set(seen).add(symbol);
245
+ if (this.worldVariableInitializer(symbol, checker, nextSeen)) return true;
246
+ return this.expressionFunctions(expression, checker).some(fn => this.functionUsesWorld(fn, checker, nextSeen));
247
+ }
248
+
249
+ callIsWorld(call, checker, sourceFile, seen) { return this.calleeIsWorld(call.expression, checker, sourceFile, seen); }
250
+
251
+ functionUsesWorld(fn, checker, seen = new Set()) {
252
+ if (this.functionWorldCache.has(fn)) return this.functionWorldCache.get(fn);
253
+ this.functionWorldCache.set(fn, false);
254
+ let found = false;
255
+ const sourceFile = fn.getSourceFile();
256
+ const visit = node => {
257
+ if (found) return;
258
+ if (this.ts.isCallExpression(node) && this.callIsWorld(node, checker, sourceFile, seen)) { found = true; return; }
259
+ this.ts.forEachChild(node, visit);
260
+ };
261
+ visit(fn.body ?? fn);
262
+ this.functionWorldCache.set(fn, found);
263
+ return found;
264
+ }
265
+
266
+ collectFunctionNodes(node, functions) {
267
+ if (this.ts.isFunctionDeclaration(node) || this.ts.isMethodDeclaration(node) || this.ts.isArrowFunction(node) || this.ts.isFunctionExpression(node)) functions.push(node);
268
+ this.ts.forEachChild(node, child => this.collectFunctionNodes(child, functions));
269
+ }
270
+
271
+ inspectBlockProductHooks(sourceFile, checker, tier, functions) {
272
+ for (const fn of functions) {
273
+ const name = this.declarationName(this.ts, fn);
274
+ if (tier?.tier === 'blocks' && name && /^use[A-Z0-9]/.test(name.text) && this.functionUsesWorld(fn, checker)) {
275
+ this.violations.push(this.violation(this.config, sourceFile, name, 'FE_BLOCK_PRODUCT_HOOK_DEFINITION',
276
+ 'A block may call a product hook from hooks but cannot define product-world or data lifecycle under the block.', { ts: this.ts }));
277
+ }
278
+ }
279
+ }
280
+
281
+ inspectWorldOwner(sourceFile, checker, tier, fn) {
282
+ if (!this.renderer.functionHasRender(fn, checker) || !this.functionUsesWorld(fn, checker)) return;
283
+ const returned = returnedExpressions(this.ts, fn);
284
+ if (!returned.length || returned.some(expression => !this.renderer.renderBoundary(expression, checker, new Set(), true))) {
285
+ this.violations.push(this.violation(this.config, sourceFile, fn, 'FE_WORLD_OWNER_RENDER_BOUNDARY',
286
+ 'A visual owner that reads product/world state must hand every render path to a statically resolved pure render function or component; intrinsic-only interaction does not select this rule.', { ts: this.ts }));
287
+ }
288
+ if (tier && LOWER_COMPONENT_TIERS.has(tier.tier)) {
289
+ this.violations.push(this.violation(this.config, sourceFile, fn, 'FE_COMPONENT_WORLD_OWNERSHIP',
290
+ `${tier.tier} may own intrinsic browser interaction but cannot own product-world, navigation, locale, session or data lifecycle.`, { ts: this.ts }));
291
+ }
292
+ if (tier?.tier === 'blocks') this.inspectConnectedBlock(sourceFile, checker, fn, returned);
293
+ }
294
+
295
+ inspectConnectedBlock(sourceFile, checker, fn, returned) {
296
+ const expected = path.resolve(path.dirname(sourceFile.fileName), 'component.tsx');
297
+ const index = path.basename(sourceFile.fileName).toLowerCase() === 'index.tsx';
298
+ if (!index || !this.sourceSet.has(expected) || !returned.length
299
+ || returned.some(expression => !this.renderer.renderBoundary(expression, checker, new Set(), true, expected))) {
300
+ this.violations.push(this.violation(this.config, sourceFile, fn, 'FE_CONNECTED_BLOCK_RENDER_PAIR',
301
+ 'A connected blocks/index.tsx must hand every nonempty render path to a resolved pure export from its sibling component.tsx; pure blocks and lower tiers need no twin.', { ts: this.ts }));
302
+ }
303
+ }
304
+
305
+ inspectWorldOwners(sourceFile, checker, tier, functions) {
306
+ for (const fn of functions) this.inspectWorldOwner(sourceFile, checker, tier, fn);
307
+ }
308
+
309
+ inspectSourceFile(sourceFile) {
310
+ if ((!this.insideAny(this.roots.components, sourceFile.fileName) && !this.insideAny(this.roots.features, sourceFile.fileName))
311
+ || !/\.[cm]?tsx$/i.test(sourceFile.fileName)) return;
312
+ const checker = this.context.checkerFor(sourceFile.fileName);
313
+ const tier = this.tierOf(this.roots.components, sourceFile.fileName);
314
+ const functions = [];
315
+ this.collectFunctionNodes(sourceFile, functions);
316
+ this.inspectBlockProductHooks(sourceFile, checker, tier, functions);
317
+ this.inspectWorldOwners(sourceFile, checker, tier, functions);
318
+ }
319
+
320
+ check() {
321
+ for (const sourceFile of this.context.files) this.inspectSourceFile(sourceFile);
322
+ return this.violations;
323
+ }
324
+ }
325
+
326
+ export function checkWorldRenderBoundaries(config, context, roots, helpers) {
327
+ return new WorldAnalysis(config, context, roots, helpers).check();
328
+ }