@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
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { machineKit } from './machine-ast.mjs';
4
4
  import { constructorDependencies } from './constructor-deps.mjs';
5
5
  import { treeOf } from './required-files.mjs';
6
+ import { byCodeUnit } from '../../lib/list.mjs';
6
7
 
7
8
  /**
8
9
  * R48 `unit-spec-providers` (BE_SPEC_QUALITY), the unit test standard's rule 3: the unit spec of a service builds its
@@ -22,7 +23,6 @@ import { treeOf } from './required-files.mjs';
22
23
  export const UNIT_SPEC_PROVIDERS_RULE_IDS = ['BE_SPEC_QUALITY'];
23
24
 
24
25
  const RULE = 'BE_SPEC_QUALITY';
25
- const SERVICE_SPEC = /\.service\.spec\.ts$/u;
26
26
  const SPEC_ROOT = /^(?:src|apps)\//u;
27
27
 
28
28
  /** local name -> {name, module} for every named import of a source file. */
@@ -49,6 +49,87 @@ function testingModules(ts, sourceFile) {
49
49
 
50
50
  const literalKey = (ts, property) => (ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) && (ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name)) ? property.name.text : null;
51
51
 
52
+ function providerList(ts, call, rel, report) {
53
+ const options = call.arguments[0];
54
+ const property = options && ts.isObjectLiteralExpression(options) ? options.properties.find(item => literalKey(ts, item) === 'providers') : null;
55
+ const list = property && ts.isPropertyAssignment(property) && ts.isArrayLiteralExpression(property.initializer) ? property.initializer : null;
56
+ if (!list) report(call, `${rel}: Test.createTestingModule has no literal \`providers\` array; list the service and each constructor dependency there.`);
57
+ return list;
58
+ }
59
+
60
+ function readProviders(ts, list, rel, nameOf, report) {
61
+ const provided = [];
62
+ let readable = true;
63
+ for (const element of list.elements) {
64
+ if (ts.isIdentifier(element)) { provided.push({ name: nameOf(element), node: element, isClass: true }); continue; }
65
+ if (ts.isObjectLiteralExpression(element)) {
66
+ const keys = element.properties.map(item => literalKey(ts, item));
67
+ const provide = element.properties.find(item => literalKey(ts, item) === 'provide');
68
+ const value = provide && ts.isPropertyAssignment(provide) ? provide.initializer : null;
69
+ if (keys.length === 2 && keys.includes('useValue') && value && ts.isIdentifier(value)) { provided.push({ name: nameOf(value), node: element, isClass: false }); continue; }
70
+ }
71
+ readable = false;
72
+ report(element, `${rel}: a provider is a class or \`{ provide: TOKEN, useValue: double }\` and nothing else (no useClass, useFactory, useExisting, spread or computed token).`);
73
+ }
74
+ return { provided, readable };
75
+ }
76
+
77
+ function serviceClassFor(ts, rel, serviceRel, serviceFile, imports, list, report) {
78
+ const first = list.elements[0];
79
+ const serviceName = first && ts.isIdentifier(first) ? imports.get(first.text) : null;
80
+ const target = serviceName ? path.posix.normalize(path.posix.join(path.posix.dirname(rel), serviceName.module)).replace(/\.[cm]?[jt]s$/u, '') : null;
81
+ const serviceClass = serviceName && target === serviceRel.replace(/\.ts$/u, '')
82
+ ? serviceFile.sourceFile.statements.find(statement => ts.isClassDeclaration(statement) && statement.name?.text === serviceName.name) : null;
83
+ if (!serviceClass) report(first ?? list, `${rel}: the first provider is the service under test, imported from ./${path.posix.basename(serviceRel, '.ts')}.`);
84
+ return serviceClass ? { serviceClass, serviceName } : null;
85
+ }
86
+
87
+ function checkDependencies(kit, rel, service, serviceFile, list, provided, report) {
88
+ const { serviceName, serviceClass } = service;
89
+ const dependencies = constructorDependencies(kit, serviceFile, serviceClass);
90
+ const expected = [];
91
+ for (const dependency of dependencies) {
92
+ if (dependency.kind === 'unresolved') {
93
+ report(list, `${rel}: a constructor dependency of ${serviceName.name} cannot be resolved to a token (${dependency.reason}), so its provider cannot be checked; inject it through an exported Inject<Thing>() decorator or a class type.`);
94
+ continue;
95
+ }
96
+ expected.push(dependency.name);
97
+ }
98
+ const remaining = [...expected];
99
+ for (const item of provided.slice(1)) {
100
+ const index = remaining.indexOf(item.name);
101
+ if (index === -1) report(item.node, `${rel}: ${item.name} is provided but is not a constructor dependency of ${serviceName.name}; a unit spec provides exactly what the constructor needs.`);
102
+ else remaining.splice(index, 1);
103
+ }
104
+ for (const name of remaining) report(list, `${rel}: ${name} is a constructor dependency of ${serviceName.name} but is not provided; add \`{ provide: ${name}, useValue: double }\` (or the class itself) to the providers.`);
105
+ }
106
+
107
+ function checkTestingModule({ ts, kit, rel, serviceRel, serviceFile, imports, call, report }) {
108
+ const list = providerList(ts, call, rel, report);
109
+ if (!list) return;
110
+ const nameOf = identifier => imports.get(identifier.text)?.name ?? identifier.text;
111
+ const { provided, readable } = readProviders(ts, list, rel, nameOf, report);
112
+ if (!readable) return;
113
+ const service = serviceClassFor(ts, rel, serviceRel, serviceFile, imports, list, report);
114
+ if (!service) return;
115
+ checkDependencies(kit, rel, service, serviceFile, list, provided, report);
116
+ }
117
+
118
+ function checkServiceSpec({ config, graph, kit, ts, rel, violations }) {
119
+ const abs = path.join(config.root, ...rel.split('/'));
120
+ if (!fs.existsSync(abs)) return false;
121
+ const sourceFile = ts.createSourceFile(abs, fs.readFileSync(abs, 'utf8'), ts.ScriptTarget.Latest, true);
122
+ const report = (node, message) => violations.push({ ruleId: RULE, path: rel, ...kit.at(rel, sourceFile, node), message });
123
+ const imports = importsOf(ts, sourceFile);
124
+ const serviceRel = rel.replace(/\.spec\.ts$/u, '.ts');
125
+ const serviceFile = graph.files.get(serviceRel);
126
+ if (!serviceFile) { report(sourceFile, `${rel} has no ${path.posix.basename(serviceRel)} beside it; a service spec sits next to the service it tests.`); return true; }
127
+ const modules = testingModules(ts, sourceFile);
128
+ if (!modules.length) { report(sourceFile, `${rel} never calls Test.createTestingModule({ providers: [...] }); build the service from a testing module holding exactly its constructor dependencies.`); return true; }
129
+ for (const call of modules) checkTestingModule({ ts, kit, rel, serviceRel, serviceFile, imports, call, report });
130
+ return true;
131
+ }
132
+
52
133
  export function checkUnitSpecProviders(input) {
53
134
  const { config, graph } = input;
54
135
  const kit = machineKit(input);
@@ -56,67 +137,8 @@ export function checkUnitSpecProviders(input) {
56
137
  const violations = [];
57
138
  let specs = 0;
58
139
  const tree = treeOf(config.root);
59
- for (const rel of [...tree.files].filter(file => SPEC_ROOT.test(file) && SERVICE_SPEC.test(file)).sort()) {
60
- const abs = path.join(config.root, ...rel.split('/'));
61
- if (!fs.existsSync(abs)) continue;
62
- specs += 1;
63
- const sourceFile = ts.createSourceFile(abs, fs.readFileSync(abs, 'utf8'), ts.ScriptTarget.Latest, true);
64
- const report = (node, message) => violations.push({ ruleId: RULE, path: rel, ...kit.at(rel, sourceFile, node), message });
65
- const imports = importsOf(ts, sourceFile);
66
- const nameOf = identifier => imports.get(identifier.text)?.name ?? identifier.text;
67
-
68
- const serviceRel = rel.replace(/\.spec\.ts$/u, '.ts');
69
- const serviceFile = graph.files.get(serviceRel);
70
- if (!serviceFile) { report(sourceFile, `${rel} has no ${path.posix.basename(serviceRel)} beside it; a service spec sits next to the service it tests.`); continue; }
71
-
72
- const modules = testingModules(ts, sourceFile);
73
- if (!modules.length) { report(sourceFile, `${rel} never calls Test.createTestingModule({ providers: [...] }); build the service from a testing module holding exactly its constructor dependencies.`); continue; }
74
-
75
- for (const call of modules) {
76
- const options = call.arguments[0];
77
- const property = options && ts.isObjectLiteralExpression(options) ? options.properties.find(item => literalKey(ts, item) === 'providers') : null;
78
- const list = property && ts.isPropertyAssignment(property) && ts.isArrayLiteralExpression(property.initializer) ? property.initializer : null;
79
- if (!list) { report(call, `${rel}: Test.createTestingModule has no literal \`providers\` array; list the service and each constructor dependency there.`); continue; }
80
-
81
- const provided = []; // [{name, node}]
82
- let readable = true;
83
- for (const element of list.elements) {
84
- if (ts.isIdentifier(element)) { provided.push({ name: nameOf(element), node: element, isClass: true }); continue; }
85
- if (ts.isObjectLiteralExpression(element)) {
86
- const keys = element.properties.map(item => literalKey(ts, item));
87
- const provide = element.properties.find(item => literalKey(ts, item) === 'provide');
88
- const value = provide && ts.isPropertyAssignment(provide) ? provide.initializer : null;
89
- if (keys.length === 2 && keys.includes('useValue') && value && ts.isIdentifier(value)) { provided.push({ name: nameOf(value), node: element, isClass: false }); continue; }
90
- }
91
- readable = false;
92
- report(element, `${rel}: a provider is a class or \`{ provide: TOKEN, useValue: double }\` and nothing else (no useClass, useFactory, useExisting, spread or computed token).`);
93
- }
94
- if (!readable) continue;
95
-
96
- const first = list.elements[0];
97
- const serviceName = first && ts.isIdentifier(first) ? imports.get(first.text) : null;
98
- const target = serviceName ? path.posix.normalize(path.posix.join(path.posix.dirname(rel), serviceName.module)).replace(/\.[cm]?[jt]s$/u, '') : null;
99
- const serviceClass = serviceName && target === serviceRel.replace(/\.ts$/u, '')
100
- ? serviceFile.sourceFile.statements.find(statement => ts.isClassDeclaration(statement) && statement.name?.text === serviceName.name) : null;
101
- if (!serviceClass) { report(first ?? list, `${rel}: the first provider is the service under test, imported from ./${path.posix.basename(serviceRel, '.ts')}.`); continue; }
102
-
103
- const dependencies = constructorDependencies(kit, serviceFile, serviceClass);
104
- const expected = [];
105
- for (const dependency of dependencies) {
106
- if (dependency.kind === 'unresolved') {
107
- report(list, `${rel}: a constructor dependency of ${serviceName.name} cannot be resolved to a token (${dependency.reason}), so its provider cannot be checked; inject it through an exported Inject<Thing>() decorator or a class type.`);
108
- continue;
109
- }
110
- expected.push(dependency.name);
111
- }
112
- const remaining = [...expected];
113
- for (const item of provided.slice(1)) {
114
- const index = remaining.indexOf(item.name);
115
- if (index === -1) report(item.node, `${rel}: ${item.name} is provided but is not a constructor dependency of ${serviceName.name}; a unit spec provides exactly what the constructor needs.`);
116
- else remaining.splice(index, 1);
117
- }
118
- for (const name of remaining) report(list, `${rel}: ${name} is a constructor dependency of ${serviceName.name} but is not provided; add \`{ provide: ${name}, useValue: double }\` (or the class itself) to the providers.`);
119
- }
140
+ for (const rel of [...tree.files].filter(file => SPEC_ROOT.test(file) && file.endsWith('.service.spec.ts')).sort(byCodeUnit)) {
141
+ if (checkServiceSpec({ config, graph, kit, ts, rel, violations })) specs += 1;
120
142
  }
121
143
  return { violations, coverage: { status: 'checked', specs, files: graph.files.size } };
122
144
  }
@@ -5,7 +5,6 @@
5
5
  // Prints one starci/architecture-check@1 record. Exit 0: ok. Exit 1: violations or errors (a check that
6
6
  // cannot run is an error, never a pass). Exit 2: bad arguments.
7
7
  import path from 'node:path';
8
- import {fileURLToPath} from 'node:url';
9
8
  import {checkArchitecture} from './architecture/index.mjs';
10
9
  import { isMain } from '../lib/is-main.mjs';
11
10
 
@@ -19,11 +18,11 @@ function parseArchitectureArgs(argv) {
19
18
  const value = argv[index];
20
19
  if (value === '--base') {
21
20
  base = argv[++index];
22
- if (!base) throw Error(`--base needs a commit; ${USAGE}`);
23
- } else if (value.startsWith('-') || root !== null) throw Error(`unexpected argument ${value}; ${USAGE}`);
21
+ if (!base) throw new Error(`--base needs a commit; ${USAGE}`);
22
+ } else if (value.startsWith('-') || root !== null) throw new Error(`unexpected argument ${value}; ${USAGE}`);
24
23
  else root = value;
25
24
  }
26
- if (root === null) throw Error(USAGE);
25
+ if (root === null) throw new Error(USAGE);
27
26
  return {root, base};
28
27
  }
