@starci/hfs 4.0.10 → 4.1.0

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