@starci/hfs 4.0.10 → 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 (271) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/emit/operations.mjs +1 -1
  3. package/emit/type-schema.mjs +2 -1
  4. package/lint/run.mjs +3 -2
  5. package/package.json +1 -1
  6. package/report/order.mjs +2 -0
  7. package/runtime/config.example.yaml +187 -0
  8. package/runtime/engine/by-code-unit.mjs +12 -0
  9. package/runtime/engine/config.mjs +221 -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/removed-vocabulary.mjs +28 -0
  14. package/runtime/engine/resources-config.mjs +52 -0
  15. package/runtime/engine/runtime-root.mjs +17 -0
  16. package/runtime/engine/secrets.mjs +35 -31
  17. package/runtime/engine/sonar-config.mjs +21 -0
  18. package/runtime/engine/temp-root.mjs +32 -0
  19. package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
  20. package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
  21. package/runtime/knowledge/hfs/rules.yaml +98 -65
  22. package/runtime/knowledge/hfs/slots.yaml +11 -14
  23. package/runtime/knowledge/patterns/be/api.yaml +7 -24
  24. package/runtime/knowledge/patterns/be/cli.yaml +3 -12
  25. package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
  26. package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
  27. package/runtime/modules/kernel/failure-codes.yaml +1 -41
  28. package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
  29. package/runtime/modules/models/registry.yaml +25 -359
  30. package/runtime/modules/models/runtimes.yaml +92 -256
  31. package/runtime/modules/models/tiers.yaml +65 -0
  32. package/runtime/scripts/api/fs/claim-file.mjs +30 -0
  33. package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
  34. package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
  35. package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
  36. package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
  37. package/runtime/scripts/api/git/lib.mjs +34 -1
  38. package/runtime/scripts/api/process/resolve-real-tool.mjs +14 -8
  39. package/runtime/scripts/api/process/run-program.mjs +2 -1
  40. package/runtime/scripts/api/sops/lib.mjs +198 -120
  41. package/runtime/scripts/hfs/allows.mjs +3 -3
  42. package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
  43. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
  44. package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
  45. package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
  46. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
  47. package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
  48. package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
  49. package/runtime/scripts/hfs/architecture/config.mjs +95 -80
  50. package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
  51. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
  52. package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
  53. package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
  54. package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
  55. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
  56. package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
  57. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
  58. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
  59. package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
  60. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
  61. package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
  62. package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
  63. package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
  64. package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
  65. package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
  66. package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
  67. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
  68. package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
  69. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
  70. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
  71. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
  72. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
  73. package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
  74. package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
  75. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
  76. package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
  77. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
  78. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
  79. package/runtime/scripts/hfs/architecture/index.mjs +158 -102
  80. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
  81. package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
  82. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
  83. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
  84. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
  85. package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
  86. package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
  87. package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
  88. package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
  89. package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
  90. package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
  91. package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
  92. package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
  93. package/runtime/scripts/hfs/architecture/required-files.mjs +100 -68
  94. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
  95. package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
  96. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
  97. package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
  98. package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
  99. package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
  100. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
  101. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
  102. package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
  103. package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
  104. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
  105. package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
  106. package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
  107. package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
  108. package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
  109. package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
  110. package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
  111. package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
  112. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
  113. package/runtime/scripts/hfs/architecture.mjs +3 -4
  114. package/runtime/scripts/hfs/check.mjs +61 -70
  115. package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
  116. package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
  117. package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
  118. package/runtime/scripts/hfs/edition-slots.mjs +5 -3
  119. package/runtime/scripts/hfs/linear-text.mjs +30 -0
  120. package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
  121. package/runtime/scripts/hfs/path-findings.mjs +58 -38
  122. package/runtime/scripts/hfs/pin-findings.mjs +31 -0
  123. package/runtime/scripts/hfs/repo-identity.mjs +5 -2
  124. package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
  125. package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
  126. package/runtime/scripts/hfs/rules/cli.mjs +5 -1
  127. package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
  128. package/runtime/scripts/hfs/rules/contract.mjs +35 -27
  129. package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
  130. package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
  131. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
  132. package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
  133. package/runtime/scripts/hfs/rules/database.mjs +36 -23
  134. package/runtime/scripts/hfs/rules/deps.mjs +64 -32
  135. package/runtime/scripts/hfs/rules/docker.mjs +113 -65
  136. package/runtime/scripts/hfs/rules/edition.mjs +118 -111
  137. package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
  138. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
  139. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
  140. package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
  141. package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
  142. package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
  143. package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
  144. package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
  145. package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
  146. package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
  147. package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
  148. package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
  149. package/runtime/scripts/hfs/rules/saga.mjs +128 -61
  150. package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
  151. package/runtime/scripts/hfs/rules/services.mjs +34 -16
  152. package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
  153. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
  154. package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
  155. package/runtime/scripts/hfs/secret.mjs +59 -48
  156. package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
  157. package/runtime/scripts/hfs/slot-classify.mjs +65 -0
  158. package/runtime/scripts/hfs/slot-errors.mjs +11 -0
  159. package/runtime/scripts/hfs/slot-imports.mjs +76 -0
  160. package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
  161. package/runtime/scripts/hfs/slot-match.mjs +153 -0
  162. package/runtime/scripts/hfs/slot-path.mjs +8 -0
  163. package/runtime/scripts/hfs/slot-required.mjs +76 -0
  164. package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
  165. package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
  166. package/runtime/scripts/hfs/slots.mjs +72 -693
  167. package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
  168. package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
  169. package/runtime/scripts/hfs/tree.mjs +19 -14
  170. package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
  171. package/runtime/scripts/hfs/view.mjs +1 -1
  172. package/runtime/scripts/lib/dockerfile.mjs +56 -33
  173. package/runtime/scripts/lib/event-contract.mjs +17 -13
  174. package/runtime/scripts/lib/git.mjs +2 -2
  175. package/runtime/scripts/lib/graphql-contract.mjs +158 -322
  176. package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
  177. package/runtime/scripts/lib/i18n.mjs +3 -2
  178. package/runtime/scripts/lib/in-order.mjs +71 -0
  179. package/runtime/scripts/lib/language.mjs +3 -3
  180. package/runtime/scripts/lib/list.mjs +8 -1
  181. package/runtime/scripts/lib/path-key.mjs +38 -5
  182. package/runtime/scripts/lib/pid-alive.mjs +7 -0
  183. package/runtime/scripts/lib/regex.mjs +1 -1
  184. package/runtime/scripts/lib/same-text.mjs +1 -1
  185. package/runtime/scripts/lib/secret-patterns.mjs +6 -2
  186. package/runtime/scripts/lib/sleep-sync.mjs +2 -2
  187. package/runtime/scripts/lib/sops-envelope.mjs +95 -48
  188. package/runtime/scripts/lib/stack-services.mjs +1 -2
  189. package/runtime/scripts/lib/ts-ast.mjs +9 -0
  190. package/runtime/scripts/lib/walk.mjs +6 -1
  191. package/scaffold/add-cli-lite.mjs +2 -1
  192. package/scaffold/add-table.mjs +2 -2
  193. package/scaffold/add.mjs +3 -2
  194. package/scaffold/app.mjs +4 -2
  195. package/scaffold/edition-gate.mjs +25 -25
  196. package/scaffold/lite-exports.mjs +2 -1
  197. package/scaffold/service.mjs +5 -4
  198. package/sync/hygiene.mjs +15 -9
  199. package/sync/index.mjs +7 -3
  200. package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
  201. package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
  202. package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
  203. package/templates/app/ci-workflows/sonar-steps.yml +2 -2
  204. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
  205. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
  206. package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
  207. package/templates/app/starciwork.gitignore +1 -1
  208. package/templates/be/image/api/Dockerfile +1 -1
  209. package/templates/be/image/cli/Dockerfile +1 -1
  210. package/templates/be/image/worker/Dockerfile +1 -1
  211. package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
  212. package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
  213. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
  214. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
  215. package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
  216. package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
  217. package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
  218. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
  219. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
  220. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
  221. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
  222. package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
  223. package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
  224. package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
  225. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
  226. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  227. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
  228. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
  229. package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
  230. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  231. package/templates/fe/image/next/Dockerfile +2 -1
  232. package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
  233. package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
  234. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
  235. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
  236. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
  237. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
  238. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
  239. package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
  240. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
  241. package/upgrade/index.mjs +9 -2
  242. package/runtime/engine/admission.mjs +0 -308
  243. package/runtime/engine/canonical-json.mjs +0 -10
  244. package/runtime/engine/db/blob.mjs +0 -315
  245. package/runtime/engine/db/ledger-paths.mjs +0 -83
  246. package/runtime/engine/db/ledger.mjs +0 -1118
  247. package/runtime/engine/db/machine-connection.mjs +0 -135
  248. package/runtime/engine/db/machine-schema.mjs +0 -84
  249. package/runtime/engine/db/machine.mjs +0 -1367
  250. package/runtime/engine/db/migrations/machine/0001-init.sql +0 -924
  251. package/runtime/engine/db/migrations/runtime/0001-init.sql +0 -1108
  252. package/runtime/engine/db/provider-reservations.mjs +0 -101
  253. package/runtime/engine/digest.mjs +0 -16
  254. package/runtime/engine/refuse.mjs +0 -11
  255. package/runtime/modules/ops/_labels.yaml +0 -53
  256. package/runtime/scripts/api/node/lib.mjs +0 -14
  257. package/runtime/scripts/api/node/spawn-node.mjs +0 -6
  258. package/runtime/scripts/api/process/lib.mjs +0 -111
  259. package/runtime/scripts/api/process/owned-process.mjs +0 -78
  260. package/runtime/scripts/api/process/stop-owned-process.mjs +0 -9
  261. package/runtime/scripts/connectors/lib.mjs +0 -488
  262. package/runtime/scripts/lib/clip.mjs +0 -19
  263. package/runtime/scripts/lib/display-names.mjs +0 -259
  264. package/runtime/scripts/lib/example-refs.mjs +0 -158
  265. package/runtime/scripts/lib/json-schema.mjs +0 -52
  266. package/runtime/scripts/lib/process-identity.mjs +0 -6
  267. package/runtime/scripts/lib/read-yaml.mjs +0 -18
  268. package/runtime/scripts/lib/redact.mjs +0 -161
  269. package/runtime/scripts/lib/sleep.mjs +0 -7
  270. package/runtime/scripts/lib/source-phrases.mjs +0 -34
  271. package/runtime/scripts/lib/sqlite.mjs +0 -21