29
28
 
@@ -0,0 +1,15 @@
1
+ import fs from 'node:fs';
2
+ import { parseYaml } from '../../engine/yaml.mjs';
3
+
4
+ const parsed = new Map();
5
+
6
+ /** The parsed YAML catalog at `file`; the megabyte of text is parsed once per file state (path, size, mtime), not once per check. */
7
+ export function readCatalogOnce(file) {
8
+ const { size, mtimeMs } = fs.statSync(file);
9
+ const key = `${file}|${size}|${mtimeMs}`;
10
+ if (!parsed.has(key)) {
11
+ parsed.clear();
12
+ parsed.set(key, parseYaml(fs.readFileSync(file, 'utf8')));
13
+ }
14
+ return parsed.get(key);
15
+ }
@@ -57,8 +57,8 @@ import path from 'node:path';
57
57
  import { skillRoot } from '../../engine/runtime-root.mjs';
58
58
  import { ARCHITECTURE_RULE_IDS, checkArchitecture } from './architecture/index.mjs';
59
59
  import { parseYaml } from '../../engine/yaml.mjs';
60
+ import { readCatalogOnce } from './catalog-once.mjs';
60
61
  import { APP_SCOPE, HFS_DECLARATION_FILE, appRelativeMessages, HfsSlotsError, SIDES, createSlotResolver, loadRuleCatalog, loadSlotManifest, readRepoDeclaration, resolveRepoDeclaration } from './slots.mjs';
