@starci/hfs 1.0.1 → 2.0.1

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 (246) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +110 -12
  3. package/bin/hfs.mjs +119 -15
  4. package/emit/compiler.mjs +35 -0
  5. package/emit/contracts.mjs +97 -0
  6. package/emit/operations-worker.mjs +24 -0
  7. package/emit/operations.mjs +126 -0
  8. package/emit/schema-worker.mjs +117 -0
  9. package/emit/static-graph.mjs +670 -0
  10. package/emit/type-schema.mjs +145 -0
  11. package/package.json +4 -1
  12. package/report/sonar.mjs +180 -0
  13. package/runtime/engine/admission.mjs +284 -0
  14. package/runtime/engine/digest.mjs +10 -0
  15. package/runtime/engine/ledger-db.mjs +1245 -0
  16. package/runtime/engine/machine-db.mjs +1484 -0
  17. package/runtime/engine/migrations/machine/0001-init.sql +887 -0
  18. package/runtime/engine/migrations/runtime/0001-init.sql +1072 -0
  19. package/runtime/engine/migrations/runtime/0003-usage-unavailable.sql +13 -0
  20. package/runtime/engine/migrations/runtime/0004-attempt-why.sql +43 -0
  21. package/runtime/engine/plain-object.mjs +5 -0
  22. package/runtime/knowledge/hfs/canon-pins.yaml +16 -58
  23. package/runtime/knowledge/hfs/slots.yaml +405 -137
  24. package/runtime/knowledge/patterns/fe/folder.yaml +309 -0
  25. package/runtime/knowledge/sonar-gate.yaml +85 -0
  26. package/runtime/modules/kernel/failure-codes.yaml +1480 -16
  27. package/runtime/scripts/checks/architecture/backend.mjs +350 -0
  28. package/runtime/scripts/checks/architecture/background-unowned.mjs +107 -0
  29. package/runtime/scripts/checks/architecture/client-reaches-server.mjs +94 -0
  30. package/runtime/scripts/checks/architecture/clones.mjs +200 -0
  31. package/runtime/scripts/checks/architecture/config-unread.mjs +35 -0
  32. package/runtime/scripts/checks/architecture/config.mjs +310 -0
  33. package/runtime/scripts/checks/architecture/connection-map.mjs +208 -0
  34. package/runtime/scripts/checks/architecture/constructor-deps.mjs +100 -0
  35. package/runtime/scripts/checks/architecture/contract-fixture-guard.mjs +128 -0
  36. package/runtime/scripts/checks/architecture/contracts.mjs +792 -0
  37. package/runtime/scripts/checks/architecture/cross-app-duplicate.mjs +73 -0
  38. package/runtime/scripts/checks/architecture/dead-exports.mjs +265 -0
  39. package/runtime/scripts/checks/architecture/default-deny.mjs +129 -0
  40. package/runtime/scripts/checks/architecture/doc-language.mjs +39 -0
  41. package/runtime/scripts/checks/architecture/entrypoint.mjs +57 -0
  42. package/runtime/scripts/checks/architecture/error-codes.mjs +45 -0
  43. package/runtime/scripts/checks/architecture/error-masked.mjs +60 -0
  44. package/runtime/scripts/checks/architecture/fe-slot-allows.mjs +38 -0
  45. package/runtime/scripts/checks/architecture/feature-shape.mjs +49 -0
  46. package/runtime/scripts/checks/architecture/framework-pinned.mjs +83 -0
  47. package/runtime/scripts/checks/architecture/frontend.mjs +995 -0
  48. package/runtime/scripts/checks/architecture/hfs-graph.mjs +61 -0
  49. package/runtime/scripts/checks/architecture/hfs.mjs +521 -0
  50. package/runtime/scripts/checks/architecture/hooks-are-hooks.mjs +88 -0
  51. package/runtime/scripts/checks/architecture/i18n-keys.mjs +146 -0
  52. package/runtime/scripts/checks/architecture/index.mjs +316 -0
  53. package/runtime/scripts/checks/architecture/injection-token-exported.mjs +62 -0
  54. package/runtime/scripts/checks/architecture/machine-ast.mjs +138 -0
  55. package/runtime/scripts/checks/architecture/module-per-transport.mjs +130 -0
  56. package/runtime/scripts/checks/architecture/next-data.mjs +775 -0
  57. package/runtime/scripts/checks/architecture/owners.mjs +89 -0
  58. package/runtime/scripts/checks/architecture/package-shape.mjs +63 -0
  59. package/runtime/scripts/checks/architecture/reachability.mjs +233 -0
  60. package/runtime/scripts/checks/architecture/register-once.mjs +141 -0
  61. package/runtime/scripts/checks/architecture/registration.mjs +319 -0
  62. package/runtime/scripts/checks/architecture/required-files.mjs +172 -0
  63. package/runtime/scripts/checks/architecture/route-files-thin.mjs +97 -0
  64. package/runtime/scripts/checks/architecture/schema-owner.mjs +261 -0
  65. package/runtime/scripts/checks/architecture/size-growth.mjs +73 -0
  66. package/runtime/scripts/checks/architecture/source-names.mjs +607 -0
  67. package/runtime/scripts/checks/architecture/sql-owner.mjs +142 -0
  68. package/runtime/scripts/checks/architecture/sql-tokens.mjs +327 -0
  69. package/runtime/scripts/checks/architecture/symbols.mjs +193 -0
  70. package/runtime/scripts/checks/architecture/test-world-files.mjs +163 -0
  71. package/runtime/scripts/checks/architecture/tiers.mjs +130 -0
  72. package/runtime/scripts/checks/architecture/transport-owner.mjs +112 -0
  73. package/runtime/scripts/checks/architecture/typescript.mjs +500 -0
  74. package/runtime/scripts/checks/architecture/unit-spec-providers.mjs +122 -0
  75. package/runtime/scripts/checks/architecture.mjs +41 -0
  76. package/runtime/scripts/checks/common.mjs +37 -0
  77. package/runtime/scripts/checks/typescript-programs.mjs +82 -0
  78. package/runtime/scripts/lib/artifact-hold.mjs +89 -0
  79. package/runtime/scripts/lib/artifact-store.mjs +103 -0
  80. package/runtime/scripts/lib/fs-kind.mjs +10 -0
  81. package/runtime/scripts/lib/git.mjs +53 -0
  82. package/runtime/scripts/lib/hfs-allows.mjs +57 -0
  83. package/runtime/scripts/lib/hfs-check.mjs +254 -28
  84. package/runtime/scripts/lib/hfs-rules/contract.mjs +126 -0
  85. package/runtime/scripts/lib/hfs-rules/deps.mjs +63 -0
  86. package/runtime/scripts/lib/hfs-rules/fe-no-tests.mjs +47 -0
  87. package/runtime/scripts/lib/hfs-rules/frontend.mjs +124 -0
  88. package/runtime/scripts/lib/hfs-rules/lint-suppression.mjs +34 -0
  89. package/runtime/scripts/lib/hfs-rules/pipeline.mjs +51 -0
  90. package/runtime/scripts/lib/hfs-rules/proof-commands.mjs +64 -0
  91. package/runtime/scripts/lib/hfs-rules/read.mjs +28 -0
  92. package/runtime/scripts/lib/hfs-rules/repo-local-checks.mjs +32 -0
  93. package/runtime/scripts/lib/hfs-rules/secrets.mjs +54 -0
  94. package/runtime/scripts/lib/hfs-rules/spec-placement.mjs +31 -0
  95. package/runtime/scripts/lib/hfs-rules/stacks.mjs +54 -0
  96. package/runtime/scripts/lib/hfs-rules/test-topology.mjs +31 -0
  97. package/runtime/scripts/lib/hfs-slots.mjs +88 -40
  98. package/runtime/scripts/lib/hfs-tree.mjs +80 -0
  99. package/runtime/scripts/lib/hfs-view.mjs +68 -0
  100. package/runtime/scripts/lib/json.mjs +22 -0
  101. package/runtime/scripts/lib/language.mjs +107 -0
  102. package/runtime/scripts/lib/path-key.mjs +2 -0
  103. package/runtime/scripts/lib/redact.mjs +148 -0
  104. package/runtime/scripts/lib/repo-identity.mjs +50 -0
  105. package/runtime/scripts/lib/safe-remove.mjs +179 -0
  106. package/runtime/scripts/lib/secret-patterns.mjs +44 -0
  107. package/runtime/scripts/lib/sleep-sync.mjs +17 -0
  108. package/runtime/scripts/lib/stack-declaration.mjs +52 -0
  109. package/runtime/scripts/lib/stack-services.mjs +217 -0
  110. package/runtime/scripts/lib/test-secrets.mjs +120 -0
  111. package/scaffold/service.mjs +333 -0
  112. package/sync/format.mjs +46 -0
  113. package/sync/hygiene.mjs +56 -24
  114. package/sync/index.mjs +133 -41
  115. package/sync/managed.mjs +170 -0
  116. package/sync/skeleton.mjs +32 -10
  117. package/sync/sonar-key.mjs +13 -0
  118. package/sync/ts-strict.mjs +48 -0
  119. package/templates/{common → be/ci-workflows/github/workflows}/ci.yml +13 -13
  120. package/templates/be/{e2e.yml → ci-workflows/github/workflows/e2e.yml} +1 -0
  121. package/templates/be/hooks/husky/pre-commit +13 -0
  122. package/templates/be/hooks/husky/pre-push +7 -0
  123. package/templates/be/package-scripts/package.json +22 -0
  124. package/templates/be/{sonar-project.properties → quality-config/sonar-project.properties} +2 -3
  125. package/templates/be/skeleton/apps/__app__/src/app.module.ts +14 -4
  126. package/templates/be/skeleton/apps/__app__/src/main.ts +9 -6
  127. package/templates/be/skeleton/scripts/.gitkeep +0 -0
  128. package/templates/be/skeleton/src/features/system-health/application/check-liveness.contracts.ts +2 -0
  129. package/templates/be/skeleton/src/features/system-health/application/check-liveness.handler.ts +22 -0
  130. package/templates/be/skeleton/src/features/system-health/application/check-liveness.query.ts +11 -0
  131. package/templates/be/skeleton/src/features/system-health/index.ts +1 -1
  132. package/templates/be/skeleton/src/features/system-health/system-health.module.ts +3 -3
  133. package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.ts +8 -9
  134. package/templates/be/skeleton/src/features/system-health/transport/http/system-health-http.module.ts +3 -6
  135. package/templates/be/skeleton/src/modules/domain/liveness/index.ts +3 -0
  136. package/templates/be/skeleton/src/modules/domain/liveness/liveness.contracts.ts +9 -0
  137. package/templates/be/skeleton/src/modules/domain/liveness/liveness.module-definition.ts +7 -0
  138. package/templates/be/skeleton/src/modules/domain/liveness/liveness.module.ts +14 -0
  139. package/templates/be/skeleton/src/modules/domain/liveness/liveness.options.ts +2 -0
  140. package/templates/be/skeleton/src/modules/domain/liveness/liveness.service.spec.ts +40 -0
  141. package/templates/be/skeleton/src/modules/domain/liveness/liveness.service.ts +26 -0
  142. package/templates/be/skeleton/src/modules/platform/clock/clock.decorators.ts +9 -0
  143. package/templates/be/skeleton/src/modules/platform/clock/clock.module-definition.ts +7 -0
  144. package/templates/be/skeleton/src/modules/platform/clock/clock.module.ts +19 -0
  145. package/templates/be/skeleton/src/modules/platform/clock/clock.options.ts +2 -0
  146. package/templates/be/skeleton/src/modules/platform/clock/clock.port.ts +5 -0
  147. package/templates/be/skeleton/src/modules/platform/clock/index.ts +4 -0
  148. package/templates/be/skeleton/src/modules/platform/clock/system-clock.service.spec.ts +20 -0
  149. package/templates/be/skeleton/src/modules/platform/clock/system-clock.service.ts +9 -0
  150. package/templates/be/skeleton/src/modules/platform/composition/composition.decorators.ts +8 -0
  151. package/templates/be/skeleton/src/modules/platform/composition/index.ts +2 -0
  152. package/templates/be/skeleton/src/modules/platform/config/config.decorators.ts +9 -0
  153. package/templates/be/skeleton/src/modules/platform/config/index.ts +2 -3
  154. package/templates/be/skeleton/src/modules/platform/config/server.config.ts +1 -1
  155. package/templates/be/skeleton/src/modules/platform/config/server.options.ts +0 -3
  156. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.contracts.ts +5 -0
  157. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.decorators.ts +9 -0
  158. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.handler.ts +27 -0
  159. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.log-events.ts +5 -0
  160. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module-definition.ts +7 -0
  161. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module.ts +18 -0
  162. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.options.ts +2 -0
  163. package/templates/be/skeleton/src/modules/platform/cqrs/index.ts +4 -0
  164. package/templates/be/skeleton/src/modules/platform/errors/error.filter.ts +8 -12
  165. package/templates/be/skeleton/src/modules/platform/errors/errors.log-events.ts +5 -0
  166. package/templates/be/skeleton/src/modules/platform/errors/index.ts +1 -1
  167. package/templates/be/skeleton/src/modules/platform/logging/index.ts +4 -4
  168. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +99 -0
  169. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +38 -0
  170. package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +9 -0
  171. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +7 -0
  172. package/templates/be/skeleton/src/modules/platform/logging/logging.module-definition.ts +7 -0
  173. package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +23 -10
  174. package/templates/be/skeleton/src/modules/platform/logging/logging.options.ts +2 -0
  175. package/templates/be/skeleton/src/modules/platform/logging/logging.port.ts +15 -0
  176. package/templates/be/tool-config/eslint.config.mjs +3 -0
  177. package/templates/be/tool-config/jest.config.js +1 -0
  178. package/templates/be/tool-config/prettierignore +8 -0
  179. package/templates/be/tool-config/prettierrc +1 -0
  180. package/templates/be/tool-config/src/tests/tsconfig.json +5 -0
  181. package/templates/be/tool-config/tsconfig.build.json +5 -0
  182. package/templates/be/tool-config/tsconfig.json +11 -0
  183. package/templates/common/gitignore.base +1 -1
  184. package/templates/fe/ci-workflows/github/workflows/ci.yml +54 -0
  185. package/templates/fe/hooks/husky/pre-commit +16 -0
  186. package/templates/fe/hooks/husky/pre-push +6 -0
  187. package/templates/fe/package-scripts/package.json +17 -0
  188. package/templates/fe/parts/api-client.ts +44 -0
  189. package/templates/fe/parts/api-outcome.ts +7 -0
  190. package/templates/fe/quality-config/sonar-project.properties +8 -0
  191. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/layout.tsx +1 -1
  192. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/not-found.tsx +1 -1
  193. package/templates/fe/skeleton/apps/__app__/src/app/global-error.tsx +1 -1
  194. package/templates/fe/skeleton/scripts/.gitkeep +0 -0
  195. package/templates/fe/skeleton-app/apps/__app__/src/modules/api/client.ts +1 -0
  196. package/templates/fe/skeleton-app/apps/__app__/src/modules/api/index.ts +3 -0
  197. package/templates/fe/skeleton-app/apps/__app__/src/modules/api/outcome.ts +1 -0
  198. package/templates/fe/skeleton-app/apps/__app__/src/modules/i18n/index.ts +4 -0
  199. package/templates/fe/skeleton-shared/apps/__app__/next.config.ts +12 -0
  200. package/templates/fe/skeleton-shared/apps/__app__/src/modules/api/index.ts +2 -0
  201. package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/index.ts +9 -0
  202. package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/request.ts +5 -0
  203. package/templates/fe/skeleton-shared/apps/__app__/src/proxy.ts +5 -0
  204. package/templates/fe/skeleton-shared/packages/__family__-api/package.json +12 -0
  205. package/templates/fe/skeleton-shared/packages/__family__-api/src/client.ts +1 -0
  206. package/templates/fe/skeleton-shared/packages/__family__-api/src/index.ts +3 -0
  207. package/templates/fe/skeleton-shared/packages/__family__-api/src/outcome.ts +1 -0
  208. package/templates/fe/skeleton-shared/packages/__family__-api/tsconfig.json +5 -0
  209. package/templates/fe/skeleton-shared/packages/__family__-i18n/package.json +18 -0
  210. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/app.ts +19 -0
  211. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/index.ts +2 -0
  212. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/proxy.ts +12 -0
  213. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/request.ts +15 -0
  214. package/templates/fe/skeleton-shared/packages/__family__-i18n/tsconfig.json +5 -0
  215. package/templates/fe/tool-config/eslint.config.mjs +3 -0
  216. package/templates/fe/tool-config/prettierignore +10 -0
  217. package/templates/fe/tool-config/prettierrc +1 -0
  218. package/templates/fe/tool-config/stylelint.config.mjs +3 -0
  219. package/templates/fe/tool-config/tsconfig.json +4 -0
  220. package/templates/be/pre-commit +0 -8
  221. package/templates/be/skeleton/apps/__app__/src/__app__.composition.spec.ts +0 -31
  222. package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.spec.ts +0 -13
  223. package/templates/be/skeleton/src/modules/platform/config/env-source.spec.ts +0 -36
  224. package/templates/be/skeleton/src/modules/platform/config/server.config.spec.ts +0 -19
  225. package/templates/be/skeleton/src/modules/platform/errors/domain-error.spec.ts +0 -15
  226. package/templates/be/skeleton/src/modules/platform/errors/error.filter.spec.ts +0 -38
  227. package/templates/be/skeleton/src/modules/platform/logging/json-logger.spec.ts +0 -33
  228. package/templates/be/skeleton/src/modules/platform/logging/json-logger.ts +0 -35
  229. package/templates/be/skeleton/src/modules/platform/logging/log-id.ts +0 -9
  230. package/templates/be/skeleton/src/modules/platform/logging/logger.port.ts +0 -19
  231. package/templates/common/codecov.yml +0 -13
  232. package/templates/common/pre-push +0 -5
  233. package/templates/fe/e2e.yml +0 -22
  234. package/templates/fe/pre-commit +0 -7
  235. package/templates/fe/skeleton/apps/__app__/src/app/health/live/route.spec.ts +0 -10
  236. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages.spec.ts +0 -27
  237. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/routing.spec.ts +0 -10
  238. package/templates/fe/sonar-project.properties +0 -11
  239. /package/templates/be/skeleton/src/modules/platform/config/{env-source.ts → env-source.config.ts} +0 -0
  240. /package/templates/be/skeleton/src/modules/platform/errors/{domain-error.ts → domain.error.ts} +0 -0
  241. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/next.config.ts +0 -0
  242. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/config.ts +0 -0
  243. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/navigation.ts +0 -0
  244. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/request.ts +0 -0
  245. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/routing.ts +0 -0
  246. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/proxy.ts +0 -0