@@ -3,6 +3,7 @@ import { machineKit, pascal } from './machine-ast.mjs';
3
3
  import { treeOf } from './required-files.mjs';
4
4
  import { DATABASE_DIR } from './connection-map.mjs';
5
5
  import { collectRegistrations, isPerConnection, persistenceOfFactory } from './context-map.mjs';
6
+ import { byCodeUnit } from '../../lib/list.mjs';
6
7
 
7
8
  /**
8
9
  * R35 `schema-owner` (BE_SCHEMA_OWNER, BE-CONVENTION 1.4.6). Schema lives with the capability that owns the table:
@@ -24,141 +25,214 @@ export const SCHEMA_OWNER_RULE_IDS = ['BE_SCHEMA_OWNER'];
24
25
 
25
26
  const RULE = 'BE_SCHEMA_OWNER';
26
27
  const PERSISTENCE_SLOT = 'be.persistence';
27
- const SCHEMA_FOLDERS = ['entities', 'migrations'];
28
+ const SCHEMA_FOLDERS = new Set(['entities', 'migrations']);
28
29
  const MIGRATION_FILE = /^(\d{13})-([a-z0-9]+(?:-[a-z0-9]+)*)\.ts$/u;
29
30
  const camel = name => { const text = pascal(name); return text[0].toLowerCase() + text.slice(1); };
30
31
 
31
- export function checkSchemaOwner(input) {
32
- const { config, graph } = input;
33
- const kit = machineKit(input);
34
- const { ts, resolver } = kit;
35
- const connections = new Set(resolver.repo.connections.map(item => item.name));
36
- const violations = [];
37
- const report = (file, node, message, extra = {}) => violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
38
- const plain = (rel, message, extra = {}) => violations.push({ ruleId: RULE, path: rel, line: 1, column: 1, message, ...extra });
39
-
40
- const persistenceOf = persistenceOfFactory(resolver);
41
-
42
- let entities = 0;
43
- let migrations = 0;
44
- const found = new Map(); // capability root -> {name, entities, migrations}
45
- const note = (persistence, kind) => {
46
- const key = path.posix.dirname(persistence.root);
47
- if (!found.has(key)) found.set(key, { name: persistence.capability, entities: false, migrations: false });
48
- found.get(key)[kind] = true;
49
- };
32
+ function noteSchemaOwner(found, persistence, kind) {
33
+ const key = path.posix.dirname(persistence.root);
34
+ if (!found.has(key)) found.set(key, { name: persistence.capability, entities: false, migrations: false });
35
+ found.get(key)[kind] = true;
36
+ }
50
37
 
51
- const tree = treeOf(config.root);
52
- for (const rel of [...tree.files].sort()) {
38
+ function reportSchemaFolders(tree, resolver, plain) {
39
+ for (const rel of [...tree.files].sort(byCodeUnit)) {
53
40
  const classified = resolver.classifyPath(rel);
54
41
  if (classified.status === 'no-slot' || !classified.slot?.startsWith('be.') || classified.slot === PERSISTENCE_SLOT) continue;
55
- const folder = rel.split('/').slice(0, -1).find(segment => SCHEMA_FOLDERS.includes(segment));
42
+ const folder = rel.split('/').slice(0, -1).find(segment => SCHEMA_FOLDERS.has(segment));
56
43
  if (folder) plain(rel, `${rel} sits in a ${folder}/ folder outside the persistence of a capability; ${folder} live only in src/modules/{domain,platform}/<capability>/persistence/${folder}/ of the capability that owns the table.`, { folder });
57
44
  }
58
- for (const rel of [...tree.files].sort()) {
45
+ }
46
+
47
+ function reportMigrationPaths(tree, persistenceOf, plain) {
48
+ for (const rel of [...tree.files].sort(byCodeUnit)) {
59
49
  const persistence = persistenceOf(rel);
60
- if (!persistence || persistence.folder !== 'migrations') continue;
50
+ if (persistence?.folder !== 'migrations') continue;
61
51
  if (/\.spec\.[cm]?tsx?$/u.test(rel)) plain(rel, `${rel} is a unit spec of a migration; a migration has no spec, the e2e run over the same migrations is its proof. Delete it.`);
62
52
  else if (!MIGRATION_FILE.test(persistence.below.at(-1)) || persistence.below.length !== 2) plain(rel, `${rel} is not named <epochMs13>-<kebab-name>.ts (13 digits of epoch milliseconds, a dash, a kebab-case name) directly in persistence/migrations/.`);
63
53
  }
54
+ }
55
+
56
+ function isEntityDeclaration(kit, checker, statement) {
57
+ return kit.decorators(statement).some(decorator => {
58
+ const call = kit.ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression;
59
+ return kit.isImportOf(checker, call, 'Entity', 'typeorm');
60
+ });
61
+ }
62
+
63
+ function isMigrationDeclaration(kit, checker, statement) {
64
+ const heritage = (statement.heritageClauses ?? []).flatMap(clause => clause.types.map(type => type.expression));
65
+ return heritage.some(expression => kit.isImportOf(checker, expression, 'MigrationInterface', 'typeorm'));
66
+ }
67
+
68
+ function inspectEntityDeclaration(resolver, file, statement, persistence, found, report) {
69
+ // The table of a projection belongs to the projection that alone writes it: `<name>.projection-entity.ts` of slot be.projections.
70
+ const ownProjectionTable = resolver.classifyPath(file.rel).slot === 'be.projections' && file.rel.endsWith('.projection-entity.ts');
71
+ if (ownProjectionTable) noteSchemaOwner(found, { root: path.posix.join(path.posix.dirname(file.rel), 'persistence'), capability: path.posix.basename(path.posix.dirname(file.rel)) }, 'entities');
72
+ else if (persistence?.folder !== 'entities') report(file, statement.name, `Entity ${statement.name.text} is declared outside persistence/entities/ of a domain or platform capability; an entity lives with the capability that owns its table, in src/modules/{domain,platform}/<capability>/persistence/entities/<table>.entity.ts.`, { class: statement.name.text });
73
+ else noteSchemaOwner(found, persistence, 'entities');
74
+ }
75
+
76
+ function inspectMigrationDeclaration(kit, file, statement, persistence, checker, found, report) {
77
+ if (persistence?.folder !== 'migrations') {
78
+ report(file, statement.name, `Migration ${statement.name.text} is declared outside persistence/migrations/ of a domain or platform capability; a migration lives with the capability that owns the table, in persistence/migrations/<epochMs13>-<kebab-name>.ts.`, { class: statement.name.text });
79
+ return;
80
+ }
81
+ noteSchemaOwner(found, persistence, 'migrations');
82
+ const match = MIGRATION_FILE.exec(path.posix.basename(file.rel));
83
+ if (!match) return;
84
+ const expected = `${pascal(match[2])}${match[1]}`;
85
+ if (statement.name.text !== expected) report(file, statement.name, `Migration class ${statement.name.text} must be named ${expected}: the PascalCase of the file name followed by its 13-digit epoch, because TypeORM reads the timestamp from the class name.`, { class: statement.name.text, expected });
86
+ const nameProperty = statement.members.find(member => kit.ts.isPropertyDeclaration(member) && kit.propertyNameText(member.name) === 'name');
87
+ const nameValue = nameProperty?.initializer ? kit.stringValue(checker, nameProperty.initializer) : null;
88
+ if (nameValue !== statement.name.text) report(file, nameProperty ?? statement.name, `Migration ${statement.name.text} must declare \`name = "${statement.name.text}"\`, equal to its class name; TypeORM records that value in its migrations table.`, { class: statement.name.text });
89
+ }
90
+
91
+ function inspectSchemaClass(scan, site, statement) {
92
+ const { kit, found, counts, report } = scan;
93
+ const { file, persistence, checker } = site;
94
+ if (!kit.ts.isClassDeclaration(statement) || !statement.name) return;
95
+ const isEntity = isEntityDeclaration(kit, checker, statement);
96
+ const isMigration = isMigrationDeclaration(kit, checker, statement);
97
+ if (isEntity) {
98
+ counts.entities += 1;
99
+ inspectEntityDeclaration(kit.resolver, file, statement, persistence, found, report);
100
+ }
101
+ if (!isMigration) return;
102
+ counts.migrations += 1;
103
+ inspectMigrationDeclaration(kit, file, statement, persistence, checker, found, report);
104
+ }
64
105
 
106
+ function inspectSchemaFiles(input, kit, persistenceOf, found, report) {
107
+ const { graph } = input;
108
+ const counts = { entities: 0, migrations: 0 };
109
+ const scan = { kit, found, counts, report };
65
110
  for (const file of graph.files.values()) {
66
111
  const persistence = persistenceOf(file.rel);
67
112
  const checker = kit.checkerOf(file.sourceFile);
68
- const heritage = statement => (statement.heritageClauses ?? []).flatMap(clause => clause.types.map(type => type.expression));
69
- for (const statement of file.sourceFile.statements) {
70
- if (!ts.isClassDeclaration(statement) || !statement.name) continue;
71
- const isEntity = kit.decorators(statement).some(decorator => {
72
- const call = ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression;
73
- return kit.isImportOf(checker, call, 'Entity', 'typeorm');
74
- });
75
- const isMigration = heritage(statement).some(expression => kit.isImportOf(checker, expression, 'MigrationInterface', 'typeorm'));
76
- if (isEntity) {
77
- entities += 1;
78
- // The table of a projection belongs to the projection that alone writes it: `<name>.projection-entity.ts` of slot be.projections.
79
- const ownProjectionTable = resolver.classifyPath(file.rel).slot === 'be.projections' && file.rel.endsWith('.projection-entity.ts');
80
- if (ownProjectionTable) note({ root: path.posix.join(path.posix.dirname(file.rel), 'persistence'), capability: path.posix.basename(path.posix.dirname(file.rel)) }, 'entities');
81
- else if (persistence?.folder !== 'entities') report(file, statement.name, `Entity ${statement.name.text} is declared outside persistence/entities/ of a domain or platform capability; an entity lives with the capability that owns its table, in src/modules/{domain,platform}/<capability>/persistence/entities/<table>.entity.ts.`, { class: statement.name.text });
82
- else note(persistence, 'entities');
83
- }
84
- if (!isMigration) continue;
85
- migrations += 1;
86
- if (persistence?.folder !== 'migrations') {
87
- report(file, statement.name, `Migration ${statement.name.text} is declared outside persistence/migrations/ of a domain or platform capability; a migration lives with the capability that owns the table, in persistence/migrations/<epochMs13>-<kebab-name>.ts.`, { class: statement.name.text });
88
- continue;
89
- }
90
- note(persistence, 'migrations');
91
- const match = MIGRATION_FILE.exec(path.posix.basename(file.rel));
92
- if (!match) continue;
93
- const expected = `${pascal(match[2])}${match[1]}`;
94
- if (statement.name.text !== expected) report(file, statement.name, `Migration class ${statement.name.text} must be named ${expected}: the PascalCase of the file name followed by its 13-digit epoch, because TypeORM reads the timestamp from the class name.`, { class: statement.name.text, expected });
95
- const nameProperty = statement.members.find(member => ts.isPropertyDeclaration(member) && kit.propertyNameText(member.name) === 'name');
96
- const nameValue = nameProperty?.initializer ? kit.stringValue(checker, nameProperty.initializer) : null;
97
- if (nameValue !== statement.name.text) report(file, nameProperty ?? statement.name, `Migration ${statement.name.text} must declare \`name = "${statement.name.text}"\`, equal to its class name; TypeORM records that value in its migrations table.`, { class: statement.name.text });
98
- }
113
+ for (const statement of file.sourceFile.statements) inspectSchemaClass(scan, { file, persistence, checker }, statement);
99
114
  }
115
+ return counts;
116
+ }
100
117
 
101
- const { registered, registrations } = collectRegistrations({ kit, graph, persistenceOf });
118
+ function reportRegistrationSites(registered, connections, resolver, report) {
102
119
  for (const [key, { name, connections: sites }] of registered) {
103
120
  for (const [connection, site] of sites) {
104
- if (!connections.has(connection)) report(site.file, site.node, `The ${name} capability is registered on connection "${connection}", which hfs.json does not declare (declared: ${[...connections].sort().join(', ') || 'none'}); the tables of a capability live on one declared connection.`, { connection, capability: name });
121
+ if (!connections.has(connection)) report(site.file, site.node, `The ${name} capability is registered on connection "${connection}", which hfs.json does not declare (declared: ${[...connections].sort(byCodeUnit).join(', ') || 'none'}); the tables of a capability live on one declared connection.`, { connection, capability: name });
105
122
  }
106
123
  if (sites.size > 1 && !isPerConnection(resolver, key, name)) {
107
- const all = [...sites.keys()].sort().join(' and ');
124
+ const all = [...sites.keys()].sort(byCodeUnit).join(' and ');
108
125
  for (const [connection, site] of [...sites].slice(1)) report(site.file, site.node, `The ${name} capability's entities and migrations are registered on connections ${all}; a capability's tables live on exactly one connection, so register its arrays under one connection across every app.`, { connection, capability: name });
109
126
  }
110
127
  }
111
- for (const [key, kinds] of found) {
112
- if (!registered.get(key)?.connections.size) plain(`${key}/persistence/connection.ts`, `The ${kinds.name} capability has entities or migrations but no app registers them; add ${camel(kinds.name)}Entities and ${camel(kinds.name)}Migrations to a connection of DatabaseModule.register and of the cli app.`, { capability: kinds.name });
113
- const connectionFile = graph.files.get(`${key}/persistence/connection.ts`);
114
- const declared = new Set((connectionFile?.sourceFile.statements.filter(ts.isVariableStatement).flatMap(statement => statement.declarationList.declarations) ?? []).map(item => item.name.text));
115
- for (const [kind, has] of [['Entities', kinds.entities], ['Migrations', kinds.migrations]]) {
116
- if (has && !declared.has(`${camel(kinds.name)}${kind}`)) plain(`${key}/persistence/connection.ts`, `${key}/persistence/connection.ts must export ${camel(kinds.name)}${kind}, the ${kind.toLowerCase()} of the ${kinds.name} capability, the one file that lists them for the apps.`, { capability: kinds.name });
117
- }
118
- const only = registered.get(key)?.connections;
119
- if (!only || only.size < 1) continue;
120
- // A per-connection platform capability (event-bus, jobs) lives on every connection that uses it; any other on exactly one (reported above).
121
- const perConnection = only.size > 1 && isPerConnection(resolver, key, kinds.name);
122
- if (only.size > 1 && !perConnection) continue;
123
- const home = [...only.keys()][0];
124
- for (const file of graph.files.values()) {
125
- if (!file.rel.startsWith(`${key}/`)) continue;
126
- const checker = kit.checkerOf(file.sourceFile);
127
- kit.walk(file.sourceFile, node => {
128
- if (!ts.isCallExpression(node)) return true;
129
- const declaration = kit.declarationsOf(checker, node.expression)[0];
130
- const rel = declaration ? kit.graphPath(declaration) : null;
131
- const used = rel ? [...connections].find(name => rel === `${DATABASE_DIR}/${name}.decorators.ts`) : null;
132
- if (used && perConnection && !only.has(used)) report(file, node, `${file.rel} injects the entity manager of connection ${used}, but the ${kinds.name} capability's tables are registered on ${[...only.keys()].sort().join(' and ')}; register the capability on ${used} too or inject one of its connections.`, { connection: used, expected: [...only.keys()].sort().join(','), capability: kinds.name });
133
- else if (used && !perConnection && used !== home) report(file, node, `${file.rel} injects the entity manager of connection ${used}, but the ${kinds.name} capability's tables are registered on ${home}; a capability reads only its own database, so use Inject${pascal(home)}EntityManager or move the tables.`, { connection: used, expected: home, capability: kinds.name });
134
- return true;
135
- });
136
- }
128
+ }
129
+
130
+ function reportDeclaredPersistenceArrays(key, kinds, registered, graph, ts, plain) {
131
+ if (!registered.get(key)?.connections.size) plain(`${key}/persistence/connection.ts`, `The ${kinds.name} capability has entities or migrations but no app registers them; add ${camel(kinds.name)}Entities and ${camel(kinds.name)}Migrations to a connection of DatabaseModule.register and of the cli app.`, { capability: kinds.name });
132
+ const connectionFile = graph.files.get(`${key}/persistence/connection.ts`);
133
+ const declared = new Set((connectionFile?.sourceFile.statements.filter(ts.isVariableStatement).flatMap(statement => statement.declarationList.declarations) ?? []).map(item => item.name.text));
134
+ for (const [kind, has] of [['Entities', kinds.entities], ['Migrations', kinds.migrations]]) {
135
+ if (has && !declared.has(`${camel(kinds.name)}${kind}`)) plain(`${key}/persistence/connection.ts`, `${key}/persistence/connection.ts must export ${camel(kinds.name)}${kind}, the ${kind.toLowerCase()} of the ${kinds.name} capability, the one file that lists them for the apps.`, { capability: kinds.name });
137
136
  }
137
+ }
138
138
 
139
- // The owner's index exports `<c>Entities` and `<c>Migrations`, the only persistence names.
140
- for (const [capabilityRoot, kinds] of found) {
141
- const index = graph.files.get(`${capabilityRoot}/index.ts`);
142
- if (!index) continue;
143
- const wanted = { Entities: `${camel(kinds.name)}Entities`, Migrations: `${camel(kinds.name)}Migrations` };
144
- const exported = new Set();
145
- for (const statement of index.sourceFile.statements) {
146
- if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
147
- for (const element of statement.exportClause.elements) {
148
- exported.add(element.name.text);
149
- const from = statement.moduleSpecifier?.text;
150
- const inPersistence = from?.startsWith('.') && path.posix.join(capabilityRoot, from).split('/').includes('persistence');
151
- if (inPersistence && !Object.values(wanted).includes(element.name.text)) {
152
- report(index, element, `${element.name.text} is exported from persistence/; the persistence of a capability is exposed only as ${wanted.Entities} and ${wanted.Migrations}.`, { name: element.name.text });
153
- }
154
- }
155
- } else if (ts.isVariableStatement(statement) && kit.isExported(statement)) {
156
- for (const declaration of statement.declarationList.declarations) if (ts.isIdentifier(declaration.name)) exported.add(declaration.name.text);
157
- }
158
- }
159
- for (const [kind, has] of [['Entities', kinds.entities], ['Migrations', kinds.migrations]]) {
160
- if (has && !exported.has(wanted[kind])) report(index, index.sourceFile, `${capabilityRoot}/index.ts must export ${wanted[kind]}, the ${kind.toLowerCase()} of the capability; apps concatenate them per connection into DatabaseModule.register.`, { capability: kinds.name, expected: wanted[kind] });
139
+ function reportEntityManagerUse(kit, capability, file, checker, report) {
140
+ const { kinds, only, home, connections } = capability;
141
+ kit.walk(file.sourceFile, node => {
142
+ if (!kit.ts.isCallExpression(node)) return;
143
+ const declaration = kit.declarationsOf(checker, node.expression)[0];
144
+ const rel = declaration ? kit.graphPath(declaration) : null;
145
+ const used = rel ? [...connections].find(name => rel === `${DATABASE_DIR}/${name}.decorators.ts`) : null;
146
+ if (used && only.size > 1 && !only.has(used)) report(file, node, `${file.rel} injects the entity manager of connection ${used}, but the ${kinds.name} capability's tables are registered on ${[...only.keys()].sort(byCodeUnit).join(' and ')}; register the capability on ${used} too or inject one of its connections.`, { connection: used, expected: [...only.keys()].sort(byCodeUnit).join(','), capability: kinds.name });
147
+ else if (used && only.size === 1 && used !== home) report(file, node, `${file.rel} injects the entity manager of connection ${used}, but the ${kinds.name} capability's tables are registered on ${home}; a capability reads only its own database, so use Inject${pascal(home)}EntityManager or move the tables.`, { connection: used, expected: home, capability: kinds.name });
148
+ });
149
+ }
150
+
151
+ function reportEntityManagerUses(input, kit, key, kinds, only, connections, report) {
152
+ const capability = { kinds, only, home: [...only.keys()][0], connections };
153
+ for (const file of input.graph.files.values()) {
154
+ if (!file.rel.startsWith(`${key}/`)) continue;
155
+ reportEntityManagerUse(kit, capability, file, kit.checkerOf(file.sourceFile), report);
156
+ }
157
+ }
158
+
159
+ function reportFoundCapability(input, kit, key, kinds, view) {
160
+ const { registered, connections, plain, report } = view;
161
+ const { graph } = input;
162
+ const { resolver } = kit;
163
+ reportDeclaredPersistenceArrays(key, kinds, registered, graph, kit.ts, plain);
164
+ const only = registered.get(key)?.connections;
165
+ if (!only || only.size < 1) return;
166
+ // A per-connection platform capability (event-bus, jobs) lives on every connection that uses it; any other on exactly one (reported above).
167
+ const perConnection = only.size > 1 && isPerConnection(resolver, key, kinds.name);
168
+ if (only.size > 1 && !perConnection) return;
169
+ reportEntityManagerUses(input, kit, key, kinds, only, connections, report);
170
+ }
171
+
172
+ function reportFoundCapabilities(input, kit, found, registered, connections, plain, report) {
173
+ const view = { registered, connections, plain, report };
174
+ for (const [key, kinds] of found) reportFoundCapability(input, kit, key, kinds, view);
175
+ }
176
+
177
+ function reportPersistenceExport(capabilityRoot, index, statement, element, wanted, report) {
178
+ const from = statement.moduleSpecifier?.text;
179
+ const inPersistence = from?.startsWith('.') && path.posix.join(capabilityRoot, from).split('/').includes('persistence');
180
+ if (inPersistence && !Object.values(wanted).includes(element.name.text)) {
181
+ report(index, element, `${element.name.text} is exported from persistence/; the persistence of a capability is exposed only as ${wanted.Entities} and ${wanted.Migrations}.`, { name: element.name.text });
182
+ }
183
+ }
184
+
185
+ function addStatementExports(kit, capabilityRoot, index, statement, wanted, report, exported) {
186
+ const { ts } = kit;
187
+ if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
188
+ for (const element of statement.exportClause.elements) {
189
+ exported.add(element.name.text);
190
+ reportPersistenceExport(capabilityRoot, index, statement, element, wanted, report);
161
191
  }
192
+ } else if (ts.isVariableStatement(statement) && kit.isExported(statement)) {
193
+ for (const declaration of statement.declarationList.declarations) if (ts.isIdentifier(declaration.name)) exported.add(declaration.name.text);
162
194
  }
163
- return { violations, coverage: { status: 'checked', entities, migrations, capabilities: found.size, connections: connections.size, registrations } };
195
+ }
196
+
197
+ function collectIndexExports(kit, capabilityRoot, index, wanted, report) {
198
+ const exported = new Set();
199
+ for (const statement of index.sourceFile.statements) addStatementExports(kit, capabilityRoot, index, statement, wanted, report, exported);
200
+ return exported;
201
+ }
202
+
203
+ function reportCapabilityIndex(input, kit, capabilityRoot, kinds, report) {
204
+ const { graph } = input;
205
+ const index = graph.files.get(`${capabilityRoot}/index.ts`);
206
+ if (!index) return;
207
+ const wanted = { Entities: `${camel(kinds.name)}Entities`, Migrations: `${camel(kinds.name)}Migrations` };
208
+ const exported = collectIndexExports(kit, capabilityRoot, index, wanted, report);
209
+ for (const [kind, has] of [['Entities', kinds.entities], ['Migrations', kinds.migrations]]) {
210
+ if (has && !exported.has(wanted[kind])) report(index, index.sourceFile, `${capabilityRoot}/index.ts must export ${wanted[kind]}, the ${kind.toLowerCase()} of the capability; apps concatenate them per connection into DatabaseModule.register.`, { capability: kinds.name, expected: wanted[kind] });
211
+ }
212
+ }
213
+
214
+ function reportCapabilityIndexes(input, kit, found, report) {
215
+ // The owner's index exports `<c>Entities` and `<c>Migrations`, the only persistence names.
216
+ for (const [capabilityRoot, kinds] of found) reportCapabilityIndex(input, kit, capabilityRoot, kinds, report);
217
+ }
218
+
219
+ export function checkSchemaOwner(input) {
220
+ const { config, graph } = input;
221
+ const kit = machineKit(input);
222
+ const { resolver } = kit;
223
+ const connections = new Set(resolver.repo.connections.map(item => item.name));
224
+ const violations = [];
225
+ const report = (file, node, message, extra = {}) => violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
226
+ const plain = (rel, message, extra = {}) => violations.push({ ruleId: RULE, path: rel, line: 1, column: 1, message, ...extra });
227
+ const persistenceOf = persistenceOfFactory(resolver);
228
+ const tree = treeOf(config.root);
229
+ reportSchemaFolders(tree, resolver, plain);
230
+ reportMigrationPaths(tree, persistenceOf, plain);
231
+ const found = new Map(); // capability root -> {name, entities, migrations}
232
+ const counts = inspectSchemaFiles(input, kit, persistenceOf, found, report);
233
+ const { registered, registrations } = collectRegistrations({ kit, graph, persistenceOf });
234
+ reportRegistrationSites(registered, connections, resolver, report);
235
+ reportFoundCapabilities(input, kit, found, registered, connections, plain, report);
236
+ reportCapabilityIndexes(input, kit, found, report);
237
+ return { violations, coverage: { status: 'checked', entities: counts.entities, migrations: counts.migrations, capabilities: found.size, connections: connections.size, registrations } };
164
238
  }