@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
@@ -19,111 +19,136 @@ const ORIGIN_HEADERS = new Set(['origin', 'referer', 'referrer']);
19
19
  const METADATA_READERS = new Set(['get', 'getAll', 'getAllAndMerge', 'getAllAndOverride']);
20
20
  const LABEL = { throttler: 'the throttler guard', csrf: 'the CSRF origin guard of platform/http-security', auth: 'AuthGuard of domain/identity' };
21
21
 
22
+ const keySymbol = (kit, checker, node) => (node ? kit.aliased(checker, kit.symbolAt(checker, node)) ?? null : null);
23
+
24
+ const extendsThrottlerType = (kit, checker, declaration, type, depth) => {
25
+ if (kit.isImportOf(checker, type.expression, 'ThrottlerGuard', '@nestjs/throttler')) return true;
26
+ return kit.declarationsOf(checker, type.expression).some(parent => parent !== declaration && extendsThrottler(kit, parent, depth + 1));
27
+ };
28
+
29
+ function extendsThrottler(kit, declaration, depth = 0) {
30
+ if (!kit.ts.isClassDeclaration(declaration) || depth > 8) return false;
31
+ const checker = kit.checkerOf(declaration.getSourceFile());
32
+ if (!checker) return false;
33
+ const types = (declaration.heritageClauses ?? []).flatMap(clause => [...clause.types]);
34
+ return types.some(type => extendsThrottlerType(kit, checker, declaration, type, depth));
35
+ }
36
+
37
+ const collectSetMetadataKeys = (kit, file, keys) => {
38
+ const { ts } = kit;
39
+ const checker = kit.checkerOf(file.sourceFile);
40
+ if (!checker) return;
41
+ kit.walk(file.sourceFile, node => {
42
+ if (ts.isCallExpression(node) && kit.isImportOf(checker, node.expression, 'SetMetadata', '@nestjs/common')) {
43
+ const key = keySymbol(kit, checker, node.arguments[0]);
44
+ if (key) keys.add(key);
45
+ }
46
+ return true;
47
+ });
48
+ };
49
+
50
+ /** The symbols a SetMetadata call of the owner writes as metadata keys: what its door decorators (`@RateLimit(tier)`) attach. */
51
+ const writtenKeys = (kit, graph, owner, cache) => {
52
+ if (cache.has(owner.root)) return cache.get(owner.root);
53
+ const keys = new Set();
54
+ for (const file of graph.files.values()) {
55
+ if (file.owner?.root === owner.root) collectSetMetadataKeys(kit, file, keys);
56
+ }
57
+ cache.set(owner.root, keys);
58
+ return keys;
59
+ };
60
+
61
+ /** True when the class reads a request origin or referer header (the origin/referer allowlist of the CSRF guard). */
62
+ const readsOrigin = (kit, declaration) => {
63
+ const { ts } = kit;
64
+ let found = false;
65
+ kit.walk(declaration, node => {
66
+ let name = null;
67
+ if (ts.isPropertyAccessExpression(node)) name = node.name.text;
68
+ else if (ts.isElementAccessExpression(node) && ts.isStringLiteralLike(node.argumentExpression)) name = node.argumentExpression.text;
69
+ if (name && ORIGIN_HEADERS.has(name.toLowerCase())) found = true;
70
+ return !found;
71
+ });
72
+ return found;
73
+ };
74
+
75
+ const isMetadataRead = (ts, node) => ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && METADATA_READERS.has(node.expression.name.text);
76
+
77
+ /** True when the class reads, through a metadata getter, a key its own capability's SetMetadata decorators write. */
78
+ const readsDoorMetadata = (kit, graph, keyCache, declaration, owner) => {
79
+ const checker = kit.checkerOf(declaration.getSourceFile());
80
+ if (!checker) return false;
81
+ const keys = writtenKeys(kit, graph, owner, keyCache);
82
+ let found = false;
83
+ kit.walk(declaration, node => {
84
+ if (isMetadataRead(kit.ts, node)) {
85
+ const key = keySymbol(kit, checker, node.arguments[0]);
86
+ if (key && keys.has(key)) found = true;
87
+ }
88
+ return !found;
89
+ });
90
+ return found;
91
+ };
92
+
93
+ const classifyDeclaration = (kit, graph, keyCache, declaration) => {
94
+ const { ts } = kit;
95
+ const owner = kit.ownerOfDeclaration(declaration);
96
+ if (owner?.tier === 'platform' && owner.name === 'http-security' && ts.isClassDeclaration(declaration)) {
97
+ if (readsOrigin(kit, declaration)) return 'csrf';
98
+ if (readsDoorMetadata(kit, graph, keyCache, declaration, owner)) return 'throttler';
99
+ }
100
+ if (owner?.tier === 'domain' && owner.name === 'identity' && ts.isClassDeclaration(declaration) && declaration.name?.text === 'AuthGuard') return 'auth';
101
+ return null;
102
+ };
103
+
104
+ const classify = (kit, graph, keyCache, checker, node) => {
105
+ if (!node) return null;
106
+ if (kit.importBinding(checker, node)?.module === '@nestjs/throttler') return 'throttler';
107
+ const declarations = kit.declarationsOf(checker, node);
108
+ if (declarations.some(declaration => extendsThrottler(kit, declaration))) return 'throttler';
109
+ for (const declaration of declarations) {
110
+ const role = classifyDeclaration(kit, graph, keyCache, declaration);
111
+ if (role) return role;
112
+ }
113
+ return null;
114
+ };
115
+
116
+ const roleMessages = (app, roles, anchor) => {
117
+ const out = [];
118
+ for (const role of ORDER) {
119
+ const entries = roles.filter(item => item.role === role);
120
+ if (entries.length === 0) out.push({ node: anchor, message: `App ${app.name} does not provide ${LABEL[role]} as an APP_GUARD; an api app is denied by default: throttler, then CSRF origin guard, then AuthGuard.` });
121
+ else if (entries.length > 1) out.push({ node: entries[1].guard.node, message: `App ${app.name} provides ${LABEL[role]} more than once as an APP_GUARD.` });
122
+ }
123
+ return out;
124
+ };
125
+
126
+ const orderMessages = (app, roles) => {
127
+ const indexes = ORDER.map(role => roles.findIndex(item => item.role === role));
128
+ if (!indexes.every(index => index >= 0)) return [];
129
+ const out = [];
130
+ for (let i = 1; i < indexes.length; i += 1) {
131
+ if (indexes[i] < indexes[i - 1]) out.push({ node: roles[indexes[i]].guard.node, message: `App ${app.name} provides ${LABEL[ORDER[i]]} before ${LABEL[ORDER[i - 1]]}; the APP_GUARD order is throttler, CSRF origin guard, AuthGuard.` });
132
+ }
133
+ return out;
134
+ };
135
+
22
136
  export function checkDefaultDeny(input) {
23
137
  const { config } = input;
24
138
  const kit = machineKit(input);
25
- const { ts } = kit;
26
139
  const violations = [];
27
- let apps = 0;
28
-
29
- const extendsThrottler = (declaration, depth = 0) => {
30
- if (!ts.isClassDeclaration(declaration) || depth > 8) return false;
31
- const checker = kit.checkerOf(declaration.getSourceFile());
32
- if (!checker) return false;
33
- for (const clause of declaration.heritageClauses ?? []) {
34
- for (const type of clause.types) {
35
- if (kit.isImportOf(checker, type.expression, 'ThrottlerGuard', '@nestjs/throttler')) return true;
36
- if (kit.declarationsOf(checker, type.expression).some(parent => parent !== declaration && extendsThrottler(parent, depth + 1))) return true;
37
- }
38
- }
39
- return false;
40
- };
41
-
42
- const keySymbol = (checker, node) => (node ? kit.aliased(checker, kit.symbolAt(checker, node)) ?? null : null);
43
-
44
- /** The symbols a SetMetadata call of the owner writes as metadata keys: what its door decorators (`@RateLimit(tier)`) attach. */
45
- const writtenKeys = (owner, cache = new Map()) => {
46
- if (cache.has(owner.root)) return cache.get(owner.root);
47
- const keys = new Set();
48
- for (const file of input.graph.files.values()) {
49
- if (file.owner?.root !== owner.root) continue;
50
- const checker = kit.checkerOf(file.sourceFile);
51
- if (!checker) continue;
52
- kit.walk(file.sourceFile, node => {
53
- if (ts.isCallExpression(node) && kit.isImportOf(checker, node.expression, 'SetMetadata', '@nestjs/common')) {
54
- const key = keySymbol(checker, node.arguments[0]);
55
- if (key) keys.add(key);
56
- }
57
- return true;
58
- });
59
- }
60
- cache.set(owner.root, keys);
61
- return keys;
62
- };
63
140
  const keyCache = new Map();
64
-
65
- /** True when the class reads a request origin or referer header (the origin/referer allowlist of the CSRF guard). */
66
- const readsOrigin = declaration => {
67
- let found = false;
68
- kit.walk(declaration, node => {
69
- if (ts.isPropertyAccessExpression(node) && ORIGIN_HEADERS.has(node.name.text.toLowerCase())) found = true;
70
- else if (ts.isElementAccessExpression(node) && ts.isStringLiteralLike(node.argumentExpression) && ORIGIN_HEADERS.has(node.argumentExpression.text.toLowerCase())) found = true;
71
- return !found;
72
- });
73
- return found;
74
- };
75
-
76
- /** True when the class reads, through a metadata getter, a key its own capability's SetMetadata decorators write. */
77
- const readsDoorMetadata = (declaration, owner) => {
78
- const checker = kit.checkerOf(declaration.getSourceFile());
79
- if (!checker) return false;
80
- const keys = writtenKeys(owner, keyCache);
81
- let found = false;
82
- kit.walk(declaration, node => {
83
- if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && METADATA_READERS.has(node.expression.name.text)) {
84
- const key = keySymbol(checker, node.arguments[0]);
85
- if (key && keys.has(key)) found = true;
86
- }
87
- return !found;
88
- });
89
- return found;
90
- };
91
-
92
- const classify = (checker, node) => {
93
- if (!node) return null;
94
- if (kit.importBinding(checker, node)?.module === '@nestjs/throttler') return 'throttler';
95
- const declarations = kit.declarationsOf(checker, node);
96
- if (declarations.some(declaration => extendsThrottler(declaration))) return 'throttler';
97
- for (const declaration of declarations) {
98
- const owner = kit.ownerOfDeclaration(declaration);
99
- if (owner?.tier === 'platform' && owner.name === 'http-security' && ts.isClassDeclaration(declaration)) {
100
- if (readsOrigin(declaration)) return 'csrf';
101
- if (readsDoorMetadata(declaration, owner)) return 'throttler';
102
- }
103
- if (owner?.tier === 'domain' && owner.name === 'identity' && ts.isClassDeclaration(declaration) && declaration.name?.text === 'AuthGuard') return 'auth';
104
- }
105
- return null;
106
- };
141
+ let apps = 0;
107
142
 