@@ -0,0 +1,261 @@
1
+ import path from 'node:path';
2
+ import { machineKit, pascal } from './machine-ast.mjs';
3
+ import { treeOf } from './required-files.mjs';
4
+ import { DATABASE_DIR } from './connection-map.mjs';
5
+
6
+ /**
7
+ * R35 `schema-owner` (BE_SCHEMA_OWNER, BE-CONVENTION 1.4.6). Schema lives with the capability that owns the table:
8
+ *
9
+ * - a TypeORM `@Entity` class and a class implementing `MigrationInterface` sit in `persistence/entities/` and
10
+ * `persistence/migrations/` of a domain or platform capability (slot be.persistence), and nowhere else; a file in a
11
+ * folder named `entities` or `migrations` outside be.persistence is refused too;
12
+ * - `persistence/connection.ts` declares `<c>Entities` / `<c>Migrations`, the arrays the apps register; a capability has no
13
+ * `CONNECTION` alias (C0 lead decision 2026-09-30): its connection is the one those arrays are REGISTERED on, in the
14
+ * `entities` / `migrations` of a connection literal of `DatabaseModule.register` and of the migrate app. The arrays are
15
+ * registered under exactly one connection across all apps, that connection is declared in hfs.json, and every
16
+ * `Inject<Conn>EntityManager()` used inside the capability is that same connection;
17
+ * - the capability's `index.ts` exports `<c>Entities` when it has entities and `<c>Migrations` when it has migrations
18
+ * (`<c>` the camelCase capability name), and no other name coming from `persistence/`;
19
+ * - a migration file is `<epochMs13>-<kebab-name>.ts`, holds a class `<Pascal><epochMs13>` and a `name` property equal to
20
+ * that class name, and has no unit spec beside it.
21
+ */
22
+ export const SCHEMA_OWNER_RULE_IDS = ['BE_SCHEMA_OWNER'];
23
+
24
+ const RULE = 'BE_SCHEMA_OWNER';
25
+ const PERSISTENCE_SLOT = 'be.persistence';
26
+ const SCHEMA_FOLDERS = ['entities', 'migrations'];
27
+ const MIGRATION_FILE = /^(\d{13})-([a-z0-9]+(?:-[a-z0-9]+)*)\.ts$/u;
28
+ const camel = name => { const text = pascal(name); return text[0].toLowerCase() + text.slice(1); };
29
+
30
+ export function checkSchemaOwner(input) {
31
+ const { config, graph } = input;
32
+ const kit = machineKit(input);
33
+ const { ts, resolver } = kit;
34
+ const connections = new Set(resolver.repo.connections.map(item => item.name));
35
+ const violations = [];
36
+ const report = (file, node, message, extra = {}) => violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
37
+ const plain = (rel, message, extra = {}) => violations.push({ ruleId: RULE, path: rel, line: 1, column: 1, message, ...extra });
38
+
39
+ /** {root, capability, folder, below} of a be.persistence file: `folder` is the directory below persistence/ ('' at its root). */
40
+ const persistenceOf = rel => {
41
+ const classified = resolver.classifyPath(rel);
42
+ if (classified.slot !== PERSISTENCE_SLOT) return null;
43
+ const below = rel.slice(classified.root.length + 1).split('/');
44
+ return { root: classified.root, capability: path.posix.basename(path.posix.dirname(classified.root)), folder: below.length > 1 ? below[0] : '', below };
45
+ };
46
+
47
+ let entities = 0;
48
+ let migrations = 0;
49
+ const found = new Map(); // capability root -> {name, entities, migrations}
50
+ const note = (persistence, kind) => {
51
+ const key = path.posix.dirname(persistence.root);
52
+ if (!found.has(key)) found.set(key, { name: persistence.capability, entities: false, migrations: false });
53
+ found.get(key)[kind] = true;
54
+ };
55
+
56
+ const tree = treeOf(config.root);
57
+ for (const rel of [...tree.files].sort()) {
58
+ const classified = resolver.classifyPath(rel);
59
+ if (classified.status === 'no-slot' || !classified.slot?.startsWith('be.') || classified.slot === PERSISTENCE_SLOT) continue;
60
+ const folder = rel.split('/').slice(0, -1).find(segment => SCHEMA_FOLDERS.includes(segment));
61
+ 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 });
62
+ }
63
+ for (const rel of [...tree.files].sort()) {
64
+ const persistence = persistenceOf(rel);
65
+ if (!persistence || persistence.folder !== 'migrations') continue;
66
+ 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.`);
67
+ 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/.`);
68
+ }
69
+
70
+ for (const file of graph.files.values()) {
71
+ const persistence = persistenceOf(file.rel);
72
+ const checker = kit.checkerOf(file.sourceFile);
73
+ const heritage = statement => (statement.heritageClauses ?? []).flatMap(clause => clause.types.map(type => type.expression));
74
+ for (const statement of file.sourceFile.statements) {
75
+ if (!ts.isClassDeclaration(statement) || !statement.name) continue;
76
+ const isEntity = kit.decorators(statement).some(decorator => {
77
+ const call = ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression;
78
+ return kit.isImportOf(checker, call, 'Entity', 'typeorm');
79
+ });
80
+ const isMigration = heritage(statement).some(expression => kit.isImportOf(checker, expression, 'MigrationInterface', 'typeorm'));
81
+ if (isEntity) {
82
+ entities += 1;
83
+ 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 });
84
+ else note(persistence, 'entities');
85
+ }
86
+ if (!isMigration) continue;
87
+ migrations += 1;
88
+ if (persistence?.folder !== 'migrations') {
89
+ 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 });
90
+ continue;
91
+ }
92
+ note(persistence, 'migrations');
93
+ const match = MIGRATION_FILE.exec(path.posix.basename(file.rel));
94
+ if (!match) continue;
95
+ const expected = `${pascal(match[2])}${match[1]}`;
96
+ 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 });
97
+ const nameProperty = statement.members.find(member => ts.isPropertyDeclaration(member) && kit.propertyNameText(member.name) === 'name');
98
+ const nameValue = nameProperty?.initializer ? kit.stringValue(checker, nameProperty.initializer) : null;
99
+ 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 });
100
+ }
101
+ }
102
+
103
+ // The connection of a capability is where its arrays are registered (no CONNECTION alias in persistence/connection.ts).
104
+ 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; };
105
+ const returned = declaration => {
106
+ const fn = ts.isVariableDeclaration(declaration) && declaration.initializer ? unparen(declaration.initializer) : declaration;
107
+ if (!(ts.isArrowFunction(fn) || ts.isFunctionDeclaration(fn) || ts.isFunctionExpression(fn) || ts.isMethodDeclaration(fn)) || !fn.body) return null;
108
+ if (!ts.isBlock(fn.body)) return fn.body;
109
+ const statements = fn.body.statements.filter(ts.isReturnStatement);
110
+ return statements.length === 1 ? statements[0].expression ?? null : null;
111
+ };
112
+ const propertySources = new Map(); // property name -> [{declaration, initializer, checker}] over the program
113
+ const assignmentsOf = name => {
114
+ if (propertySources.has(name)) return propertySources.get(name);
115
+ const list = [];
116
+ for (const file of graph.files.values()) {
117
+ if (!file.sourceFile.text.includes(name)) continue;
118
+ const checker = kit.checkerOf(file.sourceFile);
119
+ kit.walk(file.sourceFile, node => {
120
+ if (ts.isPropertyAssignment(node) && kit.propertyNameText(node.name) === name && ts.isObjectLiteralExpression(node.parent)) {
121
+ // the property of the type the literal is checked against, so a typed const and a typed return meet the same declaration
122
+ const contextual = checker.getContextualType(node.parent)?.getProperty(name);
123
+ const symbol = contextual ?? checker.getSymbolAtLocation(node.name);
124
+ list.push({ declaration: symbol?.declarations?.[0] ?? null, initializer: node.initializer, checker });
125
+ }
126
+ return true;
127
+ });
128
+ }
129
+ propertySources.set(name, list);
130
+ return list;
131
+ };
132
+ const nameOfLiteral = (checker, literal, depth) => {
133
+ const own = kit.propertyOf(literal, 'name');
134
+ if (own) return kit.stringValue(checker, kit.valueOfProperty(own));
135
+ for (const property of literal.properties) {
136
+ if (!ts.isSpreadAssignment(property)) continue;
137
+ const value = nameOfExpression(checker, property.expression, depth + 1);
138
+ if (value) return value;
139
+ }
140
+ return null;
141
+ };
142
+ const nameOfExpression = (checker, expression, depth) => {
143
+ const node = unparen(expression);
144
+ if (!node || depth > 5) return null;
145
+ if (ts.isObjectLiteralExpression(node)) return nameOfLiteral(checker, node, depth);
146
+ if (ts.isCallExpression(node)) {
147
+ for (const declaration of kit.declarationsOf(checker, node.expression)) {
148
+ const body = returned(declaration);
149
+ const value = body ? nameOfExpression(kit.checkerOf(declaration.getSourceFile()), body, depth + 1) : null;
150
+ if (value) return value;
151
+ }
152
+ return null;
153
+ }
154
+ if (ts.isIdentifier(node)) {
155
+ const declaration = kit.declarationsOf(checker, node)[0];
156
+ return declaration && ts.isVariableDeclaration(declaration) && declaration.initializer ? nameOfExpression(kit.checkerOf(declaration.getSourceFile()), declaration.initializer, depth + 1) : null;
157
+ }
158
+ if (ts.isPropertyAccessExpression(node)) {
159
+ const target = kit.declarationsOf(checker, node.name)[0];
160
+ if (!target) return null;
161
+ const names = new Set(assignmentsOf(node.name.text).filter(item => item.declaration === target).map(item => nameOfExpression(item.checker, item.initializer, depth + 1)).filter(Boolean));
162
+ return names.size === 1 ? [...names][0] : null;
163
+ }
164
+ return null;
165
+ };
166
+ /** The capability arrays (`<c>Entities` / `<c>Migrations` of persistence/connection.ts) an expression lists. */
167
+ const arraysIn = (checker, expression, out, depth) => {
168
+ const node = unparen(expression);
169
+ if (!node || depth > 6) return;
170
+ if (ts.isArrayLiteralExpression(node)) { for (const element of node.elements) arraysIn(checker, ts.isSpreadElement(element) ? element.expression : element, out, depth + 1); return; }
171
+ if (!ts.isIdentifier(node) && !ts.isPropertyAccessExpression(node)) return;
172
+ const declaration = kit.declarationsOf(checker, ts.isPropertyAccessExpression(node) ? node.name : node)[0];
173
+ if (!declaration || !ts.isVariableDeclaration(declaration) || !ts.isIdentifier(declaration.name)) return;
174
+ const rel = kit.graphPath(declaration);
175
+ const owner = rel ? persistenceOf(rel) : null;
176
+ if (owner && owner.below.length === 1 && owner.below[0] === 'connection.ts' && [`${camel(owner.capability)}Entities`, `${camel(owner.capability)}Migrations`].includes(declaration.name.text)) {
177
+ out.push({ key: path.posix.dirname(owner.root), name: owner.capability });
178
+ } else if (declaration.initializer) arraysIn(kit.checkerOf(declaration.getSourceFile()), declaration.initializer, out, depth + 1);
179
+ };
180
+ const registered = new Map(); // capability root -> {name, connections: Map(connection -> first site)}
181
+ let registrations = 0;
182
+ for (const file of graph.files.values()) {
183
+ const checker = kit.checkerOf(file.sourceFile);
184
+ kit.walk(file.sourceFile, node => {
185
+ if (!ts.isObjectLiteralExpression(node)) return true;
186
+ const arrays = [];
187
+ for (const key of ['entities', 'migrations']) {
188
+ const property = kit.propertyOf(node, key);
189
+ if (property) arraysIn(checker, kit.valueOfProperty(property), arrays, 0);
190
+ }
191
+ if (!arrays.length) return true;
192
+ registrations += 1;
193
+ const connection = nameOfLiteral(checker, node, 0);
194
+ if (connection === null) return true;
195
+ for (const { key, name } of new Map(arrays.map(item => [item.key, item])).values()) {
196
+ if (!registered.has(key)) registered.set(key, { name, connections: new Map() });
197
+ const sites = registered.get(key).connections;
198
+ if (!sites.has(connection)) sites.set(connection, { file, node });
199
+ }
200
+ return true;
201
+ });
202
+ }
203
+ for (const { name, connections: sites } of registered.values()) {
204
+ for (const [connection, site] of sites) {
205
+ 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 });
206
+ }
207
+ if (sites.size > 1) {
208
+ const all = [...sites.keys()].sort().join(' and ');
209
+ 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 });
210
+ }
211
+ }
212
+ for (const [key, kinds] of found) {
213
+ 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 migrate app.`, { capability: kinds.name });
214
+ const connectionFile = graph.files.get(`${key}/persistence/connection.ts`);
215
+ const declared = new Set((connectionFile?.sourceFile.statements.filter(ts.isVariableStatement).flatMap(statement => statement.declarationList.declarations) ?? []).map(item => item.name.text));
216
+ for (const [kind, has] of [['Entities', kinds.entities], ['Migrations', kinds.migrations]]) {
217
+ 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 });
218
+ }
219
+ const only = registered.get(key)?.connections;
220
+ if (!only || only.size !== 1) continue;
221
+ const home = [...only.keys()][0];
222
+ for (const file of graph.files.values()) {
223
+ if (!file.rel.startsWith(`${key}/`)) continue;
224
+ const checker = kit.checkerOf(file.sourceFile);
225
+ kit.walk(file.sourceFile, node => {
226
+ if (!ts.isCallExpression(node)) return true;
227
+ const declaration = kit.declarationsOf(checker, node.expression)[0];
228
+ const rel = declaration ? kit.graphPath(declaration) : null;
229
+ const used = rel ? [...connections].find(name => rel === `${DATABASE_DIR}/${name}.decorators.ts`) : null;
230
+ if (used && 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 });
231
+ return true;
232
+ });
233
+ }
234
+ }
235
+
236
+ // The owner's index exports `<c>Entities` and `<c>Migrations`, the only persistence names.
237
+ for (const [capabilityRoot, kinds] of found) {
238
+ const index = graph.files.get(`${capabilityRoot}/index.ts`);
239
+ if (!index) continue;
240
+ const wanted = { Entities: `${camel(kinds.name)}Entities`, Migrations: `${camel(kinds.name)}Migrations` };
241
+ const exported = new Set();
242
+ for (const statement of index.sourceFile.statements) {
243
+ if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
244
+ for (const element of statement.exportClause.elements) {
245
+ exported.add(element.name.text);
246
+ const from = statement.moduleSpecifier?.text;
247
+ const inPersistence = from?.startsWith('.') && path.posix.join(capabilityRoot, from).split('/').includes('persistence');
248
+ if (inPersistence && !Object.values(wanted).includes(element.name.text)) {
249
+ 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 });
250
+ }
251
+ }
252
+ } else if (ts.isVariableStatement(statement) && kit.isExported(statement)) {
253
+ for (const declaration of statement.declarationList.declarations) if (ts.isIdentifier(declaration.name)) exported.add(declaration.name.text);
254
+ }
255
+ }
256
+ for (const [kind, has] of [['Entities', kinds.entities], ['Migrations', kinds.migrations]]) {
257
+ 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] });
258
+ }
259
+ }
260
+ return { violations, coverage: { status: 'checked', entities, migrations, capabilities: found.size, connections: connections.size, registrations } };
261
+ }
@@ -0,0 +1,73 @@
1
+ import fs from 'node:fs';
2
+ import { gitOutput } from '../../lib/git.mjs';
3
+
4
+ /**
5
+ * HFS check 6, file size growth (knowledge/hfs/slots.yaml ruleParams.<profile>.fileLines {soft, hardGrowth}):
6
+ * a production source file over `soft` lines may not grow against the merge-base, and a file that is new at the base
7
+ * must stay within `soft`. A file at or under `soft` passes, even when it grows, as long as it does not cross `soft`
8
+ * (crossing is growth of an over-budget file: the base had fewer lines than soft). Files are compared to the base file
9
+ * of the same path only, so a moved file is judged as new. The per-slot budgets (component 300, hook 200 ...) belong to
10
+ * the lint layer and are not judged here.
11
+ */
12
+ export const SIZE_GROWTH_RULE_IDS = ['HFS_SIZE_GROWTH'];
13
+
14
+ const MAX_BUFFER = 64 * 1024 * 1024;
15
+
16
+ function git(root, args) {
17
+ try {
18
+ return gitOutput(args, { cwd: root, maxBuffer: MAX_BUFFER });
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ /** The commit the growth is measured against: the given commit-ish, else merge-base with upstream, origin/main, origin/master, else HEAD's first parent. */
25
+ function resolveBase(root, requested) {
26
+ const commit = (rev) => { const out = git(root, ['rev-parse', '--verify', '--quiet', `${rev}^{commit}`]); return out ? out.trim() : null; };
27
+ if (requested) return commit(requested);
28
+ if (!commit('HEAD')) return null;
29
+ for (const other of ['@{upstream}', 'origin/main', 'origin/master']) {
30
+ if (!commit(other)) continue;
31
+ const merged = git(root, ['merge-base', 'HEAD', other]);
32
+ if (merged?.trim()) return merged.trim();
33
+ }
34
+ return commit('HEAD^');
35
+ }
36
+
37
+ export function countLines(text) {
38
+ if (!text) return 0;
39
+ const count = text.split('\n').length;
40
+ return text.endsWith('\n') ? count - 1 : count;
41
+ }
42
+
43
+ export function checkSizeGrowth({ config, graph, base } = {}) {
44
+ const root = config.root;
45
+ const sha = resolveBase(root, base);
46
+ if (!sha) return { violations: [], coverage: { status: 'unavailable', reason: 'no merge-base' } };
47
+ const { soft, hardGrowth } = graph.resolver.ruleParams().fileLines;
48
+ const violations = [];
49
+ let files = 0;
50
+ let overSoft = 0;
51
+ let grown = 0;
52
+ let newOverSoft = 0;
53
+ for (const rel of [...graph.files.keys()].sort()) {
54
+ let text;
55
+ try { text = fs.readFileSync(graph.files.get(rel).abs, 'utf8'); } catch { continue; }
56
+ files += 1;
57
+ const lines = countLines(text);
58
+ if (lines <= soft) continue;
59
+ overSoft += 1;
60
+ const baseText = git(root, ['cat-file', 'blob', `${sha}:${rel}`]);
61
+ const baseLines = baseText === null ? null : countLines(baseText);
62
+ if (baseLines === null) {
63
+ newOverSoft += 1;
64
+ if (hardGrowth) violations.push({ ruleId: 'HFS_SIZE_GROWTH', path: rel, line: 1, lines, baseLines: null, soft,
65
+ message: `${rel} is a new file of ${lines} lines, over the ${soft}-line soft budget; split it into smaller files before adding it.` });
66
+ } else if (lines > baseLines) {
67
+ grown += 1;
68
+ if (hardGrowth) violations.push({ ruleId: 'HFS_SIZE_GROWTH', path: rel, line: 1, lines, baseLines, soft,
69
+ message: `${rel} has ${lines} lines, over the ${soft}-line soft budget, and grew from ${baseLines} lines at the base; a file over budget may only shrink, so split the new code into another file.` });
70
+ }
71
+ }
72
+ return { violations, coverage: { status: 'checked', base: sha, files, overSoft, grown, newOverSoft } };
73
+ }