@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
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { lsFiles } from '../../api/git/ls-files.mjs';
4
4
  import { revParseQuery } from '../../api/git/rev-parse-query.mjs';
5
5
  import { gitOutputOf } from '../../lib/git.mjs';
6
+ import { trimTrailingSlashes } from '../trailing-slashes.mjs';
6
7
 
7
8
  /**
8
9
  * HFS check 5: the files and directories the slot manifest requires (knowledge/hfs/slots.yaml `requires`,
@@ -48,8 +49,20 @@ function diskFiles(root, relative = '') {
48
49
  return out;
49
50
  }
50
51
 
51
- /** The repository tree as file and directory sets over posix relatives. */
52
- export function treeOf(root) {
52
+ let runTrees = null;
53
+
54
+ /** Runs `run` with one tree read per root: every checker of one machine run shares the repository listing instead of spawning git again. */
55
+ export function withTreeCache(run) {
56
+ const outer = runTrees;
57
+ runTrees = outer ?? new Map();
58
+ try {
59
+ return run();
60
+ } finally {
61
+ runTrees = outer;
62
+ }
63
+ }
64
+
65
+ function readTree(root) {
53
66
  const list = gitFiles(root) ?? diskFiles(root);
54
67
  const files = new Set(list);
55
68
  const directories = new Set();
@@ -60,6 +73,13 @@ export function treeOf(root) {
60
73
  return { files, directories };
61
74
  }
62
75
 
76
+ /** The repository tree as file and directory sets over posix relatives. */
77
+ export function treeOf(root) {
78
+ if (!runTrees) return readTree(root);
79
+ if (!runTrees.has(root)) runTrees.set(root, readTree(root));
80
+ return runTrees.get(root);
81
+ }
82
+
63
83
  /**
64
84
  * The prelude every tree-walking architecture checker shares: the input's config/graph/context, the compiler,
65
85
  * the slot resolver and the tracked repository tree.
@@ -68,28 +88,27 @@ export function checkerScope({ config, graph, context }) {
68
88
  return { config, graph, context, ts: context.ts, resolver: graph.resolver, tree: treeOf(config.root) };
69
89
  }
70
90
 
71
- const strip = entry => entry.replace(/\/+$/u, '');
72
- const rootOfTarget = target => (target.endsWith('/') ? strip(target) : (path.posix.dirname(target) === '.' ? '' : path.posix.dirname(target)));
91
+ const rootOfTarget = target => {
92
+ if (target.endsWith('/')) return trimTrailingSlashes(target);
93
+ const parent = path.posix.dirname(target);
94
+ return parent === '.' ? '' : parent;
95
+ };
73
96
 
74
97
  function ruleFor(slotId, target, rootExists) {
75
- if (slotId === 'fe.app.next' && rootExists && ERROR_BOUNDARY_FILE.test(path.posix.basename(strip(target)))) return 'FE_ERROR_BOUNDARY_MISSING';
98
+ if (slotId === 'fe.app.next' && rootExists && ERROR_BOUNDARY_FILE.test(path.posix.basename(trimTrailingSlashes(target)))) return 'FE_ERROR_BOUNDARY_MISSING';
76
99
  return 'HFS_REQUIRED_FILE_MISSING';
77
100
  }
78
101
 
79
- export function checkRequiredFiles({ config, graph }) {
80
- const resolver = graph?.resolver ?? config.hfs;
81
- const tree = treeOf(config.root);
82
- const judged = slotId => {
83
- const slot = resolver.slot(slotId);
84
- return Boolean(slot) && (slot.tier !== 'none' || ALSO_JUDGED_TIER_NONE.has(slotId));
85
- };
102
+ function slotIsJudged(resolver, slotId) {
103
+ const slot = resolver.slot(slotId);
104
+ return Boolean(slot) && (slot.tier !== 'none' || ALSO_JUDGED_TIER_NONE.has(slotId));
105
+ }
86
106
 
87
- const requirements = new Map(); // repository-relative target (as written, dirs end with /) -> {slot, target, root}
88
- const instances = new Set();
89
- const addRequirement = (slot, target, root) => { if (!requirements.has(target)) requirements.set(target, { slot, target, root }); };
107
+ function addRequirement(requirements, slot, target, root) {
108
+ if (!requirements.has(target)) requirements.set(target, { slot, target, root });
109
+ }
90
110
 
91
- // 1. What the repository must hold whatever it contains: fixed instances (requiredInstances, one per declared app).
92
- const declared = resolver.requiredPaths();
111
+ function collectDeclaredRequirements(declared, requirements, instances, judged) {
93
112
  const currentRoot = new Map();
94
113
  for (const item of declared.paths) {
95
114
  if (!judged(item.slot)) continue;
@@ -97,84 +116,116 @@ export function checkRequiredFiles({ config, graph }) {
97
116
  const root = rootOfTarget(item.path);
98
117
  currentRoot.set(item.slot, root);
99
118
  instances.add(`${item.slot}|${root}`);
100
- addRequirement(item.slot, item.path, root);
119
+ addRequirement(requirements, item.slot, item.path, root);
101
120
  } else {
102
- addRequirement(item.slot, item.path, currentRoot.get(item.slot) ?? '');
121
+ addRequirement(requirements, item.slot, item.path, currentRoot.get(item.slot) ?? '');
103
122
  }
104
123
  }
124
+ }
125
+
126
+ function addInstanceRequirements(resolver, classified, representative, requirements, instances, seenInstances, judged) {
127
+ const { slot: slotId, root } = classified;
128
+ const key = `${slotId}|${root}`;
129
+ if (seenInstances.has(key) || !judged(slotId)) return;
130
+ seenInstances.add(key);
131
+ instances.add(key);
132
+ for (const target of resolver.requiredFiles(representative)) addRequirement(requirements, slotId, target, root);
133
+ }
134
+
135
+ function addOwnerRequirements(resolver, owner, requirements, instances, seenInstances, judged) {
136
+ const key = `${owner.slot}|${owner.root}`;
137
+ if (seenInstances.has(key) || !judged(owner.slot) || !resolver.slotEnabled(resolver.slot(owner.slot))) return;
138
+ seenInstances.add(key);
139
+ instances.add(key);
140
+ for (const entry of resolver.slot(owner.slot).requires ?? []) {
141
+ const rooted = entry.startsWith('/');
142
+ const filled = (rooted ? entry.slice(1) : entry).replace(/<([A-Za-z][A-Za-z0-9-]*)>/g, (whole, name) => owner.bindings[name] ?? whole);
143
+ addRequirement(requirements, owner.slot, rooted || !owner.root ? filled : `${owner.root}/${filled}`, owner.root);
144
+ }
145
+ }
105
146
 
106
- // 2. What each instance found by walking the tree requires (features, domains, integrations, packages, transports, ...).
147
+ function noteInstance(found, slotId, root) {
148
+ if (!found.has(slotId)) found.set(slotId, new Set());
149
+ found.get(slotId).add(root);
150
+ }
151
+
152
+ function collectTreeRequirements(tree, resolver, requirements, instances, judged) {
107
153
  const seenInstances = new Set();
108
- const instanceRequires = (slotId, root, representative) => {
109
- const key = `${slotId}|${root}`;
110
- if (seenInstances.has(key) || !judged(slotId)) return;
111
- seenInstances.add(key);
112
- instances.add(key);
113
- for (const target of resolver.requiredFiles(representative)) addRequirement(slotId, target, root);
114
- };
115
- const ownerRequires = owner => {
116
- const key = `${owner.slot}|${owner.root}`;
117
- if (seenInstances.has(key) || !judged(owner.slot) || !resolver.slotEnabled(resolver.slot(owner.slot))) return;
118
- seenInstances.add(key);
119
- instances.add(key);
120
- for (const entry of resolver.slot(owner.slot).requires ?? []) {
121
- const rooted = entry.startsWith('/');
122
- const filled = (rooted ? entry.slice(1) : entry).replace(/<([A-Za-z][A-Za-z0-9-]*)>/g, (whole, name) => owner.bindings[name] ?? whole);
123
- addRequirement(owner.slot, rooted || !owner.root ? filled : `${owner.root}/${filled}`, owner.root);
124
- }
125
- };
126
154
  const found = new Map(); // slot -> Set(root) of instances seen while walking
127
- const note = (slotId, root) => {
128
- if (!found.has(slotId)) found.set(slotId, new Set());
129
- found.get(slotId).add(root);
130
- };
131
155
  for (const file of tree.files) {
132
156
  const classified = resolver.classifyPath(file);
133
157
  if (classified.status === 'owned' && classified.slot) {
134
- note(classified.slot, classified.root);
135
- instanceRequires(classified.slot, classified.root, file);
158
+ noteInstance(found, classified.slot, classified.root);
159
+ addInstanceRequirements(resolver, classified, file, requirements, instances, seenInstances, judged);
136
160
  }
137
161
  const owner = resolver.ownerOf(file);
138
162
  if (owner) {
139
- note(owner.slot, owner.root);
163
+ noteInstance(found, owner.slot, owner.root);
140
164
  // an owner root may classify to a more specific slot (modules/i18n) or a nested one, so expand the owner slot's own requires
141
- ownerRequires(owner);
165
+ addOwnerRequirements(resolver, owner, requirements, instances, seenInstances, judged);
142
166
  }
143
167
  }
168
+ return found;
169
+ }
144
170
 
145
- const violations = [];
146
- const reported = new Set();
147
- const report = (ruleId, target, slotId, root, extra, message) => {
148
- const key = `${ruleId}|${target}`;
149
- if (reported.has(key)) return;
150
- reported.add(key);
151
- violations.push({ ruleId, path: target, slot: slotId, ...(root ? { root } : {}), ...extra, message });
152
- };
153
- const exists = target => (target.endsWith('/') ? tree.directories.has(strip(target)) : tree.files.has(target));
154
-
155
- for (const requirement of requirements.values()) {
156
- if (exists(requirement.target)) continue;
157
- const rootExists = !requirement.root || tree.directories.has(requirement.root);
158
- if (!rootExists) {
159
- report(ruleFor(requirement.slot, requirement.target, false), requirement.root, requirement.slot, requirement.root, { kind: 'directory' },
160
- `Required directory ${requirement.root} (slot ${requirement.slot}) does not exist; the slot manifest requires it, along with what it holds.`);
161
- continue;
162
- }
163
- const kind = requirement.target.endsWith('/') ? 'directory' : 'file';
164
- report(ruleFor(requirement.slot, requirement.target, true), strip(requirement.target), requirement.slot, requirement.root, { kind },
165
- `Required ${kind} ${strip(requirement.target)} is missing; slot ${requirement.slot} requires it in every instance${requirement.root ? ` (${requirement.root})` : ''}.`);
171
+ function requirementExists(tree, target) {
172
+ return target.endsWith('/') ? tree.directories.has(trimTrailingSlashes(target)) : tree.files.has(target);
173
+ }
174
+
175
+ function reportMissingRequirement(requirement, tree, report) {
176
+ if (requirementExists(tree, requirement.target)) return;
177
+ const rootExists = !requirement.root || tree.directories.has(requirement.root);
178
+ if (!rootExists) {
179
+ report(ruleFor(requirement.slot, requirement.target, false), requirement.root, requirement.slot, requirement.root, { kind: 'directory' },
180
+ `Required directory ${requirement.root} (slot ${requirement.slot}) does not exist; the slot manifest requires it, along with what it holds.`);
181
+ return;
166
182
  }
183
+ const kind = requirement.target.endsWith('/') ? 'directory' : 'file';
184
+ const rootNote = requirement.root ? ` (${requirement.root})` : '';
185
+ report(ruleFor(requirement.slot, requirement.target, true), trimTrailingSlashes(requirement.target), requirement.slot, requirement.root, { kind },
186
+ `Required ${kind} ${trimTrailingSlashes(requirement.target)} is missing; slot ${requirement.slot} requires it in every instance${rootNote}.`);
187
+ }
188
+
189
+ function reportMissingRequirements(requirements, tree, report) {
190
+ for (const requirement of requirements.values()) reportMissingRequirement(requirement, tree, report);
191
+ }
167
192
 
168
- // 3. Minimums: at least minInstances instances of a required slot.
169
- for (const minimum of declared.minimums) {
193
+ function reportMinimumRequirements(minimums, resolver, judged, found, report) {
194
+ for (const minimum of minimums) {
170
195
  if (!judged(minimum.slot)) continue;
171
196
  const count = minimum.appKind !== undefined
172
197
  ? resolver.repo.apps.filter(app => app.kind === minimum.appKind).length
173
198
  : (found.get(minimum.slot)?.size ?? 0);
174
199
  if (count >= minimum.min) continue;
200
+ const appKindNote = minimum.appKind ? ' (an app of kind ' + minimum.appKind + ' declared in hfs.json)' : '';
175
201
  report('HFS_REQUIRED_FILE_MISSING', 'hfs.json', minimum.slot, '', { minimum: minimum.min, found: count },
176
- `Slot ${minimum.slot} needs at least ${minimum.min} instance${minimum.min === 1 ? '' : 's'}${minimum.appKind ? ` (an app of kind ${minimum.appKind} declared in hfs.json)` : ''}; the repository has ${count}.`);
202
+ `Slot ${minimum.slot} needs at least ${minimum.min} instance${minimum.min === 1 ? '' : 's'}${appKindNote}; the repository has ${count}.`);
177
203
  }
204
+ }
205
+
206
+ export function checkRequiredFiles({ config, graph }) {
207
+ const resolver = graph?.resolver ?? config.hfs;
208
+ const tree = treeOf(config.root);
209
+ const judged = slotId => slotIsJudged(resolver, slotId);
210
+
211
+ const requirements = new Map(); // repository-relative target (as written, dirs end with /) -> {slot, target, root}
212
+ const instances = new Set();
213
+ const declared = resolver.requiredPaths();
214
+ collectDeclaredRequirements(declared, requirements, instances, judged);
215
+
216
+ const found = collectTreeRequirements(tree, resolver, requirements, instances, judged);
217
+
218
+ const violations = [];
219
+ const reported = new Set();
220
+ const report = (ruleId, target, slotId, root, extra, message) => {
221
+ const key = `${ruleId}|${target}`;
222
+ if (reported.has(key)) return;
223
+ reported.add(key);
224
+ violations.push({ ruleId, path: target, slot: slotId, ...(root ? { root } : {}), ...extra, message });
225
+ };
226
+ reportMissingRequirements(requirements, tree, report);
227
+
228
+ reportMinimumRequirements(declared.minimums, resolver, judged, found, report);
178
229
 
179
230
  return { violations, coverage: { status: 'checked', instances: instances.size, requirements: requirements.size, missing: violations.length } };
180
231
  }
@@ -24,74 +24,99 @@ const HOOK_ROUTES = new Set([...MOUNTING_ROUTES, 'page']);
24
24
  const HOOK_NAME = /^use[A-Z0-9]/u;
25
25
  const HOOKS_SLOT = 'fe.hooks';
26
26
 
27
+ const isFunction = (ts, node) => ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node);
28
+ const jsxOf = (ts, node) => ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node);
29
+
30
+ function tagOf(ts, node) {
31
+ if (ts.isJsxElement(node)) return node.openingElement.tagName;
32
+ if (ts.isJsxSelfClosingElement(node)) return node.tagName;
33
+ return null;
34
+ }
35
+
36
+ function addExportAssignmentDefaults(ts, statement, kit, checker, defaults) {
37
+ const target = statement.expression;
38
+ if (!ts.isIdentifier(target)) { defaults.add(target); return; }
39
+ for (const declaration of kit.declarationsOf(checker, target)) defaults.add(ts.isVariableDeclaration(declaration) ? declaration.initializer ?? declaration : declaration);
40
+ }
41
+
42
+ function routeDefaults(ts, file, kit, checker) {
43
+ const defaults = new Set();
44
+ for (const statement of file.sourceFile.statements) {
45
+ if (ts.isFunctionDeclaration(statement) && statement.modifiers?.some(item => item.kind === ts.SyntaxKind.DefaultKeyword)) defaults.add(statement);
46
+ if (ts.isExportAssignment(statement) && !statement.isExportEquals) addExportAssignmentDefaults(ts, statement, kit, checker, defaults);
47
+ }
48
+ return defaults;
49
+ }
50
+
51
+ /** True when the function contains JSX that is not inside a nested function. */
52
+ function drawsDirectly(ts, fn) {
53
+ let found = false;
54
+ const visit = node => {
55
+ if (found) return;
56
+ if (node !== fn && isFunction(ts, node)) return;
57
+ if (jsxOf(ts, node)) { found = true; return; }
58
+ ts.forEachChild(node, visit);
59
+ };
60
+ visit(fn);
61
+ return found;
62
+ }
63
+
64
+ function reportRoute(violations, kit, file, stem, node, message, extra = {}) {
65
+ violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message, route: stem, ...extra });
66
+ }
67
+
68
+ function inspectMountingTag(tag, { graph, kit, ts, file, stem, checker, violations, state }) {
69
+ if (ts.isIdentifier(tag) && /^[a-z]/u.test(tag.text)) {
70
+ reportRoute(violations, kit, file, stem, tag, `<${tag.text}> is drawing in a ${stem} route file; a route file mounts one feature and draws nothing. Move the markup into the feature.`);
71
+ } else if (kit.declarationsOf(checker, ts.isPropertyAccessExpression(tag) ? tag.name : tag).some(declaration => graph.files.get(kit.graphPath(declaration) ?? '')?.tier === 'feature')) {
72
+ state.owners += 1;
73
+ }
74
+ }
75
+
76
+ function inspectHookCall(node, { graph, kit, ts, file, stem, checker, violations }) {
77
+ const callee = ts.isIdentifier(node.expression) ? node.expression : null;
78
+ if (!callee || !HOOK_NAME.test(callee.text)) return;
79
+ const binding = kit.importBinding(checker, callee);
80
+ const inHooks = kit.declarationsOf(checker, callee).some(declaration => graph.files.get(kit.graphPath(declaration) ?? '')?.slot === HOOKS_SLOT);
81
+ if (binding || inHooks) reportRoute(violations, kit, file, stem, callee, `${callee.text} is a hook called in a ${stem} route file; a route file is a server adapter and holds no hook. Read the data in the feature it mounts.`);
82
+ }
83
+
84
+ function inspectRouteNode(node, context) {
85
+ const { kit, ts, file, stem, mounting, defaults, violations, state } = context;
86
+ if (jsxOf(ts, node)) state.hasJsx = true;
87
+ const tag = tagOf(ts, node);
88
+ if (mounting && tag) inspectMountingTag(tag, context);
89
+ if (mounting && isFunction(ts, node) && !defaults.has(node) && drawsDirectly(ts, node)) {
90
+ reportRoute(violations, kit, file, stem, node, `A function other than the default export draws JSX in a ${stem} route file; that is an inline component. Move it into the feature the route mounts.`);
91
+ }
92
+ if (ts.isCallExpression(node)) inspectHookCall(node, context);
93
+ return true;
94
+ }
95
+
96
+ function inspectRouteFile(graph, kit, file, stem, violations) {
97
+ const { ts } = kit;
98
+ const mounting = MOUNTING_ROUTES.has(stem);
99
+ const checker = kit.checkerOf(file.sourceFile);
100
+ const defaults = routeDefaults(ts, file, kit, checker);
101
+ const state = { hasJsx: false, owners: 0 };
102
+ kit.walk(file.sourceFile, node => inspectRouteNode(node, { graph, kit, ts, file, stem, mounting, checker, defaults, violations, state }));
103
+ if (mounting && state.hasJsx && state.owners !== 1) {
104
+ violations.push({ ruleId: RULE, path: file.rel, line: 1, column: 1, route: stem, owners: state.owners,
105
+ message: `${file.rel} mounts ${state.owners} feature owners; a ${stem} route file mounts exactly one feature (a layouts- or pages-tier component of the app), optionally inside a package shell.` });
106
+ }
107
+ }
108
+
27
109
  export function checkRouteFilesThin(input) {
28
110
  const { graph } = input;
29
111
  const kit = machineKit(input);
30
- const { ts } = kit;
31
112
  const violations = [];
32
113
  let routes = 0;
33
-
34
114
  for (const file of graph.files.values()) {
35
115
  if (file.slot !== ROUTE_SLOT) continue;
36
116
  const stem = path.posix.basename(file.rel).replace(/\.[cm]?[jt]sx?$/u, '');
37
117
  if (!HOOK_ROUTES.has(stem)) continue;
38
118
  routes += 1;
39
- const mounting = MOUNTING_ROUTES.has(stem);
40
- const checker = kit.checkerOf(file.sourceFile);
41
- const report = (node, message, extra = {}) => violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message, route: stem, ...extra });
42
-
43
- // The default export function: `export default function X`, `export default X` (X a function), `export default () => ...`.
44
- const defaults = new Set();
45
- for (const statement of file.sourceFile.statements) {
46
- if (ts.isFunctionDeclaration(statement) && statement.modifiers?.some(item => item.kind === ts.SyntaxKind.DefaultKeyword)) defaults.add(statement);
47
- if (ts.isExportAssignment(statement) && !statement.isExportEquals) {
48
- const target = statement.expression;
49
- if (ts.isIdentifier(target)) for (const declaration of kit.declarationsOf(checker, target)) defaults.add(ts.isVariableDeclaration(declaration) ? declaration.initializer ?? declaration : declaration);
50
- else defaults.add(target);
51
- }
52
- }
53
- const isFunction = node => ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node);
54
- const jsxOf = node => ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node);
55
- const tagOf = node => (ts.isJsxElement(node) ? node.openingElement.tagName : ts.isJsxSelfClosingElement(node) ? node.tagName : null);
56
- /** True when the function contains JSX that is not inside a nested function. */
57
- const drawsDirectly = fn => {
58
- let found = false;
59
- const visit = node => {
60
- if (found) return;
61
- if (node !== fn && isFunction(node)) return;
62
- if (jsxOf(node)) { found = true; return; }
63
- ts.forEachChild(node, visit);
64
- };
65
- visit(fn);
66
- return found;
67
- };
68
-
69
- let hasJsx = false;
70
- let owners = 0;
71
- kit.walk(file.sourceFile, node => {
72
- if (jsxOf(node)) hasJsx = true;
73
- const tag = tagOf(node);
74
- if (mounting && tag) {
75
- if (ts.isIdentifier(tag) && /^[a-z]/u.test(tag.text)) report(tag, `<${tag.text}> is drawing in a ${stem} route file; a route file mounts one feature and draws nothing. Move the markup into the feature.`);
76
- else if (kit.declarationsOf(checker, ts.isPropertyAccessExpression(tag) ? tag.name : tag).some(declaration => graph.files.get(kit.graphPath(declaration) ?? '')?.tier === 'feature')) owners += 1;
77
- }
78
- if (mounting && isFunction(node) && !defaults.has(node) && drawsDirectly(node)) {
79
- report(node, `A function other than the default export draws JSX in a ${stem} route file; that is an inline component. Move it into the feature the route mounts.`);
80
- }
81
- if (ts.isCallExpression(node)) {
82
- const callee = ts.isIdentifier(node.expression) ? node.expression : null;
83
- if (callee && HOOK_NAME.test(callee.text)) {
84
- const binding = kit.importBinding(checker, callee);
85
- const inHooks = kit.declarationsOf(checker, callee).some(declaration => graph.files.get(kit.graphPath(declaration) ?? '')?.slot === HOOKS_SLOT);
86
- if (binding || inHooks) report(callee, `${callee.text} is a hook called in a ${stem} route file; a route file is a server adapter and holds no hook. Read the data in the feature it mounts.`);
87
- }
88
- }
89
- return true;
90
- });
91
- if (mounting && hasJsx && owners !== 1) {
92
- violations.push({ ruleId: RULE, path: file.rel, line: 1, column: 1, route: stem, owners,
93
- message: `${file.rel} mounts ${owners} feature owners; a ${stem} route file mounts exactly one feature (a layouts- or pages-tier component of the app), optionally inside a package shell.` });
94
- }
119
+ inspectRouteFile(graph, kit, file, stem, violations);
95
120
  }
96
121
  return { violations, coverage: { status: 'checked', routes } };
97
122
  }