108
143
  for (const app of config.apps.filter(item => item.kind === 'api')) {
109
144
  const root = kit.appRoot(app.name);
110
145
  if (!root) continue;
111
146
  apps += 1;
112
147
  const guards = kit.providersOf(root, 'APP_GUARD', '@nestjs/core');
113
- const roles = guards.map(guard => ({ guard, role: classify(guard.checker, guard.useClass) }));
148
+ const roles = guards.map(guard => ({ guard, role: classify(kit, input.graph, keyCache, guard.checker, guard.useClass) }));
114
149
  const anchor = guards[0]?.node ?? root.sourceFile.statements[0] ?? root.sourceFile;
115
- const finding = (node, message) => violations.push({ ruleId: RULE, path: root.rel, ...kit.at(root.rel, root.sourceFile, node), app: app.name, message });
116
- for (const role of ORDER) {
117
- const entries = roles.filter(item => item.role === role);
118
- if (entries.length === 0) finding(anchor, `App ${app.name} does not provide ${LABEL[role]} as an APP_GUARD; an api app is denied by default: throttler, then CSRF origin guard, then AuthGuard.`);
119
- else if (entries.length > 1) finding(entries[1].guard.node, `App ${app.name} provides ${LABEL[role]} more than once as an APP_GUARD.`);
120
- }
121
- const indexes = ORDER.map(role => roles.findIndex(item => item.role === role));
122
- if (indexes.every(index => index >= 0)) {
123
- for (let i = 1; i < indexes.length; i += 1) {
124
- if (indexes[i] < indexes[i - 1]) finding(roles[indexes[i]].guard.node, `App ${app.name} provides ${LABEL[ORDER[i]]} before ${LABEL[ORDER[i - 1]]}; the APP_GUARD order is throttler, CSRF origin guard, AuthGuard.`);
125
- }
126
- }
150
+ const messages = [...roleMessages(app, roles, anchor), ...orderMessages(app, roles)];
151
+ for (const { node, message } of messages) violations.push({ ruleId: RULE, path: root.rel, ...kit.at(root.rel, root.sourceFile, node), app: app.name, message });
127
152
  }
