@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
@@ -4,6 +4,140 @@ import { DATABASE_DIR } from './connection-map.mjs';
4
4
 
5
5
  const camel = name => { const text = pascal(name); return text[0].toLowerCase() + text.slice(1); };
6
6
 
7
+ const unparenWhile = (node, isWrapper) => {
8
+ let current = node;
9
+ while (current && isWrapper(current)) current = current.expression;
10
+ return current;
11
+ };
12
+
13
+ const isExpressionWrapper = (ts, node) => ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isNonNullExpression(node);
14
+
15
+ const unparenRegistration = (ts, node) => unparenWhile(node, current => isExpressionWrapper(ts, current) || ts.isSatisfiesExpression?.(current));
16
+
17
+ const returnedExpression = (ts, declaration) => {
18
+ const fn = ts.isVariableDeclaration(declaration) && declaration.initializer ? unparenRegistration(ts, declaration.initializer) : declaration;
19
+ if (!(ts.isArrowFunction(fn) || ts.isFunctionDeclaration(fn) || ts.isFunctionExpression(fn) || ts.isMethodDeclaration(fn)) || !fn.body) return null;
20
+ if (!ts.isBlock(fn.body)) return fn.body;
21
+ const statements = fn.body.statements.filter(ts.isReturnStatement);
22
+ return statements.length === 1 ? statements[0].expression ?? null : null;
23
+ };
24
+
25
+ /** Every `name:` property assignment of an object literal over the program: [{declaration, initializer, checker}], memoised per name. */
26
+ const assignmentsOf = (ctx, name) => {
27
+ if (ctx.propertySources.has(name)) return ctx.propertySources.get(name);
28
+ const { kit } = ctx;
29
+ const { ts } = kit;
30
+ const list = [];
31
+ for (const file of ctx.graph.files.values()) {
32
+ if (!file.sourceFile.text.includes(name)) continue;
33
+ const checker = kit.checkerOf(file.sourceFile);
34
+ kit.walk(file.sourceFile, node => {
35
+ if (ts.isPropertyAssignment(node) && kit.propertyNameText(node.name) === name && ts.isObjectLiteralExpression(node.parent)) {
36
+ // the property of the type the literal is checked against, so a typed const and a typed return meet the same declaration
37
+ const contextual = checker.getContextualType(node.parent)?.getProperty(name);
38
+ const symbol = contextual ?? checker.getSymbolAtLocation(node.name);
39
+ list.push({ declaration: symbol?.declarations?.[0] ?? null, initializer: node.initializer, checker });
40
+ }
41
+ return true;
42
+ });
43
+ }
44
+ ctx.propertySources.set(name, list);
45
+ return list;
46
+ };
47
+
48
+ const nameOfLiteral = (ctx, checker, literal, depth) => {
49
+ const { kit } = ctx;
50
+ const own = kit.propertyOf(literal, 'name');
51
+ if (own) return kit.stringValue(checker, kit.valueOfProperty(own));
52
+ for (const property of literal.properties) {
53
+ if (!kit.ts.isSpreadAssignment(property)) continue;
54
+ const value = nameOfExpression(ctx, checker, property.expression, depth + 1);
55
+ if (value) return value;
56
+ }
57
+ return null;
58
+ };
59
+
60
+ const nameFromCall = (ctx, checker, node, depth) => {
61
+ const { kit } = ctx;
62
+ for (const declaration of kit.declarationsOf(checker, node.expression)) {
63
+ const body = returnedExpression(kit.ts, declaration);
64
+ const value = body ? nameOfExpression(ctx, kit.checkerOf(declaration.getSourceFile()), body, depth + 1) : null;
65
+ if (value) return value;
66
+ }
67
+ return null;
68
+ };
69
+
70
+ const nameFromIdentifier = (ctx, checker, node, depth) => {
71
+ const { kit } = ctx;
72
+ const declaration = kit.declarationsOf(checker, node)[0];
73
+ return declaration && kit.ts.isVariableDeclaration(declaration) && declaration.initializer
74
+ ? nameOfExpression(ctx, kit.checkerOf(declaration.getSourceFile()), declaration.initializer, depth + 1) : null;
75
+ };
76
+
77
+ const nameFromProperty = (ctx, checker, node, depth) => {
78
+ const target = ctx.kit.declarationsOf(checker, node.name)[0];
79
+ if (!target) return null;
80
+ const names = new Set(assignmentsOf(ctx, node.name.text).filter(item => item.declaration === target)
81
+ .map(item => nameOfExpression(ctx, item.checker, item.initializer, depth + 1)).filter(Boolean));
82
+ return names.size === 1 ? [...names][0] : null;
83
+ };
84
+
85
+ function nameOfExpression(ctx, checker, expression, depth) {
86
+ const { ts } = ctx.kit;
87
+ const node = unparenRegistration(ts, expression);
88
+ if (!node || depth > 5) return null;
89
+ if (ts.isObjectLiteralExpression(node)) return nameOfLiteral(ctx, checker, node, depth);
90
+ if (ts.isCallExpression(node)) return nameFromCall(ctx, checker, node, depth);
91
+ if (ts.isIdentifier(node)) return nameFromIdentifier(ctx, checker, node, depth);
92
+ if (ts.isPropertyAccessExpression(node)) return nameFromProperty(ctx, checker, node, depth);
93
+ return null;
94
+ }
95
+
96
+ const isCapabilityArray = (owner, name) => owner?.below.length === 1 && owner.below[0] === 'connection.ts' && [`${camel(owner.capability)}Entities`, `${camel(owner.capability)}Migrations`].includes(name);
97
+
98
+ const arrayDeclarationOf = (kit, checker, node) => {
99
+ const { ts } = kit;
100
+ const declaration = kit.declarationsOf(checker, ts.isPropertyAccessExpression(node) ? node.name : node)[0];
101
+ return declaration && ts.isVariableDeclaration(declaration) && ts.isIdentifier(declaration.name) ? declaration : null;
102
+ };
103
+
104
+ /** The capability arrays (`<c>Entities` / `<c>Migrations` of persistence/connection.ts) an expression lists. */
105
+ function arraysIn(ctx, checker, expression, out, depth) {
106
+ const { kit } = ctx;
107
+ const { ts } = kit;
108
+ const node = unparenRegistration(ts, expression);
109
+ if (!node || depth > 6) return;
110
+ if (ts.isArrayLiteralExpression(node)) {
111
+ for (const element of node.elements) { arraysIn(ctx, checker, ts.isSpreadElement(element) ? element.expression : element, out, depth + 1); }
112
+ return;
113
+ }
114
+ if (!ts.isIdentifier(node) && !ts.isPropertyAccessExpression(node)) return;
115
+ const declaration = arrayDeclarationOf(kit, checker, node);
116
+ if (!declaration) return;
117
+ const rel = kit.graphPath(declaration);
118
+ const owner = rel ? ctx.persistenceOf(rel) : null;
119
+ if (isCapabilityArray(owner, declaration.name.text)) {
120
+ out.push({ key: path.posix.dirname(owner.root), name: owner.capability });
121
+ } else if (declaration.initializer) arraysIn(ctx, kit.checkerOf(declaration.getSourceFile()), declaration.initializer, out, depth + 1);
122
+ }
123
+
124
+ const registrationArrays = (ctx, checker, literal) => {
125
+ const arrays = [];
126
+ for (const key of ['entities', 'migrations']) {
127
+ const property = ctx.kit.propertyOf(literal, key);
128
+ if (property) arraysIn(ctx, checker, ctx.kit.valueOfProperty(property), arrays, 0);
129
+ }
130
+ return arrays;
131
+ };
132
+
133
+ const recordRegistration = (registered, arrays, connection, site) => {
134
+ for (const { key, name } of new Map(arrays.map(item => [item.key, item])).values()) {
135
+ if (!registered.has(key)) registered.set(key, { name, connections: new Map() });
136
+ const sites = registered.get(key).connections;
137
+ if (!sites.has(connection)) sites.set(connection, site);
138
+ }
139
+ };
140
+
7
141
  /**
8
142
  * Where the persistence arrays of every capability are registered: the object literals (`DatabaseModule.register` connections and the
9
143
  * migrate/cli app's) that list `<c>Entities` / `<c>Migrations` of a capability's `persistence/connection.ts` under a connection `name`.
@@ -15,105 +149,20 @@ const camel = name => { const text = pascal(name); return text[0].toLowerCase()
15
149
  */
