@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
@@ -1,11 +1,12 @@
1
1
  import path from 'node:path';
2
2
  import { isInside } from './config.mjs';
3
- import { referencedExports, relativePath, UNPROVEN_FRAMEWORK, unwrapExpression } from './typescript.mjs';
4
- import { anyDescendant, commonJsRequireReasons, constructedDecoratorKind as sharedConstructedDecoratorKind, decoratorCallee, moduleExportsOf, nodeDecorators, normalizedSymbol, normalizedSymbolValue, programSourcesOf, returnedExpressions, selectedNode, valueSymbol, violation } from './ast-walks.mjs';
5
- import { sourceLocation } from '../../lib/ts-ast.mjs';
6
-
3
+ import { referencedExports, relativePath, unwrapExpression } from './typescript.mjs';
4
+ import { commonJsRequireReasons, constructedDecoratorKind as sharedConstructedDecoratorKind, decoratorCallee, moduleExportsOf, nodeDecorators, normalizedSymbol, normalizedSymbolValue, programSourcesOf, selectedNode, valueSymbol, violation } from './ast-walks.mjs';
5
+ import { byCodeUnit } from '../../lib/list.mjs';
6
+ import { hasAnyFlag } from '../../lib/ts-ast.mjs';
7
+ import { canonical, checkInjectedClass, checkMessageReadonly, isPublicMember, messageClass } from './contracts-readonly.mjs';
7
8
  export const PUBLIC_CONTRACT_RULE_ID = 'BE_PUBLIC_CONTRACT_FORM';
8
- export const READONLY_BOUNDARY_RULE_ID = 'BE_READONLY_BOUNDARY';
9
+ export { READONLY_BOUNDARY_RULE_ID } from './contracts-readonly.mjs';
9
10
 