128
153
  return { violations, coverage: { status: 'checked', apps } };
129
154
  }
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { treeOf } from './required-files.mjs';
4
4
  import { documentLanguageHits, isDocument, isLocalizedDataFile } from '../../lib/language.mjs';
5
+ import { byCodeUnit } from '../../lib/list.mjs';
5
6
 
6
7
  /**
7
8
  * R96 `doc-language` (HFS_DOC_NOT_ENGLISH). Every Markdown and YAML document under knowledge/, docs/, src/ and apps/ is English:
@@ -23,7 +24,7 @@ export function checkDocLanguage({ config, graph }) {
23
24
  const tree = treeOf(config.root);
24
25
  const violations = [];
25
26
  let files = 0;
26
- for (const file of [...tree.files].sort()) {
27
+ for (const file of [...tree.files].sort(byCodeUnit)) {
27
28
  if (!DOCUMENT_ROOTS.includes(file.split('/')[0]) || !isDocument(file) || file.split('/').includes('node_modules')) continue;
28
29
  const classified = resolver.classifyPath(file);
29
30
  if (isLocalizedDataFile(file, classified.status === 'owned' ? classified.slot : null)) continue;
@@ -16,43 +16,58 @@ export const ENTRYPOINT_RULE_IDS = ['BE_ENTRYPOINT_ONLY_IN_APPS'];
16
16
 
17
17
  const RULE = 'BE_ENTRYPOINT_ONLY_IN_APPS';
18
18
 
19
+ const TRANSPARENT = ['isVoidExpression', 'isAwaitExpression', 'isParenthesizedExpression', 'isNonNullExpression'];
20
+
21
+ /** The identifier a top-level statement calls: `bootstrap()`, `void bootstrap()`, `bootstrap().catch(...)`. */
22
+ function calledIdentifier(ts, expression) {
23
+ let current = expression;
24
+ for (;;) {
25
+ if (TRANSPARENT.some(test => ts[test](current))) current = current.expression;
26
+ else if (ts.isCallExpression(current)) {
27
+ if (ts.isIdentifier(current.expression)) return current.expression;
28
+ current = current.expression;
29
+ } else if (ts.isPropertyAccessExpression(current)) current = current.expression;
30
+ else return null;
31
+ }
32
+ }
33
+
34
+ const isNestFactoryCreate = (kit, ts, checker, node) => ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text.startsWith('create')
35
+ && kit.isImportOf(checker, node.expression.expression, 'NestFactory', '@nestjs/core');
36
+
37
+ const isMainFile = file => (Boolean(file.slot?.startsWith('be.app.')) && path.posix.basename(file.rel) === 'main.ts') || isTestWorldSlot(file.slot);
38
+
39
+ /** Counts the NestFactory.create* calls of an entrypoint owner and reports them in any other file. */
40
+ function inspectFactoryCalls(kit, file, isMain, report) {
41
+ const { ts } = kit;
42
+ const checker = kit.checkerOf(file.sourceFile);
43
+ let entrypoints = 0;
44
+ kit.walk(file.sourceFile, node => {
45
+ if (!isNestFactoryCreate(kit, ts, checker, node)) return;
46
+ if (isMain) entrypoints += 1;
47
+ else report(node, `NestFactory.${node.expression.name.text}() starts a process outside apps/<app>/src/main.ts; entrypoints live in an app's main.ts only.`);
48
+ });
49
+ return entrypoints;
50
+ }
51
+
52
+ function inspectBootstrapCalls(ts, file, report) {
53
+ for (const statement of file.sourceFile.statements) {
54
+ if (!ts.isExpressionStatement(statement)) continue;
55
+ if (calledIdentifier(ts, statement.expression)?.text === 'bootstrap') {
56
+ report(statement, "A top-level bootstrap() call starts a process outside apps/<app>/src/main.ts; entrypoints live in an app's main.ts only.");
57
+ }
58
+ }
59
+ }
60
+
19
61
  export function checkEntrypoints(input) {
20
62
  const { graph } = input;
21
63
  const kit = machineKit(input);
22
- const { ts } = kit;
23
64
  const violations = [];
24
65
  let entrypoints = 0;
25
- /** The identifier a top-level statement calls: `bootstrap()`, `void bootstrap()`, `bootstrap().catch(...)`. */
26
- const calledIdentifier = expression => {
27
- let current = expression;
28
- for (;;) {
29
- if (ts.isVoidExpression(current) || ts.isAwaitExpression(current) || ts.isParenthesizedExpression(current) || ts.isNonNullExpression(current)) current = current.expression;
30
- else if (ts.isCallExpression(current)) {
31
- if (ts.isIdentifier(current.expression)) return current.expression;
32
- current = current.expression;
33
- } else if (ts.isPropertyAccessExpression(current)) current = current.expression;
34
- else return null;
35
- }
36
- };
37
66
  for (const file of graph.files.values()) {
38
- const isMain = (Boolean(file.slot?.startsWith('be.app.')) && path.posix.basename(file.rel) === 'main.ts') || isTestWorldSlot(file.slot);
39
- const checker = kit.checkerOf(file.sourceFile);
67
+ const isMain = isMainFile(file);
40
68
  const report = (node, message) => violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message });
