@starci/hfs 4.0.9 → 4.1.0

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