16
150
  export function collectRegistrations({ kit, graph, persistenceOf }) {
17
151
  const { ts } = kit;
18
- const unparen = node => { let current = node; while (current && (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isSatisfiesExpression?.(current) || ts.isNonNullExpression(current))) current = current.expression; return current; };
19
- const returned = declaration => {
20
- const fn = ts.isVariableDeclaration(declaration) && declaration.initializer ? unparen(declaration.initializer) : declaration;
21
- if (!(ts.isArrowFunction(fn) || ts.isFunctionDeclaration(fn) || ts.isFunctionExpression(fn) || ts.isMethodDeclaration(fn)) || !fn.body) return null;
22
- if (!ts.isBlock(fn.body)) return fn.body;
23
- const statements = fn.body.statements.filter(ts.isReturnStatement);
24
- return statements.length === 1 ? statements[0].expression ?? null : null;
25
- };
26
- const propertySources = new Map(); // property name -> [{declaration, initializer, checker}] over the program
27
- const assignmentsOf = name => {
28
- if (propertySources.has(name)) return propertySources.get(name);
29
- const list = [];
30
- for (const file of graph.files.values()) {
31
- if (!file.sourceFile.text.includes(name)) continue;
32
- const checker = kit.checkerOf(file.sourceFile);
33
- kit.walk(file.sourceFile, node => {
34
- if (ts.isPropertyAssignment(node) && kit.propertyNameText(node.name) === name && ts.isObjectLiteralExpression(node.parent)) {
35
- // the property of the type the literal is checked against, so a typed const and a typed return meet the same declaration
36
- const contextual = checker.getContextualType(node.parent)?.getProperty(name);
37
- const symbol = contextual ?? checker.getSymbolAtLocation(node.name);
38
- list.push({ declaration: symbol?.declarations?.[0] ?? null, initializer: node.initializer, checker });
39
- }
40
- return true;
41
- });
42
- }
43
- propertySources.set(name, list);
44
- return list;
45
- };
46
- const nameOfLiteral = (checker, literal, depth) => {
47
- const own = kit.propertyOf(literal, 'name');
48
- if (own) return kit.stringValue(checker, kit.valueOfProperty(own));
49
- for (const property of literal.properties) {
50
- if (!ts.isSpreadAssignment(property)) continue;
51
- const value = nameOfExpression(checker, property.expression, depth + 1);
52
- if (value) return value;
53
- }
54
- return null;
55
- };
56
- const nameOfExpression = (checker, expression, depth) => {
57
- const node = unparen(expression);
58
- if (!node || depth > 5) return null;
59
- if (ts.isObjectLiteralExpression(node)) return nameOfLiteral(checker, node, depth);
60
- if (ts.isCallExpression(node)) {
61
- for (const declaration of kit.declarationsOf(checker, node.expression)) {
62
- const body = returned(declaration);
63
- const value = body ? nameOfExpression(kit.checkerOf(declaration.getSourceFile()), body, depth + 1) : null;
64
- if (value) return value;
65
- }
66
- return null;
67
- }
68
- if (ts.isIdentifier(node)) {
69
- const declaration = kit.declarationsOf(checker, node)[0];
70
- return declaration && ts.isVariableDeclaration(declaration) && declaration.initializer ? nameOfExpression(kit.checkerOf(declaration.getSourceFile()), declaration.initializer, depth + 1) : null;
71
- }
72
- if (ts.isPropertyAccessExpression(node)) {
73
- const target = kit.declarationsOf(checker, node.name)[0];
74
- if (!target) return null;
75
- const names = new Set(assignmentsOf(node.name.text).filter(item => item.declaration === target).map(item => nameOfExpression(item.checker, item.initializer, depth + 1)).filter(Boolean));
76
- return names.size === 1 ? [...names][0] : null;
77
- }
78
- return null;
79
- };
80
- /** The capability arrays (`<c>Entities` / `<c>Migrations` of persistence/connection.ts) an expression lists. */
81
- const arraysIn = (checker, expression, out, depth) => {
82
- const node = unparen(expression);
83
- if (!node || depth > 6) return;
84
- if (ts.isArrayLiteralExpression(node)) { for (const element of node.elements) arraysIn(checker, ts.isSpreadElement(element) ? element.expression : element, out, depth + 1); return; }
85
- if (!ts.isIdentifier(node) && !ts.isPropertyAccessExpression(node)) return;
86
- const declaration = kit.declarationsOf(checker, ts.isPropertyAccessExpression(node) ? node.name : node)[0];
87
- if (!declaration || !ts.isVariableDeclaration(declaration) || !ts.isIdentifier(declaration.name)) return;
88
- const rel = kit.graphPath(declaration);
89
- const owner = rel ? persistenceOf(rel) : null;
90
- if (owner && owner.below.length === 1 && owner.below[0] === 'connection.ts' && [`${camel(owner.capability)}Entities`, `${camel(owner.capability)}Migrations`].includes(declaration.name.text)) {
91
- out.push({ key: path.posix.dirname(owner.root), name: owner.capability });
92
- } else if (declaration.initializer) arraysIn(kit.checkerOf(declaration.getSourceFile()), declaration.initializer, out, depth + 1);
93
- };
152
+ const ctx = { kit, graph, persistenceOf, propertySources: new Map() }; // property name -> [{declaration, initializer, checker}] over the program
94
153
  const all = []; // every registration literal: {file, node, connection | null when the name cannot be resolved}
95
154
  const registered = new Map(); // capability root -> {name, connections: Map(connection -> first site)}
96
155
  let registrations = 0;
97
156
  for (const file of graph.files.values()) {
98
157
  const checker = kit.checkerOf(file.sourceFile);
99
158
  kit.walk(file.sourceFile, node => {
100
- if (!ts.isObjectLiteralExpression(node)) return true;
101
- const arrays = [];
102
- for (const key of ['entities', 'migrations']) {
103
- const property = kit.propertyOf(node, key);
104
- if (property) arraysIn(checker, kit.valueOfProperty(property), arrays, 0);
105
- }
106
- if (!arrays.length) return true;
159
+ if (!ts.isObjectLiteralExpression(node)) return;
160
+ const arrays = registrationArrays(ctx, checker, node);
161
+ if (!arrays.length) return;
107
162
  registrations += 1;
108
- const connection = nameOfLiteral(checker, node, 0);
163
+ const connection = nameOfLiteral(ctx, checker, node, 0);
109
164
  all.push({ file, node, connection });
110
- if (connection === null) return true;
111
- for (const { key, name } of new Map(arrays.map(item => [item.key, item])).values()) {
112
- if (!registered.has(key)) registered.set(key, { name, connections: new Map() });
113
- const sites = registered.get(key).connections;
114
- if (!sites.has(connection)) sites.set(connection, { file, node });
115
- }
116
- return true;
165
+ if (connection !== null) recordRegistration(registered, arrays, connection, { file, node });
117
166
  });
118
167
  }
119
168
  return { registered, registrations, all };
@@ -137,6 +186,54 @@ export const persistenceOfFactory = resolver => rel => {
137
186
  return { root: classified.root, capability: path.posix.basename(path.posix.dirname(classified.root)), folder: below.length > 1 ? below[0] : '', below };
138
187
  };
139
188
 
189
+ /** The declared connection named by a decorator whose callee is declared in src/modules/platform/database/<conn>.decorators.ts, else null. */
190
+ const connectionOfDecorators = (kit, declared, checker, declaration) => {
191
+ const { ts } = kit;
192
+ for (const decorator of kit.decorators(declaration)) {
193
+ const callee = ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression;
194
+ for (const target of kit.declarationsOf(checker, callee)) {
195
+ const rel = kit.graphPath(target);
196
+ const name = rel ? [...declared.keys()].find(connection => rel === `${DATABASE_DIR}/${connection}.decorators.ts`) : null;
197
+ if (name) return name;
198
+ }
199
+ }
200
+ return null;
201
+ };
202
+
203
+ const managerTarget = (ts, node) => {
204
+ if (ts.isPropertyAccessExpression(node)) return node.name;
205
+ return ts.isIdentifier(node) ? node : null;
206
+ };
207
+
208
+ /** The receiver expression of the `.transaction(callback)` call whose first callback parameter is `declaration`, else null. */
209
+ const transactionReceiver = (ts, declaration) => {
210
+ const callback = declaration.parent;
211
+ if (!ts.isParameter(declaration) || !(ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) || callback.parameters[0] !== declaration) return null;
212
+ const call = callback.parent;
213
+ if (!ts.isCallExpression(call) || !call.arguments.includes(callback) || !ts.isPropertyAccessExpression(call.expression) || call.expression.name.text !== 'transaction') return null;
214
+ return call.expression.expression;
215
+ };
216
+
217
+ /**
218
+ * The declared connection whose entity manager `expression` is: a property or parameter injected with `Inject<Conn>EntityManager`, or the
219
+ * manager parameter of the callback of a `.transaction(...)` on such a manager. Null when the origin cannot be proven (never guessed).
220
+ */
221
+ function connectionOfManagerIn(kit, declared, checker, expression, depth = 0) {
222
+ const { ts } = kit;
223
+ const node = unparenWhile(expression, current => isExpressionWrapper(ts, current));
224
+ if (!node || depth > 4) return null;
225
+ const target = managerTarget(ts, node);
226
+ if (!target) return null;
227
+ const declaration = kit.declarationsOf(checker, target)[0];
228
+ if (!declaration) return null;
229
+ if (ts.isPropertyDeclaration(declaration) || ts.isParameter(declaration)) {
230
+ const direct = connectionOfDecorators(kit, declared, checker, declaration);
231
+ if (direct) return direct;
232
+ }
233
+ const receiver = transactionReceiver(ts, declaration);
234
+ return receiver ? connectionOfManagerIn(kit, declared, checker, receiver, depth + 1) : null;
235
+ }
236
+
140
237
  /**
141
238
  * The bounded-context model of a program: which context (declared connection) each capability belongs to, and which connection an expression's
142
239
  * entity manager is. A context is a connection; a capability belongs to the one declared connection its persistence arrays are registered on
@@ -167,47 +264,35 @@ export function contextModelOf(kit, graph) {
167
264
  if (!file?.owner || !CONTEXT_TIERS.has(file.tier)) return null;
168
265
  return contextOfCapability(file.owner.root);
169
266
  };
170
- const unparen = node => { let current = node; while (current && (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isNonNullExpression(current))) current = current.expression; return current; };
171
- /** The declared connection named by a decorator whose callee is declared in src/modules/platform/database/<conn>.decorators.ts, else null. */
172
- const connectionOfDecorators = (checker, declaration) => {
173
- for (const decorator of kit.decorators(declaration)) {
174
- const callee = ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression;
175
- for (const target of kit.declarationsOf(checker, callee)) {
176
- const rel = kit.graphPath(target);
177
- const name = rel ? [...declared.keys()].find(connection => rel === `${DATABASE_DIR}/${connection}.decorators.ts`) : null;
178
- if (name) return name;
179
- }
180
- }
181
- return null;
267
+ const model = {
268
+ declared, registered, registrations: all, perConnection, contextOfCapability, contextOfFile,
269
+ connectionOfManager: (checker, expression) => connectionOfManagerIn(kit, declared, checker, expression),
270
+ unparen: node => unparenWhile(node, current => isExpressionWrapper(ts, current)),
182
271
  };
183
- /**
184
- * The declared connection whose entity manager `expression` is: a property or parameter injected with `Inject<Conn>EntityManager`, or the
185
- * manager parameter of the callback of a `.transaction(...)` on such a manager. Null when the origin cannot be proven (never guessed).
186
- */
187
- const connectionOfManager = (checker, expression, depth = 0) => {
188
- const node = unparen(expression);
189
- if (!node || depth > 4) return null;
190
- const target = ts.isPropertyAccessExpression(node) ? node.name : (ts.isIdentifier(node) ? node : null);
191
- if (!target) return null;
192
- const declaration = kit.declarationsOf(checker, target)[0];
193
- if (!declaration) return null;
194
- if (ts.isPropertyDeclaration(declaration) || ts.isParameter(declaration)) {
195
- const direct = connectionOfDecorators(checker, declaration);
196
- if (direct) return direct;
197
- }
198
- if (ts.isParameter(declaration) && (ts.isArrowFunction(declaration.parent) || ts.isFunctionExpression(declaration.parent)) && declaration.parent.parameters[0] === declaration) {
199
- const call = declaration.parent.parent;
200
- if (ts.isCallExpression(call) && call.arguments.includes(declaration.parent) && ts.isPropertyAccessExpression(call.expression) && call.expression.name.text === 'transaction') {
201
- return connectionOfManager(checker, call.expression.expression, depth + 1);
202
- }
203
- }
204
- return null;
205
- };
206
- const model = { declared, registered, registrations: all, perConnection, contextOfCapability, contextOfFile, connectionOfManager, unparen };
207
272
  MODELS.set(graph, model);
208
273
  return model;
209
274
  }
210
275
 
276
+ const composedConnection = (kit, declared, checker, inner) => {
277
+ const { ts } = kit;
278
+ const parent = inner.parent;
279
+ if (parent?.name === inner && (ts.isPropertyAssignment(parent) || ts.isPropertyAccessExpression(parent))) return null;
280
+ if (!(ts.isIdentifier(inner) || ts.isStringLiteralLike(inner) || ts.isPropertyAccessExpression(inner))) return null;
281
+ const value = kit.stringValue(checker, inner);
282
+ return value !== null && declared.has(value) ? value : null;
283
+ };
284
+
285
+ const collectComposedConnections = (kit, declared, checker, call, items) => {
286
+ for (const argument of call.arguments) {
287
+ kit.walk(argument, inner => {
288
+ const value = composedConnection(kit, declared, checker, inner);
289
+ if (value === null) return true;
290
+ items.push({ connection: value, node: inner });
291
+ return false;
292
+ });
293
+ }
294
+ };
295
+
211
296
  /** The declared connection names the composition root of `app` passes to the database module registration, as [{connection, node}] in source order. */
212
297
  export function connectionsComposedBy(kit, app) {
213
298
  const { ts } = kit;
@@ -217,21 +302,10 @@ export function connectionsComposedBy(kit, app) {
217
302
  const checker = kit.checkerOf(root.sourceFile);
218
303
  const items = [];
219
304
  kit.walk(root.sourceFile, node => {
220
- if (!(ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'register')) return true;
305
+ if (!(ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'register')) return;
221
306
  const owner = kit.declarationsOf(checker, node.expression.expression).map(kit.ownerOfDeclaration).find(Boolean);
222
- if (!owner || owner.tier !== 'platform' || owner.name !== 'database') return true;
223
- for (const argument of node.arguments) {
224
- kit.walk(argument, inner => {
225
- if (ts.isPropertyAssignment(inner.parent ?? {}) && inner.parent.name === inner) return true;
226
- if (ts.isPropertyAccessExpression(inner.parent ?? {}) && inner.parent.name === inner) return true;
227
- if (!(ts.isIdentifier(inner) || ts.isStringLiteralLike(inner) || ts.isPropertyAccessExpression(inner))) return true;
228
- const value = kit.stringValue(checker, inner);
229
- if (value === null || !declared.has(value)) return true;
230
- items.push({ connection: value, node: inner });
231
- return false;
232
- });
233
- }
234
- return true;
307
+ if (owner?.tier !== 'platform' || owner.name !== 'database') return;
308
+ collectComposedConnections(kit, declared, checker, node, items);
235
309
  });
236
310
  return { root, items };
237
311
  }
@@ -19,6 +19,43 @@ const RULE = 'BE_CONTEXT_OWNER';
19
19
  const SERVICE_KINDS = new Set(['api', 'worker']);
20
20
  const RUNNER_KINDS = new Set(['migrate', 'cli']);
21
21
 
22
+ // What each app composes: [{connection, file, node}] from the database module registration and from the registration literals under its files.
23
+ const compositionsOf = (kit, resolver, model, appOf) => {
24
+ const composed = new Map(resolver.repo.apps.map(app => [app.name, { items: [], unresolved: false }]));
25
+ for (const app of resolver.repo.apps) {
26
+ const { root, items } = connectionsComposedBy(kit, app.name);
27
+ for (const item of items) composed.get(app.name).items.push({ ...item, file: root });
28
+ }
29
+ for (const site of model.registrations) {
30
+ const app = appOf(site.file.rel);
31
+ if (!app || !composed.has(app)) continue;
32
+ if (site.connection === null) composed.get(app).unresolved = true;
33
+ else composed.get(app).items.push({ connection: site.connection, file: site.file, node: site.node });
34
+ }
35
+ return composed;
36
+ };
37
+
38
+ // A service app composes only the connections it owns; a runner app composes every declared connection with tables.
39
+ const judgeComposition = (app, { items, unresolved }, { owners, model, kit, once }) => {
40
+ if (SERVICE_KINDS.has(app.kind)) {
41
+ for (const { connection, file, node } of items) {
42
+ const owner = owners.get(connection);
43
+ if (owner && owner !== app.name) {
44
+ once(`${app.name}|${connection}`, { ...kit.at(file.rel, file.sourceFile, node), message: `App ${app.name} composes connection ${connection}, the context owned by app ${owner}; an app composes only the contexts it owns. Reach that context through the API or the events of ${owner}.`, connection, app: app.name, owner });
45
+ }
46
+ }
47
+ } else if (RUNNER_KINDS.has(app.kind) && !unresolved) {
48
+ const root = kit.appRoot(app.name);
49
+ const present = new Set(items.map(item => item.connection));
50
+ const held = new Set([...model.registered.values()].flatMap(entry => [...entry.connections.keys()]));
51
+ for (const connection of owners.keys()) {
52
+ if (!root || present.has(connection) || !held.has(connection) || !items.length) continue;
53
+ once(`${app.name}|missing|${connection}`, { ...kit.at(root.rel, root.sourceFile, root.sourceFile), message: `The ${app.kind} app ${app.name} does not compose connection ${connection}; migrations run only through it, once per connection, so it registers every declared connection that holds tables.`, connection, app: app.name });
54
+ }
55
+ }
56
+ return items.length;
57
+ };
58
+
22
59
  export function checkContextOwner(input) {
23
60
  const { graph } = input;
24
61
  const kit = machineKit(input);
@@ -32,38 +69,10 @@ export function checkContextOwner(input) {
32
69
  const once = (key, violation) => { if (!seen.has(key)) { seen.add(key); violations.push({ ruleId: RULE, ...violation }); } };
33
70
  const appOf = rel => { const classified = resolver.classifyPath(rel); return classified.slot?.startsWith('be.app.') ? classified.bindings?.app ?? null : null; };
34
71
 
35
- // What each app composes: [{connection, file, node}] from the database module registration and from the registration literals under its files.
36
- const composed = new Map(resolver.repo.apps.map(app => [app.name, { items: [], unresolved: false }]));
37
- for (const app of resolver.repo.apps) {
38
- const { root, items } = connectionsComposedBy(kit, app.name);
39
- for (const item of items) composed.get(app.name).items.push({ ...item, file: root });
40
- }
41
- for (const site of model.registrations) {
42
- const app = appOf(site.file.rel);
43
- if (!app || !composed.has(app)) continue;
44
- if (site.connection === null) composed.get(app).unresolved = true;
45
- else composed.get(app).items.push({ connection: site.connection, file: site.file, node: site.node });
46
- }
72
+ const composed = compositionsOf(kit, resolver, model, appOf);
47
73
 
48
74
  for (const app of resolver.repo.apps) {
49
- const { items, unresolved } = composed.get(app.name);
50
- compositions += items.length;
51
- if (SERVICE_KINDS.has(app.kind)) {
52
- for (const { connection, file, node } of items) {
53
- const owner = owners.get(connection);
54
- if (owner && owner !== app.name) {
55
- once(`${app.name}|${connection}`, { ...kit.at(file.rel, file.sourceFile, node), message: `App ${app.name} composes connection ${connection}, the context owned by app ${owner}; an app composes only the contexts it owns. Reach that context through the API or the events of ${owner}.`, connection, app: app.name, owner });
56
- }
57
- }
58
- } else if (RUNNER_KINDS.has(app.kind) && !unresolved) {
59
- const root = kit.appRoot(app.name);
60
- const present = new Set(items.map(item => item.connection));
61
- const held = new Set([...model.registered.values()].flatMap(entry => [...entry.connections.keys()]));
62
- for (const connection of owners.keys()) {
63
- if (!root || present.has(connection) || !held.has(connection) || !items.length) continue;
64
- once(`${app.name}|missing|${connection}`, { ...kit.at(root.rel, root.sourceFile, root.sourceFile), message: `The ${app.kind} app ${app.name} does not compose connection ${connection}; migrations run only through it, once per connection, so it registers every declared connection that holds tables.`, connection, app: app.name });
65
- }
66
- }
75
+ compositions += judgeComposition(app, composed.get(app.name), { owners, model, kit, once });
67
76
  }
68
77
 
69
78
  const kinds = new Map(resolver.repo.apps.map(app => [app.name, app.kind]));
@@ -1,5 +1,6 @@
1
1
  import { contextModelOf } from './context-map.mjs';
2
2
  import { machineKit } from './machine-ast.mjs';
3
+ import { byCodeUnit } from '../../lib/list.mjs';
3
4
 
4
5
  /**
5
6
  * R178 `context-platform-tables` (BE_CONTEXT_PLATFORM_TABLES). A platform capability the manifest lists as `perConnection` (the event-bus outbox,
@@ -12,36 +13,49 @@ export const CONTEXT_PLATFORM_TABLES_RULE_IDS = ['BE_CONTEXT_PLATFORM_TABLES'];
12
13
 
13
14
  const RULE = 'BE_CONTEXT_PLATFORM_TABLES';
14
15
 
16
+ function perConnectionHome(kit, model, checker, file, node) {
17
+ const { ts } = kit;
18
+ const callee = ts.isPropertyAccessExpression(node.expression) ? node.expression.name : node.expression;
19
+ const home = kit.declarationsOf(checker, callee).map(kit.ownerOfDeclaration).find(Boolean);
20
+ if (!home || !model.perConnection.has(home.root) || file.owner?.root === home.root) return undefined;
21
+ return home;
22
+ }
23
+
24
+ function unregisteredViolations(kit, model, scope, home, connections, seen) {
25
+ const { file, node } = scope;
26
+ const registeredOn = model.registered.get(home.root)?.connections ?? new Map();
27
+ const violations = [];
28
+ for (const connection of connections) {
29
+ if (registeredOn.has(connection)) continue;
30
+ const key = `${file.rel}|${node.getStart()}|${connection}`;
31
+ if (seen.has(key)) continue;
32
+ seen.add(key);
33
+ violations.push({ ruleId: RULE, ...kit.at(file.rel, file.sourceFile, node), message: `${file.rel} passes the entity manager of connection ${connection} to ${home.name}, but the tables of ${home.name} are not registered on ${connection} (registered: ${[...registeredOn.keys()].sort(byCodeUnit).join(', ') || 'none'}); register its entities and migrations on ${connection} too, so the table exists in the database of that context.`, connection, capability: home.name });
34
+ }
35
+ return violations;
36
+ }
37
+
15
38
  export function checkContextPlatformTables(input) {
16
39
  const { graph } = input;
17
40
  const kit = machineKit(input);
18
41
  const { ts } = kit;
19
42
  const model = contextModelOf(kit, graph);
20
43
  const violations = [];
21
- let calls = 0;
22
- let unproven = 0;
23
- if (!model.perConnection.size) return { violations, coverage: { status: 'checked', perConnectionCapabilities: 0, calls, unproven } };
44
+ const counts = { calls: 0, unproven: 0 };
45
+ if (!model.perConnection.size) return { violations, coverage: { status: 'checked', perConnectionCapabilities: 0, ...counts } };
24
46
  const seen = new Set();
25
47
  for (const file of graph.files.values()) {
26
48
  const checker = kit.checkerOf(file.sourceFile);
27
49
  kit.walk(file.sourceFile, node => {
28
50
  if (!ts.isCallExpression(node)) return true;
29
- const callee = ts.isPropertyAccessExpression(node.expression) ? node.expression.name : node.expression;
30
- const home = kit.declarationsOf(checker, callee).map(kit.ownerOfDeclaration).find(Boolean);
31
- if (!home || !model.perConnection.has(home.root) || file.owner?.root === home.root) return true;
32
- calls += 1;
51
+ const home = perConnectionHome(kit, model, checker, file, node);
52
+ if (!home) return true;
53
+ counts.calls += 1;
33
54
  const connections = new Set(node.arguments.map(argument => model.connectionOfManager(checker, argument)).filter(Boolean));
34
- if (!connections.size) { unproven += 1; return true; }
35
- const registeredOn = model.registered.get(home.root)?.connections ?? new Map();
36
- for (const connection of connections) {
37
- if (registeredOn.has(connection)) continue;
38
- const key = `${file.rel}|${node.getStart()}|${connection}`;
39
- if (seen.has(key)) continue;
40
- seen.add(key);
41
- violations.push({ ruleId: RULE, ...kit.at(file.rel, file.sourceFile, node), message: `${file.rel} passes the entity manager of connection ${connection} to ${home.name}, but the tables of ${home.name} are not registered on ${connection} (registered: ${[...registeredOn.keys()].sort().join(', ') || 'none'}); register its entities and migrations on ${connection} too, so the table exists in the database of that context.`, connection, capability: home.name });
42
- }
55
+ if (!connections.size) { counts.unproven += 1; return true; }
56
+ violations.push(...unregisteredViolations(kit, model, { file, node }, home, connections, seen));
43
57
  return true;
44
58
  });
45
59
  }
46
- return { violations, coverage: { status: 'checked', perConnectionCapabilities: model.perConnection.size, calls, unproven } };
60
+ return { violations, coverage: { status: 'checked', perConnectionCapabilities: model.perConnection.size, ...counts } };
47
61
  }
@@ -13,14 +13,47 @@ export const CONTEXT_TRANSACTION_RULE_IDS = ['BE_CONTEXT_TRANSACTION'];
13
13
 
14
14
  const RULE = 'BE_CONTEXT_TRANSACTION';
15
15
 
16
+ const SPAN_ADVICE = "one transaction touches one context's connection. Span contexts with a saga: one transaction per context, linked by events.";
17
+
18
+ /** Reports an entity manager of another connection, or a call into another context's capability, found inside a transaction callback. */
19
+ function inspectInner(scope, file, checker, connection, inner) {
20
+ const { ts, kit, model, graph, report } = scope;
21
+ if (ts.isPropertyAccessExpression(inner)) {
22
+ const other = model.connectionOfManager(checker, inner);
23
+ if (other && other !== connection) report(file, inner, `A transaction on connection ${connection} uses the entity manager of connection ${other}; ${SPAN_ADVICE}`, { connection, other });
24
+ }
25
+ if (!ts.isCallExpression(inner)) return;
26
+ const callee = ts.isPropertyAccessExpression(inner.expression) ? inner.expression.name : inner.expression;
27
+ const declaration = kit.declarationsOf(checker, callee)[0];
28
+ const rel = declaration ? kit.graphPath(declaration) : null;
29
+ const context = rel ? model.contextOfFile(rel) : null;
30
+ if (context && context !== connection) {
31
+ report(file, inner, `A transaction on connection ${connection} calls ${graph.files.get(rel).owner.root} of context ${context}; ${SPAN_ADVICE}`, { connection, other: context });
32
+ }
33
+ }
34
+
35
+ /** Judges a `.transaction(...)` call: counts it as proven or unproven and inspects its callback body; any other node is skipped. */
36
+ function inspectTransaction(scope, file, checker, node) {
37
+ const { ts, kit, model, counts } = scope;
38
+ if (!(ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'transaction')) return;
39
+ const callback = node.arguments.find(argument => ts.isArrowFunction(argument) || ts.isFunctionExpression(argument));
40
+ if (!callback) return;
41
+ const connection = model.connectionOfManager(checker, node.expression.expression);
42
+ if (!connection) { counts.unproven += 1; return; }
43
+ counts.transactions += 1;
44
+ kit.walk(callback.body, inner => {
45
+ inspectInner(scope, file, checker, connection, inner);
46
+ return true;
47
+ });
48
+ }
49
+
16
50
  export function checkContextTransaction(input) {
17
51
  const { graph } = input;
18
52
  const kit = machineKit(input);
19
53
  const { ts } = kit;
20
54
  const model = contextModelOf(kit, graph);
21
55
  const violations = [];
22
- let transactions = 0;
23
- let unproven = 0;
56
+ const counts = { transactions: 0, unproven: 0 };
24
57
  const seen = new Set();
25
58
  const report = (file, node, message, extra) => {
26
59
  const key = `${file.rel}|${node.getStart()}|${message}`;
@@ -28,33 +61,13 @@ export function checkContextTransaction(input) {
28
61
  seen.add(key);
29
62
  violations.push({ ruleId: RULE, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
30
63
  };
64
+ const scope = { ts, kit, model, graph, report, counts };
31
65
  for (const file of graph.files.values()) {
32
66
  const checker = kit.checkerOf(file.sourceFile);
33
67
  kit.walk(file.sourceFile, node => {
34
- if (!(ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'transaction')) return true;
35
- const callback = node.arguments.find(argument => ts.isArrowFunction(argument) || ts.isFunctionExpression(argument));
36
- if (!callback) return true;
37
- const connection = model.connectionOfManager(checker, node.expression.expression);
38
- if (!connection) { unproven += 1; return true; }
39
- transactions += 1;
40
- kit.walk(callback.body, inner => {
41
- if (ts.isPropertyAccessExpression(inner)) {
42
- const other = model.connectionOfManager(checker, inner);
43
- if (other && other !== connection) report(file, inner, `A transaction on connection ${connection} uses the entity manager of connection ${other}; one transaction touches one context's connection. Span contexts with a saga: one transaction per context, linked by events.`, { connection, other });
44
- }
45
- if (ts.isCallExpression(inner)) {
46
- const callee = ts.isPropertyAccessExpression(inner.expression) ? inner.expression.name : inner.expression;
47
- const declaration = kit.declarationsOf(checker, callee)[0];
48
- const rel = declaration ? kit.graphPath(declaration) : null;
49
- const context = rel ? model.contextOfFile(rel) : null;
50
- if (context && context !== connection) {
51
- report(file, inner, `A transaction on connection ${connection} calls ${graph.files.get(rel).owner.root} of context ${context}; one transaction touches one context's connection. Span contexts with a saga: one transaction per context, linked by events.`, { connection, other: context });
52
- }
53
- }
54
- return true;
55
- });
68
+ inspectTransaction(scope, file, checker, node);
56
69
  return true;
57
70
  });
58
71
  }
59
- return { violations, coverage: { status: 'checked', transactions, unproven } };
72
+ return { violations, coverage: { status: 'checked', transactions: counts.transactions, unproven: counts.unproven } };
60
73
  }