41
- kit.walk(file.sourceFile, node => {
42
- if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text.startsWith('create')
43
- && kit.isImportOf(checker, node.expression.expression, 'NestFactory', '@nestjs/core')) {
44
- if (isMain) entrypoints += 1;
45
- else report(node, `NestFactory.${node.expression.name.text}() starts a process outside apps/<app>/src/main.ts; entrypoints live in an app's main.ts only.`);
46
- }
47
- return true;
48
- });
49
- if (isMain) continue;
50
- for (const statement of file.sourceFile.statements) {
51
- if (!ts.isExpressionStatement(statement)) continue;
52
- if (calledIdentifier(statement.expression)?.text === 'bootstrap') {
53
- report(statement, "A top-level bootstrap() call starts a process outside apps/<app>/src/main.ts; entrypoints live in an app's main.ts only.");
54
- }
55
- }
69
+ entrypoints += inspectFactoryCalls(kit, file, isMain, report);
70
+ if (!isMain) inspectBootstrapCalls(kit.ts, file, report);
56
71
  }
57
72
  return { violations, coverage: { status: 'checked', entrypoints } };
58
73
  }
@@ -11,6 +11,24 @@ export const ERROR_CODE_RULE_IDS = ['BE_ERROR_HOME'];
11
11
 
12
12
  const RULE = 'BE_ERROR_HOME';