10
11
  const FRAMEWORK_EXPORTS = new Map([
11
12
  ['@nestjs/common', new Set(['Controller', 'Inject', 'Injectable', 'Module'])],
@@ -17,12 +18,6 @@ const MESSAGE_HANDLERS = new Set(['CommandHandler', 'QueryHandler']);
17
18
  const NON_API_SOURCE_ROLES = new Set(['config', 'configuration', 'constants', 'main', 'module', 'module-definition', 'providers']);
18
19
  const SOURCE_EXTENSION = /\.[cm]?[jt]sx?$/i;
19
20
 
20
- function canonical(file) {
21
- return path.resolve(file);
22
- }
23
-
24
-
25
-
26
21
  function frameworkKindForSymbol(symbol, targets) {
27
22
  const selected = targets.get(symbol);
28
23
  if (selected) return selected;
@@ -43,23 +38,28 @@ const FRAMEWORK_TRACE = {
43
38
  shorthandOf: (ts, checker, target, targets) => frameworkKindForSymbol(target, targets),
44
39
  };
45
40
 
41
+ function addFrameworkTargets(config, context, checker, sourceFile, statement, bySymbol, reasons) {
42
+ const bound = moduleExportsOf(context.ts, checker, statement);
43
+ if (!bound || !FRAMEWORK_EXPORTS.has(bound.specifier)) return;
44
+ if (!bound.symbol) {
45
+ reasons.push(`${relativePath(config.root, sourceFile.fileName)} cannot resolve ${bound.specifier}`);
46
+ return;
47
+ }
48
+ for (const name of referencedExports(context.ts, statement, FRAMEWORK_EXPORTS.get(bound.specifier))) {
49
+ const target = bound.exports.get(name);
50
+ if (target) bySymbol.set(target, name);
51
+ else reasons.push(`${relativePath(config.root, sourceFile.fileName)} cannot resolve ${name} from ${bound.specifier}`);
52
+ }
53
+ }
54
+
46
55
  function frameworkTargets(config, context, checker, localFiles) {
47
56
  const bySymbol = new Map();
48
57
  const reasons = [];
49
58
  const files = programSourcesOf(context, checker, localFiles, { root: config.root });
50
59
  if (!files) return { bySymbol, reasons: ['a TypeScript program checker could not be associated with its source'] };
51
60
  for (const sourceFile of files) {
52
- for (const statement of sourceFile.statements) {
53
- const bound = moduleExportsOf(context.ts, checker, statement);
54
- if (!bound || !FRAMEWORK_EXPORTS.has(bound.specifier)) continue;
55
- if (!bound.symbol) { reasons.push(`${relativePath(config.root, sourceFile.fileName)} cannot resolve ${bound.specifier}`); continue; }
56
- for (const name of referencedExports(context.ts, statement, FRAMEWORK_EXPORTS.get(bound.specifier))) {
57
- const target = bound.exports.get(name);
58
- if (target) bySymbol.set(target, name);
59
- else reasons.push(`${relativePath(config.root, sourceFile.fileName)} cannot resolve ${name} from ${bound.specifier}`);
60
- }
61
- }
62
- commonJsRequireReasons(context.ts, checker, sourceFile, (specifier) => FRAMEWORK_EXPORTS.has(specifier),
61
+ for (const statement of sourceFile.statements) addFrameworkTargets(config, context, checker, sourceFile, statement, bySymbol, reasons);
62
+ commonJsRequireReasons(context.ts, checker, sourceFile, specifier => FRAMEWORK_EXPORTS.has(specifier),
63
63
  reasons, relativePath(config.root, sourceFile.fileName), 'binding whose contract role cannot be proved');
64
64
  }
65
65
  return { bySymbol, reasons };
@@ -73,36 +73,18 @@ function decoratorKind(ts, checker, decorator, targets) {
73
73
  const constructedDecoratorKind = (ts, checker, decorator, targets) =>
74
74
  sharedConstructedDecoratorKind(ts, checker, decorator, targets, FRAMEWORK_TRACE);
75
75
 
76
- function modifiers(ts, node) {
77
- return ts.canHaveModifiers(node) ? ts.getModifiers(node) ?? [] : node.modifiers ?? [];
78
- }
79
-
80
- function hasModifier(ts, node, kind) {
81
- return modifiers(ts, node).some(modifier => modifier.kind === kind);
82
- }
83
-
84
- function isPublicMember(ts, node) {
85
- return !hasModifier(ts, node, ts.SyntaxKind.PrivateKeyword) && !hasModifier(ts, node, ts.SyntaxKind.ProtectedKeyword);
86
- }
87
-
88
- function readonlyMember(ts, node) {
89
- return hasModifier(ts, node, ts.SyntaxKind.ReadonlyKeyword);
90
- }
91
-
92
-
93
-
94
76
  function sourceRole(sourceFile) {
95
77
  const normalized = sourceFile.fileName.replaceAll('\\', '/');
96
78
  const base = path.basename(sourceFile.fileName).replace(SOURCE_EXTENSION, '').toLowerCase();
97
79
  const segments = base.split('.');
98
- const role = segments.length > 1 ? segments.at(-1) : base === 'use-case' || base.endsWith('-use-case') ? 'use-case' : null;
80
+ const role = segments.length > 1 ? segments.at(-1) : ((base === 'use-case' || base.endsWith('-use-case')) && 'use-case') || null;
99
81
  return { base, role, transport: normalized.includes('/transport/') };
100
82
  }
101
83
 
102
84
  function isFrameworkHelper(sourceFile, declaration, framework) {
103
85
  const role = sourceRole(sourceFile);
104
86
  if (role.transport || NON_API_SOURCE_ROLES.has(role.role) || NON_API_SOURCE_ROLES.has(role.base)) return true;
105
- if (declaration && declaration.name && /Module$/.test(declaration.name.text ?? '')) {
87
+ if (declaration?.name && (declaration.name.text ?? '').endsWith('Module')) {
106
88
  return nodeDecorators(framework.ts, declaration).some(decorator => decoratorKind(framework.ts, framework.checker, decorator, framework.targets) === 'Module');
107
89
  }
108
90
  return false;
@@ -115,65 +97,81 @@ function builtinSymbol(ts, symbol, name) {
115
97
  && /(?:^|[/\\])lib\.[^/\\]+\.d\.ts$/i.test(declaration.getSourceFile().fileName));
116
98
  }
117
99
 
100
+ function typeReferenceStatus(ts, checker, node, seen, depth) {
101
+ const symbol = normalizedSymbol(ts, checker, node.typeName);
102
+ if (!symbol) return 'unavailable';
103
+ if (builtinSymbol(ts, symbol, 'Promise') || builtinSymbol(ts, symbol, 'Readonly') || builtinSymbol(ts, symbol, 'Awaited')
104
+ || builtinSymbol(ts, symbol, 'Array') || builtinSymbol(ts, symbol, 'ReadonlyArray')) {
105
+ return node.typeArguments?.length === 1 ? contractTypeStatus(ts, checker, node.typeArguments[0], seen, depth + 1) : 'unavailable';
106
+ }
107
+ if (builtinSymbol(ts, symbol, 'Record') || builtinSymbol(ts, symbol, 'Partial') || builtinSymbol(ts, symbol, 'Pick')
108
+ || builtinSymbol(ts, symbol, 'Omit') || builtinSymbol(ts, symbol, 'Required')) return 'inline';
109
+ const declarations = symbol.getDeclarations?.() ?? [];
110
+ if (!declarations.length) return 'unavailable';
111
+ return declarations.every(declaration => ts.isTypeParameterDeclaration(declaration)
112
+ || ts.isInterfaceDeclaration(declaration) || ts.isTypeAliasDeclaration(declaration)
113
+ || ts.isClassDeclaration(declaration) || ts.isEnumDeclaration(declaration)) ? 'named' : 'unavailable';
114
+ }
115
+
116
+ function isInlineContractType(ts, node) {
117
+ return ts.isTypeLiteralNode(node) || ts.isMappedTypeNode(node) || ts.isUnionTypeNode(node)
118
+ || ts.isIntersectionTypeNode(node) || ts.isTupleTypeNode(node) || ts.isFunctionTypeNode(node);
119
+ }
120
+
121
+ function isUnavailableContractType(ts, node) {
122
+ return ts.isConditionalTypeNode(node) || ts.isIndexedAccessTypeNode(node) || ts.isInferTypeNode(node)
123
+ || ts.isTypeQueryNode(node) || ts.isImportTypeNode(node);
124
+ }
125
+
118
126
  function contractTypeStatus(ts, checker, node, seen = new Set(), depth = 0) {
119
127
  if (!node || depth > 12) return 'unavailable';
120
128
  if (ts.isParenthesizedTypeNode(node)) return contractTypeStatus(ts, checker, node.type, seen, depth + 1);
121
129
  if (ts.isTypeOperatorNode(node) && node.operator === ts.SyntaxKind.ReadonlyKeyword) {
122
130
  return contractTypeStatus(ts, checker, node.type, seen, depth + 1);
123
131
  }
124
- if (ts.isTypeReferenceNode(node)) {
125
- const symbol = normalizedSymbol(ts, checker, node.typeName);
126
- if (!symbol) return 'unavailable';
127
- if (builtinSymbol(ts, symbol, 'Promise') || builtinSymbol(ts, symbol, 'Readonly') || builtinSymbol(ts, symbol, 'Awaited')) {
128
- return node.typeArguments?.length === 1 ? contractTypeStatus(ts, checker, node.typeArguments[0], seen, depth + 1) : 'unavailable';
129
- }
130
- if (builtinSymbol(ts, symbol, 'Array') || builtinSymbol(ts, symbol, 'ReadonlyArray')) {
131
- return node.typeArguments?.length === 1 ? contractTypeStatus(ts, checker, node.typeArguments[0], seen, depth + 1) : 'unavailable';
132
- }
133
- if (builtinSymbol(ts, symbol, 'Record') || builtinSymbol(ts, symbol, 'Partial') || builtinSymbol(ts, symbol, 'Pick')
134
- || builtinSymbol(ts, symbol, 'Omit') || builtinSymbol(ts, symbol, 'Required')) return 'inline';
135
- const declarations = symbol.getDeclarations?.() ?? [];
136
- if (!declarations.length) return 'unavailable';
137
- return declarations.every(declaration => ts.isTypeParameterDeclaration(declaration)
138
- || ts.isInterfaceDeclaration(declaration) || ts.isTypeAliasDeclaration(declaration)
139
- || ts.isClassDeclaration(declaration) || ts.isEnumDeclaration(declaration)) ? 'named' : 'unavailable';
140
- }
132
+ if (ts.isTypeReferenceNode(node)) return typeReferenceStatus(ts, checker, node, seen, depth);
141
133
  if (ts.isArrayTypeNode(node)) return contractTypeStatus(ts, checker, node.elementType, seen, depth + 1);
142
- if (ts.isTypeLiteralNode(node) || ts.isMappedTypeNode(node) || ts.isUnionTypeNode(node)
143
- || ts.isIntersectionTypeNode(node) || ts.isTupleTypeNode(node) || ts.isFunctionTypeNode(node)) return 'inline';
144
- if (ts.isConditionalTypeNode(node) || ts.isIndexedAccessTypeNode(node) || ts.isInferTypeNode(node)
145
- || ts.isTypeQueryNode(node) || ts.isImportTypeNode(node)) return 'unavailable';
134
+ if (isInlineContractType(ts, node)) return 'inline';
135
+ if (isUnavailableContractType(ts, node)) return 'unavailable';
146
136
  return 'scalar';
147
137
  }
148
138
 
149
- function contractTypeStatusFromType(ts, checker, type, seen = new Set(), depth = 0) {
150
- if (!type || depth > 12 || seen.has(type)) return 'unavailable';
151
- if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) return 'unavailable';
152
- const nextSeen = new Set(seen).add(type);
153
- const named = type.aliasSymbol ?? type.symbol ?? null;
139
+ function wrappedTypeStatus(ts, checker, type, named, nextSeen, depth) {
154
140
  for (const wrapper of ['Promise', 'Readonly', 'Awaited', 'Array', 'ReadonlyArray']) if (builtinSymbol(ts, named, wrapper)) {
155
- const argumentsList = type.aliasTypeArguments ?? (checker.getTypeArguments && (type.objectFlags & ts.ObjectFlags.Reference)
141
+ const argumentsList = type.aliasTypeArguments ?? (checker.getTypeArguments && hasAnyFlag(type.objectFlags, ts.ObjectFlags.Reference)
156
142
  ? checker.getTypeArguments(type) : type.typeArguments) ?? [];
157
143
  return argumentsList.length === 1 ? contractTypeStatusFromType(ts, checker, argumentsList[0], nextSeen, depth + 1) : 'unavailable';
158
144
  }
159
145
  for (const anonymous of ['Record', 'Partial', 'Pick', 'Omit', 'Required']) if (builtinSymbol(ts, named, anonymous)) return 'inline';
146
+ return null;
147
+ }
148
+
149
+ function objectContractTypeStatus(ts, checker, type, nextSeen, depth) {
150
+ if (checker.isTupleType?.(type)) return 'inline';
151
+ if (checker.isArrayType?.(type)) {
152
+ const argumentsList = checker.getTypeArguments?.(type) ?? [];
153
+ return argumentsList.length === 1 ? contractTypeStatusFromType(ts, checker, argumentsList[0], nextSeen, depth + 1) : 'unavailable';
154
+ }
155
+ const declarations = type.symbol?.getDeclarations?.() ?? [];
156
+ if (type.symbol?.getName().startsWith('__') === false && declarations.some(declaration => ts.isInterfaceDeclaration(declaration)
157
+ || ts.isClassDeclaration(declaration) || ts.isTypeAliasDeclaration(declaration) || ts.isEnumDeclaration(declaration))) return 'named';
158
+ return 'inline';
159
+ }
160
+
161
+ function contractTypeStatusFromType(ts, checker, type, seen = new Set(), depth = 0) {
162
+ if (!type || depth > 12 || seen.has(type)) return 'unavailable';
163
+ if (hasAnyFlag(type.flags, ts.TypeFlags.Any | ts.TypeFlags.Unknown)) return 'unavailable';
164
+ const nextSeen = new Set(seen).add(type);
165
+ const named = type.aliasSymbol ?? type.symbol ?? null;
166
+ const wrapperStatus = wrappedTypeStatus(ts, checker, type, named, nextSeen, depth);
167
+ if (wrapperStatus !== null) return wrapperStatus;
160
168
  if (type.aliasSymbol) return 'named';
161
169
  if ((type.symbol?.getDeclarations?.() ?? []).some(declaration => ts.isEnumDeclaration(declaration))) return 'named';
162
- if (type.flags & ts.TypeFlags.TypeParameter) return 'named';
163
- if (type.flags & ts.TypeFlags.Boolean) return 'scalar';
164
- if (type.flags & (ts.TypeFlags.Union | ts.TypeFlags.Intersection)) return 'inline';
165
- if (type.flags & ts.TypeFlags.Object) {
166
- if (checker.isTupleType?.(type)) return 'inline';
167
- if (checker.isArrayType?.(type)) {
168
- const argumentsList = checker.getTypeArguments?.(type) ?? [];
169
- return argumentsList.length === 1 ? contractTypeStatusFromType(ts, checker, argumentsList[0], nextSeen, depth + 1) : 'unavailable';
170
- }
171
- const declarations = type.symbol?.getDeclarations?.() ?? [];
172
- if (type.symbol && !type.symbol.getName().startsWith('__') && declarations.some(declaration => ts.isInterfaceDeclaration(declaration)
173
- || ts.isClassDeclaration(declaration) || ts.isTypeAliasDeclaration(declaration) || ts.isEnumDeclaration(declaration))) return 'named';
174
- return 'inline';
175
- }
176
- if (type.flags & (ts.TypeFlags.Conditional | ts.TypeFlags.IndexedAccess | ts.TypeFlags.Substitution)) return 'unavailable';
170
+ if (hasAnyFlag(type.flags, ts.TypeFlags.TypeParameter)) return 'named';
171
+ if (hasAnyFlag(type.flags, ts.TypeFlags.Boolean)) return 'scalar';
172
+ if (hasAnyFlag(type.flags, ts.TypeFlags.Union | ts.TypeFlags.Intersection)) return 'inline';
173
+ if (hasAnyFlag(type.flags, ts.TypeFlags.Object)) return objectContractTypeStatus(ts, checker, type, nextSeen, depth);
174
+ if (hasAnyFlag(type.flags, ts.TypeFlags.Conditional | ts.TypeFlags.IndexedAccess | ts.TypeFlags.Substitution)) return 'unavailable';
177
175
  return 'scalar';
178
176
  }
179
177
 
@@ -184,9 +182,19 @@ function callableSignatures(ts, symbol, checker, location) {
184
182
  return bodyless.length ? bodyless : signatures.filter(signature => signature.getDeclaration());
185
183
  }
186
184
 
187
- function checkSignature(config, context, checker, signature, location, reportSource, reportNode, displayName, violations, reasons) {
185
+ function signatureParameterStatus(ts, checker, signature, location, parameter, signatureIndex) {
186
+ const parameterSymbol = signature.getParameters()[signatureIndex];
187
+ const actual = parameterSymbol ? checker.getTypeOfSymbolAtLocation(parameterSymbol, location) : null;
188
+ if (actual && parameter.questionToken && !ts.isUnionTypeNode(parameter.type) && hasAnyFlag(actual.flags, ts.TypeFlags.Union)) {
189
+ const selected = actual.types.filter(type => !hasAnyFlag(type.flags, ts.TypeFlags.Undefined));
190
+ return selected.length === 1 ? contractTypeStatusFromType(ts, checker, selected[0]) : 'inline';
191
+ }
192
+ return actual ? contractTypeStatusFromType(ts, checker, actual) : contractTypeStatus(ts, checker, parameter.type);
193
+ }
194
+
195
+ function checkSignatureInputs(state, declaration) {
196
+ const { config, context, checker, signature, location, reportSource, reportNode, displayName, violations, reasons } = state;
188
197
  const ts = context.ts;
189
- const declaration = signature.getDeclaration();
190
198
  let signatureIndex = 0;
191
199
  for (let index = 0; index < (declaration.parameters?.length ?? 0); index += 1) {
192
200
  const parameter = declaration.parameters[index];
@@ -197,18 +205,17 @@ function checkSignature(config, context, checker, signature, location, reportSou
197
205
  signatureIndex += 1;
198
206
  continue;
199
207
  }
200
- const parameterSymbol = signature.getParameters()[signatureIndex];
208
+ const status = signatureParameterStatus(ts, checker, signature, location, parameter, signatureIndex);
201
209
  signatureIndex += 1;
202
- const actual = parameterSymbol ? checker.getTypeOfSymbolAtLocation(parameterSymbol, location) : null;
203
- let status;
204
- if (actual && parameter.questionToken && !ts.isUnionTypeNode(parameter.type) && (actual.flags & ts.TypeFlags.Union)) {
205
- const selected = actual.types.filter(type => !(type.flags & ts.TypeFlags.Undefined));
206
- status = selected.length === 1 ? contractTypeStatusFromType(ts, checker, selected[0]) : 'inline';
207
- } else status = actual ? contractTypeStatusFromType(ts, checker, actual) : contractTypeStatus(ts, checker, parameter.type);
208
210
  if (status === 'inline') violations.push(violation(config, reportSource, reportNode ?? parameter.type, PUBLIC_CONTRACT_RULE_ID,
209
211
  `${displayName} uses an inline object/union input; expose a named contract or a positional primitive.`));
210
212
  else if (status === 'unavailable') reasons.push(`${relativePath(config.root, reportSource.fileName)} cannot prove the public input type of ${displayName}`);
211
213
  }
214
+ }
215
+
216
+ function checkSignatureOutput(state, declaration) {
217
+ const { config, context, checker, signature, reportSource, reportNode, displayName, violations, reasons } = state;
218
+ const ts = context.ts;
212
219
  if (!declaration.type) {
213
220
  violations.push(violation(config, reportSource, reportNode ?? declaration, PUBLIC_CONTRACT_RULE_ID,
214
221
  `${displayName} must declare an explicit public output type.`));
@@ -221,6 +228,12 @@ function checkSignature(config, context, checker, signature, location, reportSou
221
228
  else if (status === 'unavailable') reasons.push(`${relativePath(config.root, reportSource.fileName)} cannot prove the public output type of ${displayName}`);
222
229
  }
223
230
 
231
+ function checkSignature(state) {
232
+ const declaration = state.signature.getDeclaration();
233
+ checkSignatureInputs(state, declaration);
234
+ checkSignatureOutput(state, declaration);
235
+ }
236
+
224
237
  function symbolName(symbol) {
225
238
  const name = symbol.getName?.() ?? '(anonymous)';
226
239
  return name === '__call' ? '(callable)' : name;
@@ -230,6 +243,17 @@ function isRepositoryDeclaration(declaration, localFiles) {
230
243
  return localFiles.has(canonical(declaration.getSourceFile().fileName));
231
244
  }
232
245
 
246
+ const isPublicCallableDeclaration = (ts, declaration) => ts.isMethodDeclaration(declaration) || ts.isMethodSignature(declaration)
247
+ || ts.isPropertyDeclaration(declaration) || ts.isPropertySignature(declaration) || ts.isGetAccessorDeclaration(declaration);
248
+
249
+ function isCallableMember(ts, checker, member, location, localFiles) {
250
+ if (member.getName() === 'prototype') return false;
251
+ const selected = member.getDeclarations?.() ?? [];
252
+ return selected.some(declaration => isPublicCallableDeclaration(ts, declaration)
253
+ && isPublicMember(ts, declaration) && isRepositoryDeclaration(declaration, localFiles))
254
+ && checker.getSignaturesOfType(checker.getTypeOfSymbolAtLocation(member, location), ts.SignatureKind.Call).length;
255
+ }
256
+
233
257
  function callableMembers(ts, checker, symbol, location, localFiles) {
234
258
  const declarations = symbol.getDeclarations?.() ?? [];
235
259
  const classExpressions = declarations.flatMap(declaration => ts.isVariableDeclaration(declaration)
@@ -242,14 +266,7 @@ function callableMembers(ts, checker, symbol, location, localFiles) {
242
266
  if (declaredClassLike || classExpressions.length) types.push(valueType);
243
267
  for (const signature of checker.getSignaturesOfType(valueType, ts.SignatureKind.Construct)) types.push(checker.getReturnTypeOfSignature(signature));
244
268
  const direct = checker.getSignaturesOfType(primary, ts.SignatureKind.Call).length ? [symbol] : [];
245
- const members = types.flatMap(type => checker.getPropertiesOfType(type)).filter(member => {
246
- if (member.getName() === 'prototype') return false;
247
- const selected = member.getDeclarations?.() ?? [];
248
- return selected.some(declaration => (ts.isMethodDeclaration(declaration) || ts.isMethodSignature(declaration)
249
- || ts.isPropertyDeclaration(declaration) || ts.isPropertySignature(declaration) || ts.isGetAccessorDeclaration(declaration))
250
- && isPublicMember(ts, declaration) && isRepositoryDeclaration(declaration, localFiles))
251
- && checker.getSignaturesOfType(checker.getTypeOfSymbolAtLocation(member, location), ts.SignatureKind.Call).length;
252
- });
269
+ const members = types.flatMap(type => checker.getPropertiesOfType(type)).filter(member => isCallableMember(ts, checker, member, location, localFiles));
253
270
  return [...new Set([...direct, ...members])];
254
271
  }
255
272
 
@@ -258,7 +275,7 @@ function exportedSymbols(ts, checker, sourceFile) {
258
275
  return moduleSymbol ? checker.getExportsOfModule(moduleSymbol).map(symbol => normalizedSymbolValue(ts, checker, symbol)).filter(Boolean) : [];
259
276
  }
260
277
 
261
- function checkCallableSymbol(config, context, checker, symbol, location, reportSource, reportNode, violations, reasons, seen, localFiles) {
278
+ function checkCallableSymbol({ config, context, checker, symbol, location, reportSource, reportNode, violations, reasons, seen, localFiles }) {
262
279
  if (seen.has(symbol)) return 0;
263
280
  seen.add(symbol);
264
281
  let checked = 0;
@@ -272,392 +289,183 @@ function checkCallableSymbol(config, context, checker, symbol, location, reportS
272
289
  }
273
290
  for (const signature of signatures) {
274
291
  checked += 1;
275
- checkSignature(config, context, checker, signature, location, reportSource, reportNode,
276
- callable === symbol ? symbolName(symbol) : `${symbolName(symbol)}.${symbolName(callable)}`,
277
- violations, reasons);
292
+ checkSignature({ config, context, checker, signature, location, reportSource, reportNode,
293
+ displayName: callable === symbol ? symbolName(symbol) : `${symbolName(symbol)}.${symbolName(callable)}`,
294
+ violations, reasons });
278
295
  }
279
296
  }
280
297
  return checked;
281
298
  }
282
299
 
283
- function propertyName(ts, node) {
284
- const name = node.name;
285
- if (ts.isIdentifier(name) || ts.isStringLiteralLike(name) || ts.isPrivateIdentifier(name)) return name.text;
286
- return null;
287
- }
288
-
289
- function thisOriginStatus(ts, checker, expression, seen = new Set(), depth = 0) {
290
- if (!expression || depth > 8) return 'no';
291
- expression = unwrapExpression(ts, expression);
292
- if (expression.kind === ts.SyntaxKind.ThisKeyword) return 'yes';
293
- const symbol = normalizedSymbol(ts, checker, expression);
294
- if (!symbol || seen.has(symbol)) return 'no';
295
- const declarations = symbol.getDeclarations?.() ?? [];
296
- if (declarations.length !== 1 || !ts.isVariableDeclaration(declarations[0]) || !declarations[0].initializer) return 'no';
297
- const origin = thisOriginStatus(ts, checker, declarations[0].initializer, new Set(seen).add(symbol), depth + 1);
298
- if (origin === 'no') return 'no';
299
- return (ts.getCombinedNodeFlags(declarations[0].parent) & ts.NodeFlags.Const) ? origin : 'unavailable';
300
- }
301
-
302
- function instanceAssignmentTarget(ts, checker, expression) {
303
- expression = unwrapExpression(ts, expression);
304
- if (!ts.isPropertyAccessExpression(expression) && !ts.isElementAccessExpression(expression)) return { instance: false };
305
- const owner = thisOriginStatus(ts, checker, expression.expression);
306
- if (owner === 'no') return { instance: false };
307
- if (owner === 'unavailable') return { instance: true, name: null, supported: false };
308
- if (ts.isPropertyAccessExpression(expression)) return { instance: true, name: expression.name.text, supported: true };
309
- if (ts.isStringLiteralLike(expression.argumentExpression)) return { instance: true, name: expression.argumentExpression.text, supported: true };
310
- return { instance: true, name: null, supported: false };
311
- }
312
-
313
- function expressionOrigin(ts, checker, expression, seen = new Set(), depth = 0) {
314
- if (!expression || depth > 8) return null;
315
- expression = unwrapExpression(ts, expression);
316
- const shorthand = ts.isShorthandPropertyAssignment(expression) ? checker.getShorthandAssignmentValueSymbol?.(expression) : null;
317
- const symbol = normalizedSymbolValue(ts, checker, shorthand) ?? normalizedSymbol(ts, checker, expression);
318
- if (!symbol || seen.has(symbol)) return symbol;
319
- const declarations = symbol.getDeclarations?.() ?? [];
320
- if (declarations.length === 1 && ts.isVariableDeclaration(declarations[0]) && declarations[0].initializer
321
- && (ts.getCombinedNodeFlags(declarations[0].parent) & ts.NodeFlags.Const)) {
322
- return expressionOrigin(ts, checker, declarations[0].initializer, new Set(seen).add(symbol), depth + 1);
300
+ function frameworkFor(state, checker) {
301
+ if (!state.frameworkByChecker.has(checker)) {
302
+ const selected = frameworkTargets(state.config, state.context, checker, state.localFiles);
303
+ state.frameworkByChecker.set(checker, { ...selected, ts: state.ts, checker, targets: selected.bySymbol });
304
+ state.readonlyReasons.push(...selected.reasons);
323
305
  }
324
- return symbol;
325
- }
326
-
327
- const expressionReferencesOrigin = (ts, checker, expression, expected) =>
328
- anyDescendant(ts, expression, (node) => expressionOrigin(ts, checker, node) === expected);
329
-
330
- function isSafePrimitiveProjection(ts, checker, expression, expected) {
331
- expression = unwrapExpression(ts, expression);
332
- if (!ts.isPropertyAccessExpression(expression) && !ts.isElementAccessExpression(expression)) return false;
333
- if (expressionOrigin(ts, checker, expression.expression) !== expected) return false;
334
- const type = checker.getTypeAtLocation(expression);
335
- const primitive = ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike | ts.TypeFlags.BooleanLike
336
- | ts.TypeFlags.BigIntLike | ts.TypeFlags.ESSymbolLike | ts.TypeFlags.Null | ts.TypeFlags.Undefined
337
- | ts.TypeFlags.Void | ts.TypeFlags.Never;
338
- return Boolean(type.flags & primitive) && !(type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown));
339
- }
340
-
341
- function assignmentPatternHasInstanceTarget(ts, checker, expression) {
342
- let found = false;
343
- const visit = node => {
344
- if (found) return;
345
- const target = instanceAssignmentTarget(ts, checker, node);
346
- if (target.instance) {
347
- found = true;
348
- return;
349
- }
350
- ts.forEachChild(node, visit);
351
- };
352
- visit(expression);
353
- return found;
306
+ return state.frameworkByChecker.get(checker);
354
307
  }
355
308
 
356
- function globalLibraryIdentifier(ts, checker, node, expected) {
357
- if (!ts.isIdentifier(node) || node.text !== expected) return false;
358
- const symbol = normalizedSymbol(ts, checker, node);
359
- const declarations = symbol?.getDeclarations?.() ?? [];
360
- return declarations.length > 0 && declarations.every(declaration => declaration.getSourceFile().isDeclarationFile
361
- && /(?:^|[/\\])lib\.[^/\\]+\.d\.ts$/i.test(declaration.getSourceFile().fileName));
309
+ function checkOwnerCoverage(state) {
310
+ const { config, context, publicReasons } = state;
311
+ if (!config.owners?.length) publicReasons.push('no slot owner with an entry file exists, so capability API discovery has no owners to list');
312
+ const ownerRoots = (config.owners ?? []).map(owner => path.resolve(config.root, ...owner.root.split('/')));
313
+ const publicSourceRoots = [...config.backend.features, ...config.backend.modules].map(root => path.resolve(config.root, ...root.split('/')));
314
+ for (const sourceFile of context.files) if (publicSourceRoots.some(root => isInside(root, sourceFile.fileName))
315
+ && !ownerRoots.some(root => isInside(root, sourceFile.fileName))) {
316
+ publicReasons.push(`${relativePath(config.root, sourceFile.fileName)} is outside every declared owner root`);
317
+ }
362
318
  }
363
319
 
364
- function instanceMutationCall(ts, checker, node) {
365
- if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) return null;
366
- const owner = node.expression.expression;
367
- const method = node.expression.name.text;
368
- const known = (method === 'assign' || method === 'defineProperty') && globalLibraryIdentifier(ts, checker, owner, 'Object')
369
- || method === 'set' && globalLibraryIdentifier(ts, checker, owner, 'Reflect');
370
- if (!known || thisOriginStatus(ts, checker, node.arguments[0]) === 'no') return null;
371
- return { node, values: node.arguments.slice(method === 'assign' ? 1 : 2) };
372
- }
373
-
374
- function constructorAssignments(ts, checker, constructor, parameter) {
375
- if (!constructor.body) return [];
376
- const parameterSymbol = normalizedSymbol(ts, checker, parameter.name);
377
- const assignments = [];
378
- const visit = node => {
379
- if (node !== constructor.body && (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node)
380
- || ts.isArrowFunction(node) || ts.isMethodDeclaration(node))) return;
381
- if (ts.isBinaryExpression(node) && expressionReferencesOrigin(ts, checker, node.right, parameterSymbol)
382
- && !isSafePrimitiveProjection(ts, checker, node.right, parameterSymbol)) {
383
- const target = instanceAssignmentTarget(ts, checker, node.left);
384
- if (target.instance) assignments.push({ node, name: target.name,
385
- supported: target.supported && node.operatorToken.kind === ts.SyntaxKind.EqualsToken });
386
- else if (assignmentPatternHasInstanceTarget(ts, checker, node.left)) assignments.push({ node, name: null, supported: false });
387
- else ts.forEachChild(node, visit);
388
- } else {
389
- const mutation = instanceMutationCall(ts, checker, node);
390
- if (mutation?.values.some(argument => expressionReferencesOrigin(ts, checker, argument, parameterSymbol))) {
391
- assignments.push({ node, name: null, supported: false });
392
- } else ts.forEachChild(node, visit);
393
- }
394
- };
395
- visit(constructor.body);
396
- return assignments;
320
+ function checkExportedSymbol(state, checker, framework, symbol) {
321
+ const { context, localFiles, publicReasons, violations, publicSeen } = state;
322
+ const declarations = (symbol.getDeclarations?.() ?? []).filter(declaration => localFiles.has(canonical(declaration.getSourceFile().fileName))) ?? [];
323
+ if (!declarations.length) return;
324
+ const representative = declarations[0];
325
+ if (isFrameworkHelper(representative.getSourceFile(), representative, framework)) return;
326
+ state.callableSignatures += checkCallableSymbol({ config: state.config, context, checker, symbol, location: representative,
327
+ reportSource: representative.getSourceFile(), reportNode: representative.name ?? representative,
328
+ violations, reasons: publicReasons, seen: publicSeen, localFiles });
397
329
  }
398
330
 
399
- function checkReadonlyProperty(config, context, sourceFile, node, label, violations) {
400
- if (!readonlyMember(context.ts, node)) violations.push(violation(config, sourceFile, node.name ?? node, READONLY_BOUNDARY_RULE_ID,
401
- `${label} must be readonly; execution state must not mutate an injected dependency or received message.`));
331
+ function checkOwnerEntry(state, owner) {
332
+ const { config, context, ts, sourceFiles, publicReasons } = state;
333
+ const entry = sourceFiles.get(canonical(path.resolve(config.root, ...owner.entry.split('/'))));
334
+ if (!entry) {
335
+ publicReasons.push(`${owner.entry} is outside the checked production program`);
336
+ return;
337
+ }
338
+ const checker = context.checkerFor(entry.fileName);
339
+ const framework = frameworkFor(state, checker);
340
+ if (entry.statements.some(statement => ts.isExportAssignment(statement) && statement.isExportEquals)) {
341
+ publicReasons.push(`${owner.entry} uses export =, so named capability API discovery is unavailable`);
342
+ }
343
+ for (const symbol of exportedSymbols(ts, checker, entry)) checkExportedSymbol(state, checker, framework, symbol);
402
344
  }
403
345
 
404
- function classValuesForSymbol(ts, symbol, localFiles) {
405
- return (symbol?.getDeclarations?.() ?? []).flatMap(declaration => {
406
- if (ts.isClassDeclaration(declaration)) return [declaration];
407
- if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
408
- const initializer = unwrapExpression(ts, declaration.initializer);
409
- if (ts.isClassExpression(initializer)) return [initializer];
410
- }
411
- return [];
412
- }).filter(declaration => localFiles.has(canonical(declaration.getSourceFile().fileName)));
346
+ function checkPublicOwners(state) {
347
+ for (const owner of state.config.owners ?? []) checkOwnerEntry(state, owner);
413
348
  }
414
349
 
415
- function messageClass(config, context, checker, decorator, localFiles, reasons) {
416
- const expression = decorator.expression;
417
- if (!context.ts.isCallExpression(expression) || expression.arguments.length < 1) {
418
- reasons.push(`${relativePath(config.root, decorator.getSourceFile().fileName)} has a handler decorator without a static message class`);
419
- return null;
420
- }
421
- const symbol = valueSymbol(context.ts, checker, expression.arguments[0]);
422
- const declarations = classValuesForSymbol(context.ts, symbol, localFiles);
423
- if (declarations.length !== 1) {
424
- reasons.push(`${relativePath(config.root, decorator.getSourceFile().fileName)} cannot resolve one local message class from its handler decorator`);
425
- return null;
426
- }
427
- return declarations[0];
350
+ function classesInSource(ts, sourceFile) {
351
+ const classes = [];
352
+ const collect = node => {
353
+ if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) classes.push(node);
354
+ ts.forEachChild(node, collect);
355
+ };
356
+ collect(sourceFile);
357
+ return classes;
428
358
  }
429
359
 
430
- function checkMessageReadonly(config, context, checker, declaration, localFiles, violations, reasons) {
431
- const ts = context.ts;
432
- const visited = new Set();
433
- const visitClass = selected => {
434
- const key = `${canonical(selected.getSourceFile().fileName)}:${selected.pos}`;
435
- if (visited.has(key)) return;
436
- visited.add(key);
437
- const sourceFile = selected.getSourceFile();
438
- for (const member of selected.members) {
439
- if (ts.isPropertyDeclaration(member) && !hasModifier(ts, member, ts.SyntaxKind.StaticKeyword)) {
440
- checkReadonlyProperty(config, context, sourceFile, member, `Message field ${propertyName(ts, member) ?? '(computed)'}`, violations);
441
- }
442
- if (ts.isSetAccessorDeclaration(member)) violations.push(violation(config, sourceFile, member.name, READONLY_BOUNDARY_RULE_ID,
443
- `Message field ${propertyName(ts, member) ?? '(computed)'} cannot expose a setter.`));
444
- if (ts.isIndexSignatureDeclaration(member)) reasons.push(`${relativePath(config.root, sourceFile.fileName)} has a message index signature whose mutation boundary cannot be proved`);
445
- if (ts.isConstructorDeclaration(member)) {
446
- for (const parameter of member.parameters) if (modifiers(ts, parameter).some(modifier => [ts.SyntaxKind.PublicKeyword,
447
- ts.SyntaxKind.PrivateKeyword, ts.SyntaxKind.ProtectedKeyword, ts.SyntaxKind.ReadonlyKeyword].includes(modifier.kind))) {
448
- checkReadonlyProperty(config, context, sourceFile, parameter, `Message field ${propertyName(ts, parameter) ?? '(computed)'}`, violations);
449
- }
450
- const inspectAssign = node => {
451
- if (node !== member.body && (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node))) return;
452
- if (instanceMutationCall(ts, checker, node)) {
453
- reasons.push(`${relativePath(config.root, sourceFile.fileName)} constructs message fields through an unsupported instance mutation`);
454
- } else if (ts.isBinaryExpression(node) && assignmentPatternHasInstanceTarget(ts, checker, node.left)
455
- && !instanceAssignmentTarget(ts, checker, node.left).supported) {
456
- reasons.push(`${relativePath(config.root, sourceFile.fileName)} constructs message fields through an unsupported instance assignment`);
457
- } else ts.forEachChild(node, inspectAssign);
458
- };
459
- if (member.body) inspectAssign(member.body);
460
- }
360
+ function checkConstructedClassDecorators(state, sourceFile, checker, decorators, kinds, targets) {
361
+ for (const [index, decorator] of decorators.entries()) if (!kinds[index]) {
362
+ const constructed = constructedDecoratorKind(state.ts, checker, decorator, targets);
363
+ if (constructed && (NEST_CREATED.has(constructed) || MESSAGE_HANDLERS.has(constructed) || constructed.includes('framework'))) {
364
+ state.readonlyReasons.push(`${relativePath(state.config.root, sourceFile.fileName)} has a constructed ${constructed} decorator identity`);
461
365
  }
462
- const type = classInstanceType(ts, checker, selected);
463
- for (const base of type && checker.getBaseTypes ? checker.getBaseTypes(type) : []) {
464
- const declarations = (base.symbol?.getDeclarations?.() ?? []).filter(item => ts.isClassDeclaration(item)) ?? [];
465
- if (declarations.length !== 1) {
466
- reasons.push(`${relativePath(config.root, sourceFile.fileName)} inherits message fields from a class outside the checked production program`);
467
- } else if (localFiles.has(canonical(declarations[0].getSourceFile().fileName))) visitClass(declarations[0]);
468
- else if (classCarriesState(ts, declarations[0])) {
469
- reasons.push(`${relativePath(config.root, sourceFile.fileName)} inherits message state from a class outside the checked production program`);
470
- }
471
- }
472
- };
473
- visitClass(declaration);
366
+ }
474
367
  }
475
368
 
476
- function classCarriesState(ts, declaration) {
477
- return declaration.members.some(member => (ts.isPropertyDeclaration(member) && !hasModifier(ts, member, ts.SyntaxKind.StaticKeyword))
478
- || ts.isSetAccessorDeclaration(member) || ts.isIndexSignatureDeclaration(member)
479
- || (ts.isConstructorDeclaration(member) && member.parameters.length > 0));
369
+ function checkConstructedMemberDecorator(state, sourceFile, checker, decorator, targets) {
370
+ const { ts, config, readonlyReasons } = state;
371
+ if (decoratorKind(ts, checker, decorator, targets)) return;
372
+ const constructed = constructedDecoratorKind(ts, checker, decorator, targets);
373
+ if (constructed === 'Inject' || constructed === 'unproven framework') {
374
+ readonlyReasons.push(`${relativePath(config.root, sourceFile.fileName)} has a constructed ${constructed} injection decorator identity`);
375
+ }
480
376
  }
481
377
 
482
- function classInstanceType(ts, checker, declaration) {
483
- const symbol = declaration.name ? normalizedSymbol(ts, checker, declaration.name) : null;
484
- if (symbol && ts.isClassDeclaration(declaration)) return checker.getDeclaredTypeOfSymbol(symbol);
485
- const value = checker.getTypeAtLocation(declaration);
486
- const constructor = checker.getSignaturesOfType(value, ts.SignatureKind.Construct)[0];
487
- return constructor ? checker.getReturnTypeOfSignature(constructor) : value;
378
+ function checkConstructedMemberDecorators(state, sourceFile, declaration, checker, targets) {
379
+ const { ts } = state;
380
+ for (const member of declaration.members) for (const decorated of [member, ...(ts.isConstructorDeclaration(member) ? member.parameters : [])]) {
381
+ for (const decorator of nodeDecorators(ts, decorated)) checkConstructedMemberDecorator(state, sourceFile, checker, decorator, targets);
382
+ }
488
383
  }
489
384
 
490
- function checkInjectedClass(config, context, checker, declaration, classKind, targets, localFiles, violations, reasons, visited = new Set()) {
491
- const ts = context.ts;
492
- const classKey = `${canonical(declaration.getSourceFile().fileName)}:${declaration.pos}`;
493
- if (visited.has(classKey)) return;
494
- visited.add(classKey);
495
- const sourceFile = declaration.getSourceFile();
496
- const properties = new Map(declaration.members.filter(member => ts.isPropertyDeclaration(member))
497
- .map(member => [propertyName(ts, member), member]).filter(([name]) => name));
498
- for (const member of declaration.members) {
499
- if (ts.isPropertyDeclaration(member)) {
500
- const injected = nodeDecorators(ts, member).some(decorator => decoratorKind(ts, checker, decorator, targets) === 'Inject');
501
- if (injected) checkReadonlyProperty(config, context, sourceFile, member, `Injected property ${propertyName(ts, member) ?? '(computed)'}`, violations);
502
- continue;
503
- }
504
- if (!ts.isConstructorDeclaration(member)) continue;
505
- for (const parameter of member.parameters) {
506
- const explicitInject = nodeDecorators(ts, parameter).some(decorator => decoratorKind(ts, checker, decorator, targets) === 'Inject');
507
- if (!classKind && !explicitInject) continue;
508
- const parameterProperty = modifiers(ts, parameter).some(modifier => [ts.SyntaxKind.PublicKeyword, ts.SyntaxKind.PrivateKeyword,
509
- ts.SyntaxKind.ProtectedKeyword, ts.SyntaxKind.ReadonlyKeyword].includes(modifier.kind));
510
- if (parameterProperty) {
511
- checkReadonlyProperty(config, context, sourceFile, parameter, `Injected dependency ${propertyName(ts, parameter) ?? '(computed)'}`, violations);
512
- continue;
513
- }
514
- for (const assignment of constructorAssignments(ts, checker, member, parameter)) {
515
- if (!assignment.supported || !assignment.name) {
516
- reasons.push(`${relativePath(config.root, sourceFile.fileName)}:${sourceLocation(sourceFile, assignment.node).line} stores an injected dependency through an unsupported instance assignment`);
517
- continue;
518
- }
519
- const property = properties.get(assignment.name);
520
- if (!property) reasons.push(`${relativePath(config.root, sourceFile.fileName)} assigns injected dependency ${assignment.name} without a declared field`);
521
- else checkReadonlyProperty(config, context, sourceFile, property, `Injected dependency ${assignment.name}`, violations);
522
- }
523
- }
524
- }
525
- if (classKind) {
526
- const type = classInstanceType(ts, checker, declaration);
527
- for (const base of type && checker.getBaseTypes ? checker.getBaseTypes(type) : []) {
528
- const declarations = (base.symbol?.getDeclarations?.() ?? []).filter(item => ts.isClassDeclaration(item)) ?? [];
529
- if (declarations.length !== 1) {
530
- reasons.push(`${relativePath(config.root, sourceFile.fileName)} inherits an injected constructor from a class outside the checked production program`);
531
- } else if (localFiles.has(canonical(declarations[0].getSourceFile().fileName))) {
532
- checkInjectedClass(config, context, checker, declarations[0], classKind, targets, localFiles, violations, reasons, visited);
533
- } else if (classCarriesState(ts, declarations[0])) {
534
- reasons.push(`${relativePath(config.root, sourceFile.fileName)} inherits an injected constructor from a class outside the checked production program`);
535
- }
536
- }
385
+ function hasInjectedMember(ts, checker, declaration, targets) {
386
+ return declaration.members.some(member => nodeDecorators(ts, member).some(decorator => decoratorKind(ts, checker, decorator, targets) === 'Inject')
387
+ || (ts.isConstructorDeclaration(member) && member.parameters.some(parameter => nodeDecorators(ts, parameter)
388
+ .some(decorator => decoratorKind(ts, checker, decorator, targets) === 'Inject'))));
389
+ }
390
+
391
+ function checkMessageHandlers(state, sourceFile, checker, decorators, kinds) {
392
+ for (const [index, decorator] of decorators.entries()) {
393
+ const kind = kinds[index];
394
+ if (!MESSAGE_HANDLERS.has(kind)) continue;
395
+ const message = messageClass(state.config, state.context, checker, decorator, state.localFiles, state.readonlyReasons);
396
+ if (!message) continue;
397
+ const key = `${message.getSourceFile().fileName}:${message.pos}`;
398
+ if (state.messageSeen.has(key)) continue;
399
+ state.messageSeen.add(key);
400
+ state.messages += 1;
401
+ checkMessageReadonly(state.config, state.context, state.context.checkerFor(message.getSourceFile().fileName), message,
402
+ state.localFiles, state.violations, state.readonlyReasons);
537
403
  }
538
404
  }
539
405
 
540
- /** Enforce mechanically selected public contract and readonly boundaries without treating DTOs or arbitrary interfaces as APIs. */
541
- export function checkBackendContracts(config, context) {
542
- const ts = context.ts;
543
- const localFiles = new Set(context.files.map(file => canonical(file.fileName)));
544
- const sourceFiles = new Map(context.files.map(file => [canonical(file.fileName), file]));
545
- const frameworkByChecker = new Map();
546
- const violations = [];
547
- const publicReasons = [];
548
- const readonlyReasons = [];
549
- const publicSeen = new Set();
550
- const messageSeen = new Set();
551
- let callableSignatures = 0;
552
- let injectedClasses = 0;
553
- let messages = 0;
554
-
555
- const frameworkFor = checker => {
556
- if (!frameworkByChecker.has(checker)) {
557
- const selected = frameworkTargets(config, context, checker, localFiles);
558
- frameworkByChecker.set(checker, { ...selected, ts, checker, targets: selected.bySymbol });
559
- readonlyReasons.push(...selected.reasons);
560
- }
561
- return frameworkByChecker.get(checker);
562
- };
563
-
564
- if (!config.owners?.length) publicReasons.push('no slot owner with an entry file exists, so capability API discovery has no owners to list');
565
- const ownerRoots = (config.owners ?? []).map(owner => path.resolve(config.root, ...owner.root.split('/')));
566
- const publicSourceRoots = [...config.backend.features, ...config.backend.modules].map(root => path.resolve(config.root, ...root.split('/')));
567
- for (const sourceFile of context.files) if (publicSourceRoots.some(root => isInside(root, sourceFile.fileName))
568
- && !ownerRoots.some(root => isInside(root, sourceFile.fileName))) {
569
- publicReasons.push(`${relativePath(config.root, sourceFile.fileName)} is outside every declared owner root`);
570
- }
571
- if (config.owners) for (const owner of config.owners) {
572
- const entry = sourceFiles.get(canonical(path.resolve(config.root, ...owner.entry.split('/'))));
573
- if (!entry) {
574
- publicReasons.push(`${owner.entry} is outside the checked production program`);
575
- continue;
576
- }
577
- const checker = context.checkerFor(entry.fileName);
578
- const framework = frameworkFor(checker);
579
- if (entry.statements.some(statement => ts.isExportAssignment(statement) && statement.isExportEquals)) {
580
- publicReasons.push(`${owner.entry} uses export =, so named capability API discovery is unavailable`);
581
- }
582
- for (const symbol of exportedSymbols(ts, checker, entry)) {
583
- const declarations = (symbol.getDeclarations?.() ?? []).filter(declaration => localFiles.has(canonical(declaration.getSourceFile().fileName))) ?? [];
584
- if (!declarations.length) continue;
585
- const representative = declarations[0];
586
- if (isFrameworkHelper(representative.getSourceFile(), representative, framework)) continue;
587
- callableSignatures += checkCallableSymbol(config, context, checker, symbol, representative, representative.getSourceFile(), representative.name ?? representative,
588
- violations, publicReasons, publicSeen, localFiles);
589
- }
406
+ function checkUseCase(state, role, sourceFile, checker, declaration) {
407
+ const { ts, config, context, publicReasons, publicSeen, localFiles, violations } = state;
408
+ if (!(role.role === 'use-case' && !role.transport && ts.isClassDeclaration(declaration) && declaration.name)) return;
409
+ const exported = exportedSymbols(ts, checker, sourceFile).some(symbol => (symbol.getDeclarations?.() ?? []).includes(declaration));
410
+ if (!exported) return;
411
+ const classSymbol = normalizedSymbol(ts, checker, declaration.name);
412
+ const instanceType = classSymbol ? checker.getDeclaredTypeOfSymbol(classSymbol) : null;
413
+ const execute = instanceType ? checker.getPropertyOfType(instanceType, 'execute') : null;
414
+ if (!execute) publicReasons.push(`${relativePath(config.root, sourceFile.fileName)} exports a use-case class without a statically resolved execute contract`);
415
+ else state.callableSignatures += checkCallableSymbol({ config, context, checker, symbol: execute, location: declaration,
416
+ reportSource: sourceFile, reportNode: declaration.name, violations, reasons: publicReasons, seen: publicSeen, localFiles });
417
+ }
418
+
419
+ function checkClassContracts(state, sourceFile, checker, role, declaration, framework) {
420
+ const { ts, config, context, violations, readonlyReasons, localFiles } = state;
421
+ const decorators = nodeDecorators(ts, declaration);
422
+ const kinds = decorators.map(decorator => decoratorKind(ts, checker, decorator, framework.targets));
423
+ const directKinds = kinds.filter(Boolean);
424
+ checkConstructedClassDecorators(state, sourceFile, checker, decorators, kinds, framework.targets);
425
+ const classKind = directKinds.find(kind => NEST_CREATED.has(kind)) ?? null;
426
+ const targets = framework.targets;
427
+ checkConstructedMemberDecorators(state, sourceFile, declaration, checker, targets);
428
+ if (classKind || hasInjectedMember(ts, checker, declaration, targets)) {
429
+ state.injectedClasses += 1;
430
+ checkInjectedClass({ config, context, checker, declaration, classKind, targets, decoratorKind,
431
+ localFiles, violations, reasons: readonlyReasons });
590
432
  }
433
+ checkMessageHandlers(state, sourceFile, checker, decorators, kinds);
434
+ checkUseCase(state, role, sourceFile, checker, declaration);
435
+ }
591
436
 
592
- for (const sourceFile of context.files) {
593
- const checker = context.checkerFor(sourceFile.fileName);
594
- const framework = frameworkFor(checker);
595
- const role = sourceRole(sourceFile);
596
- const classes = [];
597
- const collectClasses = node => {
598
- if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) classes.push(node);
599
- ts.forEachChild(node, collectClasses);
600
- };
601
- collectClasses(sourceFile);
602
- for (const statement of classes) {
603
- const classDecorators = nodeDecorators(ts, statement);
604
- const kindsByDecorator = classDecorators.map(decorator => decoratorKind(ts, checker, decorator, framework.targets));
605
- const directKinds = kindsByDecorator.filter(Boolean);
606
- for (const [index, decorator] of classDecorators.entries()) if (!kindsByDecorator[index]) {
607
- const constructed = constructedDecoratorKind(ts, checker, decorator, framework.targets);
608
- if (constructed && (NEST_CREATED.has(constructed) || MESSAGE_HANDLERS.has(constructed) || constructed.includes('framework'))) {
609
- readonlyReasons.push(`${relativePath(config.root, sourceFile.fileName)} has a constructed ${constructed} decorator identity`);
610
- }
611
- }
612
- const classKind = directKinds.find(kind => NEST_CREATED.has(kind)) ?? null;
613
- for (const member of statement.members) for (const decorated of [member, ...(ts.isConstructorDeclaration(member) ? member.parameters : [])]) {
614
- for (const decorator of nodeDecorators(ts, decorated)) if (!decoratorKind(ts, checker, decorator, framework.targets)) {
615
- const constructed = constructedDecoratorKind(ts, checker, decorator, framework.targets);
616
- if (constructed === 'Inject' || constructed === 'unproven framework') {
617
- readonlyReasons.push(`${relativePath(config.root, sourceFile.fileName)} has a constructed ${constructed} injection decorator identity`);
618
- }
619
- }
620
- }
621
- const hasInjectedMember = statement.members.some(member => nodeDecorators(ts, member).some(decorator => decoratorKind(ts, checker, decorator, framework.targets) === 'Inject')
622
- || (ts.isConstructorDeclaration(member) && member.parameters.some(parameter => nodeDecorators(ts, parameter)
623
- .some(decorator => decoratorKind(ts, checker, decorator, framework.targets) === 'Inject'))));
624
- if (classKind || hasInjectedMember) {
625
- injectedClasses += 1;
626
- checkInjectedClass(config, context, checker, statement, classKind, framework.targets, localFiles, violations, readonlyReasons);
627
- }
628
- for (let index = 0; index < classDecorators.length; index += 1) {
629
- const kind = kindsByDecorator[index];
630
- if (!MESSAGE_HANDLERS.has(kind)) continue;
631
- const message = messageClass(config, context, checker, classDecorators[index], localFiles, readonlyReasons);
632
- if (!message) continue;
633
- const key = `${message.getSourceFile().fileName}:${message.pos}`;
634
- if (messageSeen.has(key)) continue;
635
- messageSeen.add(key);
636
- messages += 1;
637
- checkMessageReadonly(config, context, context.checkerFor(message.getSourceFile().fileName), message, localFiles, violations, readonlyReasons);
638
- }
639
- if (role.role === 'use-case' && !role.transport && ts.isClassDeclaration(statement) && statement.name) {
640
- const exported = exportedSymbols(ts, checker, sourceFile).some(symbol => (symbol.getDeclarations?.() ?? []).includes(statement));
641
- if (!exported) continue;
642
- const classSymbol = normalizedSymbol(ts, checker, statement.name);
643
- const instanceType = classSymbol ? checker.getDeclaredTypeOfSymbol(classSymbol) : null;
644
- const execute = instanceType ? checker.getPropertyOfType(instanceType, 'execute') : null;
645
- if (!execute) publicReasons.push(`${relativePath(config.root, sourceFile.fileName)} exports a use-case class without a statically resolved execute contract`);
646
- else callableSignatures += checkCallableSymbol(config, context, checker, execute, statement, sourceFile, statement.name,
647
- violations, publicReasons, publicSeen, localFiles);
648
- }
649
- }
650
- }
437
+ function checkSourceFile(state, sourceFile) {
438
+ const checker = state.context.checkerFor(sourceFile.fileName);
439
+ const framework = frameworkFor(state, checker);
440
+ const role = sourceRole(sourceFile);
441
+ for (const declaration of classesInSource(state.ts, sourceFile)) checkClassContracts(state, sourceFile, checker, role, declaration, framework);
442
+ }
651
443
 
652
- const publicDetails = [...new Set(publicReasons)].sort();
653
- const readonlyDetails = [...new Set(readonlyReasons)].sort();
444
+ function contractCoverage(state) {
445
+ const publicDetails = [...new Set(state.publicReasons)].sort(byCodeUnit);
446
+ const readonlyDetails = [...new Set(state.readonlyReasons)].sort(byCodeUnit);
654
447
  return {
655
- violations,
448
+ violations: state.violations,
656
449
  coverage: {
657
- publicContracts: publicDetails.length ? { status: 'unavailable', callableSignatures, details: publicDetails }
658
- : { status: 'checked', callableSignatures },
659
- readonlyBoundaries: readonlyDetails.length ? { status: 'unavailable', injectedClasses, messages, details: readonlyDetails }
660
- : { status: 'checked', injectedClasses, messages },
450
+ publicContracts: publicDetails.length ? { status: 'unavailable', callableSignatures: state.callableSignatures, details: publicDetails }
451
+ : { status: 'checked', callableSignatures: state.callableSignatures },
452
+ readonlyBoundaries: readonlyDetails.length ? { status: 'unavailable', injectedClasses: state.injectedClasses, messages: state.messages, details: readonlyDetails }
453
+ : { status: 'checked', injectedClasses: state.injectedClasses, messages: state.messages },
661
454
  },
662
455
  };
663
456
  }
457
+
458
+ /** Enforce mechanically selected public contract and readonly boundaries without treating DTOs or arbitrary interfaces as APIs. */
459
+ export function checkBackendContracts(config, context) {
460
+ const state = {
461
+ config, context, ts: context.ts,
462
+ localFiles: new Set(context.files.map(file => canonical(file.fileName))),
463
+ sourceFiles: new Map(context.files.map(file => [canonical(file.fileName), file])),
464
+ frameworkByChecker: new Map(), violations: [], publicReasons: [], readonlyReasons: [],
465
+ publicSeen: new Set(), messageSeen: new Set(), callableSignatures: 0, injectedClasses: 0, messages: 0,
466
+ };
467
+ checkOwnerCoverage(state);
468
+ checkPublicOwners(state);
469
+ for (const sourceFile of context.files) checkSourceFile(state, sourceFile);
470
+ return contractCoverage(state);
471
+ }