@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
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { checkerScope } from './required-files.mjs';
4
4
  import { allowsFile } from '../allows.mjs';
5
5
  import { unwrapEach } from './ast-walks.mjs';
6
+ import { byCodeUnit } from '../../lib/list.mjs';
6
7
 
7
8
  /**
8
9
  * R47 `contract-fixture-guard` (BE_CONTRACT_UNGUARDED). A fake at the network edge serves payload fixtures; a fixture drifts
@@ -37,51 +38,87 @@ function walk(ts, root, visit) {
37
38
  step(root);
38
39
  }
39
40
 
40
- /** Facts one contract spec establishes: the providers whose fixtures it references and whether it compares shapes. */
41
- function readSpec(ts, sourceFile, helper, providers) {
41
+ const importsFakesOf = (from, provider) => from.includes(`${FAKES_DIRECTORY}${provider}/`) || from.endsWith(`${FAKES_DIRECTORY}${provider}`);
42
+
43
+ const shapeNamesOf = (ts, statement, helper) => {
44
+ const named = statement.importClause?.namedBindings;
45
+ if (!named || !ts.isNamedImports(named)) return [];
46
+ return named.elements.filter(element => (element.propertyName ?? element.name).text === helper).map(element => element.name.text);
47
+ };
48
+
49
+ const importsOfSpec = (ts, sourceFile, helper, providers) => {
42
50
  const shapeNames = new Set();
43
51
  const referenced = new Set();
44
52
  for (const statement of sourceFile.statements) {
45
53
  if (!ts.isImportDeclaration(statement)) continue;
46
54
  const from = statement.moduleSpecifier.text;
47
- for (const provider of providers) if (from.includes(`${FAKES_DIRECTORY}${provider}/`) || from.endsWith(`${FAKES_DIRECTORY}${provider}`)) referenced.add(provider);
48
- const named = statement.importClause?.namedBindings;
49
- if (!named || !ts.isNamedImports(named)) continue;
50
- for (const element of named.elements) if ((element.propertyName ?? element.name).text === helper) shapeNames.add(element.name.text);
55
+ for (const provider of providers) if (importsFakesOf(from, provider)) referenced.add(provider);
56
+ for (const name of shapeNamesOf(ts, statement, helper)) shapeNames.add(name);
57
+ }
58
+ return { shapeNames, referenced };
59
+ };
60
+
61
+ const callsShape = (ts, node, shapeNames) => {
62
+ let found = false;
63
+ walk(ts, node, inner => { if (ts.isCallExpression(inner) && ts.isIdentifier(inner.expression) && shapeNames.has(inner.expression.text)) found = true; });
64
+ return found;
65
+ };
66
+
67
+ const TEST_BLOCKS = new Set(['describe', 'it', 'test']);
68
+
69
+ const referenceProviders = (ts, node, providers, referenced) => {
70
+ if (ts.isStringLiteralLike(node)) for (const provider of providers) if (node.text.includes(`${FAKES_DIRECTORY}${provider}/`)) referenced.add(provider);
71
+ if (!ts.isCallExpression(node)) return;
72
+ const named = ts.isIdentifier(node.expression) ? node.expression.text : null;
73
+ for (const argument of node.arguments) {
74
+ const value = unwrap(ts, argument);
75
+ if (value && ts.isStringLiteralLike(value) && providers.has(value.text) && !TEST_BLOCKS.has(named)) referenced.add(value.text);
51
76
  }
52
- const callsShape = node => {
53
- let found = false;
54
- walk(ts, node, inner => { if (ts.isCallExpression(inner) && ts.isIdentifier(inner.expression) && shapeNames.has(inner.expression.text)) found = true; });
55
- return found;
56
- };
77
+ };
78
+
79
+ /** The `expect(...)` call a shape matcher call hangs off (through `.not` / `.resolves`), or null. */
80
+ const expectReceiverOf = (ts, callee) => {
81
+ let receiver = unwrap(ts, callee.expression);
82
+ while (receiver && ts.isPropertyAccessExpression(receiver)) receiver = unwrap(ts, receiver.expression);
83
+ return receiver && ts.isCallExpression(receiver) && ts.isIdentifier(receiver.expression) && receiver.expression.text === 'expect' ? receiver : null;
84
+ };
85
+
86
+ const comparesShapes = (ts, node, shapeNames) => {
87
+ if (!ts.isCallExpression(node)) return false;
88
+ const callee = node.expression;
89
+ if (!ts.isPropertyAccessExpression(callee) || !SHAPE_MATCHERS.has(callee.name.text)) return false;
90
+ const receiver = expectReceiverOf(ts, callee);
91
+ if (!receiver) return false;
92
+ const [actual] = receiver.arguments;
93
+ const [expected] = node.arguments;
94
+ return Boolean(actual && expected && callsShape(ts, actual, shapeNames) && callsShape(ts, expected, shapeNames));
95
+ };
96
+
97
+ const referenceAndCompare = (ts, node, providers, referenced, shapeNames) => {
98
+ referenceProviders(ts, node, providers, referenced);
99
+ return comparesShapes(ts, node, shapeNames);
100
+ };
101
+
102
+ /** Facts one contract spec establishes: the providers whose fixtures it references and whether it compares shapes. */
103
+ function readSpec(ts, sourceFile, helper, providers) {
104
+ const { shapeNames, referenced } = importsOfSpec(ts, sourceFile, helper, providers);
57
105
  let compares = false;
58
- walk(ts, sourceFile, node => {
59
- if (ts.isStringLiteralLike(node)) for (const provider of providers) if (node.text.includes(`${FAKES_DIRECTORY}${provider}/`)) referenced.add(provider);
60
- if (!ts.isCallExpression(node)) return;
61
- const named = ts.isIdentifier(node.expression) ? node.expression.text : null;
62
- for (const argument of node.arguments) {
63
- const value = unwrap(ts, argument);
64
- if (value && ts.isStringLiteralLike(value) && providers.has(value.text) && named !== 'describe' && named !== 'it' && named !== 'test') referenced.add(value.text);
65
- }
66
- const callee = node.expression;
67
- if (!ts.isPropertyAccessExpression(callee) || !SHAPE_MATCHERS.has(callee.name.text)) return;
68
- let receiver = unwrap(ts, callee.expression);
69
- while (receiver && ts.isPropertyAccessExpression(receiver)) receiver = unwrap(ts, receiver.expression); // .not / .resolves
70
- if (!receiver || !ts.isCallExpression(receiver) || !ts.isIdentifier(receiver.expression) || receiver.expression.text !== 'expect') return;
71
- const [actual] = receiver.arguments;
72
- const [expected] = node.arguments;
73
- if (actual && expected && callsShape(actual) && callsShape(expected)) compares = true;
74
- });
106
+ walk(ts, sourceFile, node => { if (referenceAndCompare(ts, node, providers, referenced, shapeNames)) compares = true; });
75
107
  return { referenced, compares };
76
108
  }
77
109
 
78
- export function checkContractFixtureGuard(input) {
79
- const { config, graph, ts, resolver, tree } = checkerScope(input);
80
- const { contractShape } = resolver.ruleParams();
81
- const violations = [];
110
+ /** The payload fixture a file of the world slot is, as `[provider, file]`, or null. */
111
+ const payloadFixtureOf = (resolver, file) => {
112
+ const verdict = allowsFile(resolver, file);
113
+ if (verdict?.slot !== WORLD_SLOT || !verdict.relative.startsWith(FAKES_DIRECTORY)) return null;
114
+ const match = PAYLOADS.exec(verdict.relative.slice(FAKES_DIRECTORY.length));
115
+ return match ? [match[1], file] : null;
116
+ };
117
+
118
+ const fixtureFilesOf = (tree, resolver) => {
82
119
  const fixtures = new Map(); // provider -> first payload fixture file
83
120
  const specs = new Map(); // provider -> [contract spec file]
84
- for (const file of [...tree.files].sort()) {
121
+ for (const file of [...tree.files].sort(byCodeUnit)) {
85
122
  const classified = resolver.classifyPath(file);
86
123
  if (classified.status !== 'owned') continue;
87
124
  if (classified.slot === CONTRACT_SLOT) {
@@ -89,14 +126,13 @@ export function checkContractFixtureGuard(input) {
89
126
  if (provider && file.endsWith('.contract-spec.ts')) specs.set(provider, [...(specs.get(provider) ?? []), file]);
90
127
  continue;
91
128
  }
92
- const verdict = classified.slot === WORLD_SLOT ? allowsFile(resolver, file) : null;
93
- if (verdict?.slot === WORLD_SLOT) {
94
- if (!verdict.relative.startsWith(FAKES_DIRECTORY)) continue;
95
- const match = PAYLOADS.exec(verdict.relative.slice(FAKES_DIRECTORY.length));
96
- if (match && !fixtures.has(match[1])) fixtures.set(match[1], file);
97
- }
129
+ const fixture = classified.slot === WORLD_SLOT ? payloadFixtureOf(resolver, file) : null;
130
+ if (fixture && !fixtures.has(fixture[0])) fixtures.set(...fixture);
98
131
  }
99
- const providers = new Set(fixtures.keys());
132
+ return { fixtures, specs };
133
+ };
134
+
135
+ const guardedProvidersOf = (config, ts, contractShape, providers, specs) => {
100
136
  const guarded = new Set();
101
137
  for (const [provider, files] of specs) {
102
138
  for (const file of files) {
@@ -107,6 +143,16 @@ export function checkContractFixtureGuard(input) {
107
143
  if (facts.compares && facts.referenced.has(provider)) guarded.add(provider);
108
144
  }
109
145
  }
146
+ return guarded;
147
+ };
148
+
149
+ export function checkContractFixtureGuard(input) {
150
+ const { config, ts, resolver, tree } = checkerScope(input);
151
+ const { contractShape } = resolver.ruleParams();
152
+ const violations = [];
153
+ const { fixtures, specs } = fixtureFilesOf(tree, resolver);
154
+ const providers = new Set(fixtures.keys());
155
+ const guarded = guardedProvidersOf(config, ts, contractShape, providers, specs);
110
156
  for (const [provider, file] of [...fixtures].sort(([a], [b]) => a.localeCompare(b))) {
111
157
  if (guarded.has(provider)) continue;
112
158
  const has = specs.get(provider)?.length ?? 0;
@@ -0,0 +1,342 @@
1
+ import path from 'node:path';
2
+ import { relativePath, unwrapExpression } from './typescript.mjs';
3
+ import { anyDescendant, nodeDecorators, normalizedSymbol, normalizedSymbolValue, valueSymbol, violation } from './ast-walks.mjs';
4
+ import { hasAnyFlag, isConstVariable, sourceLocation } from '../../lib/ts-ast.mjs';
5
+
6
+ export const READONLY_BOUNDARY_RULE_ID = 'BE_READONLY_BOUNDARY';
7
+
8
+ export function canonical(file) {
9
+ return path.resolve(file);
10
+ }
11
+
12
+ export function modifiers(ts, node) {
13
+ return ts.canHaveModifiers(node) ? ts.getModifiers(node) ?? [] : node.modifiers ?? [];
14
+ }
15
+
16
+ export function hasModifier(ts, node, kind) {
17
+ return modifiers(ts, node).some(modifier => modifier.kind === kind);
18
+ }
19
+
20
+ export function isPublicMember(ts, node) {
21
+ return !hasModifier(ts, node, ts.SyntaxKind.PrivateKeyword) && !hasModifier(ts, node, ts.SyntaxKind.ProtectedKeyword);
22
+ }
23
+
24
+ function readonlyMember(ts, node) {
25
+ return hasModifier(ts, node, ts.SyntaxKind.ReadonlyKeyword);
26
+ }
27
+
28
+ function propertyName(ts, node) {
29
+ const name = node.name;
30
+ if (ts.isIdentifier(name) || ts.isStringLiteralLike(name) || ts.isPrivateIdentifier(name)) return name.text;
31
+ return null;
32
+ }
33
+
34
+ function thisOriginStatus(ts, checker, expression, seen = new Set(), depth = 0) {
35
+ if (!expression || depth > 8) return 'no';
36
+ expression = unwrapExpression(ts, expression);
37
+ if (expression.kind === ts.SyntaxKind.ThisKeyword) return 'yes';
38
+ const symbol = normalizedSymbol(ts, checker, expression);
39
+ if (!symbol || seen.has(symbol)) return 'no';
40
+ const declarations = symbol.getDeclarations?.() ?? [];
41
+ if (declarations.length !== 1 || !ts.isVariableDeclaration(declarations[0]) || !declarations[0].initializer) return 'no';
42
+ const origin = thisOriginStatus(ts, checker, declarations[0].initializer, new Set(seen).add(symbol), depth + 1);
43
+ if (origin === 'no') return 'no';
44
+ return isConstVariable(ts, declarations[0]) ? origin : 'unavailable';
45
+ }
46
+
47
+ function instanceAssignmentTarget(ts, checker, expression) {
48
+ expression = unwrapExpression(ts, expression);
49
+ if (!ts.isPropertyAccessExpression(expression) && !ts.isElementAccessExpression(expression)) return { instance: false };
50
+ const owner = thisOriginStatus(ts, checker, expression.expression);
51
+ if (owner === 'no') return { instance: false };
52
+ if (owner === 'unavailable') return { instance: true, name: null, supported: false };
53
+ if (ts.isPropertyAccessExpression(expression)) return { instance: true, name: expression.name.text, supported: true };
54
+ if (ts.isStringLiteralLike(expression.argumentExpression)) return { instance: true, name: expression.argumentExpression.text, supported: true };
55
+ return { instance: true, name: null, supported: false };
56
+ }
57
+
58
+ function expressionOrigin(ts, checker, expression, seen = new Set(), depth = 0) {
59
+ if (!expression || depth > 8) return null;
60
+ expression = unwrapExpression(ts, expression);
61
+ const shorthand = ts.isShorthandPropertyAssignment(expression) ? checker.getShorthandAssignmentValueSymbol?.(expression) : null;
62
+ const symbol = normalizedSymbolValue(ts, checker, shorthand) ?? normalizedSymbol(ts, checker, expression);
63
+ if (!symbol || seen.has(symbol)) return symbol;
64
+ const declarations = symbol.getDeclarations?.() ?? [];
65
+ if (declarations.length === 1 && ts.isVariableDeclaration(declarations[0]) && declarations[0].initializer
66
+ && isConstVariable(ts, declarations[0])) {
67
+ return expressionOrigin(ts, checker, declarations[0].initializer, new Set(seen).add(symbol), depth + 1);
68
+ }
69
+ return symbol;
70
+ }
71
+
72
+ const expressionReferencesOrigin = (ts, checker, expression, expected) =>
73
+ anyDescendant(ts, expression, node => expressionOrigin(ts, checker, node) === expected);
74
+
75
+ function isSafePrimitiveProjection(ts, checker, expression, expected) {
76
+ expression = unwrapExpression(ts, expression);
77
+ if (!ts.isPropertyAccessExpression(expression) && !ts.isElementAccessExpression(expression)) return false;
78
+ if (expressionOrigin(ts, checker, expression.expression) !== expected) return false;
79
+ const type = checker.getTypeAtLocation(expression);
80
+ const primitive = ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike | ts.TypeFlags.BooleanLike
81
+ | ts.TypeFlags.BigIntLike | ts.TypeFlags.ESSymbolLike | ts.TypeFlags.Null | ts.TypeFlags.Undefined
82
+ | ts.TypeFlags.Void | ts.TypeFlags.Never;
83
+ return hasAnyFlag(type.flags, primitive) && !hasAnyFlag(type.flags, ts.TypeFlags.Any | ts.TypeFlags.Unknown);
84
+ }
85
+
86
+ function assignmentPatternHasInstanceTarget(ts, checker, expression) {
87
+ let found = false;
88
+ const visit = node => {
89
+ if (found) return;
90
+ const target = instanceAssignmentTarget(ts, checker, node);
91
+ if (target.instance) {
92
+ found = true;
93
+ return;
94
+ }
95
+ ts.forEachChild(node, visit);
96
+ };
97
+ visit(expression);
98
+ return found;
99
+ }
100
+
101
+ function globalLibraryIdentifier(ts, checker, node, expected) {
102
+ if (!ts.isIdentifier(node) || node.text !== expected) return false;
103
+ const symbol = normalizedSymbol(ts, checker, node);
104
+ const declarations = symbol?.getDeclarations?.() ?? [];
105
+ return declarations.length > 0 && declarations.every(declaration => declaration.getSourceFile().isDeclarationFile
106
+ && /(?:^|[/\\])lib\.[^/\\]+\.d\.ts$/i.test(declaration.getSourceFile().fileName));
107
+ }
108
+
109
+ function instanceMutationCall(ts, checker, node) {
110
+ if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) return null;
111
+ const owner = node.expression.expression;
112
+ const method = node.expression.name.text;
113
+ const known = (method === 'assign' || method === 'defineProperty') && globalLibraryIdentifier(ts, checker, owner, 'Object')
114
+ || method === 'set' && globalLibraryIdentifier(ts, checker, owner, 'Reflect');
115
+ if (!known || thisOriginStatus(ts, checker, node.arguments[0]) === 'no') return null;
116
+ return { node, values: node.arguments.slice(method === 'assign' ? 1 : 2) };
117
+ }
118
+
119
+ const isFunctionBoundary = (ts, node) => ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node)
120
+ || ts.isArrowFunction(node) || ts.isMethodDeclaration(node);
121
+
122
+ /** The instance assignment a `<left> = <origin-derived>` expression performs, or null when its target is no instance member. */
123
+ function instanceAssignment(ts, checker, node) {
124
+ const target = instanceAssignmentTarget(ts, checker, node.left);
125
+ if (target.instance) {
126
+ return { node, name: target.name, supported: target.supported && node.operatorToken.kind === ts.SyntaxKind.EqualsToken };
127
+ }
128
+ return assignmentPatternHasInstanceTarget(ts, checker, node.left) ? { node, name: null, supported: false } : null;
129
+ }
130
+
131
+ /** The assignment or mutation call at `node` that stores the constructor parameter on the instance, or null. */
132
+ function constructorFinding(ts, checker, node, parameterSymbol) {
133
+ if (ts.isBinaryExpression(node) && expressionReferencesOrigin(ts, checker, node.right, parameterSymbol)
134
+ && !isSafePrimitiveProjection(ts, checker, node.right, parameterSymbol)) {
135
+ return instanceAssignment(ts, checker, node);
136
+ }
137
+ const mutation = instanceMutationCall(ts, checker, node);
138
+ return mutation?.values.some(argument => expressionReferencesOrigin(ts, checker, argument, parameterSymbol))
139
+ ? { node, name: null, supported: false }
140
+ : null;
141
+ }
142
+
143
+ function constructorAssignments(ts, checker, constructor, parameter) {
144
+ if (!constructor.body) return [];
145
+ const parameterSymbol = normalizedSymbol(ts, checker, parameter.name);
146
+ const assignments = [];
147
+ const visit = node => {
148
+ if (node !== constructor.body && isFunctionBoundary(ts, node)) return;
149
+ const finding = constructorFinding(ts, checker, node, parameterSymbol);
150
+ if (finding) assignments.push(finding);
151
+ else ts.forEachChild(node, visit);
152
+ };
153
+ visit(constructor.body);
154
+ return assignments;
155
+ }
156
+
157
+ function checkReadonlyProperty(config, context, sourceFile, node, label, violations) {
158
+ if (!readonlyMember(context.ts, node)) violations.push(violation(config, sourceFile, node.name ?? node, READONLY_BOUNDARY_RULE_ID,
159
+ `${label} must be readonly; execution state must not mutate an injected dependency or received message.`));
160
+ }
161
+
162
+ function classValuesForSymbol(ts, symbol, localFiles) {
163
+ return (symbol?.getDeclarations?.() ?? []).flatMap(declaration => {
164
+ if (ts.isClassDeclaration(declaration)) return [declaration];
165
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
166
+ const initializer = unwrapExpression(ts, declaration.initializer);
167
+ if (ts.isClassExpression(initializer)) return [initializer];
168
+ }
169
+ return [];
170
+ }).filter(declaration => localFiles.has(canonical(declaration.getSourceFile().fileName)));
171
+ }
172
+
173
+ export function messageClass(config, context, checker, decorator, localFiles, reasons) {
174
+ const expression = decorator.expression;
175
+ if (!context.ts.isCallExpression(expression) || expression.arguments.length < 1) {
176
+ reasons.push(`${relativePath(config.root, decorator.getSourceFile().fileName)} has a handler decorator without a static message class`);
177
+ return null;
178
+ }
179
+ const symbol = valueSymbol(context.ts, checker, expression.arguments[0]);
180
+ const declarations = classValuesForSymbol(context.ts, symbol, localFiles);
181
+ if (declarations.length !== 1) {
182
+ reasons.push(`${relativePath(config.root, decorator.getSourceFile().fileName)} cannot resolve one local message class from its handler decorator`);
183
+ return null;
184
+ }
185
+ return declarations[0];
186
+ }
187
+
188
+ function inspectMessageConstructor(state, constructor) {
189
+ const { config, context, checker, sourceFile, violations, reasons } = state;
190
+ const ts = context.ts;
191
+ for (const parameter of constructor.parameters) {
192
+ if (modifiers(ts, parameter).some(modifier => [ts.SyntaxKind.PublicKeyword,
193
+ ts.SyntaxKind.PrivateKeyword, ts.SyntaxKind.ProtectedKeyword, ts.SyntaxKind.ReadonlyKeyword].includes(modifier.kind))) {
194
+ checkReadonlyProperty(config, context, sourceFile, parameter, `Message field ${propertyName(ts, parameter) ?? '(computed)'}`, violations);
195
+ }
196
+ }
197
+ if (!constructor.body) return;
198
+ const inspectAssign = node => {
199
+ if (node !== constructor.body && (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node))) return;
200
+ if (instanceMutationCall(ts, checker, node)) {
201
+ reasons.push(`${relativePath(config.root, sourceFile.fileName)} constructs message fields through an unsupported instance mutation`);
202
+ } else if (ts.isBinaryExpression(node) && assignmentPatternHasInstanceTarget(ts, checker, node.left)
203
+ && !instanceAssignmentTarget(ts, checker, node.left).supported) {
204
+ reasons.push(`${relativePath(config.root, sourceFile.fileName)} constructs message fields through an unsupported instance assignment`);
205
+ } else ts.forEachChild(node, inspectAssign);
206
+ };
207
+ inspectAssign(constructor.body);
208
+ }
209
+
210
+ function inspectMessageMembers(state) {
211
+ const { config, context, declaration, violations, reasons } = state;
212
+ const ts = context.ts;
213
+ const sourceFile = declaration.getSourceFile();
214
+ const classState = { ...state, sourceFile };
215
+ for (const member of declaration.members) {
216
+ if (ts.isPropertyDeclaration(member) && !hasModifier(ts, member, ts.SyntaxKind.StaticKeyword)) {
217
+ checkReadonlyProperty(config, context, sourceFile, member, `Message field ${propertyName(ts, member) ?? '(computed)'}`, violations);
218
+ }
219
+ if (ts.isSetAccessorDeclaration(member)) violations.push(violation(config, sourceFile, member.name, READONLY_BOUNDARY_RULE_ID,
220
+ `Message field ${propertyName(ts, member) ?? '(computed)'} cannot expose a setter.`));
221
+ if (ts.isIndexSignatureDeclaration(member)) reasons.push(`${relativePath(config.root, sourceFile.fileName)} has a message index signature whose mutation boundary cannot be proved`);
222
+ if (ts.isConstructorDeclaration(member)) inspectMessageConstructor(classState, member);
223
+ }
224
+ }
225
+
226
+ function classCarriesState(ts, declaration) {
227
+ return declaration.members.some(member => (ts.isPropertyDeclaration(member) && !hasModifier(ts, member, ts.SyntaxKind.StaticKeyword))
228
+ || ts.isSetAccessorDeclaration(member) || ts.isIndexSignatureDeclaration(member)
229
+ || (ts.isConstructorDeclaration(member) && member.parameters.length > 0));
230
+ }
231
+
232
+ function classInstanceType(ts, checker, declaration) {
233
+ const symbol = declaration.name ? normalizedSymbol(ts, checker, declaration.name) : null;
234
+ if (symbol && ts.isClassDeclaration(declaration)) return checker.getDeclaredTypeOfSymbol(symbol);
235
+ const value = checker.getTypeAtLocation(declaration);
236
+ const constructor = checker.getSignaturesOfType(value, ts.SignatureKind.Construct)[0];
237
+ return constructor ? checker.getReturnTypeOfSignature(constructor) : value;
238
+ }
239
+
240
+ function inspectMessageBases(state) {
241
+ const { config, context, checker, declaration, localFiles, sourceFile, reasons } = state;
242
+ const ts = context.ts;
243
+ const type = classInstanceType(ts, checker, declaration);
244
+ for (const base of type && checker.getBaseTypes ? checker.getBaseTypes(type) : []) {
245
+ const declarations = (base.symbol?.getDeclarations?.() ?? []).filter(item => ts.isClassDeclaration(item)) ?? [];
246
+ if (declarations.length !== 1) {
247
+ reasons.push(`${relativePath(config.root, sourceFile.fileName)} inherits message fields from a class outside the checked production program`);
248
+ } else if (localFiles.has(canonical(declarations[0].getSourceFile().fileName))) {
249
+ visitMessageClass({ ...state, declaration: declarations[0], sourceFile: declarations[0].getSourceFile() });
250
+ } else if (classCarriesState(ts, declarations[0])) {
251
+ reasons.push(`${relativePath(config.root, sourceFile.fileName)} inherits message state from a class outside the checked production program`);
252
+ }
253
+ }
254
+ }
255
+
256
+ function visitMessageClass(state) {
257
+ const { declaration, visited } = state;
258
+ const key = `${canonical(declaration.getSourceFile().fileName)}:${declaration.pos}`;
259
+ if (visited.has(key)) return;
260
+ visited.add(key);
261
+ const classState = { ...state, sourceFile: declaration.getSourceFile() };
262
+ inspectMessageMembers(classState);
263
+ inspectMessageBases(classState);
264
+ }
265
+
266
+ export function checkMessageReadonly(config, context, checker, declaration, localFiles, violations, reasons) {
267
+ visitMessageClass({ config, context, checker, declaration, localFiles, visited: new Set(), violations, reasons });
268
+ }
269
+
270
+ function inspectInjectedParameter(state, member, parameter) {
271
+ const { config, context, checker, sourceFile, classKind, properties, framework, violations, reasons } = state;
272
+ const ts = context.ts;
273
+ const explicitInject = nodeDecorators(ts, parameter).some(decorator => framework.decoratorKind(ts, checker, decorator, framework.targets) === 'Inject');
274
+ if (!classKind && !explicitInject) return;
275
+ const parameterProperty = modifiers(ts, parameter).some(modifier => [ts.SyntaxKind.PublicKeyword, ts.SyntaxKind.PrivateKeyword,
276
+ ts.SyntaxKind.ProtectedKeyword, ts.SyntaxKind.ReadonlyKeyword].includes(modifier.kind));
277
+ if (parameterProperty) {
278
+ checkReadonlyProperty(config, context, sourceFile, parameter, `Injected dependency ${propertyName(ts, parameter) ?? '(computed)'}`, violations);
279
+ return;
280
+ }
281
+ for (const assignment of constructorAssignments(ts, checker, member, parameter)) {
282
+ if (!assignment.supported || !assignment.name) {
283
+ reasons.push(`${relativePath(config.root, sourceFile.fileName)}:${sourceLocation(sourceFile, assignment.node).line} stores an injected dependency through an unsupported instance assignment`);
284
+ continue;
285
+ }
286
+ const property = properties.get(assignment.name);
287
+ if (!property) reasons.push(`${relativePath(config.root, sourceFile.fileName)} assigns injected dependency ${assignment.name} without a declared field`);
288
+ else checkReadonlyProperty(config, context, sourceFile, property, `Injected dependency ${assignment.name}`, violations);
289
+ }
290
+ }
291
+
292
+ function inspectInjectedConstructor(state, member) {
293
+ for (const parameter of member.parameters) {
294
+ inspectInjectedParameter(state, member, parameter);
295
+ }
296
+ }
297
+
298
+ function inspectInjectedMembers(state) {
299
+ const { config, context, checker, declaration, framework, violations } = state;
300
+ const ts = context.ts;
301
+ const sourceFile = declaration.getSourceFile();
302
+ const classState = { ...state, sourceFile };
303
+ for (const member of declaration.members) {
304
+ if (ts.isPropertyDeclaration(member)) {
305
+ const injected = nodeDecorators(ts, member).some(decorator => framework.decoratorKind(ts, checker, decorator, framework.targets) === 'Inject');
306
+ if (injected) checkReadonlyProperty(config, context, sourceFile, member, `Injected property ${propertyName(ts, member) ?? '(computed)'}`, violations);
307
+ continue;
308
+ }
309
+ if (!ts.isConstructorDeclaration(member)) continue;
310
+ inspectInjectedConstructor(classState, member);
311
+ }
312
+ }
313
+
314
+ function inspectInjectedBases(state) {
315
+ const { config, context, checker, declaration, localFiles, reasons } = state;
316
+ const ts = context.ts;
317
+ const sourceFile = declaration.getSourceFile();
318
+ const type = classInstanceType(ts, checker, declaration);
319
+ for (const base of type && checker.getBaseTypes ? checker.getBaseTypes(type) : []) {
320
+ const declarations = (base.symbol?.getDeclarations?.() ?? []).filter(item => ts.isClassDeclaration(item)) ?? [];
321
+ if (declarations.length !== 1) {
322
+ reasons.push(`${relativePath(config.root, sourceFile.fileName)} inherits an injected constructor from a class outside the checked production program`);
323
+ } else if (localFiles.has(canonical(declarations[0].getSourceFile().fileName))) {
324
+ checkInjectedClass({ ...state, declaration: declarations[0] });
325
+ } else if (classCarriesState(ts, declarations[0])) {
326
+ reasons.push(`${relativePath(config.root, sourceFile.fileName)} inherits an injected constructor from a class outside the checked production program`);
327
+ }
328
+ }
329
+ }
330
+
331
+ export function checkInjectedClass({ config, context, checker, declaration, classKind, targets, decoratorKind, localFiles, violations, reasons, visited = new Set() }) {
332
+ const ts = context.ts;
333
+ const classKey = `${canonical(declaration.getSourceFile().fileName)}:${declaration.pos}`;
334
+ if (visited.has(classKey)) return;
335
+ visited.add(classKey);
336
+ const properties = new Map(declaration.members.filter(member => ts.isPropertyDeclaration(member))
337
+ .map(member => [propertyName(ts, member), member]).filter(([name]) => name));
338
+ const state = { config, context, checker, declaration, classKind, properties, targets, decoratorKind,
339
+ framework: { targets, decoratorKind }, localFiles, violations, reasons, visited };
340
+ inspectInjectedMembers(state);
341
+ if (classKind) inspectInjectedBases(state);
342
+ }