13
13
 
14
+ // Judge each member of one <C>ErrorCode enum; returns how many string-literal codes it held.
15
+ const checkEnumCodes = (statement, { file, capability, shape, kit, ts, seen, violations }) => {
16
+ let checked = 0;
17
+ for (const member of statement.members) {
18
+ const report = message => violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, member), message });
19
+ const name = kit.propertyNameText(member.name) ?? '?';
20
+ const value = member.initializer && ts.isStringLiteralLike(member.initializer) ? member.initializer.text : null;
21
+ if (value === null) { report(`${statement.name.text}.${name} must be a string literal code (\`${capability}_<WHAT>\`); a computed or numeric code cannot be checked or told apart in a log.`); continue; }
22
+ checked += 1;
23
+ if (!shape.test(value) || /_(?:ERROR|EXCEPTION)$/u.test(value)) {
24
+ report(`${statement.name.text}.${name} = "${value}" must match ${capability}_<WHAT> in UPPER_SNAKE with the capability prefix and no _ERROR or _EXCEPTION suffix.`);
25
+ }
26
+ if (seen.has(value)) report(`Error code "${value}" is already declared in ${seen.get(value)}; a code is unique across the repository.`);
27
+ else seen.set(value, file.rel);
28
+ }
29
+ return checked;
30
+ };
31
+
14
32
  export function checkErrorCodes(input) {
15
33
  const { graph } = input;
16
34
  const kit = machineKit(input);
@@ -27,18 +45,7 @@ export function checkErrorCodes(input) {
27
45
  for (const statement of file.sourceFile.statements) {
28
46
  if (!ts.isEnumDeclaration(statement) || !statement.name.text.endsWith('ErrorCode')) continue;
29
47
  enums += 1;
30
- for (const member of statement.members) {
31
- const report = message => violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, member), message });
32
- const name = kit.propertyNameText(member.name) ?? '?';
33
- const value = member.initializer && ts.isStringLiteralLike(member.initializer) ? member.initializer.text : null;
34
- if (value === null) { report(`${statement.name.text}.${name} must be a string literal code (\`${capability}_<WHAT>\`); a computed or numeric code cannot be checked or told apart in a log.`); continue; }
35
- codes += 1;
36
- if (!shape.test(value) || /_(?:ERROR|EXCEPTION)$/u.test(value)) {
37
- report(`${statement.name.text}.${name} = "${value}" must match ${capability}_<WHAT> in UPPER_SNAKE with the capability prefix and no _ERROR or _EXCEPTION suffix.`);
38
- }
39
- if (seen.has(value)) report(`Error code "${value}" is already declared in ${seen.get(value)}; a code is unique across the repository.`);
40
- else seen.set(value, file.rel);
41
- }
48
+ codes += checkEnumCodes(statement, { file, capability, shape, kit, ts, seen, violations });
42
49
  }
