@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
@@ -14,7 +14,7 @@ import { buildHfsGraph } from './hfs-graph.mjs';
14
14
  import { checkTiers, TIER_RULE_IDS } from './tiers.mjs';
15
15
  import { checkReachability, REACHABILITY_RULE_IDS } from './reachability.mjs';
16
16
  import { checkDeadExports, DEAD_EXPORT_RULE_IDS } from './dead-exports.mjs';
17
- import { checkRequiredFiles, REQUIRED_FILE_RULE_IDS } from './required-files.mjs';
17
+ import { checkRequiredFiles, REQUIRED_FILE_RULE_IDS, withTreeCache } from './required-files.mjs';
18
18
  import { checkClones, CLONE_RULE_IDS } from './clones.mjs';
19
19
  import { checkSymbols, SYMBOL_RULE_IDS } from './symbols.mjs';
20
20
  import { checkConnectionMap, CONNECTION_RULE_IDS } from './connection-map.mjs';
@@ -49,6 +49,7 @@ import { checkI18nKeys, I18N_KEYS_RULE_IDS } from './i18n-keys.mjs';
49
49
  import { checkDocLanguage, DOC_LANGUAGE_RULE_IDS } from './doc-language.mjs';
50
50
  import { checkBackendSupabase, checkFrontendSupabase, BACKEND_SUPABASE_RULE_IDS, FRONTEND_SUPABASE_RULE_IDS } from './supabase.mjs';
51
51
  import { LINT_CODES, onLintSurface } from './surface.mjs';
52
+ import { byCodeUnit } from '../../lib/list.mjs';
52
53
 
53
54
  export { REGISTRATION_RULE_IDS, SWR_DATA_RULE_IDS };
54
55
 
@@ -162,7 +163,7 @@ const machineIds = () => [
162
163
  export const HFS_MACHINE_RULE_IDS = Object.freeze((() => {
163
164
  const ordinary = new Set([...COMMON_RULE_IDS, ...BACKEND_RULE_IDS, ...FRONTEND_RULE_IDS, ...OWNER_RULE_IDS, ...GRAMMAR_RULE_IDS, ...REGISTRATION_RULE_IDS,
164
165
  ...SWR_DATA_RULE_IDS, SOURCE_LAYOUT_RULE_ID, SOURCE_NAME_RULE_ID, PUBLIC_CONTRACT_RULE_ID, READONLY_BOUNDARY_RULE_ID]);
165
- return [...new Set(machineIds().filter((id) => !ordinary.has(id)))].sort();
166
+ return [...new Set(machineIds().filter((id) => !ordinary.has(id)))].sort(byCodeUnit);
166
167
  })());
167
168
 
168
169
  /** Every code the machine can emit, derived from the rule id lists of its checks (`starci app check` ships exactly these why entries). */
@@ -171,40 +172,176 @@ export const ARCHITECTURE_RULE_IDS = Object.freeze([...new Set([
171
172
  ...DEAD_EXPORT_RULE_IDS, ...DOC_LANGUAGE_RULE_IDS, ...REQUIRED_FILE_RULE_IDS, ...CLONE_RULE_IDS, ...OWNER_RULE_IDS, ...GRAMMAR_RULE_IDS,
172
173
  ...REGISTRATION_RULE_IDS, ...SWR_DATA_RULE_IDS, ...SYMBOL_RULE_IDS, SOURCE_LAYOUT_RULE_ID, SOURCE_NAME_RULE_ID, PUBLIC_CONTRACT_RULE_ID,
173
174
  READONLY_BOUNDARY_RULE_ID, ...ERROR_RULE_IDS, ...CONFIG_UNREAD_RULE_IDS, ...Object.values(BACKEND_MACHINE).flatMap(([, ids]) => ids), ...Object.values(FRONTEND_MACHINE).flatMap(([, ids]) => ids),
174
- ])].sort());
175
+ ])].sort(byCodeUnit));
175
176
 
176
177
  function stable(items) {
177
178
  return items.sort((a, b) => `${a.path ?? ''}:${a.line ?? 0}:${a.column ?? 0}:${a.ruleId}`.localeCompare(`${b.path ?? ''}:${b.line ?? 0}:${b.column ?? 0}:${b.ruleId}`));
178
179
  }
179
180
 