61
- import { allowsFile } from './allows.mjs';
62
62
  import { RUNTIME_KIND } from './manifest-shape.mjs';
63
63
  import { lsFiles } from '../api/git/ls-files.mjs';
64
64
  import { diff as gitDiff } from '../api/git/diff.mjs';
@@ -71,14 +71,15 @@ import { appRootFindings } from './rules/app-root.mjs';
71
71
  import { frontendFindings } from './rules/frontend-tree.mjs';
72
72
  import { lintSuppressionFindings } from './rules/lint-suppression.mjs';
73
73
  import { repoLocalCheckFindings } from './rules/repo-local-checks.mjs';
74
- import { readJson } from './rules/read.mjs';
75
74
  import { supabaseSecretFindings } from './rules/supabase-secrets.mjs';
76
75
  import { pathFindings } from './path-findings.mjs';
77
76
  import { onLintSurface } from './architecture/surface.mjs';
78
77
  import { checkAppRoot, trackedTreeView } from './architecture/hfs.mjs';
79
78
  import { testTopologyFindings } from './rules/test-topology.mjs';
80
- import { feNoTestsFindings, isFeTestPath } from './rules/fe-no-tests.mjs';
79
+ import { feNoTestsFindings } from './rules/fe-no-tests.mjs';
81
80
  import { editionFindings } from './rules/edition.mjs';
