@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
@@ -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,12 +172,151 @@ 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.
@@ -187,24 +327,17 @@ export function checkArchitecture({ repositoryRoot, injectedTypeScript, paths =
187
327
  try {
188
328
  config = loadArchitectureConfig(repositoryRoot, { hfs: openedHfs });
189
329
  } 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 };
330
+ return invalidConfigurationResult(repositoryRoot, error);
194
331
  }
195
332
  const hfs = checkHfs(config);
196
333
  let context;
197
334
  try {
198
335
  context = buildTypeScriptContext(config, injectedTypeScript, paths);
199
336
  } 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 };
337
+ return compilerFailureResult(config, hfs, error);
205
338
  }
206
339
  const violations = [...hfs.violations];
207
- const configUnread = checkConfigUnread({ config, context });
340
+ const configUnread = checkConfigUnread({ context });
208
341
  violations.push(...configUnread.violations);
209
342
  let moduleRegistration = { status: 'not-applicable' };
210
343
  let backendSourceShape = { status: 'not-applicable' };
@@ -212,68 +345,25 @@ export function checkArchitecture({ repositoryRoot, injectedTypeScript, paths =
212
345
  let frontendDataLifecycle = { status: 'not-applicable' };
213
346
  if (context.program) violations.push(...checkOwners(config, context));
214
347
  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;
348
+ const backend = runBackendChecks(config, context, paths, injectedTypeScript, violations);
349
+ moduleRegistration = backend.moduleRegistration;
350
+ backendSourceShape = backend.backendSourceShape;
351
+ backendContractTypeForm = backend.backendContractTypeForm;
229
352
  }
230
353
  let frontendChecked = config.kinds.includes('frontend');
231
354
  if (context.program && config.kinds.includes('frontend')) {
232
355
  // A broken authored input (the framework-pinned root list in knowledge) is an error, never a
233
356
  // 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
- }
357
+ const frontend = runFrontendChecks(config, context, violations);
358
+ frontendChecked = frontend.frontendChecked;
359
+ frontendDataLifecycle = frontend.frontendDataLifecycle;
263
360
  }
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));
361
+ const hfsChecks = runHfsMachine(config, context, paths, injectedTypeScript, base, fast, violations);
362
+ const { errors, scopedViolations } = scopeFindings({ config, context, violations, paths, surface });
273
363
  const sourceFiles = new Set(context.files.map(file => canonical(file.fileName)));
274
364
  const missingOwnerEntries = config.owners?.filter(owner => !sourceFiles.has(canonical(path.resolve(config.root, ...owner.entry.split('/'))))) ?? [];
275
365
  const coverage = {
276
- sourceFiles: context.files.map(file => relativePath(config.root, canonical(file.fileName))).sort(),
366
+ sourceFiles: context.files.map(file => relativePath(config.root, canonical(file.fileName))).sort(byCodeUnit),
277
367
  hfs: hfs.coverage,
278
368
  configUnread: configUnread.coverage,
279
369
  backendContractTypeForm,
@@ -281,44 +371,10 @@ export function checkArchitecture({ repositoryRoot, injectedTypeScript, paths =
281
371
  frontendDataLifecycle,
282
372
  moduleRegistration,
283
373
  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' },
374
+ ownerPublicApi: ownerPublicApiOf(config.owners, missingOwnerEntries),
375
+ grammarContract: grammarContractOf(config),
295
376
  };
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();
377
+ coverage.checkedRuleIds = checkedRuleIdsFor(config, hfs, hfsChecks, frontendChecked, coverage);
322
378
  return {
323
379
  schema: 'starci/architecture-check@1',
324
380
  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 } };