181
+ const ownerPublicApiOf = (owners, missingEntries) => {
182
+ if (!owners.length) return { status: 'unavailable', reason: 'no slot owner instance with an entry file exists in the repository' };
183
+ if (missingEntries.length) return { status: 'unavailable', reason: 'one or more declared owner entries are outside the checked production TypeScript or JavaScript program',
184
+ missingEntries: missingEntries.map(owner => owner.entry).sort(byCodeUnit) };
185
+ return { status: 'checked', declarations: owners.length };
186
+ };
187
+
188
+ const grammarContractOf = config => {
189
+ if (!config.kinds.includes('frontend')) return { status: 'not-applicable' };
190
+ if (config.frontend.grammar) return { status: 'checked', package: config.frontend.grammar.package };
191
+ return { status: 'unavailable', reason: 'no app has a src/app/globals.css to judge against the Grammar style entry' };
192
+ };
193
+
194
+ function invalidConfigurationResult(repositoryRoot, error) {
195
+ const hfs = checkHfsWithoutConfig(repositoryRoot);
196
+ return { schema: 'starci/architecture-check@1', ok: false, repository: String(repositoryRoot ?? ''), kinds: [], files: 0,
197
+ compiler: null, violations: stable(hfs.violations), coverage: { hfs: hfs.coverage },
198
+ errors: [{ ruleId: /^(HFS_[A-Z_]+):/.exec(String(error.message))?.[1] ?? 'ARCH_CONFIG_INVALID', message: String(error.message ?? error).replace(/^HFS_[A-Z_]+:\s*/, '') }], limitations: LIMITATIONS };
199
+ }
200
+
201
+ function compilerFailureResult(config, hfs, error) {
202
+ const message = String(error.message ?? error);
203
+ const match = /^(ARCH_[A-Z_]+):\s*/.exec(message);
204
+ return { schema: 'starci/architecture-check@1', ok: false, repository: config.root, kinds: config.kinds, files: 0,
205
+ compiler: null, violations: hfs.violations, coverage: { hfs: hfs.coverage },
206
+ errors: [{ ruleId: match?.[1] ?? 'ARCH_COMPILER_FAILURE', message: message.replace(/^(ARCH_[A-Z_]+):\s*/, '') }], limitations: LIMITATIONS };
207
+ }
208
+
209
+ function runBackendChecks(config, context, paths, injectedTypeScript, violations) {
210
+ violations.push(...checkBackend(config, context));
211
+ // A slice may omit every app root. Registration ownership needs the complete
212
+ // composition graph even when the caller asks for findings in only one path.
213
+ const registrationContext = paths.length && config.backend.moduleRegistration
214
+ ? buildTypeScriptContext(config, injectedTypeScript) : context;
215
+ const registration = checkModuleRegistration(config, registrationContext);
216
+ violations.push(...registration.violations);
217
+ const sourceShape = checkBackendSourceShape(config, context);
218
+ violations.push(...sourceShape.violations);
219
+ const contracts = checkBackendContracts(config, context);
220
+ violations.push(...contracts.violations);
221
+ return { moduleRegistration: registration.coverage, backendSourceShape: sourceShape.coverage, backendContractTypeForm: contracts.coverage };
222
+ }
223
+
224
+ function runFrontendChecks(config, context, violations) {
225
+ let frontendChecked = true;
226
+ try {
227
+ violations.push(...checkFrontend(config, context));
228
+ } catch (error) {
229
+ const message = String(error.message ?? error);
230
+ const match = /^(ARCH_[A-Z_]+):\s*/.exec(message);
231
+ if (!match) throw error;
232
+ context.errors.push({ ruleId: match[1], message: message.slice(match[0].length) });
233
+ frontendChecked = false;
234
+ }
235
+ const dataLifecycle = checkFrontendDataLifecycle(config, context);
236
+ violations.push(...dataLifecycle.violations);
237
+ return { frontendChecked, frontendDataLifecycle: dataLifecycle.coverage };
238
+ }
239
+
240
+ function runHfsMachine(config, context, paths, injectedTypeScript, base, fast, violations) {
241
+ // HFS machine: the slot-driven graph checks read the whole program, also when the caller asked for one path.
242
+ const hfsContext = paths.length ? buildTypeScriptContext(config, injectedTypeScript) : context;
243
+ const hfsChecks = { tiers: null, reachability: null, deadExports: null, requiredFiles: null, clones: null, symbols: null, docLanguage: null };
244
+ if (!hfsContext.program) return hfsChecks;
245
+ const graph = buildHfsGraph(config, hfsContext);
246
+ const input = { config, context: hfsContext, graph, base };
247
+ const runs = { tiers: () => checkTiers(graph), reachability: () => checkReachability(input), deadExports: () => checkDeadExports(input),
248
+ requiredFiles: () => checkRequiredFiles(input), clones: () => checkClones(input), symbols: () => checkSymbols(input), docLanguage: () => checkDocLanguage(input) };
249
+ if (graph.profile === 'be') for (const [name, [check]] of Object.entries(BACKEND_MACHINE)) runs[name] = () => check(input);
250
+ if (graph.profile === 'fe') for (const [name, [check]] of Object.entries(FRONTEND_MACHINE)) runs[name] = () => check(input);
251
+ if (fast) { delete runs.deadExports; delete runs.clones; delete runs.symbols; }
252
+ for (const [name, run] of Object.entries(runs)) {
253
+ const result = run();
254
+ violations.push(...result.violations.map(violation => ({ ...violation, check: name })));
255
+ hfsChecks[name] = result.coverage;
256
+ }
257
+ return hfsChecks;
258
+ }
259
+
260
+ function hfsRuleIds(hfs) {
261
+ return hfs.coverage.status === 'checked' ? HFS_RULE_IDS : [];
262
+ }
263
+
264
+ function hfsCheckRuleIds(hfsChecks) {
265
+ return [
266
+ ...(hfsChecks.tiers ? TIER_RULE_IDS : []),
267
+ ...(hfsChecks.reachability?.status === 'checked' ? REACHABILITY_RULE_IDS : []),
268
+ ...(hfsChecks.deadExports?.status === 'checked' ? DEAD_EXPORT_RULE_IDS : []),
269
+ ...(hfsChecks.requiredFiles?.status === 'checked' ? REQUIRED_FILE_RULE_IDS : []),
270
+ ...(hfsChecks.clones?.status === 'checked' ? CLONE_RULE_IDS : []),
271
+ ...(hfsChecks.symbols?.status === 'checked' ? SYMBOL_RULE_IDS : []),
272
+ ...(hfsChecks.docLanguage?.status === 'checked' ? DOC_LANGUAGE_RULE_IDS : []),
273
+ ];
274
+ }
275
+
276
+ function derivedCoverageRuleIds(coverage) {
277
+ return [
278
+ ...(coverage.ownerPublicApi.status === 'checked' ? OWNER_RULE_IDS : []),
279
+ ...(coverage.grammarContract.status === 'checked' ? GRAMMAR_RULE_IDS : []),
280
+ ...(coverage.moduleRegistration.status === 'checked' ? REGISTRATION_RULE_IDS : []),
281
+ ...(coverage.backendSourceShape.layout?.status === 'checked' ? [SOURCE_LAYOUT_RULE_ID] : []),
282
+ ...(coverage.backendSourceShape.naming?.status === 'checked' ? [SOURCE_NAME_RULE_ID] : []),
283
+ ...(coverage.backendContractTypeForm.publicContracts?.status === 'checked' ? [PUBLIC_CONTRACT_RULE_ID] : []),
284
+ ...(coverage.backendContractTypeForm.readonlyBoundaries?.status === 'checked' ? [READONLY_BOUNDARY_RULE_ID] : []),
285
+ ];
286
+ }
287
+
288
+ function checkedRuleIdsFor(config, hfs, hfsChecks, frontendChecked, coverage) {
289
+ const machineRuleIds = Object.entries({ ...BACKEND_MACHINE, ...FRONTEND_MACHINE })
290
+ .flatMap(([name, [, ids]]) => (hfsChecks[name]?.status === 'checked' ? ids : []));
291
+ const checked = [...new Set([
292
+ ...COMMON_RULE_IDS,
293
+ ...hfsRuleIds(hfs),
294
+ ...(config.kinds.includes('backend') ? BACKEND_RULE_IDS : []),
295
+ ...hfsCheckRuleIds(hfsChecks),
296
+ ...CONFIG_UNREAD_RULE_IDS,
297
+ ...machineRuleIds,
298
+ ...(frontendChecked ? FRONTEND_RULE_IDS : []),
299
+ ...derivedCoverageRuleIds(coverage),
300
+ ...(config.kinds.includes('frontend') && ['checked', 'not-applicable'].includes(coverage.frontendDataLifecycle.status) ? SWR_DATA_RULE_IDS : []),
301
+ ])];
302
+ return checked.filter(id => !(id.startsWith('FE_') && !config.kinds.includes('frontend')) && !(id.startsWith('BE_') && !config.kinds.includes('backend')))
303
+ .sort(byCodeUnit);
304
+ }
305
+
306
+ /** The errors and the violations a surface reports: findings under the requested paths, on the requested surface, in stable order. */
307
+ function scopeFindings({ config, context, violations, paths, surface }) {
308
+ const inScope = item => !paths.length || (item.path && paths.some(prefix => sameOrUnder(item.path, prefix.replace(/\/$/, ''))));
309
+ // Two findings travel as context errors (package boundary edges); they are obligations of the lint surface like any other finding.
310
+ const asViolation = item => LINT_CODES.has(item.ruleId);
311
+ const allErrors = context.errors.filter(item => item.ruleId.startsWith('ARCH_TSCONFIG_') || !item.path || inScope(item));
312
+ const obligations = surface === 'all' ? violations : [...violations, ...context.errors.filter(asViolation).filter(inScope)];
313
+ // The lint surface is what an editor can show on a line of a TypeScript file; `starci app check` keeps every other finding of the machine.
314
+ const onSurface = item => surface === 'all' || onLintSurface(config.root, item) === (surface === 'lint');
315
+ const errors = stable(surface === 'all' ? allErrors : allErrors.filter(item => !asViolation(item)));
316
+ const scopedViolations = stable(obligations.filter(inScope).filter(onSurface));
317
+ return { errors, scopedViolations };
318
+ }
319
+
180
320
  /**
181
321
  * Check a target repository. injectedTypeScript exists only for hermetic rule fixtures. `fast` leaves out the checks
182
322
  * that read the whole repository to answer (clones, dead exports, repository-wide symbols); the pre-push check of the changed owners uses it.
183
323
  */