81
+ import { byCodeUnit } from '../lib/list.mjs';
82
+ import { pinFindings } from './pin-findings.mjs';
82
83
  // The database rules (R213-R216) read SQL through a WASM parser, so they are async: `starci app check` calls them beside the other emitters and passes the findings in as `extraFindings`.
83
84
  export { checkDatabase } from './rules/database.mjs';
84
85
 
@@ -106,16 +107,15 @@ export const CHECK_CODES = Object.freeze([
106
107
  ...REFUSAL_CODES,
107
108
  ]);
108
109
  /** Every code `starci app check` can report: its own and every code the architecture machine can emit (derived from the machine's rule id lists). */
109
- export const ALL_CHECK_CODES = Object.freeze([...new Set([...CHECK_CODES, ...ARCHITECTURE_RULE_IDS])].sort());
110
+ export const ALL_CHECK_CODES = Object.freeze([...new Set([...CHECK_CODES, ...ARCHITECTURE_RULE_IDS])].sort(byCodeUnit));
110
111
  const SOURCE_EXT = /\.(?:[cm]?[jt]sx?)$/;
111
112
  const VAR = /<([a-z][a-z0-9-]*)>/g;
112
- const DEP_SECTIONS = ['dependencies', 'devDependencies'];
113
113
 
114
114
  const refuse = (code, message, details = {}) => { throw new HfsSlotsError(code, message, details); };