43
50
  }
44
51
  return { violations, coverage: { status: 'checked', enums, codes } };
@@ -10,50 +10,63 @@ export const ERROR_MASKED_RULE_IDS = ['BE_ERROR_MASKED'];
10
10
 
11
11
  const RULE = 'BE_ERROR_MASKED';
12
12
 
13
+ const inErrorsOf = kit => (checker, node) => kit.declarationsOf(checker, node).map(kit.ownerOfDeclaration).some(owner => owner?.tier === 'platform' && owner.name === 'errors');
14
+
15
+ function appFilterViolations(kit, inErrors, app, root) {
16
+ const violations = [];
17
+ const filters = kit.providersOf(root, 'APP_FILTER', '@nestjs/core');
18
+ const anchor = filters[0]?.node ?? root.sourceFile.statements[0] ?? root.sourceFile;
19
+ if (filters.length !== 1) {
20
+ violations.push({ ruleId: RULE, path: root.rel, ...kit.at(root.rel, root.sourceFile, anchor), app: app.name,
21
+ message: `App ${app.name} provides ${filters.length} APP_FILTER entries; an api app provides exactly one, the ExceptionFilter of platform/errors.` });
22
+ }
23
+ for (const filter of filters) {
24
+ if (!filter.useClass || !inErrors(filter.checker, filter.useClass)) {
25
+ violations.push({ ruleId: RULE, path: root.rel, ...kit.at(root.rel, root.sourceFile, filter.node), app: app.name,
26
+ message: 'The APP_FILTER must be `useClass` of the filter declared in platform/errors; any other filter can let an undeclared error reach the client unmasked.' });
27
+ }
28
+ }
29
+ return violations;
30
+ }
31
+
32
+ const isGraphqlRegistration = (kit, checker, node) => kit.ts.isCallExpression(node) && kit.ts.isPropertyAccessExpression(node.expression)
33
+ && ['forRoot', 'forRootAsync'].includes(node.expression.name.text)
34
+ && kit.isImportOf(checker, node.expression.expression, 'GraphQLModule', '@nestjs/graphql');
35
+
36
+ function passesMaskedFormatError(kit, inErrors, checker, call) {
37
+ const { ts } = kit;
38
+ let masked = false;
39
+ for (const argument of call.arguments) {
40
+ kit.walk(argument, inner => {
41
+ if ((ts.isPropertyAssignment(inner) || ts.isShorthandPropertyAssignment(inner)) && kit.propertyNameText(inner.name) === 'formatError'
42
+ && inErrors(checker, kit.valueOfProperty(inner))) masked = true;
43
+ });
44
+ }
45
+ return masked;
46
+ }
47
+
13
48
  export function checkErrorMasked(input) {
14
49
  const { config, graph } = input;
15
50
  const kit = machineKit(input);
16
- const { ts } = kit;
17
51
  const violations = [];
18
- const inErrors = (checker, node) => kit.declarationsOf(checker, node).map(kit.ownerOfDeclaration).some(owner => owner && owner.tier === 'platform' && owner.name === 'errors');
52
+ const inErrors = inErrorsOf(kit);
19
53
  let apps = 0;
20
54
  for (const app of config.apps.filter(item => item.kind === 'api')) {
21
55
  const root = kit.appRoot(app.name);
22
56
  if (!root) continue;
23
57
  apps += 1;
24
- const filters = kit.providersOf(root, 'APP_FILTER', '@nestjs/core');
25
- const anchor = filters[0]?.node ?? root.sourceFile.statements[0] ?? root.sourceFile;
26
- if (filters.length !== 1) {
27
- violations.push({ ruleId: RULE, path: root.rel, ...kit.at(root.rel, root.sourceFile, anchor), app: app.name,
28
- message: `App ${app.name} provides ${filters.length} APP_FILTER entries; an api app provides exactly one, the ExceptionFilter of platform/errors.` });
29
- }
30
- for (const filter of filters) {
31
- if (!filter.useClass || !inErrors(filter.checker, filter.useClass)) {
32
- violations.push({ ruleId: RULE, path: root.rel, ...kit.at(root.rel, root.sourceFile, filter.node), app: app.name,
33
- message: 'The APP_FILTER must be `useClass` of the filter declared in platform/errors; any other filter can let an undeclared error reach the client unmasked.' });
34
- }
35
- }
58
+ violations.push(...appFilterViolations(kit, inErrors, app, root));
36
59
  }
37
60
  let graphql = 0;
38
61
  for (const file of graph.files.values()) {
39
62
  const checker = kit.checkerOf(file.sourceFile);
40
63
  kit.walk(file.sourceFile, node => {
41
- if (!(ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ['forRoot', 'forRootAsync'].includes(node.expression.name.text)
42
- && kit.isImportOf(checker, node.expression.expression, 'GraphQLModule', '@nestjs/graphql'))) return true;
64
+ if (!isGraphqlRegistration(kit, checker, node)) return;
43
65
  graphql += 1;
44
- let masked = false;
45
- for (const argument of node.arguments) {
46
- kit.walk(argument, inner => {
47
- if ((ts.isPropertyAssignment(inner) || ts.isShorthandPropertyAssignment(inner)) && kit.propertyNameText(inner.name) === 'formatError'
48
- && inErrors(checker, kit.valueOfProperty(inner))) masked = true;
49
- return true;
50
- });
51
- }
52
- if (!masked) {
66
+ if (!passesMaskedFormatError(kit, inErrors, checker, node)) {
53
67
  violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node),
54
68
  message: `GraphQLModule.${node.expression.name.text} must pass the \`formatError\` exported by platform/errors; without it a resolver error reaches the client with its internal message.` });
55
69
  }
56
- return true;
57
70
  });
