@starci/hfs 4.0.10 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/CHANGELOG.md +5 -1
  2. package/emit/operations.mjs +1 -1
  3. package/emit/type-schema.mjs +2 -1
  4. package/lint/run.mjs +65 -24
  5. package/package.json +1 -1
  6. package/report/order.mjs +2 -0
  7. package/runtime/config.example.yaml +194 -0
  8. package/runtime/engine/by-code-unit.mjs +12 -0
  9. package/runtime/engine/config.mjs +222 -239
  10. package/runtime/engine/invalid-config.mjs +5 -5
  11. package/runtime/engine/model-config.mjs +90 -0
  12. package/runtime/engine/orca-config.mjs +3 -1
  13. package/runtime/engine/release-config.mjs +28 -0
  14. package/runtime/engine/removed-vocabulary.mjs +28 -0
  15. package/runtime/engine/resources-config.mjs +52 -0
  16. package/runtime/engine/runtime-root.mjs +17 -0
  17. package/runtime/engine/secrets.mjs +35 -31
  18. package/runtime/engine/sonar-config.mjs +21 -0
  19. package/runtime/engine/temp-root.mjs +32 -0
  20. package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
  21. package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
  22. package/runtime/knowledge/hfs/rules.yaml +161 -65
  23. package/runtime/knowledge/hfs/slots.yaml +11 -14
  24. package/runtime/knowledge/patterns/be/api.yaml +7 -24
  25. package/runtime/knowledge/patterns/be/cli.yaml +3 -12
  26. package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
  27. package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
  28. package/runtime/modules/kernel/failure-codes.yaml +1 -41
  29. package/runtime/modules/kernel/removed-vocabulary.yaml +268 -0
  30. package/runtime/modules/models/registry.yaml +25 -359
  31. package/runtime/modules/models/runtimes.yaml +126 -265
  32. package/runtime/modules/models/tiers.yaml +66 -0
  33. package/runtime/scripts/api/fs/claim-file.mjs +30 -0
  34. package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
  35. package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
  36. package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
  37. package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
  38. package/runtime/scripts/api/git/lib.mjs +34 -1
  39. package/runtime/scripts/api/process/resolve-real-tool.mjs +14 -8
  40. package/runtime/scripts/api/process/run-program.mjs +2 -1
  41. package/runtime/scripts/api/sops/lib.mjs +198 -120
  42. package/runtime/scripts/hfs/allows.mjs +3 -3
  43. package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
  44. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
  45. package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
  46. package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
  47. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
  48. package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
  49. package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
  50. package/runtime/scripts/hfs/architecture/config.mjs +95 -80
  51. package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
  52. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
  53. package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
  54. package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
  55. package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
  56. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
  57. package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
  58. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
  59. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
  60. package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
  61. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
  62. package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
  63. package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
  64. package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
  65. package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
  66. package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
  67. package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
  68. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
  69. package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
  70. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
  71. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
  72. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
  73. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
  74. package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
  75. package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
  76. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
  77. package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
  78. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
  79. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
  80. package/runtime/scripts/hfs/architecture/index.mjs +164 -104
  81. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
  82. package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
  83. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
  84. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
  85. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
  86. package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
  87. package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
  88. package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
  89. package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
  90. package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
  91. package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
  92. package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
  93. package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
  94. package/runtime/scripts/hfs/architecture/required-files.mjs +121 -70
  95. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
  96. package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
  97. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
  98. package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
  99. package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
  100. package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
  101. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
  102. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
  103. package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
  104. package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
  105. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
  106. package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
  107. package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
  108. package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
  109. package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
  110. package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
  111. package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
  112. package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
  113. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
  114. package/runtime/scripts/hfs/architecture.mjs +3 -4
  115. package/runtime/scripts/hfs/catalog-once.mjs +15 -0
  116. package/runtime/scripts/hfs/check.mjs +63 -71
  117. package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
  118. package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
  119. package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
  120. package/runtime/scripts/hfs/edition-slots.mjs +5 -3
  121. package/runtime/scripts/hfs/linear-text.mjs +30 -0
  122. package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
  123. package/runtime/scripts/hfs/path-findings.mjs +58 -38
  124. package/runtime/scripts/hfs/pin-findings.mjs +31 -0
  125. package/runtime/scripts/hfs/repo-identity.mjs +21 -4
  126. package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
  127. package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
  128. package/runtime/scripts/hfs/rules/cli.mjs +5 -1
  129. package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
  130. package/runtime/scripts/hfs/rules/contract.mjs +35 -27
  131. package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
  132. package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
  133. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
  134. package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
  135. package/runtime/scripts/hfs/rules/database.mjs +36 -23
  136. package/runtime/scripts/hfs/rules/deps.mjs +64 -32
  137. package/runtime/scripts/hfs/rules/docker.mjs +113 -65
  138. package/runtime/scripts/hfs/rules/edition.mjs +118 -111
  139. package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
  140. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
  141. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
  142. package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
  143. package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
  144. package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
  145. package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
  146. package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
  147. package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
  148. package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
  149. package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
  150. package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
  151. package/runtime/scripts/hfs/rules/saga.mjs +128 -61
  152. package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
  153. package/runtime/scripts/hfs/rules/services.mjs +34 -16
  154. package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
  155. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
  156. package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
  157. package/runtime/scripts/hfs/secret.mjs +59 -48
  158. package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
  159. package/runtime/scripts/hfs/slot-classify.mjs +65 -0
  160. package/runtime/scripts/hfs/slot-errors.mjs +11 -0
  161. package/runtime/scripts/hfs/slot-imports.mjs +76 -0
  162. package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
  163. package/runtime/scripts/hfs/slot-match.mjs +153 -0
  164. package/runtime/scripts/hfs/slot-path.mjs +8 -0
  165. package/runtime/scripts/hfs/slot-required.mjs +76 -0
  166. package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
  167. package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
  168. package/runtime/scripts/hfs/slots.mjs +74 -695
  169. package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
  170. package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
  171. package/runtime/scripts/hfs/tree.mjs +19 -14
  172. package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
  173. package/runtime/scripts/hfs/view.mjs +1 -1
  174. package/runtime/scripts/lib/dockerfile.mjs +56 -33
  175. package/runtime/scripts/lib/env.mjs +10 -0
  176. package/runtime/scripts/lib/event-contract.mjs +17 -13
  177. package/runtime/scripts/lib/git.mjs +2 -2
  178. package/runtime/scripts/lib/graphql-contract.mjs +158 -322
  179. package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
  180. package/runtime/scripts/lib/i18n.mjs +3 -2
  181. package/runtime/scripts/lib/in-order.mjs +71 -0
  182. package/runtime/scripts/lib/language.mjs +3 -3
  183. package/runtime/scripts/lib/list.mjs +8 -1
  184. package/runtime/scripts/lib/path-key.mjs +38 -5
  185. package/runtime/scripts/lib/pid-alive.mjs +7 -0
  186. package/runtime/scripts/lib/regex.mjs +1 -1
  187. package/runtime/scripts/lib/same-text.mjs +1 -1
  188. package/runtime/scripts/lib/secret-patterns.mjs +6 -2
  189. package/runtime/scripts/lib/sleep-sync.mjs +2 -2
  190. package/runtime/scripts/lib/sops-envelope.mjs +95 -48
  191. package/runtime/scripts/lib/stack-services.mjs +1 -2
  192. package/runtime/scripts/lib/ts-ast.mjs +9 -0
  193. package/runtime/scripts/lib/walk.mjs +6 -1
  194. package/runtime/scripts/lib/yaml-cached.mjs +14 -0
  195. package/scaffold/add-cli-lite.mjs +2 -1
  196. package/scaffold/add-table.mjs +2 -2
  197. package/scaffold/add.mjs +3 -2
  198. package/scaffold/app.mjs +4 -2
  199. package/scaffold/edition-gate.mjs +25 -25
  200. package/scaffold/lite-exports.mjs +2 -1
  201. package/scaffold/service.mjs +5 -4
  202. package/sync/hygiene.mjs +15 -9
  203. package/sync/index.mjs +7 -3
  204. package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
  205. package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
  206. package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
  207. package/templates/app/ci-workflows/sonar-steps.yml +2 -2
  208. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
  209. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
  210. package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
  211. package/templates/app/starciwork.gitignore +1 -1
  212. package/templates/be/image/api/Dockerfile +1 -1
  213. package/templates/be/image/cli/Dockerfile +1 -1
  214. package/templates/be/image/worker/Dockerfile +1 -1
  215. package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
  216. package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
  217. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
  218. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
  219. package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
  220. package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
  221. package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
  222. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
  223. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
  224. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
  225. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
  226. package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
  227. package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
  228. package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
  229. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
  230. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  231. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
  232. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
  233. package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
  234. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  235. package/templates/fe/image/next/Dockerfile +2 -1
  236. package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
  237. package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
  238. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
  239. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
  240. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
  241. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
  242. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
  243. package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
  244. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
  245. package/upgrade/index.mjs +9 -2
  246. package/runtime/engine/admission.mjs +0 -308
  247. package/runtime/engine/canonical-json.mjs +0 -10
  248. package/runtime/engine/db/blob.mjs +0 -315
  249. package/runtime/engine/db/ledger-paths.mjs +0 -83
  250. package/runtime/engine/db/ledger.mjs +0 -1118
  251. package/runtime/engine/db/machine-connection.mjs +0 -135
  252. package/runtime/engine/db/machine-schema.mjs +0 -84
  253. package/runtime/engine/db/machine.mjs +0 -1367
  254. package/runtime/engine/db/migrations/machine/0001-init.sql +0 -924
  255. package/runtime/engine/db/migrations/runtime/0001-init.sql +0 -1108
  256. package/runtime/engine/db/provider-reservations.mjs +0 -101
  257. package/runtime/engine/digest.mjs +0 -16
  258. package/runtime/engine/refuse.mjs +0 -11
  259. package/runtime/modules/ops/_labels.yaml +0 -53
  260. package/runtime/scripts/api/node/lib.mjs +0 -14
  261. package/runtime/scripts/api/node/spawn-node.mjs +0 -6
  262. package/runtime/scripts/api/process/lib.mjs +0 -111
  263. package/runtime/scripts/api/process/owned-process.mjs +0 -78
  264. package/runtime/scripts/api/process/stop-owned-process.mjs +0 -9
  265. package/runtime/scripts/connectors/lib.mjs +0 -488
  266. package/runtime/scripts/lib/clip.mjs +0 -19
  267. package/runtime/scripts/lib/display-names.mjs +0 -259
  268. package/runtime/scripts/lib/example-refs.mjs +0 -158
  269. package/runtime/scripts/lib/json-schema.mjs +0 -52
  270. package/runtime/scripts/lib/process-identity.mjs +0 -6
  271. package/runtime/scripts/lib/read-yaml.mjs +0 -18
  272. package/runtime/scripts/lib/redact.mjs +0 -161
  273. package/runtime/scripts/lib/sleep.mjs +0 -7
  274. package/runtime/scripts/lib/source-phrases.mjs +0 -34
  275. package/runtime/scripts/lib/sqlite.mjs +0 -21
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { machineKit } from './machine-ast.mjs';
3
3
  import { treeOf } from './required-files.mjs';