115
115
 
116
116
  /** {code: {title, title_vi, meaning_vi, nextStep_vi}} for the codes asked for, read from the failure-code catalog under `root`. */
117
117
  export function readWhy(root = skillRoot, codes = CHECK_CODES) {
118
- const catalog = parseYaml(fs.readFileSync(path.join(root, FAILURE_CODES_FILE), 'utf8'));
118
+ const catalog = readCatalogOnce(path.join(root, FAILURE_CODES_FILE));
119
119
  const why = {};
120
120
  for (const code of codes) {
121
121
  const entry = catalog?.[code];
@@ -138,30 +138,9 @@ export function trackedFiles(repoRoot) {
138
138
 
139
139
  const fill = (text, bindings) => String(text).replace(VAR, (whole, name) => bindings[name] ?? whole);
140
140
 
141
- const pinnedSpec = (spec, pin) => (spec === pin.version ? null : `declared ${spec}, pinned ${pin.version}`);
142
-
143
141
  /** The pin map of knowledge/hfs/canon-pins.yaml under `root`. */
144
142
  const readPins = (root) => parseYaml(fs.readFileSync(path.join(root, CANON_PINS_FILE), 'utf8'))?.pins ?? {};
145
143
 
146
- function pinFindings({ repoRoot, files, profile, pins, only }) {
147
- const findings = [];
148
- for (const file of files.filter((f) => (f === 'package.json' || f.endsWith('/package.json')) && (!only || only.has(f)))) {
149
- let pkg;
150
- try { pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, file), 'utf8')); } catch { continue; }
151
- for (const [name, pin] of Object.entries(pins)) {
152
- // The app root's one package.json carries the pins of both sides.
153
- if (profile !== APP_SCOPE && pin.side !== 'both' && pin.side !== profile) continue;
154
- for (const section of DEP_SECTIONS) {
155
- const spec = pkg[section]?.[name];
156
- if (spec === undefined) continue;
157
- const drift = pinnedSpec(spec, pin);
158
- if (drift) findings.push({ code: 'HFS_CANON_PIN_DRIFT', level: 'error', path: file, dependency: name, section, pinned: pin.version, declared: spec, message: `${name} in ${file} ${section}: ${drift}` });
159
- }
160
- }
161
- }
162
- return findings;
163
- }
164
-
165
144
  /** Instances (slot, root, bindings) present in the tracked tree, for every slot that names required files or a minimum. */
