@starci/hfs 4.0.10 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/CHANGELOG.md +5 -1
  2. package/emit/operations.mjs +1 -1
  3. package/emit/type-schema.mjs +2 -1
  4. package/lint/run.mjs +65 -24
  5. package/package.json +1 -1
  6. package/report/order.mjs +2 -0
  7. package/runtime/config.example.yaml +194 -0
  8. package/runtime/engine/by-code-unit.mjs +12 -0
  9. package/runtime/engine/config.mjs +222 -239
  10. package/runtime/engine/invalid-config.mjs +5 -5
  11. package/runtime/engine/model-config.mjs +90 -0
  12. package/runtime/engine/orca-config.mjs +3 -1
  13. package/runtime/engine/release-config.mjs +28 -0
  14. package/runtime/engine/removed-vocabulary.mjs +28 -0
  15. package/runtime/engine/resources-config.mjs +52 -0
  16. package/runtime/engine/runtime-root.mjs +17 -0
  17. package/runtime/engine/secrets.mjs +35 -31
  18. package/runtime/engine/sonar-config.mjs +21 -0
  19. package/runtime/engine/temp-root.mjs +32 -0
  20. package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
  21. package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
  22. package/runtime/knowledge/hfs/rules.yaml +161 -65
  23. package/runtime/knowledge/hfs/slots.yaml +11 -14
  24. package/runtime/knowledge/patterns/be/api.yaml +7 -24
  25. package/runtime/knowledge/patterns/be/cli.yaml +3 -12
  26. package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
  27. package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
  28. package/runtime/modules/kernel/failure-codes.yaml +1 -41
  29. package/runtime/modules/kernel/removed-vocabulary.yaml +268 -0
  30. package/runtime/modules/models/registry.yaml +25 -359
  31. package/runtime/modules/models/runtimes.yaml +126 -265
  32. package/runtime/modules/models/tiers.yaml +66 -0
  33. package/runtime/scripts/api/fs/claim-file.mjs +30 -0
  34. package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
  35. package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
  36. package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
  37. package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
  38. package/runtime/scripts/api/git/lib.mjs +34 -1
  39. package/runtime/scripts/api/process/resolve-real-tool.mjs +14 -8
  40. package/runtime/scripts/api/process/run-program.mjs +2 -1
  41. package/runtime/scripts/api/sops/lib.mjs +198 -120
  42. package/runtime/scripts/hfs/allows.mjs +3 -3
  43. package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
  44. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
  45. package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
  46. package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
  47. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
  48. package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
  49. package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
  50. package/runtime/scripts/hfs/architecture/config.mjs +95 -80
  51. package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
  52. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
  53. package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
  54. package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
  55. package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
  56. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
  57. package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
  58. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
  59. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
  60. package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
  61. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
  62. package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
  63. package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
  64. package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
  65. package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
  66. package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
  67. package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
  68. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
  69. package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
  70. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
  71. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
  72. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
  73. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
  74. package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
  75. package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
  76. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
  77. package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
  78. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
  79. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
  80. package/runtime/scripts/hfs/architecture/index.mjs +164 -104
  81. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
  82. package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
  83. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
  84. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
  85. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
  86. package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
  87. package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
  88. package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
  89. package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
  90. package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
  91. package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
  92. package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
  93. package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
  94. package/runtime/scripts/hfs/architecture/required-files.mjs +121 -70
  95. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
  96. package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
  97. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
  98. package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
  99. package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
  100. package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
  101. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
  102. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
  103. package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
  104. package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
  105. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
  106. package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
  107. package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
  108. package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
  109. package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
  110. package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
  111. package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
  112. package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
  113. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
  114. package/runtime/scripts/hfs/architecture.mjs +3 -4
  115. package/runtime/scripts/hfs/catalog-once.mjs +15 -0
  116. package/runtime/scripts/hfs/check.mjs +63 -71
  117. package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
  118. package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
  119. package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
  120. package/runtime/scripts/hfs/edition-slots.mjs +5 -3
  121. package/runtime/scripts/hfs/linear-text.mjs +30 -0
  122. package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
  123. package/runtime/scripts/hfs/path-findings.mjs +58 -38
  124. package/runtime/scripts/hfs/pin-findings.mjs +31 -0
  125. package/runtime/scripts/hfs/repo-identity.mjs +21 -4
  126. package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
  127. package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
  128. package/runtime/scripts/hfs/rules/cli.mjs +5 -1
  129. package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
  130. package/runtime/scripts/hfs/rules/contract.mjs +35 -27
  131. package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
  132. package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
  133. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
  134. package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
  135. package/runtime/scripts/hfs/rules/database.mjs +36 -23
  136. package/runtime/scripts/hfs/rules/deps.mjs +64 -32
  137. package/runtime/scripts/hfs/rules/docker.mjs +113 -65
  138. package/runtime/scripts/hfs/rules/edition.mjs +118 -111
  139. package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
  140. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
  141. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
  142. package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
  143. package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
  144. package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
  145. package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
  146. package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
  147. package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
  148. package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
  149. package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
  150. package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
  151. package/runtime/scripts/hfs/rules/saga.mjs +128 -61
  152. package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
  153. package/runtime/scripts/hfs/rules/services.mjs +34 -16
  154. package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
  155. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
  156. package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
  157. package/runtime/scripts/hfs/secret.mjs +59 -48
  158. package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
  159. package/runtime/scripts/hfs/slot-classify.mjs +65 -0
  160. package/runtime/scripts/hfs/slot-errors.mjs +11 -0
  161. package/runtime/scripts/hfs/slot-imports.mjs +76 -0
  162. package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
  163. package/runtime/scripts/hfs/slot-match.mjs +153 -0
  164. package/runtime/scripts/hfs/slot-path.mjs +8 -0
  165. package/runtime/scripts/hfs/slot-required.mjs +76 -0
  166. package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
  167. package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
  168. package/runtime/scripts/hfs/slots.mjs +74 -695
  169. package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
  170. package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
  171. package/runtime/scripts/hfs/tree.mjs +19 -14
  172. package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
  173. package/runtime/scripts/hfs/view.mjs +1 -1
  174. package/runtime/scripts/lib/dockerfile.mjs +56 -33
  175. package/runtime/scripts/lib/env.mjs +10 -0
  176. package/runtime/scripts/lib/event-contract.mjs +17 -13
  177. package/runtime/scripts/lib/git.mjs +2 -2
  178. package/runtime/scripts/lib/graphql-contract.mjs +158 -322
  179. package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
  180. package/runtime/scripts/lib/i18n.mjs +3 -2
  181. package/runtime/scripts/lib/in-order.mjs +71 -0
  182. package/runtime/scripts/lib/language.mjs +3 -3
  183. package/runtime/scripts/lib/list.mjs +8 -1
  184. package/runtime/scripts/lib/path-key.mjs +38 -5
  185. package/runtime/scripts/lib/pid-alive.mjs +7 -0
  186. package/runtime/scripts/lib/regex.mjs +1 -1
  187. package/runtime/scripts/lib/same-text.mjs +1 -1
  188. package/runtime/scripts/lib/secret-patterns.mjs +6 -2
  189. package/runtime/scripts/lib/sleep-sync.mjs +2 -2
  190. package/runtime/scripts/lib/sops-envelope.mjs +95 -48
  191. package/runtime/scripts/lib/stack-services.mjs +1 -2
  192. package/runtime/scripts/lib/ts-ast.mjs +9 -0
  193. package/runtime/scripts/lib/walk.mjs +6 -1
  194. package/runtime/scripts/lib/yaml-cached.mjs +14 -0
  195. package/scaffold/add-cli-lite.mjs +2 -1
  196. package/scaffold/add-table.mjs +2 -2
  197. package/scaffold/add.mjs +3 -2
  198. package/scaffold/app.mjs +4 -2
  199. package/scaffold/edition-gate.mjs +25 -25
  200. package/scaffold/lite-exports.mjs +2 -1
  201. package/scaffold/service.mjs +5 -4
  202. package/sync/hygiene.mjs +15 -9
  203. package/sync/index.mjs +7 -3
  204. package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
  205. package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
  206. package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
  207. package/templates/app/ci-workflows/sonar-steps.yml +2 -2
  208. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
  209. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
  210. package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
  211. package/templates/app/starciwork.gitignore +1 -1
  212. package/templates/be/image/api/Dockerfile +1 -1
  213. package/templates/be/image/cli/Dockerfile +1 -1
  214. package/templates/be/image/worker/Dockerfile +1 -1
  215. package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
  216. package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
  217. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
  218. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
  219. package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
  220. package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
  221. package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
  222. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
  223. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
  224. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
  225. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
  226. package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
  227. package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
  228. package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
  229. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
  230. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  231. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
  232. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
  233. package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
  234. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  235. package/templates/fe/image/next/Dockerfile +2 -1
  236. package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
  237. package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
  238. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
  239. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
  240. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
  241. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
  242. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
  243. package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
  244. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
  245. package/upgrade/index.mjs +9 -2
  246. package/runtime/engine/admission.mjs +0 -308
  247. package/runtime/engine/canonical-json.mjs +0 -10
  248. package/runtime/engine/db/blob.mjs +0 -315
  249. package/runtime/engine/db/ledger-paths.mjs +0 -83
  250. package/runtime/engine/db/ledger.mjs +0 -1118
  251. package/runtime/engine/db/machine-connection.mjs +0 -135
  252. package/runtime/engine/db/machine-schema.mjs +0 -84
  253. package/runtime/engine/db/machine.mjs +0 -1367
  254. package/runtime/engine/db/migrations/machine/0001-init.sql +0 -924
  255. package/runtime/engine/db/migrations/runtime/0001-init.sql +0 -1108
  256. package/runtime/engine/db/provider-reservations.mjs +0 -101
  257. package/runtime/engine/digest.mjs +0 -16
  258. package/runtime/engine/refuse.mjs +0 -11
  259. package/runtime/modules/ops/_labels.yaml +0 -53
  260. package/runtime/scripts/api/node/lib.mjs +0 -14
  261. package/runtime/scripts/api/node/spawn-node.mjs +0 -6
  262. package/runtime/scripts/api/process/lib.mjs +0 -111
  263. package/runtime/scripts/api/process/owned-process.mjs +0 -78
  264. package/runtime/scripts/api/process/stop-owned-process.mjs +0 -9
  265. package/runtime/scripts/connectors/lib.mjs +0 -488
  266. package/runtime/scripts/lib/clip.mjs +0 -19
  267. package/runtime/scripts/lib/display-names.mjs +0 -259
  268. package/runtime/scripts/lib/example-refs.mjs +0 -158
  269. package/runtime/scripts/lib/json-schema.mjs +0 -52
  270. package/runtime/scripts/lib/process-identity.mjs +0 -6
  271. package/runtime/scripts/lib/read-yaml.mjs +0 -18
  272. package/runtime/scripts/lib/redact.mjs +0 -161
  273. package/runtime/scripts/lib/sleep.mjs +0 -7
  274. package/runtime/scripts/lib/source-phrases.mjs +0 -34
  275. package/runtime/scripts/lib/sqlite.mjs +0 -21