58
71
  }
59
72
  return { violations, coverage: { status: 'checked', apps, graphqlRegistrations: graphql } };
@@ -1,6 +1,7 @@
1
1
  import { treeOf } from './required-files.mjs';
2
2
  import { allowsFile } from '../allows.mjs';
3
3
  import { isFeTestPath } from '../rules/fe-no-tests.mjs';
4
+ import { byCodeUnit } from '../../lib/list.mjs';
4
5
 
5
6
  /**
6
7
  * R94 `fe-slot-allows` (FE_SLOT_FILE_ROLE). A front-end slot that owns a whole directory (`fe.route`, `fe.feature`,
@@ -23,7 +24,7 @@ export function checkFeSlotAllows({ config, graph }) {
23
24
  const violations = [];
24
25
  let files = 0;
25
26
  const report = (file, message, extra = {}) => violations.push({ ruleId: RULE, path: file, line: 1, column: 1, message, ...extra });
26
- for (const file of [...tree.files].sort()) {
27
+ for (const file of [...tree.files].sort(byCodeUnit)) {
27
28
  if (isFeTestPath(file)) continue; // FE_NO_TESTS's, the one finding of that file
28
29
  const classified = resolver.classifyPath(file);
29
30
  if (classified.status !== 'owned') continue;
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { treeOf } from './required-files.mjs';
3
3
  import { allowsFile } from '../allows.mjs';
4
+ import { byCodeUnit } from '../../lib/list.mjs';
4
5
 
5
6
  /**
6
7
  * R29 `feature-shape` (BE_FEATURE_SHAPE). A feature root holds `index.ts`, `<feature>.module.ts`, `application/`,
@@ -14,6 +15,28 @@ export const FEATURE_SHAPE_RULE_IDS = ['BE_FEATURE_SHAPE'];
14
15
 
15
16
  const RULE = 'BE_FEATURE_SHAPE';
16
17
 
18
+ // {message, extra} for one feature file the slot answer refuses, or null when it sits where a slot allows it.
19
+ const featureFileFinding = (file, owner, verdict) => {
20
+ const feature = path.posix.basename(owner.root);
21
+ if (verdict.slot === owner.slot) {
22
+ // A file the feature root slot itself allows is in place (the cli feature root allows its <group>/ folders); any other file
23
+ // deeper than the root sits in a directory no slot claims.
24
+ // A directory entry (`transport/`, `application/`) is held by its own slots: below it, only those slots admit a file.
25
+ if (verdict.allowed && !String(verdict.entry ?? '').endsWith('/')) return null;
26
+ if (verdict.relative.includes('/')) {
27
+ const folder = verdict.relative.split('/')[0];
28
+ return { message: `${file} sits in ${owner.root}/${folder}/, which no slot owns; a feature root holds only ${verdict.allows.join(', ')}, and transport/ holds one folder per protocol a slot names. Move it to application/ or a transport/<protocol>/ folder, or delete it.`,
29
+ extra: { feature, folder } };
30
+ }
31
+ if (verdict.allowed) return null;
32
+ return { message: `${file} is not allowed at the root of feature ${feature}; the root holds only ${verdict.allows.join(', ')}. Move it under application/ or transport/<protocol>/.`, extra: { feature } };
33
+ }
34
+ if (verdict.allowed) return null;
35
+ const where = `${verdict.slot} (${verdict.root})`;
36
+ const forbidden = verdict.forbiddenBy ? ` ${verdict.forbiddenBy} is forbidden there;` : '';
37
+ return { message: `${file} is not allowed in ${where};${forbidden} that folder holds only ${verdict.allows.join(', ')}. Rename it to an allowed role or move it to the folder its role belongs to.`, extra: { feature, slot: verdict.slot } };
38
+ };
39
+
17
40
  export function checkFeatureShape({ config, graph }) {
18
41
  const resolver = graph.resolver;
19
42
  const tree = treeOf(config.root);
@@ -21,32 +44,15 @@ export function checkFeatureShape({ config, graph }) {
21
44
  const features = new Set();
22
45
  let files = 0;
23
46
  const report = (file, message, extra = {}) => violations.push({ ruleId: RULE, path: file, line: 1, column: 1, message, ...extra });
24
- for (const file of [...tree.files].sort()) {
47
+ for (const file of [...tree.files].sort(byCodeUnit)) {
25
48
  const owner = resolver.ownerOf(file);
26
49
  if (!owner || resolver.slot(owner.slot)?.tier !== 'feature') continue;
27
50
  features.add(owner.root);
28
51
  files += 1;
29
52
  const verdict = allowsFile(resolver, file);
30
53
  if (!verdict) continue;
31
- const feature = path.posix.basename(owner.root);
32
- const where = `${verdict.slot} (${verdict.root})`;
33
- if (verdict.slot === owner.slot) {
34
- // A file the feature root slot itself allows is in place (the cli feature root allows its <group>/ folders); any other file
35
- // deeper than the root sits in a directory no slot claims.
36
- // A directory entry (`transport/`, `application/`) is held by its own slots: below it, only those slots admit a file.
37
- if (verdict.allowed && !String(verdict.entry ?? '').endsWith('/')) continue;
38
- if (verdict.relative.includes('/')) {
39
- const folder = verdict.relative.split('/')[0];
40
- report(file, `${file} sits in ${owner.root}/${folder}/, which no slot owns; a feature root holds only ${verdict.allows.join(', ')}, and transport/ holds one folder per protocol a slot names. Move it to application/ or a transport/<protocol>/ folder, or delete it.`,
41
- { feature, folder });
42
- } else if (!verdict.allowed) {
43
- report(file, `${file} is not allowed at the root of feature ${feature}; the root holds only ${verdict.allows.join(', ')}. Move it under application/ or transport/<protocol>/.`, { feature });
44
- }
45
- continue;
46
- }
47
- if (verdict.allowed) continue;
48
- const forbidden = verdict.forbiddenBy ? ` ${verdict.forbiddenBy} is forbidden there;` : '';
49
- report(file, `${file} is not allowed in ${where};${forbidden} that folder holds only ${verdict.allows.join(', ')}. Rename it to an allowed role or move it to the folder its role belongs to.`, { feature, slot: verdict.slot });
54
+ const finding = featureFileFinding(file, owner, verdict);
55
+ if (finding) report(file, finding.message, finding.extra);
50
56
  }
51
57
  return { violations, coverage: { status: 'checked', features: features.size, files } };
52
58
  }
@@ -15,7 +15,7 @@ const cache = new Map(); // only the installed knowledge file is cached; an expl
15
15
  const EXACT_NAME = /^[^\\/*?[\]{}\s]+$/;
16
16
 
17
17
  function unavailable(file, detail) {
18
- return Error(`ARCH_KNOWLEDGE_UNAVAILABLE: ${file} ${detail}`);
18
+ return new Error(`ARCH_KNOWLEDGE_UNAVAILABLE: ${file} ${detail}`);
19
19
  }
20
20
 
21
21
  function load(file) {