@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,35 +1,18 @@
1
- import path from 'node:path';
2
- import { treeOf } from './required-files.mjs';
3
-
4
1
  /**
5
- * R24 `arch-config-unread` (HFS_ARCH_CONFIG_UNREAD). The machine reads `hfs.json` and nothing else; a config file it
6
- * no longer reads is dead text that says a rule is tuned when it is not, and a declaration that yields no analysed file
7
- * is a machine that judged nothing. Both are red:
8
- *
9
- * - a tracked `architecture.json` (the retired per-repository config; the declaration is hfs.json, the rest is derived
10
- * from the slot manifest);
11
- * - a repository whose declaration and tsconfig load but whose program holds no production source file.
2
+ * R24 `arch-config-unread` (HFS_ARCH_CONFIG_UNREAD). The machine reads `hfs.json`; a declaration that yields no analysed
3
+ * file is a machine that judged nothing, and that is red: a repository whose declaration and tsconfig load but whose
4
+ * program holds no production source file.
12
5
  */
13
6
  export const CONFIG_UNREAD_RULE_IDS = ['HFS_ARCH_CONFIG_UNREAD'];
14
7
 
15
8
  const RULE = 'HFS_ARCH_CONFIG_UNREAD';
16
- /** Config file names an earlier machine read and this one never does. */
17
- const RETIRED_CONFIG_NAMES = ['architecture.json'];
18
9
 