@@ -1,4 +1,5 @@
1
1
  import { isServerActionModule } from './server-action.mjs';
2
+ import { byCodeUnit } from '../../lib/list.mjs';
2
3
 
3
4
  /**
4
5
  * HFS checks 1 and 2 (knowledge/hfs/slots.yaml `tiers`):
@@ -21,40 +22,61 @@ const REASON_TEXT = {
21
22
  };
22
23
 
23
24
  /** Strongly connected components (Tarjan, iterative) of `adjacency` (Map<node, Set<node>>); components of one node are dropped. */
25
+ function startTraversal(state, adjacency, node) {
26
+ const at = state.counter;
27
+ state.counter += 1;
28
+ state.index.set(node, at);
29
+ state.low.set(node, at);
30
+ state.stack.push(node);
31
+ state.onStack.add(node);
32
+ state.work.push({ node, iterator: adjacency.get(node)[Symbol.iterator]() });
33
+ }
34
+
35
+ function visitTarget(state, adjacency, frame, target) {
36
+ if (!adjacency.has(target)) return;
37
+ if (!state.index.has(target)) {
38
+ startTraversal(state, adjacency, target);
39
+ return;
40
+ }
41
+ if (state.onStack.has(target)) state.low.set(frame.node, Math.min(state.low.get(frame.node), state.index.get(target)));
42
+ }
43
+
44
+ function closeComponent(state, frame) {
45
+ const component = [];
46
+ let member;
47
+ do {
48
+ member = state.stack.pop();
49
+ state.onStack.delete(member);
50
+ component.push(member);
51
+ } while (member !== frame.node);
52
+ if (component.length > 1) state.result.push(component);
53
+ }
54
+
55
+ function finishFrame(state, frame) {
56
+ state.work.pop();
57
+ if (state.work.length) {
58
+ const parent = state.work.at(-1).node;
59
+ state.low.set(parent, Math.min(state.low.get(parent), state.low.get(frame.node)));
60
+ }
61
+ if (state.low.get(frame.node) === state.index.get(frame.node)) closeComponent(state, frame);
62
+ }
63
+
24
64
  export function stronglyConnected(adjacency) {
25
- let counter = 0;
26
- const index = new Map();
27
- const low = new Map();
28
- const onStack = new Set();
29
- const stack = [];
30
- const result = [];
65
+ const state = { counter: 0, index: new Map(), low: new Map(), onStack: new Set(), stack: [], result: [], work: [] };
31
66
  for (const start of adjacency.keys()) {
32
- if (index.has(start)) continue;
33
- const work = [{ node: start, iterator: adjacency.get(start)[Symbol.iterator]() }];
34
- index.set(start, counter); low.set(start, counter); counter += 1; stack.push(start); onStack.add(start);
35
- while (work.length) {
36
- const frame = work.at(-1);
67
+ if (state.index.has(start)) continue;
68
+ startTraversal(state, adjacency, start);
69
+ while (state.work.length) {
70
+ const frame = state.work.at(-1);
37
71
  const next = frame.iterator.next();
38
- if (!next.done) {
39
- const target = next.value;
40
- if (!adjacency.has(target)) continue;
41
- if (!index.has(target)) {
42
- index.set(target, counter); low.set(target, counter); counter += 1; stack.push(target); onStack.add(target);
43
- work.push({ node: target, iterator: adjacency.get(target)[Symbol.iterator]() });
44
- } else if (onStack.has(target)) low.set(frame.node, Math.min(low.get(frame.node), index.get(target)));
72
+ if (next.done) {
73
+ finishFrame(state, frame);
45
74
  continue;
46
75
  }
47
- work.pop();
48
- if (work.length) { const parent = work.at(-1).node; low.set(parent, Math.min(low.get(parent), low.get(frame.node))); }
49
- if (low.get(frame.node) === index.get(frame.node)) {
50
- const component = [];
51
- let member;
52
- do { member = stack.pop(); onStack.delete(member); component.push(member); } while (member !== frame.node);
53
- if (component.length > 1) result.push(component);
54
- }
76
+ visitTarget(state, adjacency, frame, next.value);
55
77
  }
56
78
  }
57
- return result;
79
+ return state.result;
58
80
  }
59
81
 
60
82
  /** A shortest cycle through `origin` inside `members`, as a node path that starts and ends at origin. */
@@ -95,47 +117,57 @@ const blockCallsServerAction = (graph, edge) => {
95
117
  return from.slot === 'fe.components' && from.kind === 'blocks' && from.role === 'entry';
96
118
  };
97
119
 
98
- export function checkTiers(graph) {
99
- const { resolver, profile } = graph;
100
- const violations = [];
101
- const tierRule = profile === 'be' ? 'BE_TIER_DIRECTION' : 'FE_TIER_DIRECTION';
102
- let edgesChecked = 0;
103
- let unclassified = 0;
104
- const counts = { tierDirection: 0, layerOrder: 0, crossApp: 0 };
105
- for (const edge of graph.edges) {
106
- const verdict = resolver.importAllowed(edge.from, edge.to);
107
- if (verdict.reason === 'unowned' || verdict.reason?.startsWith('slot')) { unclassified += 1; continue; }
108
- edgesChecked += 1;
109
- const actionTarget = serverActionTarget(graph, edge);
110
- if ((verdict.allowed || verdict.reason === 'notPublicEntry') && actionTarget && graph.unit(edge.from) !== graph.unit(edge.to) && !blockCallsServerAction(graph, edge)) {
111
- counts.tierDirection += 1;
112
- violations.push({
113
- ruleId: tierRule,
114
- path: edge.from, line: edge.line, column: edge.column,
115
- specifier: edge.specifier, resolvedPath: edge.to, typeOnly: !edge.runtime,
116
- fromTier: resolver.tierOf(edge.from), toTier: resolver.tierOf(edge.to),
117
- message: `${edge.from} -> ${edge.to}: a db Server Action is called directly only by a connected block entry; hooks and other tiers use their declared data path${edge.runtime ? '' : ' (type-only imports count)'}.`,
118
- });
119
- continue;
120
- }
121
- if (verdict.allowed || (verdict.reason === 'tierDirection' && blockCallsServerAction(graph, edge)) || !REASON_TEXT[verdict.reason]) continue;
122
- counts[verdict.reason] += 1;
123
- const featureToFeature = profile === 'be' && verdict.reason === 'tierDirection' && verdict.fromTier === 'feature' && verdict.toTier === 'feature';
124
- const fromKind = featureToFeature ? triggerOf(resolver, edge.from) : null;
125
- const toKind = featureToFeature ? triggerOf(resolver, edge.to) : null;
126
- const crossKind = fromKind !== null && toKind !== null && fromKind !== toKind;
127
- violations.push({
128
- ruleId: verdict.reason === 'crossApp' ? 'FE_APP_ISOLATION' : crossKind ? 'BE_KIND_ISOLATION' : featureToFeature ? 'BE_FEATURE_IMPORTS_FEATURE' : tierRule,
129
- path: edge.from, line: edge.line, column: edge.column,
130
- specifier: edge.specifier, resolvedPath: edge.to, typeOnly: !edge.runtime,
131
- fromTier: verdict.fromTier ?? null, toTier: verdict.toTier ?? null,
132
- message: crossKind
133
- ? `${edge.from} -> ${edge.to}: a ${fromKind} feature imports a ${toKind} feature; no kind imports another, every cross-kind call is an event published with eventBus.publish(event, tx) from a domain service${edge.runtime ? '' : ' (type-only imports count)'}.`
134
- : `${edge.from} -> ${edge.to}: ${REASON_TEXT[verdict.reason](verdict)}${edge.runtime ? '' : ' (type-only imports count)'}.`,
135
- });
120
+ function reportDirectServerAction(graph, edge, verdict, actionTarget, tierRule, counts, violations) {
121
+ if (!(verdict.allowed || verdict.reason === 'notPublicEntry') || !actionTarget
122
+ || graph.unit(edge.from) === graph.unit(edge.to) || blockCallsServerAction(graph, edge)) return false;
123
+ counts.tierDirection += 1;
124
+ violations.push({
125
+ ruleId: tierRule,
126
+ path: edge.from, line: edge.line, column: edge.column,
127
+ specifier: edge.specifier, resolvedPath: edge.to, typeOnly: !edge.runtime,
128
+ fromTier: graph.resolver.tierOf(edge.from), toTier: graph.resolver.tierOf(edge.to),
129
+ message: `${edge.from} -> ${edge.to}: a db Server Action is called directly only by a connected block entry; hooks and other tiers use their declared data path${edge.runtime ? '' : ' (type-only imports count)'}.`,
130
+ });
131
+ return true;
132
+ }
133
+
134
+ function reportReasonedTierViolation(graph, edge, verdict, tierRule, counts, violations) {
135
+ if (verdict.allowed || (verdict.reason === 'tierDirection' && blockCallsServerAction(graph, edge)) || !REASON_TEXT[verdict.reason]) return;
136
+ counts[verdict.reason] += 1;
137
+ const featureToFeature = graph.profile === 'be' && verdict.reason === 'tierDirection' && verdict.fromTier === 'feature' && verdict.toTier === 'feature';
138
+ const fromKind = featureToFeature ? triggerOf(graph.resolver, edge.from) : null;
139
+ const toKind = featureToFeature ? triggerOf(graph.resolver, edge.to) : null;
140
+ const crossKind = fromKind !== null && toKind !== null && fromKind !== toKind;
141
+ let ruleId = tierRule;
142
+ if (featureToFeature) ruleId = 'BE_FEATURE_IMPORTS_FEATURE';
143
+ if (crossKind) ruleId = 'BE_KIND_ISOLATION';
144
+ if (verdict.reason === 'crossApp') ruleId = 'FE_APP_ISOLATION';
145
+ const typeOnly = edge.runtime ? '' : ' (type-only imports count)';
146
+ const message = crossKind
147
+ ? `${edge.from} -> ${edge.to}: a ${fromKind} feature imports a ${toKind} feature; no kind imports another, every cross-kind call is an event published with eventBus.publish(event, tx) from a domain service${typeOnly}.`
148
+ : `${edge.from} -> ${edge.to}: ${REASON_TEXT[verdict.reason](verdict)}${typeOnly}.`;
149
+ violations.push({
150
+ ruleId,
151
+ path: edge.from, line: edge.line, column: edge.column,
152
+ specifier: edge.specifier, resolvedPath: edge.to, typeOnly: !edge.runtime,
153
+ fromTier: verdict.fromTier ?? null, toTier: verdict.toTier ?? null,
154
+ message,
155
+ });
156
+ }
157
+
158
+ function inspectTierEdge(graph, edge, tierRule, counts, violations, coverage) {
159
+ const verdict = graph.resolver.importAllowed(edge.from, edge.to);
160
+ if (verdict.reason === 'unowned' || verdict.reason?.startsWith('slot')) {
161
+ coverage.unclassifiedEdges += 1;
162
+ return;
136
163
  }
164
+ coverage.edgesChecked += 1;
165
+ const actionTarget = serverActionTarget(graph, edge);
166
+ if (reportDirectServerAction(graph, edge, verdict, actionTarget, tierRule, counts, violations)) return;
167
+ reportReasonedTierViolation(graph, edge, verdict, tierRule, counts, violations);
168
+ }
137
169
 
138
- // Owner cycles: nodes are owner units, edges the imports between two distinct units.
170
+ function ownerAdjacency(graph, resolver) {
139
171
  const adjacency = new Map();
140
172
  const witness = new Map();
141
173
  for (const edge of graph.edges) {
@@ -148,25 +180,44 @@ export function checkTiers(graph) {
148
180
  adjacency.get(a).add(b);
149
181
  if (!witness.has(`${a}\0${b}`)) witness.set(`${a}\0${b}`, edge);
150
182
  }
183
+ return { adjacency, witness };
184
+ }
185
+
186
+ function addOwnerCycleViolations(graph, adjacency, witness, violations) {
151
187
  const components = stronglyConnected(adjacency);
152
188
  for (const component of components) {
153
189
  const members = new Set(component);
154
- const origin = [...component].sort()[0];
190
+ const origin = [...component].sort(byCodeUnit)[0];
155
191
  const cycle = cycleThrough(adjacency, members, origin);
156
192
  const hops = cycle.slice(0, -1).map((unit, i) => witness.get(`${unit}\0${cycle[i + 1]}`));
157
193
  const label = unit => unit.slice(unit.indexOf(':') + 1) || unit;
158
194
  const first = hops[0];
195
+ const connectedOwners = component.length > cycle.length - 1 ? ` (strongly connected with ${component.length} owners)` : '';
159
196
  violations.push({
160
197
  ruleId: 'ARCH_OWNER_CYCLE',
161
198
  path: first.from, line: first.line, column: first.column,
162
199
  cycle: cycle.map(label),
163
200
  cycleImports: hops.map(hop => ({ path: hop.from, line: hop.line, specifier: hop.specifier, typeOnly: !hop.runtime })),
164
201
  componentSize: component.length,
165
- message: `Owner cycle: ${cycle.map(label).join(' -> ')}${component.length > cycle.length - 1 ? ` (strongly connected with ${component.length} owners)` : ''}; type-only imports count.`,
202
+ message: `Owner cycle: ${cycle.map(label).join(' -> ')}${connectedOwners}; type-only imports count.`,
166
203
  });
167
204
  }
205
+ return components.length;
206
+ }
207
+
208
+ export function checkTiers(graph) {
209
+ const { resolver, profile } = graph;
210
+ const violations = [];
211
+ const tierRule = profile === 'be' ? 'BE_TIER_DIRECTION' : 'FE_TIER_DIRECTION';
212
+ const coverage = { edgesChecked: 0, unclassifiedEdges: 0 };
213
+ const counts = { tierDirection: 0, layerOrder: 0, crossApp: 0 };
214
+ for (const edge of graph.edges) inspectTierEdge(graph, edge, tierRule, counts, violations, coverage);
215
+
216
+ // Owner cycles: nodes are owner units, edges the imports between two distinct units.
217
+ const { adjacency, witness } = ownerAdjacency(graph, resolver);
218
+ const cycles = addOwnerCycleViolations(graph, adjacency, witness, violations);
168
219
  return {
169
220
  violations,
170
- coverage: { status: 'checked', edgesChecked, unclassifiedEdges: unclassified, ownerUnits: adjacency.size, cycles: components.length, ...counts },
221
+ coverage: { status: 'checked', ...coverage, ownerUnits: adjacency.size, cycles, ...counts },
171
222
  };
172
223
  }
@@ -33,74 +33,80 @@ const PACKAGE_API_SLOT = 'fe.package.api';
33
33
  const CLIENT_SLOTS = new Set([APP_CLIENT_SLOT, 'fe.package.api.client']);
34
34
  const NO_CLIENT = "the repository has no transport client. Create the one client (packages/<family>-api/src/client.ts, or the only app's modules/api/client.ts) and call it.";
35
35
 
36
- export function checkTransportOwner(input) {
37
- const { graph } = input;
38
- const kit = machineKit(input);
39
- const { ts, resolver } = kit;
40
- const violations = [];
41
- const report = (file, node, message, extra = {}) => violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
42
- const nextApps = resolver.repo.apps.filter(item => item.kind === 'next');
43
- let readers = 0;
36
+ /** True when `node` names the global fetch: no import, and no declaration outside a lib declaration file. */
37
+ function isGlobalObjectFetch(ts, node) {
38
+ return ts.isPropertyAccessExpression(node) && node.name.text === 'fetch' && ts.isIdentifier(node.expression) && GLOBAL_OBJECTS.has(node.expression.text);
39
+ }
44
40
 
45
- /** True when `node` names the global fetch: no import, and no declaration outside a lib declaration file. */
46
- const isGlobalFetch = (checker, node) => {
47
- if (ts.isIdentifier(node)) {
48
- if (node.text !== 'fetch' || kit.importBinding(checker, node)) return false;
49
- const parent = node.parent;
50
- if (ts.isPropertyAccessExpression(parent) && parent.name === node) return false;
51
- // A type position (`typeof fetch`, `typeof globalThis.fetch`) names the type of fetch and sends nothing.
52
- if (ts.isTypeQueryNode(parent) || ts.isQualifiedName(parent)) return false;
53
- if ((ts.isPropertyAssignment(parent) || ts.isPropertyDeclaration(parent) || ts.isMethodDeclaration(parent) || ts.isBindingElement(parent) || ts.isParameter(parent) || ts.isVariableDeclaration(parent)) && parent.name === node) return false;
54
- if (ts.isImportSpecifier(parent) || ts.isExportSpecifier(parent) || ts.isPropertySignature(parent)) return false;
55
- const declarations = kit.declarationsOf(checker, node);
56
- return declarations.every(declaration => declaration.getSourceFile().isDeclarationFile);
57
- }
58
- return ts.isPropertyAccessExpression(node) && node.name.text === 'fetch' && ts.isIdentifier(node.expression) && GLOBAL_OBJECTS.has(node.expression.text);
59
- };
41
+ function isDeclarationName(ts, parent, node) {
42
+ return (ts.isPropertyAssignment(parent) || ts.isPropertyDeclaration(parent) || ts.isMethodDeclaration(parent) || ts.isBindingElement(parent) || ts.isParameter(parent) || ts.isVariableDeclaration(parent)) && parent.name === node;
43
+ }
60
44
 
61
- const files = [...graph.files.values()].filter(file => file.slot && file.tier !== 'e2e');
62
- const clients = files.filter(file => CLIENT_SLOTS.has(file.slot));
63
- const outcomes = files.filter(file => resolver.slot(file.slot)?.outcomeHome === true);
64
- const clientRels = new Set(clients.map(file => file.rel));
65
- const list = items => items.map(item => item.rel).join(', ');
66
- const owned = clients.length ? `the repository has one transport (${list(clients)}). Call the client and take its Outcome.` : NO_CLIENT;
45
+ function isTypeOrImportPosition(ts, parent) {
46
+ return ts.isTypeQueryNode(parent) || ts.isQualifiedName(parent) || ts.isImportSpecifier(parent) || ts.isExportSpecifier(parent) || ts.isPropertySignature(parent);
47
+ }
67
48
 
68
- // Exactly one client and one Outcome union per repository.
69
- for (const [items, noun] of [[clients, 'transport client'], [outcomes, 'Outcome union']]) {
70
- for (const file of items) {
71
- if (items.length > 1) report(file, file.sourceFile, `The repository has ${items.length} ${noun}s (${list(items)}); it has exactly one: the api package's (packages/<family>-api/src/) when the apps share it, or the only app's. Keep one and delete the others.`);
72
- else if ((file.slot === APP_CLIENT_SLOT || file.slot === APP_OUTCOME_SLOT) && nextApps.length > 1) {
73
- report(file, file.sourceFile, `${file.rel} is an app's ${noun} in a repository of ${nextApps.length} apps; a repository with several apps keeps its one ${noun} in the api package (packages/<family>-api/src/).`);
74
- }
49
+ function isGlobalFetch(ts, kit, checker, node) {
50
+ if (!ts.isIdentifier(node)) return isGlobalObjectFetch(ts, node);
51
+ if (node.text !== 'fetch') return false;
52
+ if (kit.importBinding(checker, node)) return false;
53
+ const parent = node.parent;
54
+ if (ts.isPropertyAccessExpression(parent) && parent.name === node) return false;
55
+ // A type position (`typeof fetch`, `typeof globalThis.fetch`) names the type of fetch and sends nothing.
56
+ if (isTypeOrImportPosition(ts, parent)) return false;
57
+ if (isDeclarationName(ts, parent, node)) return false;
58
+ const declarations = kit.declarationsOf(checker, node);
59
+ return declarations.every(declaration => declaration.getSourceFile().isDeclarationFile);
60
+ }
61
+
62
+ function reportMultiplicity(items, noun, nextApps, report, list) {
63
+ for (const file of items) {
64
+ if (items.length > 1) {
65
+ report(file, file.sourceFile, `The repository has ${items.length} ${noun}s (${list(items)}); it has exactly one: the api package's (packages/<family>-api/src/) when the apps share it, or the only app's. Keep one and delete the others.`);
66
+ } else if ((file.slot === APP_CLIENT_SLOT || file.slot === APP_OUTCOME_SLOT) && nextApps.length > 1) {
67
+ report(file, file.sourceFile, `${file.rel} is an app's ${noun} in a repository of ${nextApps.length} apps; a repository with several apps keeps its one ${noun} in the api package (packages/<family>-api/src/).`);
75
68
  }
76
69
  }
70
+ }
77
71
 
72
+ function globalFetchUse(ts, kit, checker, node) {
73
+ if (ts.isPropertyAccessExpression(node) && isGlobalFetch(ts, kit, checker, node)) return 'property';
74
+ if (ts.isIdentifier(node) && isGlobalFetch(ts, kit, checker, node)) return 'identifier';
75
+ return null;
76
+ }
77
+
78
+ function moduleSpecifierOf(ts, node) {
79
+ if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) return node.moduleSpecifier;
80
+ if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword || (ts.isIdentifier(node.expression) && node.expression.text === 'require'))) return node.arguments[0];
81
+ return null;
82
+ }
83
+
84
+ function inspectTransportNode({ ts, kit, checker, file, node, isClient, clientCalls, clients, owned, report }) {
85
+ const fetchUse = globalFetchUse(ts, kit, checker, node);
86
+ if (fetchUse !== null) {
87
+ if (isClient) clientCalls.value += 1;
88
+ else if (fetchUse === 'property') report(file, node, `${node.getText(file.sourceFile)} reaches the global fetch outside the transport client; ${owned}`);
89
+ else report(file, node, `fetch is used outside the transport client (called, aliased or passed as a value); ${owned}`);
90
+ return false;
91
+ }
92
+ const specifier = moduleSpecifierOf(ts, node);
93
+ if (specifier && ts.isStringLiteralLike(specifier) && HTTP_LIBRARIES.has(specifier.text.split('/')[0])) {
94
+ report(file, node, `${specifier.text} is a second HTTP transport; the repository's one transport is built on fetch (${clients.length ? clients.map(item => item.rel).join(', ') : 'no client exists yet'}). Use the client.`, { library: specifier.text });
95
+ }
96
+ return true;
97
+ }
98
+
99
+ function inspectTransportFiles({ files, graph, kit, ts, clientRels, clients, owned, report }) {
78
100
  const appsWithFiles = new Set();
101
+ let readers = 0;
79
102
  for (const file of files) {
80
- const bindings = resolver.classifyPath(file.rel).bindings;
103
+ const bindings = kit.resolver.classifyPath(file.rel).bindings;
81
104
  if (bindings?.app) appsWithFiles.add(bindings.app);
82
105
  const checker = kit.checkerOf(file.sourceFile);
83
106
  const isClient = clientRels.has(file.rel);
84
- let clientCalls = 0;
85
- kit.walk(file.sourceFile, node => {
86
- if (ts.isPropertyAccessExpression(node) && isGlobalFetch(checker, node)) {
87
- if (isClient) clientCalls += 1;
88
- else report(file, node, `${node.getText(file.sourceFile)} reaches the global fetch outside the transport client; ${owned}`);
89
- return false;
90
- }
91
- if (ts.isIdentifier(node) && isGlobalFetch(checker, node)) {
92
- if (isClient) clientCalls += 1;
93
- else report(file, node, `fetch is used outside the transport client (called, aliased or passed as a value); ${owned}`);
94
- return false;
95
- }
96
- const specifier = ts.isImportDeclaration(node) || ts.isExportDeclaration(node) ? node.moduleSpecifier
97
- : ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword || (ts.isIdentifier(node.expression) && node.expression.text === 'require')) ? node.arguments[0] : null;
98
- if (specifier && ts.isStringLiteralLike(specifier) && HTTP_LIBRARIES.has(specifier.text.split('/')[0])) {
99
- report(file, node, `${specifier.text} is a second HTTP transport; the repository's one transport is built on fetch (${clients.length ? list(clients) : 'no client exists yet'}). Use the client.`, { library: specifier.text });
100
- }
101
- return true;
102
- });
103
- if (isClient && clientCalls === 0) report(file, file.sourceFile, `${file.rel} never calls the global fetch; the client is the one module that owns fetch, with its timeout and abort signal.`);
107
+ const clientCalls = { value: 0 };
108
+ kit.walk(file.sourceFile, node => inspectTransportNode({ ts, kit, checker, file, node, isClient, clientCalls, clients, owned, report }));
109
+ if (isClient && clientCalls.value === 0) report(file, file.sourceFile, `${file.rel} never calls the global fetch; the client is the one module that owns fetch, with its timeout and abort signal.`);
104
110
  if (file.slot === API_SLOT && bindings?.app && path.posix.basename(file.rel).startsWith('read-')) {
105
111
  readers += 1;
106
112
  const reachesClient = graph.edges.some(edge => edge.from === file.rel && edge.runtime
@@ -108,5 +114,26 @@ export function checkTransportOwner(input) {
108
114
  if (!reachesClient) report(file, file.sourceFile, `${file.rel} is a server reader that does not import the transport client; a reader fetches through the repository's one client, never through its own transport.`);
109
115
  }
110
116
  }
117
+ return { appsWithFiles, readers };
118
+ }
119
+
120
+ export function checkTransportOwner(input) {
121
+ const { graph } = input;
122
+ const kit = machineKit(input);
123
+ const { ts, resolver } = kit;
124
+ const violations = [];
125
+ const report = (file, node, message, extra = {}) => violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
126
+ const nextApps = resolver.repo.apps.filter(item => item.kind === 'next');
127
+ const files = [...graph.files.values()].filter(file => file.slot && file.tier !== 'e2e');
128
+ const clients = files.filter(file => CLIENT_SLOTS.has(file.slot));
129
+ const outcomes = files.filter(file => resolver.slot(file.slot)?.outcomeHome === true);
130
+ const clientRels = new Set(clients.map(file => file.rel));
131
+ const list = items => items.map(item => item.rel).join(', ');
132
+ const owned = clients.length ? `the repository has one transport (${list(clients)}). Call the client and take its Outcome.` : NO_CLIENT;
133
+
134
+ // Exactly one client and one Outcome union per repository.
135
+ for (const [items, noun] of [[clients, 'transport client'], [outcomes, 'Outcome union']]) reportMultiplicity(items, noun, nextApps, report, list);
136
+
137
+ const { appsWithFiles, readers } = inspectTransportFiles({ files, graph, kit, ts, clientRels, clients, owned, report });
111
138
  return { violations, coverage: { status: 'checked', apps: appsWithFiles.size, readers, clients: clients.length, outcomes: outcomes.length } };
112
139
  }