@starci/hfs 4.0.10 → 4.2.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 (275) hide show
  1. package/CHANGELOG.md +5 -1
  2. package/emit/operations.mjs +1 -1
  3. package/emit/type-schema.mjs +2 -1
  4. package/lint/run.mjs +65 -24
  5. package/package.json +1 -1
  6. package/report/order.mjs +2 -0
  7. package/runtime/config.example.yaml +194 -0
  8. package/runtime/engine/by-code-unit.mjs +12 -0
  9. package/runtime/engine/config.mjs +222 -239
  10. package/runtime/engine/invalid-config.mjs +5 -5
  11. package/runtime/engine/model-config.mjs +90 -0
  12. package/runtime/engine/orca-config.mjs +3 -1
  13. package/runtime/engine/release-config.mjs +28 -0
  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 +35 -31
  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 +14 -14
  21. package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
  22. package/runtime/knowledge/hfs/rules.yaml +161 -65
  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 -41
  29. package/runtime/modules/kernel/removed-vocabulary.yaml +268 -0
  30. package/runtime/modules/models/registry.yaml +25 -359
  31. package/runtime/modules/models/runtimes.yaml +126 -265
  32. package/runtime/modules/models/tiers.yaml +66 -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/make-temp-dir.mjs +11 -0
  37. package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
  38. package/runtime/scripts/api/git/lib.mjs +34 -1
  39. package/runtime/scripts/api/process/resolve-real-tool.mjs +14 -8
  40. package/runtime/scripts/api/process/run-program.mjs +2 -1
  41. package/runtime/scripts/api/sops/lib.mjs +198 -120
  42. package/runtime/scripts/hfs/allows.mjs +3 -3
  43. package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
  44. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
  45. package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
  46. package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
  47. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
  48. package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
  49. package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
  50. package/runtime/scripts/hfs/architecture/config.mjs +95 -80
  51. package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
  52. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
  53. package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
  54. package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
  55. package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
  56. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
  57. package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
  58. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
  59. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
  60. package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
  61. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
  62. package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
  63. package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
  64. package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
  65. package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
  66. package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
  67. package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
  68. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
  69. package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
  70. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
  71. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
  72. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
  73. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
  74. package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
  75. package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
  76. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
  77. package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
  78. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
  79. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
  80. package/runtime/scripts/hfs/architecture/index.mjs +164 -104
  81. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
  82. package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
  83. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
  84. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
  85. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
  86. package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
  87. package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
  88. package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
  89. package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
  90. package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
  91. package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
  92. package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
  93. package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
  94. package/runtime/scripts/hfs/architecture/required-files.mjs +121 -70
  95. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
  96. package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
  97. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
  98. package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
  99. package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
  100. package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
  101. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
  102. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
  103. package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
  104. package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
  105. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
  106. package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
  107. package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
  108. package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
  109. package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
  110. package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
  111. package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
  112. package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
  113. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
  114. package/runtime/scripts/hfs/architecture.mjs +3 -4
  115. package/runtime/scripts/hfs/catalog-once.mjs +15 -0
  116. package/runtime/scripts/hfs/check.mjs +63 -71
  117. package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
  118. package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
  119. package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
  120. package/runtime/scripts/hfs/edition-slots.mjs +5 -3
  121. package/runtime/scripts/hfs/linear-text.mjs +30 -0
  122. package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
  123. package/runtime/scripts/hfs/path-findings.mjs +58 -38
  124. package/runtime/scripts/hfs/pin-findings.mjs +31 -0
  125. package/runtime/scripts/hfs/repo-identity.mjs +21 -4
  126. package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
  127. package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
  128. package/runtime/scripts/hfs/rules/cli.mjs +5 -1
  129. package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
  130. package/runtime/scripts/hfs/rules/contract.mjs +35 -27
  131. package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
  132. package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
  133. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
  134. package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
  135. package/runtime/scripts/hfs/rules/database.mjs +36 -23
  136. package/runtime/scripts/hfs/rules/deps.mjs +64 -32
  137. package/runtime/scripts/hfs/rules/docker.mjs +113 -65
  138. package/runtime/scripts/hfs/rules/edition.mjs +118 -111
  139. package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
  140. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
  141. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
  142. package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
  143. package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
  144. package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
  145. package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
  146. package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
  147. package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
  148. package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
  149. package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
  150. package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
  151. package/runtime/scripts/hfs/rules/saga.mjs +128 -61
  152. package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
  153. package/runtime/scripts/hfs/rules/services.mjs +34 -16
  154. package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
  155. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
  156. package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
  157. package/runtime/scripts/hfs/secret.mjs +59 -48
  158. package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
  159. package/runtime/scripts/hfs/slot-classify.mjs +65 -0
  160. package/runtime/scripts/hfs/slot-errors.mjs +11 -0
  161. package/runtime/scripts/hfs/slot-imports.mjs +76 -0
  162. package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
  163. package/runtime/scripts/hfs/slot-match.mjs +153 -0
  164. package/runtime/scripts/hfs/slot-path.mjs +8 -0
  165. package/runtime/scripts/hfs/slot-required.mjs +76 -0
  166. package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
  167. package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
  168. package/runtime/scripts/hfs/slots.mjs +74 -695
  169. package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
  170. package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
  171. package/runtime/scripts/hfs/tree.mjs +19 -14
  172. package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
  173. package/runtime/scripts/hfs/view.mjs +1 -1
  174. package/runtime/scripts/lib/dockerfile.mjs +56 -33
  175. package/runtime/scripts/lib/env.mjs +10 -0
  176. package/runtime/scripts/lib/event-contract.mjs +17 -13
  177. package/runtime/scripts/lib/git.mjs +2 -2
  178. package/runtime/scripts/lib/graphql-contract.mjs +158 -322
  179. package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
  180. package/runtime/scripts/lib/i18n.mjs +3 -2
  181. package/runtime/scripts/lib/in-order.mjs +71 -0
  182. package/runtime/scripts/lib/language.mjs +3 -3
  183. package/runtime/scripts/lib/list.mjs +8 -1
  184. package/runtime/scripts/lib/path-key.mjs +38 -5
  185. package/runtime/scripts/lib/pid-alive.mjs +7 -0
  186. package/runtime/scripts/lib/regex.mjs +1 -1
  187. package/runtime/scripts/lib/same-text.mjs +1 -1
  188. package/runtime/scripts/lib/secret-patterns.mjs +6 -2
  189. package/runtime/scripts/lib/sleep-sync.mjs +2 -2
  190. package/runtime/scripts/lib/sops-envelope.mjs +95 -48
  191. package/runtime/scripts/lib/stack-services.mjs +1 -2
  192. package/runtime/scripts/lib/ts-ast.mjs +9 -0
  193. package/runtime/scripts/lib/walk.mjs +6 -1
  194. package/runtime/scripts/lib/yaml-cached.mjs +14 -0
  195. package/scaffold/add-cli-lite.mjs +2 -1
  196. package/scaffold/add-table.mjs +2 -2
  197. package/scaffold/add.mjs +3 -2
  198. package/scaffold/app.mjs +4 -2
  199. package/scaffold/edition-gate.mjs +25 -25
  200. package/scaffold/lite-exports.mjs +2 -1
  201. package/scaffold/service.mjs +5 -4
  202. package/sync/hygiene.mjs +15 -9
  203. package/sync/index.mjs +7 -3
  204. package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
  205. package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
  206. package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
  207. package/templates/app/ci-workflows/sonar-steps.yml +2 -2
  208. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
  209. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
  210. package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
  211. package/templates/app/starciwork.gitignore +1 -1
  212. package/templates/be/image/api/Dockerfile +1 -1
  213. package/templates/be/image/cli/Dockerfile +1 -1
  214. package/templates/be/image/worker/Dockerfile +1 -1
  215. package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
  216. package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
  217. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
  218. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
  219. package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
  220. package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
  221. package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
  222. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
  223. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
  224. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
  225. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
  226. package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
  227. package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
  228. package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
  229. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
  230. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  231. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
  232. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
  233. package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
  234. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  235. package/templates/fe/image/next/Dockerfile +2 -1
  236. package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
  237. package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
  238. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
  239. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
  240. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
  241. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
  242. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
  243. package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
  244. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
  245. package/upgrade/index.mjs +9 -2
  246. package/runtime/engine/admission.mjs +0 -308
  247. package/runtime/engine/canonical-json.mjs +0 -10
  248. package/runtime/engine/db/blob.mjs +0 -315
  249. package/runtime/engine/db/ledger-paths.mjs +0 -83
  250. package/runtime/engine/db/ledger.mjs +0 -1118
  251. package/runtime/engine/db/machine-connection.mjs +0 -135
  252. package/runtime/engine/db/machine-schema.mjs +0 -84
  253. package/runtime/engine/db/machine.mjs +0 -1367
  254. package/runtime/engine/db/migrations/machine/0001-init.sql +0 -924
  255. package/runtime/engine/db/migrations/runtime/0001-init.sql +0 -1108
  256. package/runtime/engine/db/provider-reservations.mjs +0 -101
  257. package/runtime/engine/digest.mjs +0 -16
  258. package/runtime/engine/refuse.mjs +0 -11
  259. package/runtime/modules/ops/_labels.yaml +0 -53
  260. package/runtime/scripts/api/node/lib.mjs +0 -14
  261. package/runtime/scripts/api/node/spawn-node.mjs +0 -6
  262. package/runtime/scripts/api/process/lib.mjs +0 -111
  263. package/runtime/scripts/api/process/owned-process.mjs +0 -78
  264. package/runtime/scripts/api/process/stop-owned-process.mjs +0 -9
  265. package/runtime/scripts/connectors/lib.mjs +0 -488
  266. package/runtime/scripts/lib/clip.mjs +0 -19
  267. package/runtime/scripts/lib/display-names.mjs +0 -259
  268. package/runtime/scripts/lib/example-refs.mjs +0 -158
  269. package/runtime/scripts/lib/json-schema.mjs +0 -52
  270. package/runtime/scripts/lib/process-identity.mjs +0 -6
  271. package/runtime/scripts/lib/read-yaml.mjs +0 -18
  272. package/runtime/scripts/lib/redact.mjs +0 -161
  273. package/runtime/scripts/lib/sleep.mjs +0 -7
  274. package/runtime/scripts/lib/source-phrases.mjs +0 -34
  275. package/runtime/scripts/lib/sqlite.mjs +0 -21
@@ -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
+ }