19
- export function checkConfigUnread({ config, context }) {
10
+ export function checkConfigUnread({ context }) {
20
11
  const violations = [];
21
- const tree = treeOf(config.root);
22
- let retired = 0;
23
- for (const file of [...tree.files].sort()) {
24
- if (!RETIRED_CONFIG_NAMES.includes(path.posix.basename(file))) continue;
25
- retired += 1;
26
- violations.push({ ruleId: RULE, path: file, line: 1, column: 1,
27
- message: `${file} is a config the architecture machine no longer reads. hfs.json declares the repository; owners, roots, tiers and budgets come from the slot manifest. Delete ${file}.` });
28
- }
29
12
  const analysed = context.files.length;
30
13
  if (analysed === 0) {
31
14
  violations.push({ ruleId: RULE, path: 'hfs.json', line: 1, column: 1,
32
15
  message: 'hfs.json and the TypeScript projects load but the machine analysed no production source file, so it judged nothing. Declare the apps that hold source in hfs.json, or fix the tsconfig include so the source is in the program.' });
33
16
  }
34
- return { violations, coverage: { status: 'checked', retired, analysed } };
17
+ return { violations, coverage: { status: 'checked', analysed } };
35
18
  }
@@ -4,6 +4,7 @@ import { slash } from '../../lib/path-key.mjs';
4
4
  import { readJsonFile as readJson } from '../../lib/json.mjs';
5
5
  import { isInside } from '../../lib/walk.mjs';
6
6
  import { openHfs } from '../slots.mjs';
7
+ import { byCodeUnit } from '../../lib/list.mjs';
7
8
 
8
9
 
9
10
  /** A file's identity: its resolved real path, or the resolved path when it does not exist. */
@@ -13,9 +14,12 @@ function canonical(file) {
13
14
  }
14
15
 
15
16
  function exactKeys(value, allowed, label) {
16
- if (!value || typeof value !== 'object' || Array.isArray(value)) throw Error(`${label} must be an object.`);
17
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${label} must be an object.`);
17
18
  const unknown = Object.keys(value).filter(key => !allowed.has(key));
18
- if (unknown.length) throw Error(`${label} has unsupported fields: ${unknown.sort().join(', ')}.`);
19
+ if (unknown.length) {
20
+ unknown.sort(byCodeUnit);
21
+ throw new Error(`${label} has unsupported fields: ${unknown.join(', ')}.`);
22
+ }
19
23
  }
20
24
 
21
25
  /**
@@ -61,6 +65,81 @@ function existingRegularFile(root, relative) {
61
65
  }
62
66
  }
63
67
 
68
+ function admitWorkspace(root, directories, queue, candidate, label = 'local package', strict = false) {
69
+ const absolute = path.resolve(candidate);
70
+ if (!isInside(root, absolute) || absolute === root || !existingDirectory(root, slash(path.relative(root, absolute)))) {
71
+ if (strict) throw new Error(`${label} must resolve to a package directory inside the repository.`);
72
+ return;
73
+ }
74
+ const manifest = path.join(absolute, 'package.json');
75
+ try {
76
+ if (!fs.lstatSync(manifest).isFile() || fs.lstatSync(manifest).isSymbolicLink()) {
77
+ if (strict) throw new Error(`${label} must resolve to a regular package.json inside the repository.`);
78
+ return;
79
+ }
80
+ } catch (error) {
81
+ if (strict) throw new Error(error.message.startsWith(label) ? error.message : `${label} must resolve to a regular package.json inside the repository.`);
82
+ return;
83
+ }
84
+ const relative = slash(path.relative(root, absolute));
85
+ if (!directories.has(relative)) { directories.add(relative); queue.push(absolute); }
86
+ }
87
+
88
+ function expandSegment(base, segment) {
89
+ if (segment !== '*') return [path.join(base, segment)];
90
+ if (!fs.existsSync(base) || !fs.lstatSync(base).isDirectory()) return [];
91
+ return fs.readdirSync(base, { withFileTypes: true })
92
+ .filter(item => item.isDirectory())
93
+ .sort((a, b) => byCodeUnit(a.name, b.name))
94
+ .map(item => path.join(base, item.name));
95
+ }
96
+
97
+ function workspaceCandidates(packageRoot, normalized) {
98
+ const segments = normalized.split('/');
99
+ if (path.isAbsolute(normalized) || segments.some(segment => segment === '..' || (segment.includes('*') && segment !== '*'))) {
100
+ throw new Error(`Unsupported local workspace pattern: ${normalized}.`);
101
+ }
102
+ let candidates = [packageRoot];
103
+ for (const segment of segments) candidates = candidates.flatMap(base => expandSegment(base, segment));
104
+ return { candidates, wildcard: segments.includes('*') };
105
+ }
106
+
107
+ const admitFileDependency = (walk, section, name, value) => {
108
+ const { root, appPackageRoot, packageRoot, sideRoot, repository, state } = walk;
109
+ if (typeof value !== 'string' || !value.startsWith('file:')) return;
110
+ const absolute = path.resolve(sideRoot ? appPackageRoot : packageRoot, value.slice('file:'.length));
111
+ if (sideRoot && !isInside(root, absolute) && isInside(appPackageRoot, absolute)) return;
112
+ const label = `${section}.${name} file dependency`;
113
+ if (isInside(root, absolute)) { admitWorkspace(root, state.directories, state.queue, absolute, label, true); return; }
114
+ if (!repository || !isInside(repository, absolute) || !existingPackageDirectory(absolute)) {
115
+ throw new Error(`${label} must resolve to a package directory inside the repository.`);
116
+ }
117
+ };
118
+
119
+ function admitFileDependencies(walk, pkg) {
120
+ for (const section of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
121
+ for (const [name, value] of Object.entries(pkg?.[section] ?? {}))
122
+ admitFileDependency(walk, section, name, value);
123
+ }
124
+ }
125
+
126
+ const admitPackageWorkspaces = (walk, pkg) => {
127
+ const { root, packageRoot, sideRoot, side, state } = walk;
128
+ const patterns = Array.isArray(pkg?.workspaces) ? pkg.workspaces : pkg?.workspaces?.packages;
129
+ for (const pattern of Array.isArray(patterns) ? patterns : []) {
130
+ if (typeof pattern !== 'string' || !pattern.trim()) throw new Error('package.json workspace entries must be non-empty paths.');
131
+ const written = slash(pattern.trim()).replace(/^\.\//, '');
132
+ if (sideRoot && !written.startsWith(`${side}/`)) continue;
133
+ const normalized = sideRoot ? written.slice(side.length + 1) : written;
134
+ const { candidates, wildcard } = workspaceCandidates(packageRoot, normalized);
135
+ // npm expands a `*` segment to the directories that hold a package.json and skips the rest (an empty or untracked folder is
136
+ // HFS_EMPTY_DIR / HFS_SLOT_UNDECLARED, never a reason to analyse nothing); a literal workspace path must resolve. A `*`
137
+ // pattern may match nothing: every app declares the same workspaces (fe/apps/*, fe/packages/*, HFS_MONO_WORKSPACES) whether
138
+ // or not it has a package yet, and the root patterns are held to that fixed list by the rule, not here.
139
+ for (const candidate of candidates) admitWorkspace(root, state.directories, state.queue, candidate, `workspace ${normalized}`, !wildcard);
140
+ }
141
+ };
142
+
64
143
  /**
65
144
  * The npm workspaces below `root`. The one package.json of an app is at `packageRoot` (the app root; `root` is its side folder):
66
145
  * its workspace patterns under `<side>/` are this side's, read relative to the side folder, and every other pattern is the other
@@ -71,82 +150,17 @@ function workspaceDirectories(root, { packageRoot: appPackageRoot = root, side =
71
150
  const queue = [root];
72
151
  const visited = new Set();
73
152
  const repository = enclosingRepository(root);
74
- const admit = (candidate, label = 'local package', strict = false) => {
75
- const absolute = path.resolve(candidate);
76
- if (!isInside(root, absolute) || absolute === root || !existingDirectory(root, slash(path.relative(root, absolute)))) {
77
- if (strict) throw Error(`${label} must resolve to a package directory inside the repository.`);
78
- return;
79
- }
80
- const manifest = path.join(absolute, 'package.json');
81
- try {
82
- if (!fs.lstatSync(manifest).isFile() || fs.lstatSync(manifest).isSymbolicLink()) {
83
- if (strict) throw Error(`${label} must resolve to a regular package.json inside the repository.`);
84
- return;
85
- }
86
- } catch (error) {
87
- if (strict) throw Error(error.message.startsWith(label) ? error.message : `${label} must resolve to a regular package.json inside the repository.`);
88
- return;
89
- }
90
- const relative = slash(path.relative(root, absolute));
91
- if (!directories.has(relative)) { directories.add(relative); queue.push(absolute); }
92
- };
93
153
  while (queue.length) {
94
154
  const packageRoot = queue.shift();
95
155
  if (visited.has(packageRoot)) continue;
96
156
  visited.add(packageRoot);
97
157
  const sideRoot = side !== null && packageRoot === root;
98
158
  const pkg = readJson(path.join(sideRoot ? appPackageRoot : packageRoot, 'package.json'));
99
- const patterns = Array.isArray(pkg?.workspaces) ? pkg.workspaces : pkg?.workspaces?.packages;
100
- for (const pattern of Array.isArray(patterns) ? patterns : []) {
101
- if (typeof pattern !== 'string' || !pattern.trim()) throw Error('package.json workspace entries must be non-empty paths.');
102
- const written = slash(pattern.trim()).replace(/^\.\//, '');
103
- if (sideRoot && !written.startsWith(`${side}/`)) continue;
104
- const normalized = sideRoot ? written.slice(side.length + 1) : written;
105
- const segments = normalized.split('/');
106
- if (path.isAbsolute(normalized) || segments.some(segment => segment === '..' || (segment.includes('*') && segment !== '*'))) {
107
- throw Error(`Unsupported local workspace pattern: ${normalized}.`);
108
- }
109
- let candidates = [packageRoot];
110
- for (const segment of segments) {
111
- const next = [];
112
- for (const base of candidates) {
113
- if (segment === '*') {
114
- if (!fs.existsSync(base) || !fs.lstatSync(base).isDirectory()) continue;
115
- next.push(...fs.readdirSync(base, { withFileTypes: true })
116
- .filter(item => item.isDirectory())
117
- .sort((a, b) => a.name.localeCompare(b.name))
118
- .map(item => path.join(base, item.name)));
119
- } else next.push(path.join(base, segment));
120
- }
121
- candidates = next;
122
- }
123
- // npm expands a `*` segment to the directories that hold a package.json and skips the rest (an empty or untracked folder is
124
- // HFS_EMPTY_DIR / HFS_SLOT_UNDECLARED, never a reason to analyse nothing); a literal workspace path must resolve. A `*`
125
- // pattern may match nothing: every app declares the same workspaces (fe/apps/*, fe/packages/*, HFS_MONO_WORKSPACES) whether
126
- // or not it has a package yet, and the root patterns are held to that fixed list by the rule, not here.
127
- const wildcard = segments.includes('*');
128
- for (const candidate of candidates) admit(candidate, `workspace ${normalized}`, !wildcard);
129
- }
130
- for (const section of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
131
- for (const [name, value] of Object.entries(pkg?.[section] ?? {})) {
132
- if (typeof value !== 'string' || !value.startsWith('file:')) continue;
133
- const absolute = path.resolve(sideRoot ? appPackageRoot : packageRoot, value.slice('file:'.length));
134
- // The app's one package.json also lists the other side's file dependencies (inside the app, outside this side): not this
135
- // side's. A path that leaves the app is judged like any other: a package of the same repository, or refused.
136
- if (sideRoot && !isInside(root, absolute) && isInside(appPackageRoot, absolute)) continue;
137
- const label = `${section}.${name} file dependency`;
138
- if (isInside(root, absolute)) { admit(absolute, label, true); continue; }
139
- // A sibling package of the same repository: this project consumes it, so the path is real and
140
- // resolvable, but it is not part of this project's own source layout and must not become one of
141
- // its workspaces - collecting it would pull another package's src into these roots. Refusing it
142
- // instead would fail the whole check closed over an ordinary monorepo shape.
143
- if (!repository || !isInside(repository, absolute) || !existingPackageDirectory(absolute)) {
144
- throw Error(`${label} must resolve to a package directory inside the repository.`);
145
- }
146
- }
147
- }
159
+ const walk = { root, appPackageRoot, packageRoot, sideRoot, side, repository, state: { directories, queue } };
160
+ admitPackageWorkspaces(walk, pkg);
161
+ admitFileDependencies(walk, pkg);
148
162
  }
149
- return [...directories].sort();
163
+ return [...directories].sort(byCodeUnit);
150
164
  }
151
165
 
152
166
  function discoveredProjects(root, workspaces) {
@@ -201,7 +215,7 @@ function assertFrontendRolesDisjoint(root, frontend) {
201
215
  const left = entries[index];
202
216
  const right = entries[other];
203
217
  if (isInside(left.absolute, right.absolute) || isInside(right.absolute, left.absolute)) {
204
- throw Error(`Architecture frontend role roots must be disjoint; ${left.role} ${left.relative} overlaps ${right.role} ${right.relative}.`);
218
+ throw new Error(`Architecture frontend role roots must be disjoint; ${left.role} ${left.relative} overlaps ${right.role} ${right.relative}.`);
205
219
  }
206
220
  }
207
221
  }
@@ -226,8 +240,10 @@ function directoriesUnder(root, relative, depth) {
226
240
 
227
241
  /** The owner entry a slot instance exposes: index.ts/index.tsx, a package's src/index.ts, an app's app.module.ts. */
228
242
  function ownerEntry(root, ownerRoot, tier) {
229
- const candidates = tier === 'package' ? ['src/index.ts', 'src/index.tsx', 'index.ts', 'index.tsx']
230
- : tier === 'app' ? ['app.module.ts'] : ['index.ts', 'index.tsx'];
243
+ let candidates;
244
+ if (tier === 'package') candidates = ['src/index.ts', 'src/index.tsx', 'index.ts', 'index.tsx'];
245
+ else if (tier === 'app') candidates = ['app.module.ts'];
246
+ else candidates = ['index.ts', 'index.tsx'];
231
247
  for (const candidate of candidates) {
232
248
  const relative = `${ownerRoot}/${candidate}`;
233
249
  if (existingRegularFile(root, relative)) return relative;
@@ -279,13 +295,12 @@ function derivedGrammar(root, packageRoot, workspaces, apps = []) {
279
295
  }
280
296
 
281
297
  /**
282
- * Resolve the layout contract from hfs.json and the slot manifest (knowledge/hfs/slots.yaml). architecture.json is
283
- * retired: a repository carries only hfs.json. Owners, roots and the tier of every path come from slots; the contract
298
+ * Resolve the layout contract from hfs.json and the slot manifest (knowledge/hfs/slots.yaml). A repository carries only hfs.json. Owners, roots and the tier of every path come from slots; the contract
284
299
  * has no ignore, waiver or baseline field.
285
300
  */
286
301
  export function loadArchitectureConfig(repositoryRoot, { hfs } = {}) {
287
302
  const root = fs.realpathSync(path.resolve(repositoryRoot));
288
- if (!fs.lstatSync(root).isDirectory()) throw Error('Repository root must be a directory.');
303
+ if (!fs.lstatSync(root).isDirectory()) throw new Error('Repository root must be a directory.');
289
304
  const opened = hfs ?? openHfs({ repoRoot: root });
290
305
  const { profile, apps } = opened.repo;
291
306
  // A side of an app (the side folder is the root the machine judges) keeps its dependencies in the app root's one package.json.
@@ -293,10 +308,10 @@ export function loadArchitectureConfig(repositoryRoot, { hfs } = {}) {
293
308
  const packageRoot = side === null ? root : path.dirname(root);
294
309
  const workspaces = workspaceDirectories(root, { packageRoot, side });
295
310
  const appDirs = apps.map(app => `apps/${app.name}`);
296
- const inferred = inferredLayout(root, [...new Set([...workspaces, ...appDirs])].sort());
311
+ const inferred = inferredLayout(root, [...new Set([...workspaces, ...appDirs])].sort(byCodeUnit));
297
312
  const kinds = [profile === 'be' ? 'backend' : 'frontend'];
298
- const projects = discoveredProjects(root, [...new Set([...workspaces, ...appDirs])].sort());
299
- if (!projects.length) throw Error('The repository has no tsconfig.json to derive a TypeScript project from.');
313
+ const projects = discoveredProjects(root, [...new Set([...workspaces, ...appDirs])].sort(byCodeUnit));
314
+ if (!projects.length) throw new Error('The repository has no tsconfig.json to derive a TypeScript project from.');
300
315
  const backend = {
301
316
  modules: ['src/modules'],
302
317
  // The layered feature root is the api kind (src/features/api/<feature>/: application/ and transport/<protocol>/); the cli