@starci/hfs 4.0.9 → 4.1.0

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