166
145
  function instancesOf(resolver, files) {
167
146
  const found = new Map();
@@ -221,7 +200,7 @@ function treeFindings({ repoRoot, resolver }) {
221
200
  const findings = [];
222
201
  const facts = treeFacts(readTree(repoRoot, { isIgnored: inIgnoredSlot }));
223
202
  for (const { path: dir, below } of facts.empty) {
224
- findings.push({ code: 'HFS_EMPTY_DIR', level: 'error', path: dir, below, message: `${dir} has no file below it${below ? ` (nor in its ${below} sub-director${below === 1 ? 'y' : 'ies'})` : ''}; git tracks no empty directory, so it is a leftover` });
203
+ findings.push({ code: 'HFS_EMPTY_DIR', level: 'error', path: dir, below, message: `${dir} has no file below it` + (below ? ` (nor in its ${below} sub-director${below === 1 && 'y' || 'ies'})` : '') + '; git tracks no empty directory, so it is a leftover' });
225
204
  }
226
205
  for (const { path: dir, of, distance } of facts.ghosts) {
227
206
  findings.push({ code: 'HFS_GHOST_TREE', level: 'error', path: dir, of, distance, message: `${dir} is empty and ${distance} edit${distance === 1 ? '' : 's'} from its sibling ${of}: a renamed or misspelt structure that was never removed` });
@@ -237,77 +216,90 @@ function treeFindings({ repoRoot, resolver }) {
237
216
  /** A side-relative path (or null) as an app-relative one. */
238
217
  const onSide = (side, p) => (p ? path.posix.normalize(`${side}/${p}`) : p);
239
218
 
240
- /**
241
- * The findings of one scope: the app root (profile app; its own files, and the rules of the files only the root holds: the one
242
- * package.json and lockfile, CI, hooks, .starciwork, .starcistacks) or one side (profile be or fe; the side folder is `repoRoot` and every path is
243
- * relative to it, exactly as the standalone repository root was). `files` are the scope's tracked paths; `all` (root only) every
244
- * tracked path of the app, for the rules that read across it (dependency skew, proof commands).
245
- */
246
- function scopeFindings({ repoRoot, root, repo, resolver, files, all = files, scoped, manifest, editionDeclaration = true }) {
247
- const inScope = (file) => !scoped || scoped.has(file);
248
- // A required directory of the root (be/, fe/) is present through the files below it, which are the sides' own.
249
- const trackedSet = new Set(all);
250
- const present = (p) => (p.endsWith('/') ? all.some((f) => f.startsWith(p)) : trackedSet.has(p));
251
- const findings = [];
252
- const isRoot = repo.profile === APP_SCOPE;
253
-
254
- findings.push(...pathFindings({ files: files.filter(inScope), resolver, profile: repo.profile }));
255
-
256
- const required = resolver.requiredPaths();
219
+ function appendRequiredPathFindings(findings, resolver, files, required, present) {
257
220
  const missing = new Set();
258
221
  const missingFile = (slot, p, via) => {
259
222
  const key = `${slot}|${p}`;
260
223
  if (missing.has(key) || present(p)) return;
261
224
  missing.add(key);
262
225
  const app = appOf(p);
263
- findings.push({ code: 'HFS_SLOT_REQUIRED_MISSING', level: 'error', path: p, slot, via, ...(app ? { app } : {}), message: `${slot} requires ${p}${app ? ` (app ${app})` : ''}, which is not tracked` });
226
+ const appNote = app ? ` (app ${app})` : '';
227
+ findings.push({ code: 'HFS_SLOT_REQUIRED_MISSING', level: 'error', path: p, slot, via, ...(app ? { app } : {}), message: `${slot} requires ${p}${appNote}, which is not tracked` });
264
228
  };
265
- // The app's own required paths; each side reports its own (the resolver of the app lists them too, with their side).
266
229
  for (const entry of required.paths) if (!entry.side) missingFile(entry.slot, entry.path, entry.via);
267
230
  const instances = instancesOf(resolver, files);
268
231
  for (const instance of instances) for (const p of requiredOf(resolver.slot(instance.slot), instance)) missingFile(instance.slot, p, 'requires');
232
+ return instances;
233
+ }
234
+
235
+ function minimumInstanceFindings(resolver, required, instances) {
236
+ const findings = [];
269
237
  for (const { slot, min, appKind, side } of required.minimums) {
270
238
  if (appKind !== undefined || side) continue; // an app-kind minimum is checked by requiredPaths (one path set per declared app)
271
- const count = instances.filter((i) => i.slot === slot).length;
239
+ const count = instances.filter((instance) => instance.slot === slot).length;
272
240
  if (count < min) findings.push({ code: 'HFS_MIN_INSTANCES', level: 'error', path: resolver.slot(slot).path, slot, min, count, message: `${slot} needs at least ${min} instance${min === 1 ? '' : 's'} (${resolver.slot(slot).path}), found ${count}` });
273
241
  }
274
- // The runtime repository (kind runtime) is judged here by its tree law only; its own rules run in scripts/hfs/runtime-check.mjs.
275
- if (repo.kind === RUNTIME_KIND) return findings;
276
-
277
- const pins = readPins(root);
278
- findings.push(...pinFindings({ repoRoot, files, profile: repo.profile, pins, only: scoped }));
242
+ return findings;
243
+ }
279
244
 
280
- // The tree checks of the rules that read file content or configuration (rules/*): whole-scope, cheap, no tool run.
281
- findings.push(
282
- ...supabaseSecretFindings({ repoRoot, files: files.filter(inScope), resolver, repo }),
283
- ...editionFindings({ repoRoot, files: files.filter(inScope), repo, resolver, withDeclaration: editionDeclaration }),
245
+ function contentRuleFindings({ repoRoot, repo, resolver, files, all, scoped, pins, inScope, isRoot, editionDeclaration }) {
246
+ const findings = [
247
+ ...pinFindings({ repoRoot, files, profile: repo.profile, pins, only: scoped }),
248
+ ...supabaseSecretFindings({ repoRoot, files: files.filter((file) => inScope(file)), resolver, repo }),
249
+ ...editionFindings({ repoRoot, files: files.filter((file) => inScope(file)), repo, resolver, withDeclaration: editionDeclaration }),
284
250
  ...repoLocalCheckFindings({ repoRoot, files }),
285
251
  ...lintSuppressionFindings({ repoRoot, files }),
286
- );
252
+ ];
287
253
  if (isRoot) {
288
254
  findings.push(
289
255
  ...appRootFindings({ repoRoot, files, all, repo, resolver, pins }),
290
- // The tree check of the app root the machine runs per side for a side folder: README, root entries, automatic gates, hooks path.
291
256
  ...checkAppRoot({ root: repoRoot, resolver, tree: trackedTreeView(all) }).violations.map((item) => ({ code: item.ruleId, level: 'error', path: item.path, line: item.line, column: item.column, source: 'machine', message: `${item.path}: ${item.message}` })),
292
257
  );
293
258
  } else if (repo.profile === 'be') {
294
259
  findings.push(...contractFindings({ repoRoot, files, repo, resolver }), ...testTopologyFindings({ repoRoot, files }));
295
260
  } else {
296
- findings.push(...frontendFindings({ repoRoot, files, repo }), ...feNoTestsFindings({ repoRoot, files: files.filter(inScope) }));
261
+ findings.push(...frontendFindings({ repoRoot, files, repo }), ...feNoTestsFindings({ repoRoot, files: files.filter((file) => inScope(file)) }));
297
262
  }
263
+ return findings;
264
+ }
298
265
 
299
- if (!isRoot) {
300
- const soft = resolver.ruleParams().fileLines.soft;
301
- for (const file of files) {
302
- if (!inScope(file) || !SOURCE_EXT.test(file) || resolver.classifyPath(file).status !== 'owned') continue;
303
- let lines;
304
- try { lines = fs.readFileSync(path.join(repoRoot, file), 'utf8').split('\n').length; } catch { continue; }
305
- if (lines > soft) findings.push({ code: 'HFS_SIZE_SOFT_BACKLOG', level: 'info', path: file, lines, soft, message: `${file} has ${lines} lines, above the soft size ${soft}; report only` });
306
- }
266
+ function sizeSoftFindings({ repoRoot, resolver, files, inScope }) {
267
+ const findings = [];
268
+ const soft = resolver.ruleParams().fileLines.soft;
269
+ for (const file of files) {
270
+ if (!inScope(file) || !SOURCE_EXT.test(file) || resolver.classifyPath(file).status !== 'owned') continue;
271
+ let lines;
272
+ try { lines = fs.readFileSync(path.join(repoRoot, file), 'utf8').split('\n').length; } catch { continue; }
273
+ if (lines > soft) findings.push({ code: 'HFS_SIZE_SOFT_BACKLOG', level: 'info', path: file, lines, soft, message: `${file} has ${lines} lines, above the soft size ${soft}; report only` });
307
274
  }
308
275
  return findings;
309
276
  }
310
277
 
278
+ /**
279
+ * The findings of one scope: the app root (profile app; its own files, and the rules of the files only the root holds: the one
280
+ * package.json and lockfile, CI, hooks, .starciwork, .starcistacks) or one side (profile be or fe; the side folder is `repoRoot` and every path is
281
+ * relative to it, exactly as the standalone repository root was). `files` are the scope's tracked paths; `all` (root only) every
282
+ * tracked path of the app, for the rules that read across it (dependency skew, proof commands).
283
+ */
284
+ function scopeFindings({ repoRoot, root, repo, resolver, files, all = files, scoped, manifest, editionDeclaration = true }) {
285
+ const inScope = (file) => !scoped || scoped.has(file);
286
+ // A required directory of the root (be/, fe/) is present through the files below it, which are the sides' own.
287
+ const trackedSet = new Set(all);
288
+ const present = (p) => (p.endsWith('/') ? all.some((f) => f.startsWith(p)) : trackedSet.has(p));
289
+ const isRoot = repo.profile === APP_SCOPE;
290
+ const findings = pathFindings({ files: files.filter(inScope), resolver, profile: repo.profile });
291
+ const required = resolver.requiredPaths();
292
+ const instances = appendRequiredPathFindings(findings, resolver, files, required, present);
293
+ findings.push(...minimumInstanceFindings(resolver, required, instances));
294
+ // The runtime repository (kind runtime) is judged here by its tree law only; its own rules run in scripts/hfs/runtime-check.mjs.
295
+ if (repo.kind === RUNTIME_KIND) return findings;
296
+
297
+ const pins = readPins(root);
298
+ findings.push(...contentRuleFindings({ repoRoot, repo, resolver, files, all, scoped, pins, inScope, isRoot, editionDeclaration }));
299
+ if (!isRoot) findings.push(...sizeSoftFindings({ repoRoot, resolver, files, inScope }));
300
+ return findings;
301
+ }
302
+
311
303
  /**
312
304
  * The check of one app. `repoRoot` is the app root (a side folder is checked as that side alone). `declaration` overrides hfs.json
313
305
  * (a dry run over an app that has none); `files` overrides git ls-files (specs), app-relative. `only` (a list of app-relative paths)
@@ -386,7 +378,7 @@ function changedSince(repoRoot, base) {
386
378
 
387
379
  /** The machine's violations and errors as findings: each keeps the machine's rule id as its code. */
388
380
  function machineFindings(report) {
389
- const of = (item) => ({ code: item.ruleId, level: 'error', ...(item.path ? { path: item.path } : {}), ...(item.line ? { line: item.line, column: item.column } : {}), source: 'machine', message: `${item.path ? `${item.path}${item.line ? `:${item.line}` : ''}: ` : ''}${item.message}` });
381
+ const of = (item) => ({ code: item.ruleId, level: 'error', ...(item.path ? { path: item.path } : {}), ...(item.line ? { line: item.line, column: item.column } : {}), source: 'machine', message: ((item.path && item.path + (item.line && ':' + item.line || '') + ': ') || '') + item.message });
390
382
  return [...report.errors.map(of), ...report.violations.map(of)];
391
383
  }
392
384
 
@@ -438,7 +430,7 @@ function machineOver({ scope, manifest, machine, changed }) {
438
430
  if (changed) {
439
431
  const resolver = createSlotResolver(manifest, readRepoDeclaration(manifest, scope.repoRoot));
440
432
  const mine = changed.files.filter((f) => f.startsWith(prefix)).map((f) => f.slice(prefix.length));
441
- paths = [...new Set(mine.filter((f) => SOURCE_EXT.test(f) || resolver.ownerOf(f)).map((f) => resolver.ownerOf(f)?.root ?? f))].sort();
433
+ paths = [...new Set(mine.filter((f) => SOURCE_EXT.test(f) || resolver.ownerOf(f)).map((f) => resolver.ownerOf(f)?.root ?? f))].sort(byCodeUnit);
442
434
  if (!paths.length) return { info: { side, status: 'skipped', reason: `no changed source file in ${label}` }, findings: [] };
443
435
  }
444
436
  let report;
@@ -485,7 +477,7 @@ export function explainPath({ repoRoot, input, root = skillRoot, declaration, ma
485
477
  tier: tier ?? 'none',
486
478
  owner: owner ? { slot: owner.slot, root: owner.root } : null,
487
479
  allowedImports: mayImport,
488
- importRule: mayImport ? manifest.crossOwner : (tier === 'none' ? 'the slot takes no part in import checks' : null),
480
+ importRule: mayImport ? manifest.crossOwner : (tier === 'none' && 'the slot takes no part in import checks' || null),
489
481
  tests: slot.tests,
490
482
  testsMeaning: TEST_KIND[slot.tests],
491
483
  requiredFiles: resolver.requiredFiles(c.path),