184
- export function checkArchitecture({ repositoryRoot, injectedTypeScript, paths = [], base, fast = false, hfs: openedHfs, surface = 'all' } = {}) {
324
+ export function checkArchitecture(options = {}) {
325
+ return withTreeCache(() => checkArchitectureOnce(options));
326
+ }
327
+
328
+ function checkArchitectureOnce({ repositoryRoot, injectedTypeScript, paths = [], base, fast = false, hfs: openedHfs, surface = 'all' }) {
185
329
  if (!['all', 'lint', 'check'].includes(surface)) throw new Error(`unknown surface ${surface}`);
186
330
  let config;
187
331
  try {
188
332
  config = loadArchitectureConfig(repositoryRoot, { hfs: openedHfs });
189
333
  } catch (error) {
190
- const hfs = checkHfsWithoutConfig(repositoryRoot);
191
- return { schema: 'starci/architecture-check@1', ok: false, repository: String(repositoryRoot ?? ''), kinds: [], files: 0,
192
- compiler: null, violations: stable(hfs.violations), coverage: { hfs: hfs.coverage },
193
- errors: [{ ruleId: /^(HFS_[A-Z_]+):/.exec(String(error.message))?.[1] ?? 'ARCH_CONFIG_INVALID', message: String(error.message ?? error).replace(/^HFS_[A-Z_]+:\s*/, '') }], limitations: LIMITATIONS };
334
+ return invalidConfigurationResult(repositoryRoot, error);
194
335
  }
195
336
  const hfs = checkHfs(config);
196
337
  let context;
197
338
  try {
198
339
  context = buildTypeScriptContext(config, injectedTypeScript, paths);
199
340
  } catch (error) {
200
- const message = String(error.message ?? error);
201
- const match = /^(ARCH_[A-Z_]+):\s*/.exec(message);
202
- return { schema: 'starci/architecture-check@1', ok: false, repository: config.root, kinds: config.kinds, files: 0,
203
- compiler: null, violations: hfs.violations, coverage: { hfs: hfs.coverage },
204
- errors: [{ ruleId: match?.[1] ?? 'ARCH_COMPILER_FAILURE', message: message.replace(/^(ARCH_[A-Z_]+):\s*/, '') }], limitations: LIMITATIONS };
341
+ return compilerFailureResult(config, hfs, error);
205
342
  }
206
343
  const violations = [...hfs.violations];
207
- const configUnread = checkConfigUnread({ config, context });
344
+ const configUnread = checkConfigUnread({ context });
208
345
  violations.push(...configUnread.violations);
209
346
  let moduleRegistration = { status: 'not-applicable' };
210
347
  let backendSourceShape = { status: 'not-applicable' };
@@ -212,68 +349,25 @@ export function checkArchitecture({ repositoryRoot, injectedTypeScript, paths =
212
349
  let frontendDataLifecycle = { status: 'not-applicable' };
213
350
  if (context.program) violations.push(...checkOwners(config, context));
214
351
  if (context.program && config.kinds.includes('backend')) {
215
- violations.push(...checkBackend(config, context));
216
- // A slice may omit every app root. Registration ownership needs the complete
217
- // composition graph even when the caller asks for findings in only one path.
218
- const registrationContext = paths.length && config.backend.moduleRegistration
219
- ? buildTypeScriptContext(config, injectedTypeScript) : context;
220
- const registration = checkModuleRegistration(config, registrationContext);
221
- violations.push(...registration.violations);
222
- moduleRegistration = registration.coverage;
223
- const sourceShape = checkBackendSourceShape(config, context);
224
- violations.push(...sourceShape.violations);
225
- backendSourceShape = sourceShape.coverage;
226
- const contracts = checkBackendContracts(config, context);
227
- violations.push(...contracts.violations);
228
- backendContractTypeForm = contracts.coverage;
352
+ const backend = runBackendChecks(config, context, paths, injectedTypeScript, violations);
353
+ moduleRegistration = backend.moduleRegistration;
354
+ backendSourceShape = backend.backendSourceShape;
355
+ backendContractTypeForm = backend.backendContractTypeForm;
229
356
  }
230
357
  let frontendChecked = config.kinds.includes('frontend');
231
358
  if (context.program && config.kinds.includes('frontend')) {
232
359
  // A broken authored input (the framework-pinned root list in knowledge) is an error, never a
233
360
  // silently different contract: the frontend rules are then not reported as checked.
234
- try {
235
- violations.push(...checkFrontend(config, context));
236
- } catch (error) {
237
- const message = String(error.message ?? error);
238
- const match = /^(ARCH_[A-Z_]+):\s*/.exec(message);
239
- if (!match) throw error;
240
- context.errors.push({ ruleId: match[1], message: message.slice(match[0].length) });
241
- frontendChecked = false;
242
- }
243
- const dataLifecycle = checkFrontendDataLifecycle(config, context);
244
- violations.push(...dataLifecycle.violations);
245
- frontendDataLifecycle = dataLifecycle.coverage;
246
- }
247
- // HFS machine: the slot-driven graph checks read the whole program, also when the caller asked for one path.
248
- const hfsContext = paths.length ? buildTypeScriptContext(config, injectedTypeScript) : context;
249
- const hfsChecks = { tiers: null, reachability: null, deadExports: null, requiredFiles: null, clones: null, symbols: null, docLanguage: null };
250
- if (hfsContext.program) {
251
- const graph = buildHfsGraph(config, hfsContext);
252
- const input = { config, context: hfsContext, graph, base };
253
- const runs = { tiers: () => checkTiers(graph), reachability: () => checkReachability(input), deadExports: () => checkDeadExports(input),
254
- requiredFiles: () => checkRequiredFiles(input), clones: () => checkClones(input), symbols: () => checkSymbols(input), docLanguage: () => checkDocLanguage(input) };
255
- if (graph.profile === 'be') for (const [name, [check]] of Object.entries(BACKEND_MACHINE)) runs[name] = () => check(input);
256
- if (graph.profile === 'fe') for (const [name, [check]] of Object.entries(FRONTEND_MACHINE)) runs[name] = () => check(input);
257
- if (fast) { delete runs.deadExports; delete runs.clones; delete runs.symbols; }
258
- for (const [name, run] of Object.entries(runs)) {
259
- const result = run();
260
- violations.push(...result.violations.map(violation => ({ ...violation, check: name })));
261
- hfsChecks[name] = result.coverage;
262
- }
361
+ const frontend = runFrontendChecks(config, context, violations);
362
+ frontendChecked = frontend.frontendChecked;
363
+ frontendDataLifecycle = frontend.frontendDataLifecycle;
263
364
  }
264
- const inScope = item => !paths.length || (item.path && paths.some(prefix => sameOrUnder(item.path, prefix.replace(/\/$/, ''))));
265
- // Two findings travel as context errors (package boundary edges); they are obligations of the lint surface like any other finding.
266
- const asViolation = item => LINT_CODES.has(item.ruleId);
267
- const allErrors = context.errors.filter(item => item.ruleId.startsWith('ARCH_TSCONFIG_') || !item.path || inScope(item));
268
- const obligations = surface === 'all' ? violations : [...violations, ...context.errors.filter(asViolation).filter(inScope)];
269
- // The lint surface is what an editor can show on a line of a TypeScript file; `starci app check` keeps every other finding of the machine.
270
- const onSurface = item => surface === 'all' || onLintSurface(config.root, item) === (surface === 'lint');
271
- const errors = stable(surface === 'all' ? allErrors : allErrors.filter(item => !asViolation(item)));
272
- const scopedViolations = stable(obligations.filter(inScope).filter(onSurface));
365
+ const hfsChecks = runHfsMachine(config, context, paths, injectedTypeScript, base, fast, violations);
366
+ const { errors, scopedViolations } = scopeFindings({ config, context, violations, paths, surface });
273
367
  const sourceFiles = new Set(context.files.map(file => canonical(file.fileName)));
274
368
  const missingOwnerEntries = config.owners?.filter(owner => !sourceFiles.has(canonical(path.resolve(config.root, ...owner.entry.split('/'))))) ?? [];
275
369
  const coverage = {
276
- sourceFiles: context.files.map(file => relativePath(config.root, canonical(file.fileName))).sort(),
370
+ sourceFiles: context.files.map(file => relativePath(config.root, canonical(file.fileName))).sort(byCodeUnit),
277
371
  hfs: hfs.coverage,
278
372
  configUnread: configUnread.coverage,
279
373
  backendContractTypeForm,
@@ -281,44 +375,10 @@ export function checkArchitecture({ repositoryRoot, injectedTypeScript, paths =
281
375
  frontendDataLifecycle,
282
376
  moduleRegistration,
283
377
  hfsMachine: hfsChecks,
284
- ownerPublicApi: !config.owners.length
285
- ? { status: 'unavailable', reason: 'no slot owner instance with an entry file exists in the repository' }
286
- : missingOwnerEntries.length
287
- ? { status: 'unavailable', reason: 'one or more declared owner entries are outside the checked production TypeScript or JavaScript program',
288
- missingEntries: missingOwnerEntries.map(owner => owner.entry).sort() }
289
- : { status: 'checked', declarations: config.owners.length },
290
- grammarContract: !config.kinds.includes('frontend')
291
- ? { status: 'not-applicable' }
292
- : config.frontend.grammar
293
- ? { status: 'checked', package: config.frontend.grammar.package }
294
- : { status: 'unavailable', reason: 'no app has a src/app/globals.css to judge against the Grammar style entry' },
378
+ ownerPublicApi: ownerPublicApiOf(config.owners, missingOwnerEntries),
379
+ grammarContract: grammarContractOf(config),
295
380
  };
296
- coverage.checkedRuleIds = [...new Set([
297
- ...COMMON_RULE_IDS,
298
- ...(hfs.coverage.status === 'checked' ? HFS_RULE_IDS : []),
299
- ...(config.kinds.includes('backend') ? BACKEND_RULE_IDS : []),
300
- ...(hfsChecks.tiers ? TIER_RULE_IDS : []),
301
- ...(hfsChecks.reachability?.status === 'checked' ? REACHABILITY_RULE_IDS : []),
302
- ...(hfsChecks.deadExports?.status === 'checked' ? DEAD_EXPORT_RULE_IDS : []),
303
- ...(hfsChecks.requiredFiles?.status === 'checked' ? REQUIRED_FILE_RULE_IDS : []),
304
- ...(hfsChecks.clones?.status === 'checked' ? CLONE_RULE_IDS : []),
305
- ...(hfsChecks.symbols?.status === 'checked' ? SYMBOL_RULE_IDS : []),
306
- ...(hfsChecks.docLanguage?.status === 'checked' ? DOC_LANGUAGE_RULE_IDS : []),
307
- ...CONFIG_UNREAD_RULE_IDS,
308
- ...Object.entries({ ...BACKEND_MACHINE, ...FRONTEND_MACHINE }).flatMap(([name, [, ids]]) => (hfsChecks[name]?.status === 'checked' ? ids : [])),
309
- ...(frontendChecked ? FRONTEND_RULE_IDS : []),
310
- ...(coverage.ownerPublicApi.status === 'checked' ? OWNER_RULE_IDS : []),
311
- ...(coverage.grammarContract.status === 'checked' ? GRAMMAR_RULE_IDS : []),
312
- ...(coverage.moduleRegistration.status === 'checked' ? REGISTRATION_RULE_IDS : []),
313
- ...(coverage.backendSourceShape.layout?.status === 'checked' ? [SOURCE_LAYOUT_RULE_ID] : []),
314
- ...(coverage.backendSourceShape.naming?.status === 'checked' ? [SOURCE_NAME_RULE_ID] : []),
315
- ...(coverage.backendContractTypeForm.publicContracts?.status === 'checked' ? [PUBLIC_CONTRACT_RULE_ID] : []),
316
- ...(coverage.backendContractTypeForm.readonlyBoundaries?.status === 'checked' ? [READONLY_BOUNDARY_RULE_ID] : []),
317
- ...(config.kinds.includes('frontend') && ['checked', 'not-applicable'].includes(coverage.frontendDataLifecycle.status) ? SWR_DATA_RULE_IDS : []),
318
- ])]
319
- // A back-end repository checked no FE_ rule and a front-end one no BE_ rule, whatever list a shared check (tiers, required files) carries.
320
- .filter(id => !(id.startsWith('FE_') && !config.kinds.includes('frontend')) && !(id.startsWith('BE_') && !config.kinds.includes('backend')))
321
- .sort();
381
+ coverage.checkedRuleIds = checkedRuleIdsFor(config, hfs, hfsChecks, frontendChecked, coverage);
322
382
  return {
323
383
  schema: 'starci/architecture-check@1',
324
384
  ok: errors.length === 0 && scopedViolations.length === 0,
@@ -15,6 +15,37 @@ export const INJECTION_TOKEN_EXPORTED_RULE_IDS = ['BE_RAW_INJECT'];
15
15
 
16
16
  const RULE = 'BE_RAW_INJECT';
17
17
 
18
+ const exportedVariableHasName = (ts, kit, statement, name) => {
19
+ if (!ts.isVariableStatement(statement) || !kit.isExported(statement)) return false;
20
+ return statement.declarationList.declarations.some(declaration =>
21
+ (ts.isIdentifier(declaration.name) && declaration.name.text === name)
22
+ || (ts.isObjectBindingPattern(declaration.name) && declaration.name.elements.some(element => ts.isIdentifier(element.name) && element.name.text === name)));
23
+ };
24
+
25
+ const exportedDeclarationHasName = (ts, statement, name) => {
26
+ if (!ts.isExportDeclaration(statement)) return false;
27
+ const clause = statement.exportClause;
28
+ return clause !== undefined && ts.isNamedExports(clause) && clause.elements.some(element => element.name.text === name);
29
+ };
30
+
31
+ const exportsName = (ts, kit, file, name) => file?.sourceFile.statements.some(statement =>
32
+ exportedVariableHasName(ts, kit, statement, name) || exportedDeclarationHasName(ts, statement, name)) === true;
33
+
34
+ const indexOf = (graph, file) => (file?.owner ? graph.files.get(`${file.owner.root}/index.ts`) : null);
35
+
36
+ const injectionViolationFor = (graph, kit, ts, dependency) => {
37
+ const decoratorFile = graph.files.get(kit.graphPath(dependency.decoratorDeclaration));
38
+ if (dependency.kind === 'unresolved') {
39
+ return decoratorFile ? { ruleId: RULE, path: decoratorFile.rel, ...kit.at(decoratorFile.rel, decoratorFile.sourceFile, dependency.injectorCall), decorator: dependency.decorator,
40
+ message: `${dependency.decorator}() cannot be followed to its token (${dependency.reason}); name an exported \`unique symbol\` const so a unit spec can provide it.` } : null;
41
+ }
42
+ const tokenFile = dependency.declaration ? graph.files.get(kit.graphPath(dependency.declaration)) : null;
43
+ const homes = [decoratorFile, tokenFile && path.posix.basename(tokenFile.rel).endsWith('.decorators.ts') ? tokenFile : null, indexOf(graph, decoratorFile), indexOf(graph, tokenFile)];
44
+ if (!decoratorFile || homes.some(home => exportsName(ts, kit, home, dependency.name))) return null;
45
+ return { ruleId: RULE, path: decoratorFile.rel, ...kit.at(decoratorFile.rel, decoratorFile.sourceFile, dependency.injectorCall), decorator: dependency.decorator, token: dependency.name,
46
+ message: `${dependency.decorator}() injects ${dependency.name}, which is not exported from ${path.posix.basename(decoratorFile.rel)} nor from the capability index.ts; export the token so a unit spec can provide it (\`{ provide: ${dependency.name}, useValue: double }\`).` };
47
+ };
48
+
18
49
  export function checkInjectionTokenExported(input) {
19
50
  const { graph } = input;
20
51
  const kit = machineKit(input);
@@ -23,39 +54,13 @@ export function checkInjectionTokenExported(input) {
23
54
  const seen = new Set();
24
55
  let decorators = 0;
25
56
 
26
- const exportsName = (file, name) => {
27
- if (!file) return false;
28
- for (const statement of file.sourceFile.statements) {
29
- if (ts.isVariableStatement(statement) && kit.isExported(statement)) {
30
- for (const declaration of statement.declarationList.declarations) {
31
- if (ts.isIdentifier(declaration.name) && declaration.name.text === name) return true;
32
- if (ts.isObjectBindingPattern(declaration.name) && declaration.name.elements.some(element => ts.isIdentifier(element.name) && element.name.text === name)) return true;
33
- }
34
- }
35
- if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)
36
- && statement.exportClause.elements.some(element => element.name.text === name)) return true;
37
- }
38
- return false;
39
- };
40
- const indexOf = file => (file?.owner ? graph.files.get(`${file.owner.root}/index.ts`) : null);
41
-
42
57
  for (const { file, declaration } of serviceClasses(kit, graph)) {
43
58
  for (const dependency of constructorDependencies(kit, file, declaration)) {
44
59
  if (dependency.external || !dependency.decoratorDeclaration || !dependency.injectorCall || seen.has(dependency.injectorCall)) continue;
45
60
  seen.add(dependency.injectorCall);
46
61
  decorators += 1;
47
- const decoratorFile = graph.files.get(kit.graphPath(dependency.decoratorDeclaration));
48
- if (dependency.kind === 'unresolved') {
49
- if (decoratorFile) violations.push({ ruleId: RULE, path: decoratorFile.rel, ...kit.at(decoratorFile.rel, decoratorFile.sourceFile, dependency.injectorCall), decorator: dependency.decorator,
50
- message: `${dependency.decorator}() cannot be followed to its token (${dependency.reason}); name an exported \`unique symbol\` const so a unit spec can provide it.` });
51
- continue;
52
- }
53
- const tokenFile = dependency.declaration ? graph.files.get(kit.graphPath(dependency.declaration)) : null;
54
- const homes = [decoratorFile, tokenFile && path.posix.basename(tokenFile.rel).endsWith('.decorators.ts') ? tokenFile : null, indexOf(decoratorFile), indexOf(tokenFile)];
55
- if (homes.some(home => exportsName(home, dependency.name))) continue;
56
- if (!decoratorFile) continue;
57
- violations.push({ ruleId: RULE, path: decoratorFile.rel, ...kit.at(decoratorFile.rel, decoratorFile.sourceFile, dependency.injectorCall), decorator: dependency.decorator, token: dependency.name,
58
- message: `${dependency.decorator}() injects ${dependency.name}, which is not exported from ${path.posix.basename(decoratorFile.rel)} nor from the capability index.ts; export the token so a unit spec can provide it (\`{ provide: ${dependency.name}, useValue: double }\`).` });
62
+ const violation = injectionViolationFor(graph, kit, ts, dependency);
63
+ if (violation) violations.push(violation);
59
64
  }
60
65
  }
61
66
  return { violations, coverage: { status: 'checked', decorators, files: graph.files.size } };