4
+ import { byCodeUnit } from '../../lib/list.mjs';
4
5
 
5
6
  /**
6
7
  * R56 `hooks-are-hooks` (FE_HOOKS_ARE_HOOKS), the repository half of the eslint rule `hooks-folder-holds-hooks-only`, which
@@ -19,16 +20,9 @@ const HOOKS_SLOT = 'fe.hooks';
19
20
  const HOOK_NAME = /^use[A-Z0-9]/u;
20
21
  const SHARED = /\.shared\.[cm]?tsx?$/u;
21
22
 
22
- export function checkHooksAreHooks(input) {
23
- const { config, graph } = input;
24
- const kit = machineKit(input);
25
- const { ts, resolver } = kit;
26
- const violations = [];
27
- const domains = new Set();
28
-
29
- const tree = treeOf(config.root);
23
+ const sharedFilesOf = (tree, resolver, domains) => {
30
24
  const shared = new Map(); // domain root -> [file]
31
- for (const rel of [...tree.files].sort()) {
25
+ for (const rel of [...tree.files].sort(byCodeUnit)) {
32
26
  const classified = resolver.classifyPath(rel);
33
27
  if (classified.slot !== HOOKS_SLOT || !classified.root) continue;
34
28
  domains.add(classified.root);
@@ -37,6 +31,10 @@ export function checkHooksAreHooks(input) {
37
31
  if (!shared.has(classified.root)) shared.set(classified.root, []);
38
32
  shared.get(classified.root).push({ rel, base });
39
33
  }
34
+ return shared;
35
+ };
36
+
37
+ const reportSharedFileViolations = (shared, violations) => {
40
38
  for (const [root, list] of shared) {
41
39
  const domain = path.posix.basename(root);
42
40
  const at = (item, message) => violations.push({ ruleId: RULE, path: item.rel, line: 1, column: 1, domain, message });
@@ -49,40 +47,62 @@ export function checkHooksAreHooks(input) {
49
47
  at(item, `${item.rel} is a second shared file of hooks domain ${domain}; a domain has exactly one, ${domain}.shared.ts. Merge the helpers into it.`);
50
48
  }
51
49
  }
50
+ };
51
+
52
+ const helperDeclarationsOf = (statement, ts) => {
53
+ const found = [];
54
+ if (ts.isFunctionDeclaration(statement) && statement.name) found.push([statement.name.text, statement.name]);
55
+ if (ts.isVariableStatement(statement)) {
56
+ for (const declaration of statement.declarationList.declarations) {
57
+ const init = declaration.initializer;
58
+ if (ts.isIdentifier(declaration.name) && init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init))) found.push([declaration.name.text, declaration.name]);
59
+ }
60
+ }
61
+ return found;
62
+ };
52
63
 
53
- // Helpers declared in more than one file of one domain.
64
+ const recordDeclaredHelper = (declared, root, name, file, node) => {
65
+ if (HOOK_NAME.test(name)) return;
66
+ if (!declared.has(root)) declared.set(root, new Map());
67
+ const byName = declared.get(root);
68
+ if (!byName.has(name)) byName.set(name, []);
69
+ byName.get(name).push({ file, node });
70
+ };
71
+
72
+ const declaredHelpersOf = (graph, resolver, ts) => {
54
73
  const declared = new Map(); // domain root -> name -> [{file, node}]
55
74
  for (const file of graph.files.values()) {
56
75
  if (file.slot !== HOOKS_SLOT) continue;
57
76
  const root = resolver.classifyPath(file.rel).root;
58
77
  for (const statement of file.sourceFile.statements) {
59
- const found = [];
60
- if (ts.isFunctionDeclaration(statement) && statement.name) found.push([statement.name.text, statement.name]);
61
- if (ts.isVariableStatement(statement)) {
62
- for (const declaration of statement.declarationList.declarations) {
63
- const init = declaration.initializer;
64
- if (ts.isIdentifier(declaration.name) && init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init))) found.push([declaration.name.text, declaration.name]);
65
- }
66
- }
67
- for (const [name, node] of found) {
68
- if (HOOK_NAME.test(name)) continue;
69
- if (!declared.has(root)) declared.set(root, new Map());
70
- const byName = declared.get(root);
71
- if (!byName.has(name)) byName.set(name, []);
72
- byName.get(name).push({ file, node });
73
- }
78
+ for (const [name, node] of helperDeclarationsOf(statement, ts)) recordDeclaredHelper(declared, root, name, file, node);
74
79
  }
75
80
  }
81
+ return declared;
82
+ };
83
+
84
+ const reportDuplicateHelpers = (declared, violations, kit) => {
76
85
  for (const [root, byName] of declared) {
77
86
  const domain = path.posix.basename(root);
78
87
  for (const [name, list] of byName) {
79
88
  if (list.length < 2) continue;
80
89
  for (const item of list.slice(1)) {
81
- const others = list.map(entry => entry.file.rel).filter(rel => rel !== item.file.rel).sort();
90
+ const others = list.map(entry => entry.file.rel).filter(rel => rel !== item.file.rel).sort(byCodeUnit);
82
91
  violations.push({ ruleId: RULE, path: item.file.rel, ...kit.at(item.file.rel, item.file.sourceFile, item.node), domain, helper: name,
83
92
  message: `${name} is declared again in hooks domain ${domain} (also in ${others.join(', ')}); a helper the hooks share is written once, in ${domain}.shared.ts.` });
84
93
  }
85
94
  }
86
95
  }
96
+ };
97
+
98
+ export function checkHooksAreHooks(input) {
99
+ const { config, graph } = input;
100
+ const kit = machineKit(input);
101
+ const { ts, resolver } = kit;
102
+ const violations = [];
103
+ const domains = new Set();
104
+ const shared = sharedFilesOf(treeOf(config.root), resolver, domains);
105
+ reportSharedFileViolations(shared, violations);
106
+ reportDuplicateHelpers(declaredHelpersOf(graph, resolver, ts), violations, kit);
87
107
  return { violations, coverage: { status: 'checked', domains: domains.size } };
88
108
  }
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { byCodeUnit } from '../../lib/list.mjs';
3
4
 
4
5
  /**
5
6
  * R106 `i18n-keys` (FE_I18N_KEYS). The catalogs of an app (`apps/<app>/src/modules/i18n/messages/<locale>.json`, slot fe.modules.i18n) and
@@ -40,15 +41,8 @@ const held = (leaves, full) => leaves.has(full) || [...leaves].some(leaf => leaf
40
41
  const tailMatches = (pattern, leafSegments) => pattern.some(segment => segment !== '*' && segment !== '') && pattern.length <= leafSegments.length
41
42
  && pattern.every((segment, index) => segment === '*' || segment === leafSegments[leafSegments.length - pattern.length + index]);
42
43
 
43
- export function checkI18nKeys({ config, graph, context }) {
44
- const ts = context.ts;
45
- const violations = [];
46
- const report = (file, node, message, extra = {}) => {
47
- const at = file.sourceFile.getLineAndCharacterOfPosition(node.getStart(file.sourceFile));
48
- violations.push({ ruleId: RULE, path: file.rel, line: at.line + 1, column: at.character + 1, message, ...extra });
49
- };
50
-
51
- const catalogsByApp = new Map();
44
+ function catalogsPerApp(config, graph) {
45
+ const catalogs = new Map();
52
46
  const seen = new Set();
53
47
  for (const file of graph.files.values()) {
54
48
  const app = APP_SOURCE.exec(file.rel)?.[1];
@@ -56,9 +50,113 @@ export function checkI18nKeys({ config, graph, context }) {
56
50
  seen.add(app);
57
51
  const dir = `apps/${app}/src/modules/i18n/messages/`;
58
52
  let names = [];
59
- try { names = fs.readdirSync(path.join(config.root, dir)).filter(name => name.endsWith('.json')).sort(); } catch { /* the app has no catalog directory: FE_I18N_PLACEMENT's finding */ }
60
- if (names.length) catalogsByApp.set(dir, names.map(name => ({ rel: `${dir}${name}`, locale: name.slice(0, -5) })));
53
+ try { names = fs.readdirSync(path.join(config.root, dir)).filter(name => name.endsWith('.json')).sort(byCodeUnit); } catch { /* the app has no catalog directory: FE_I18N_PLACEMENT's finding */ }
54
+ if (names.length) catalogs.set(dir, names.map(name => ({ rel: `${dir}${name}`, locale: name.slice(0, -5) })));
55
+ }
56
+ return catalogs;
57
+ }
58
+
59
+ function patternsOf(ts, files) {
60
+ const patterns = [];
61
+ for (const file of files) {
62
+ const visit = node => {
63
+ if (ts.isStringLiteralLike(node)) patterns.push(node.text.split('.'));
64
+ else if (ts.isTemplateExpression(node)) patterns.push([node.head.text, ...node.templateSpans.map(span => span.literal.text)].join('\u0000').replaceAll('\u0000', '*').split('.'));
65
+ ts.forEachChild(node, visit);
66
+ };
67
+ visit(file.sourceFile);
68
+ }
69
+ return patterns;
70
+ }
71
+
72
+ function translationFactories(ts, file) {
73
+ const factories = new Set();
74
+ for (const statement of file.sourceFile.statements) {
75
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || !MODULES.has(statement.moduleSpecifier.text)) continue;
76
+ const named = statement.importClause?.namedBindings;
77
+ if (!named || !ts.isNamedImports(named)) continue;
78
+ for (const element of named.elements) {
79
+ if (!element.isTypeOnly && FACTORIES.has((element.propertyName ?? element.name).text)) factories.add(element.name.text);
80
+ }
81
+ }
82
+ return factories;
83
+ }
84
+
85
+ function namespaceForCall(ts, file, init, factories) {
86
+ let expression = init;
87
+ if (ts.isAwaitExpression(expression)) expression = expression.expression;
88
+ if (!ts.isCallExpression(expression) || !ts.isIdentifier(expression.expression) || !factories.has(expression.expression.text)) return null;
89
+ const [first] = expression.arguments;
90
+ if (first === undefined) return '';
91
+ if (ts.isStringLiteralLike(first)) return first.text;
92
+ if (!ts.isObjectLiteralExpression(first)) return null;
93
+ const property = first.properties.find(candidate => ts.isPropertyAssignment(candidate) && candidate.name.getText(file.sourceFile) === 'namespace');
94
+ if (!property) return '';
95
+ return ts.isStringLiteralLike(property.initializer) ? property.initializer.text : null;
96
+ }
97
+
98
+ function translatorNamespaces(ts, file, factories) {
99
+ const namespaces = new Map();
100
+ const collect = node => {
101
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
102
+ const namespace = namespaceForCall(ts, file, node.initializer, factories);
103
+ if (namespace !== null) namespaces.set(node, namespace);
104
+ }
105
+ ts.forEachChild(node, collect);
106
+ };
107
+ collect(file.sourceFile);
108
+ return namespaces;
109
+ }
110
+
111
+ function keysOf(ts, argument) {
112
+ if (ts.isConditionalExpression(argument)) return [...keysOf(ts, argument.whenTrue), ...keysOf(ts, argument.whenFalse)];
113
+ if (ts.isParenthesizedExpression(argument) || ts.isAsExpression(argument) || ts.isSatisfiesExpression(argument)) return keysOf(ts, argument.expression);
114
+ return ts.isStringLiteralLike(argument) ? [argument] : [];
115
+ }
116
+
117
+ function reportCallKeys(reads, node) {
118
+ const { ts, file, checker, namespaces, loaded, app, report } = reads;
119
+ const callee = node.expression;
120
+ let owner = ts.isIdentifier(callee) ? callee : null;
121
+ if (ts.isPropertyAccessExpression(callee) && READERS.has(callee.name.text) && ts.isIdentifier(callee.expression)) owner = callee.expression;
122
+ const [first] = node.arguments;
123
+ const declaration = owner ? checker.getSymbolAtLocation(owner)?.valueDeclaration : null;
124
+ if (!owner || first === undefined || !namespaces.has(declaration)) return;
125
+ for (const literal of keysOf(ts, first)) {
126
+ const namespace = namespaces.get(declaration);
127
+ const full = namespace ? `${namespace}.${literal.text}` : literal.text;
128
+ const lacking = loaded.filter(catalog => !held(catalog.leaves, full)).map(catalog => catalog.locale);
129
+ if (lacking.length) report(file, literal, `${file.rel} reads "${full}" and ${lacking.join(', ')} ${lacking.length === 1 ? 'holds' : 'hold'} no such key; add it to every catalog of ${app} or read a key that exists`, { key: full, locales: lacking });
61
130
  }
131
+ }
132
+
133
+ function reportTranslationReads(ts, file, checker, namespaces, loaded, app, report) {
134
+ const reads = { ts, file, checker, namespaces, loaded, app, report };
135
+ const read = node => {
136
+ if (ts.isCallExpression(node)) reportCallKeys(reads, node);
137
+ ts.forEachChild(node, read);
138
+ };
139
+ read(file.sourceFile);
140
+ }
141
+
142
+ function reportUnusedCatalogKeys(primary, loaded, patterns, app, violations) {
143
+ const every = new Set(loaded.flatMap(catalog => [...catalog.leaves]));
144
+ for (const leaf of [...every].sort(byCodeUnit)) {
145
+ const segments = leaf.split('.');
146
+ if (patterns.some(pattern => tailMatches(pattern, segments))) continue;
147
+ violations.push({ ruleId: RULE, path: primary.rel, line: 1, column: 1, message: `${primary.rel} holds the key "${leaf}" and no source of ${app} or the shared packages reads it; delete the key from every catalog`, key: leaf });
148
+ }
149
+ }
150
+
151
+ export function checkI18nKeys({ config, graph, context }) {
152
+ const ts = context.ts;
153
+ const violations = [];
154
+ const report = (file, node, message, extra = {}) => {
155
+ const at = file.sourceFile.getLineAndCharacterOfPosition(node.getStart(file.sourceFile));
156
+ violations.push({ ruleId: RULE, path: file.rel, line: at.line + 1, column: at.character + 1, message, ...extra });
157
+ };
158
+
159
+ const catalogsByApp = catalogsPerApp(config, graph);
62
160
 
63
161
  let apps = 0;
64
162
  for (const [dir, catalogs] of catalogsByApp) {
@@ -68,79 +166,19 @@ export function checkI18nKeys({ config, graph, context }) {
68
166
  apps += 1;
69
167
  const own = [...graph.files.values()].filter(file => file.rel.startsWith(`apps/${app}/src/`));
70
168
  const shared = [...graph.files.values()].filter(file => /^packages\/[^/]+\/src\//u.test(file.rel));
71
- const patterns = [];
72
-
73
- for (const file of [...own, ...shared]) {
74
- const visit = node => {
75
- if (ts.isStringLiteralLike(node)) patterns.push(node.text.split('.'));
76
- else if (ts.isTemplateExpression(node)) patterns.push([node.head.text, ...node.templateSpans.map(span => span.literal.text)].join('\u0000').split('\u0000').join('*').split('.'));
77
- ts.forEachChild(node, visit);
78
- };
79
- visit(file.sourceFile);
80
- }
169
+ const patterns = patternsOf(ts, [...own, ...shared]);
81
170
 
82
171
  for (const file of own) {
83
- const factories = new Set();
84
- for (const statement of file.sourceFile.statements) {
85
- if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || !MODULES.has(statement.moduleSpecifier.text)) continue;
86
- const named = statement.importClause?.namedBindings;
87
- if (!named || !ts.isNamedImports(named)) continue;
88
- for (const element of named.elements) if (!element.isTypeOnly && FACTORIES.has((element.propertyName ?? element.name).text)) factories.add(element.name.text);
89
- }
172
+ const factories = translationFactories(ts, file);
90
173
  if (!factories.size) continue;
91
174
  const checker = context.checkerFor(file.sourceFile.fileName);
92
- const namespaces = new Map(); // translator variable declaration -> namespace ('' = none); a computed namespace is left out
93
- const collect = node => {
94
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
95
- let init = node.initializer;
96
- if (ts.isAwaitExpression(init)) init = init.expression;
97
- if (ts.isCallExpression(init) && ts.isIdentifier(init.expression) && factories.has(init.expression.text)) {
98
- const [first] = init.arguments;
99
- if (first === undefined) namespaces.set(node, '');
100
- else if (ts.isStringLiteralLike(first)) namespaces.set(node, first.text);
101
- else if (ts.isObjectLiteralExpression(first)) {
102
- const property = first.properties.find(candidate => ts.isPropertyAssignment(candidate) && candidate.name.getText(file.sourceFile) === 'namespace');
103
- if (!property) namespaces.set(node, '');
104
- else if (ts.isStringLiteralLike(property.initializer)) namespaces.set(node, property.initializer.text);
105
- }
106
- }
107
- }
108
- ts.forEachChild(node, collect);
109
- };
110
- collect(file.sourceFile);
175
+ const namespaces = translatorNamespaces(ts, file, factories);
111
176
  if (!namespaces.size) continue;
112
- const keysOf = argument => {
113
- if (ts.isConditionalExpression(argument)) return [...keysOf(argument.whenTrue), ...keysOf(argument.whenFalse)];
114
- if (ts.isParenthesizedExpression(argument) || ts.isAsExpression(argument) || ts.isSatisfiesExpression(argument)) return keysOf(argument.expression);
115
- return ts.isStringLiteralLike(argument) ? [argument] : [];
116
- };
117
- const read = node => {
118
- if (ts.isCallExpression(node)) {
119
- const callee = node.expression;
120
- const owner = ts.isIdentifier(callee) ? callee : (ts.isPropertyAccessExpression(callee) && READERS.has(callee.name.text) && ts.isIdentifier(callee.expression) ? callee.expression : null);
121
- const [first] = node.arguments;
122
- const declaration = owner ? checker.getSymbolAtLocation(owner)?.valueDeclaration : null;
123
- if (owner && first !== undefined && namespaces.has(declaration)) {
124
- for (const literal of keysOf(first)) {
125
- const namespace = namespaces.get(declaration);
126
- const full = namespace ? `${namespace}.${literal.text}` : literal.text;
127
- const lacking = loaded.filter(catalog => !held(catalog.leaves, full)).map(catalog => catalog.locale);
128
- if (lacking.length) report(file, literal, `${file.rel} reads "${full}" and ${lacking.join(', ')} ${lacking.length === 1 ? 'holds' : 'hold'} no such key; add it to every catalog of ${app} or read a key that exists`, { key: full, locales: lacking });
129
- }
130
- }
131
- }
132
- ts.forEachChild(node, read);
133
- };
134
- read(file.sourceFile);
177
+ reportTranslationReads(ts, file, checker, namespaces, loaded, app, report);
135
178
  }
136
179
 
137
180
  const [primary] = [...loaded].sort((a, b) => a.locale.localeCompare(b.locale));
138
- const every = new Set(loaded.flatMap(catalog => [...catalog.leaves]));
139
- for (const leaf of [...every].sort()) {
140
- const segments = leaf.split('.');
141
- if (patterns.some(pattern => tailMatches(pattern, segments))) continue;
142
- violations.push({ ruleId: RULE, path: primary.rel, line: 1, column: 1, message: `${primary.rel} holds the key "${leaf}" and no source of ${app} or the shared packages reads it; delete the key from every catalog`, key: leaf });
143
- }
181
+ reportUnusedCatalogKeys(primary, loaded, patterns, app, violations);
144
182
  }
145
183
  return { violations, coverage: { status: 'checked', apps } };
146
184
  }