@starci/hfs 4.0.9 → 4.1.0

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