@starci/hfs 4.0.10 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/emit/operations.mjs +1 -1
  3. package/emit/type-schema.mjs +2 -1
  4. package/lint/run.mjs +3 -2
  5. package/package.json +1 -1
  6. package/report/order.mjs +2 -0
  7. package/runtime/config.example.yaml +187 -0
  8. package/runtime/engine/by-code-unit.mjs +12 -0
  9. package/runtime/engine/config.mjs +221 -239
  10. package/runtime/engine/invalid-config.mjs +5 -5
  11. package/runtime/engine/model-config.mjs +90 -0
  12. package/runtime/engine/orca-config.mjs +3 -1
  13. package/runtime/engine/removed-vocabulary.mjs +28 -0
  14. package/runtime/engine/resources-config.mjs +52 -0
  15. package/runtime/engine/runtime-root.mjs +17 -0
  16. package/runtime/engine/secrets.mjs +35 -31
  17. package/runtime/engine/sonar-config.mjs +21 -0
  18. package/runtime/engine/temp-root.mjs +32 -0
  19. package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
  20. package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
  21. package/runtime/knowledge/hfs/rules.yaml +98 -65
  22. package/runtime/knowledge/hfs/slots.yaml +11 -14
  23. package/runtime/knowledge/patterns/be/api.yaml +7 -24
  24. package/runtime/knowledge/patterns/be/cli.yaml +3 -12
  25. package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
  26. package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
  27. package/runtime/modules/kernel/failure-codes.yaml +1 -41
  28. package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
  29. package/runtime/modules/models/registry.yaml +25 -359
  30. package/runtime/modules/models/runtimes.yaml +92 -256
  31. package/runtime/modules/models/tiers.yaml +65 -0
  32. package/runtime/scripts/api/fs/claim-file.mjs +30 -0
  33. package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
  34. package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
  35. package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
  36. package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
  37. package/runtime/scripts/api/git/lib.mjs +34 -1
  38. package/runtime/scripts/api/process/resolve-real-tool.mjs +14 -8
  39. package/runtime/scripts/api/process/run-program.mjs +2 -1
  40. package/runtime/scripts/api/sops/lib.mjs +198 -120
  41. package/runtime/scripts/hfs/allows.mjs +3 -3
  42. package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
  43. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
  44. package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
  45. package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
  46. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
  47. package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
  48. package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
  49. package/runtime/scripts/hfs/architecture/config.mjs +95 -80
  50. package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
  51. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
  52. package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
  53. package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
  54. package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
  55. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
  56. package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
  57. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
  58. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
  59. package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
  60. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
  61. package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
  62. package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
  63. package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
  64. package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
  65. package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
  66. package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
  67. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
  68. package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
  69. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
  70. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
  71. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
  72. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
  73. package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
  74. package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
  75. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
  76. package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
  77. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
  78. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
  79. package/runtime/scripts/hfs/architecture/index.mjs +158 -102
  80. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
  81. package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
  82. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
  83. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
  84. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
  85. package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
  86. package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
  87. package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
  88. package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
  89. package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
  90. package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
  91. package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
  92. package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
  93. package/runtime/scripts/hfs/architecture/required-files.mjs +100 -68
  94. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
  95. package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
  96. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
  97. package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
  98. package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
  99. package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
  100. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
  101. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
  102. package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
  103. package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
  104. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
  105. package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
  106. package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
  107. package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
  108. package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
  109. package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
  110. package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
  111. package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
  112. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
  113. package/runtime/scripts/hfs/architecture.mjs +3 -4
  114. package/runtime/scripts/hfs/check.mjs +61 -70
  115. package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
  116. package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
  117. package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
  118. package/runtime/scripts/hfs/edition-slots.mjs +5 -3
  119. package/runtime/scripts/hfs/linear-text.mjs +30 -0
  120. package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
  121. package/runtime/scripts/hfs/path-findings.mjs +58 -38
  122. package/runtime/scripts/hfs/pin-findings.mjs +31 -0
  123. package/runtime/scripts/hfs/repo-identity.mjs +5 -2
  124. package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
  125. package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
  126. package/runtime/scripts/hfs/rules/cli.mjs +5 -1
  127. package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
  128. package/runtime/scripts/hfs/rules/contract.mjs +35 -27
  129. package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
  130. package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
  131. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
  132. package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
  133. package/runtime/scripts/hfs/rules/database.mjs +36 -23
  134. package/runtime/scripts/hfs/rules/deps.mjs +64 -32
  135. package/runtime/scripts/hfs/rules/docker.mjs +113 -65
  136. package/runtime/scripts/hfs/rules/edition.mjs +118 -111
  137. package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
  138. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
  139. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
  140. package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
  141. package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
  142. package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
  143. package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
  144. package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
  145. package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
  146. package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
  147. package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
  148. package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
  149. package/runtime/scripts/hfs/rules/saga.mjs +128 -61
  150. package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
  151. package/runtime/scripts/hfs/rules/services.mjs +34 -16
  152. package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
  153. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
  154. package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
  155. package/runtime/scripts/hfs/secret.mjs +59 -48
  156. package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
  157. package/runtime/scripts/hfs/slot-classify.mjs +65 -0
  158. package/runtime/scripts/hfs/slot-errors.mjs +11 -0
  159. package/runtime/scripts/hfs/slot-imports.mjs +76 -0
  160. package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
  161. package/runtime/scripts/hfs/slot-match.mjs +153 -0
  162. package/runtime/scripts/hfs/slot-path.mjs +8 -0
  163. package/runtime/scripts/hfs/slot-required.mjs +76 -0
  164. package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
  165. package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
  166. package/runtime/scripts/hfs/slots.mjs +72 -693
  167. package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
  168. package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
  169. package/runtime/scripts/hfs/tree.mjs +19 -14
  170. package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
  171. package/runtime/scripts/hfs/view.mjs +1 -1
  172. package/runtime/scripts/lib/dockerfile.mjs +56 -33
  173. package/runtime/scripts/lib/event-contract.mjs +17 -13
  174. package/runtime/scripts/lib/git.mjs +2 -2
  175. package/runtime/scripts/lib/graphql-contract.mjs +158 -322
  176. package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
  177. package/runtime/scripts/lib/i18n.mjs +3 -2
  178. package/runtime/scripts/lib/in-order.mjs +71 -0
  179. package/runtime/scripts/lib/language.mjs +3 -3
  180. package/runtime/scripts/lib/list.mjs +8 -1
  181. package/runtime/scripts/lib/path-key.mjs +38 -5
  182. package/runtime/scripts/lib/pid-alive.mjs +7 -0
  183. package/runtime/scripts/lib/regex.mjs +1 -1
  184. package/runtime/scripts/lib/same-text.mjs +1 -1
  185. package/runtime/scripts/lib/secret-patterns.mjs +6 -2
  186. package/runtime/scripts/lib/sleep-sync.mjs +2 -2
  187. package/runtime/scripts/lib/sops-envelope.mjs +95 -48
  188. package/runtime/scripts/lib/stack-services.mjs +1 -2
  189. package/runtime/scripts/lib/ts-ast.mjs +9 -0
  190. package/runtime/scripts/lib/walk.mjs +6 -1
  191. package/scaffold/add-cli-lite.mjs +2 -1
  192. package/scaffold/add-table.mjs +2 -2
  193. package/scaffold/add.mjs +3 -2
  194. package/scaffold/app.mjs +4 -2
  195. package/scaffold/edition-gate.mjs +25 -25
  196. package/scaffold/lite-exports.mjs +2 -1
  197. package/scaffold/service.mjs +5 -4
  198. package/sync/hygiene.mjs +15 -9
  199. package/sync/index.mjs +7 -3
  200. package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
  201. package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
  202. package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
  203. package/templates/app/ci-workflows/sonar-steps.yml +2 -2
  204. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
  205. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
  206. package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
  207. package/templates/app/starciwork.gitignore +1 -1
  208. package/templates/be/image/api/Dockerfile +1 -1
  209. package/templates/be/image/cli/Dockerfile +1 -1
  210. package/templates/be/image/worker/Dockerfile +1 -1
  211. package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
  212. package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
  213. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
  214. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
  215. package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
  216. package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
  217. package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
  218. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
  219. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
  220. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
  221. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
  222. package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
  223. package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
  224. package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
  225. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
  226. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  227. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
  228. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
  229. package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
  230. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  231. package/templates/fe/image/next/Dockerfile +2 -1
  232. package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
  233. package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
  234. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
  235. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
  236. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
  237. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
  238. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
  239. package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
  240. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
  241. package/upgrade/index.mjs +9 -2
  242. package/runtime/engine/admission.mjs +0 -308
  243. package/runtime/engine/canonical-json.mjs +0 -10
  244. package/runtime/engine/db/blob.mjs +0 -315
  245. package/runtime/engine/db/ledger-paths.mjs +0 -83
  246. package/runtime/engine/db/ledger.mjs +0 -1118
  247. package/runtime/engine/db/machine-connection.mjs +0 -135
  248. package/runtime/engine/db/machine-schema.mjs +0 -84
  249. package/runtime/engine/db/machine.mjs +0 -1367
  250. package/runtime/engine/db/migrations/machine/0001-init.sql +0 -924
  251. package/runtime/engine/db/migrations/runtime/0001-init.sql +0 -1108
  252. package/runtime/engine/db/provider-reservations.mjs +0 -101
  253. package/runtime/engine/digest.mjs +0 -16
  254. package/runtime/engine/refuse.mjs +0 -11
  255. package/runtime/modules/ops/_labels.yaml +0 -53
  256. package/runtime/scripts/api/node/lib.mjs +0 -14
  257. package/runtime/scripts/api/node/spawn-node.mjs +0 -6
  258. package/runtime/scripts/api/process/lib.mjs +0 -111
  259. package/runtime/scripts/api/process/owned-process.mjs +0 -78
  260. package/runtime/scripts/api/process/stop-owned-process.mjs +0 -9
  261. package/runtime/scripts/connectors/lib.mjs +0 -488
  262. package/runtime/scripts/lib/clip.mjs +0 -19
  263. package/runtime/scripts/lib/display-names.mjs +0 -259
  264. package/runtime/scripts/lib/example-refs.mjs +0 -158
  265. package/runtime/scripts/lib/json-schema.mjs +0 -52
  266. package/runtime/scripts/lib/process-identity.mjs +0 -6
  267. package/runtime/scripts/lib/read-yaml.mjs +0 -18
  268. package/runtime/scripts/lib/redact.mjs +0 -161
  269. package/runtime/scripts/lib/sleep.mjs +0 -7
  270. package/runtime/scripts/lib/source-phrases.mjs +0 -34
  271. package/runtime/scripts/lib/sqlite.mjs +0 -21
@@ -0,0 +1,190 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { canonical, isInside } from './config.mjs';
4
+ import { readJsonFile as readJson } from '../../lib/json.mjs';
5
+ import { exportTargetStrings, isUnshadowedCommonJsRequire, relativePath } from './typescript.mjs';
6
+ import { byCodeUnit } from '../../lib/list.mjs';
7
+ import { sameOrUnder } from '../../lib/path-key.mjs';
8
+
9
+ function grammarExport(manifest, packageName, specifier) {
10
+ const key = specifier === packageName ? '.' : `.${specifier.slice(packageName.length)}`;
11
+ const value = manifest?.exports?.[key];
12
+ const targets = exportTargetStrings(value);
13
+ return targets.length > 0 && targets.every(target => target.startsWith('./') && !target.includes('\\') && !target.split('/').includes('..'));
14
+ }
15
+
16
+ function grammarExportTargets(manifest, packageRoot, packageName, specifier) {
17
+ const key = specifier === packageName ? '.' : `.${specifier.slice(packageName.length)}`;
18
+ return exportTargetStrings(manifest?.exports?.[key]).filter(target => target.startsWith('./') && !target.includes('\\') && !target.split('/').includes('..'))
19
+ .map(target => canonical(path.resolve(packageRoot, target)));
20
+ }
21
+
22
+ function literalModules(ts, sourceFile, checker) {
23
+ const modules = [];
24
+ const visit = node => {
25
+ if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) {
26
+ modules.push({ node: node.moduleSpecifier, specifier: node.moduleSpecifier.text });
27
+ } else if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)
28
+ && node.moduleReference.expression && ts.isStringLiteralLike(node.moduleReference.expression)) {
29
+ modules.push({ node: node.moduleReference.expression, specifier: node.moduleReference.expression.text });
30
+ } else if (ts.isCallExpression(node) && node.arguments.length > 0 && ts.isStringLiteralLike(node.arguments[0])
31
+ && (node.expression.kind === ts.SyntaxKind.ImportKeyword || isUnshadowedCommonJsRequire(ts, checker, node.expression))) {
32
+ modules.push({ node: node.arguments[0], specifier: node.arguments[0].text });
33
+ }
34
+ ts.forEachChild(node, visit);
35
+ };
36
+ visit(sourceFile);
37
+ return modules;
38
+ }
39
+
40
+ function cssImports(content) {
41
+ const imports = [];
42
+ const withoutComments = content.replace(/\/\*[\s\S]*?\*\//g, match => match.replace(/[^\r\n]/g, ' '));
43
+ const pattern = new RegExp([String.raw`@import\s+(?:url\(\s*(?:(["'])`, String.raw`(.*?)\1|([^\s)]+))\s*\)|(["'])(.*?)\4)`].join(''), 'gi');
44
+ for (const match of withoutComments.matchAll(pattern)) {
45
+ imports.push({ specifier: match[2] ?? match[3] ?? match[5], index: match.index });
46
+ }
47
+ return imports;
48
+ }
49
+
50
+ function installRootFor(config) {
51
+ return config.packageRoot ?? config.root;
52
+ }
53
+
54
+ function installedPackageRoot(installRoot, packageName, from) {
55
+ for (let dir = from; isInside(installRoot, dir); dir = path.dirname(dir)) {
56
+ const candidate = path.join(dir, 'node_modules', ...packageName.split('/'));
57
+ if (fs.existsSync(path.join(candidate, 'package.json'))) return candidate;
58
+ if (dir === installRoot || path.dirname(dir) === dir) break;
59
+ }
60
+ return path.join(installRoot, 'node_modules', ...packageName.split('/'));
61
+ }
62
+
63
+ function grammarConsumers(config, grammar, local, installRoot) {
64
+ return grammar.consumerManifests.map(relative => {
65
+ const root = path.dirname(path.join(config.root, ...relative.split('/')));
66
+ const packageRoot = local?.root ?? installedPackageRoot(installRoot, grammar.package, root);
67
+ return { relative, root, packageRoot, packageManifest: readJson(path.join(packageRoot, 'package.json')),
68
+ manifest: readJson(path.join(config.root, ...relative.split('/'))) ?? {} };
69
+ });
70
+ }
71
+
72
+ function uniqueGrammarInstalls(consumers) {
73
+ return [...new Map(consumers.map(consumer => [consumer.packageRoot, consumer])).values()];
74
+ }
75
+
76
+ function grammarPackageRoot(installs, local, installRoot, packageName) {
77
+ return installs[0]?.packageRoot ?? local?.root ?? path.join(installRoot, 'node_modules', ...packageName.split('/'));
78
+ }
79
+
80
+ function addInstallContractProblems(install, installs, grammar, configRoot, problems) {
81
+ const where = installs.length > 1 ? ` (as ${install.relative} resolves it, ${relativePath(configRoot, install.packageRoot)})` : '';
82
+ if (install.packageManifest?.name !== grammar.package) problems.push(`installed package ${grammar.package} is unavailable or has a different name${where}`);
83
+ if (!grammarExport(install.packageManifest, grammar.package, grammar.entry)) problems.push(`${grammar.entry} is not a safe declared package export${where}`);
84
+ if (!grammarExport(install.packageManifest, grammar.package, grammar.styleEntry)) problems.push(`${grammar.styleEntry} is not a safe declared style export${where}`);
85
+ const actualPeers = Object.keys(install.packageManifest?.peerDependencies ?? {}).sort(byCodeUnit);
86
+ const selectedPeers = [...grammar.peers].sort(byCodeUnit);
87
+ if (JSON.stringify(actualPeers) !== JSON.stringify(selectedPeers)) {
88
+ problems.push(`${grammar.package} peerDependencies must exactly match the selected peers ${selectedPeers.join(', ')}${where}`);
89
+ }
90
+ }
91
+
92
+ function addConsumerDependencyProblems(grammar, consumers, problems) {
93
+ for (const consumer of consumers) if (!Object.hasOwn(consumer.manifest.dependencies ?? {}, grammar.package)) {
94
+ problems.push(`${consumer.relative} does not declare ${grammar.package} as a runtime dependency`);
95
+ }
96
+ for (const peer of grammar.peers) {
97
+ for (const consumer of consumers) if (!Object.hasOwn(consumer.manifest.dependencies ?? {}, peer) && !Object.hasOwn(consumer.manifest.peerDependencies ?? {}, peer)) {
98
+ problems.push(`${consumer.relative} does not declare Grammar peer ${peer}`);
99
+ }
100
+ }
101
+ }
102
+
103
+ function addGrammarStyleProblems(config, grammar, problems, styleBypasses) {
104
+ for (const source of grammar.styleSources) {
105
+ const content = fs.readFileSync(path.join(config.root, ...source.split('/')), 'utf8');
106
+ const grammarStyles = cssImports(content).filter(item => item.specifier === grammar.package || item.specifier.startsWith(`${grammar.package}/`));
107
+ if (!grammarStyles.some(item => item.specifier === grammar.styleEntry)) problems.push(`${source} does not import ${grammar.styleEntry}`);
108
+ for (const imported of grammarStyles) if (imported.specifier !== grammar.styleEntry) {
109
+ problems.push(`${source} imports Grammar style ${imported.specifier} outside the selected style entry`);
110
+ styleBypasses.push({ ruleId: 'ARCH_GRAMMAR_EXPORT_BYPASS', path: source,
111
+ line: content.slice(0, imported.index).split(/\r?\n/u).length, column: 1, package: grammar.package, specifier: imported.specifier,
112
+ message: `Style source must use the selected Grammar style entry ${grammar.styleEntry}; ${imported.specifier} is outside that contract.` });
113
+ }
114
+ }
115
+ }
116
+
117
+ function grammarOwner(consumers, fileName) {
118
+ return consumers.filter(consumer => isInside(consumer.root, fileName))
119
+ .sort((a, b) => b.root.length - a.root.length)[0] ?? null;
120
+ }
121
+
122
+ function sourceEdges(context, sourceFile) {
123
+ return new Map((context.edges.get(path.resolve(sourceFile.fileName)) ?? []).map(edge => [edge.node.getStart(sourceFile), edge]));
124
+ }
125
+
126
+ function grammarReferenceAllowed(reference, sourceFile, owner, edgesByStart, grammar, allowed) {
127
+ if (!allowed.has(reference.specifier)) return false;
128
+ const edge = edgesByStart.get(reference.node.getStart(sourceFile));
129
+ const expected = grammarExportTargets(owner.packageManifest, owner.packageRoot, grammar.package, reference.specifier);
130
+ return !edge || expected.includes(canonical(edge.to));
131
+ }
132
+
133
+ function reportGrammarReference(scan, sourceFile, reference, owner, edgesByStart) {
134
+ const { config, grammar, allowed, violations, makeViolation } = scan;
135
+ if (!sameOrUnder(reference.specifier, grammar.package)) return;
136
+ if (grammarReferenceAllowed(reference, sourceFile, owner, edgesByStart, grammar, allowed)) return;
137
+ violations.push(makeViolation(config, sourceFile, reference.node, 'ARCH_GRAMMAR_EXPORT_BYPASS',
138
+ `Product source must import the selected Grammar code entry ${grammar.entry} or style entry ${grammar.styleEntry}; ${reference.specifier} is outside that contract.`,
139
+ { specifier: reference.specifier, package: grammar.package }));
140
+ }
141
+
142
+ function inspectGrammarSource(scan, sourceFile) {
143
+ const { context, consumers, installs } = scan;
144
+ if (installs.some(install => isInside(install.packageRoot, sourceFile.fileName))) return;
145
+ const owner = grammarOwner(consumers, sourceFile.fileName);
146
+ if (!owner) return;
147
+ const edgesByStart = sourceEdges(context, sourceFile);
148
+ const references = literalModules(context.ts, sourceFile, context.checkerFor(sourceFile.fileName));
149
+ for (const reference of references) reportGrammarReference(scan, sourceFile, reference, owner, edgesByStart);
150
+ }
151
+
152
+ function addGrammarContractFinding(config, grammar, manifestFile, problems, violations) {
153
+ if (!problems.length) return;
154
+ violations.push({
155
+ ruleId: 'ARCH_GRAMMAR_CONTRACT_INVALID',
156
+ path: fs.existsSync(manifestFile) && isInside(config.root, manifestFile) ? relativePath(config.root, manifestFile) : 'package.json',
157
+ line: 1,
158
+ column: 1,
159
+ package: grammar.package,
160
+ message: `Grammar contract is invalid: ${problems.join('; ')}.`,
161
+ });
162
+ }
163
+
164
+ export function checkGrammar(config, context, makeViolation) {
165
+ const grammar = config.frontend.grammar;
166
+ if (!grammar) return [];
167
+ const violations = [];
168
+ const local = context.workspaces.find(workspace => workspace.name === grammar.package);
169
+ // Node resolution per consumer: in an npm-workspaces monorepo a consumer whose range differs from the
170
+ // hoisted copy gets its own apps/<app>/node_modules/<package> (one consumer: apps/app on 0.5.0 beside a hoisted
171
+ // 0.4.11). Every consumer is judged against the copy it actually resolves, never the hoisted one alone.
172
+ // An app installs once, at its root (config.packageRoot): a side folder has no node_modules of its own.
173
+ const installRoot = installRootFor(config);
174
+ const consumers = grammarConsumers(config, grammar, local, installRoot);
175
+ const installs = uniqueGrammarInstalls(consumers);
176
+ const packageRoot = grammarPackageRoot(installs, local, installRoot, grammar.package);
177
+ const manifestFile = path.join(packageRoot, 'package.json');
178
+ readJson(manifestFile);
179
+ const contractProblems = [];
180
+ const styleBypasses = [];
181
+ for (const install of installs) addInstallContractProblems(install, installs, grammar, config.root, contractProblems);
182
+ addConsumerDependencyProblems(grammar, consumers, contractProblems);
183
+ addGrammarStyleProblems(config, grammar, contractProblems, styleBypasses);
184
+ addGrammarContractFinding(config, grammar, manifestFile, contractProblems, violations);
185
+ const allowed = new Set([grammar.entry, grammar.styleEntry]);
186
+ violations.push(...styleBypasses);
187
+ const scan = { config, context, grammar, consumers, installs, allowed, violations, makeViolation };
188
+ for (const sourceFile of context.files) inspectGrammarSource(scan, sourceFile);
189
+ return violations;
190
+ }
@@ -0,0 +1,186 @@
1
+ import path from 'node:path';
2
+ import { reachableViolation, relativePath } from './typescript.mjs';
3
+
4
+ function hasClientDirective(ts, sourceFile) {
5
+ return sourceFile.statements.find(statement => ts.isExpressionStatement(statement)
6
+ && ts.isStringLiteral(statement.expression) && statement.expression.text === 'use client');
7
+ }
8
+
9
+ function bindImportClause(ts, clause, edge, bindings) {
10
+ if (clause.name && !clause.isTypeOnly) bindings.set(clause.name.text, edge);
11
+ if (!clause.namedBindings) return;
12
+ if (ts.isNamespaceImport(clause.namedBindings)) bindings.set(clause.namedBindings.name.text, edge);
13
+ if (!ts.isNamedImports(clause.namedBindings)) return;
14
+ for (const element of clause.namedBindings.elements) {
15
+ if (!element.isTypeOnly && !clause.isTypeOnly) bindings.set(element.name.text, edge);
16
+ }
17
+ }
18
+
19
+ function importBindings(ts, sourceFile, edges) {
20
+ const byStart = new Map(edges.map(edge => [edge.node.getStart(sourceFile), edge]));
21
+ const bindings = new Map();
22
+ for (const statement of sourceFile.statements) {
23
+ if (!ts.isImportDeclaration(statement) || !statement.importClause || !ts.isStringLiteralLike(statement.moduleSpecifier)) continue;
24
+ const edge = byStart.get(statement.moduleSpecifier.getStart(sourceFile));
25
+ if (!edge) continue;
26
+ bindImportClause(ts, statement.importClause, edge, bindings);
27
+ }
28
+ return bindings;
29
+ }
30
+
31
+ function importedNames(ts, sourceFile, moduleName) {
32
+ const names = new Map();
33
+ for (const statement of sourceFile.statements) {
34
+ if (!ts.isImportDeclaration(statement) || statement.moduleSpecifier.text !== moduleName || !statement.importClause || statement.importClause.isTypeOnly) continue;
35
+ const named = statement.importClause.namedBindings;
36
+ if (!named || !ts.isNamedImports(named)) continue;
37
+ for (const element of named.elements) if (!element.isTypeOnly) names.set(element.name.text, element.propertyName?.text ?? element.name.text);
38
+ }
39
+ return names;
40
+ }
41
+
42
+ function jsxRootName(ts, tagName) {
43
+ if (ts.isIdentifier(tagName)) return tagName.text;
44
+ if (ts.isPropertyAccessExpression(tagName)) {
45
+ let current = tagName;
46
+ while (ts.isPropertyAccessExpression(current)) current = current.expression;
47
+ return ts.isIdentifier(current) ? current.text : null;
48
+ }
49
+ return null;
50
+ }
51
+
52
+ function addRouteVariableFunctions(ts, statement, variables) {
53
+ for (const declaration of statement.declarationList.declarations) {
54
+ if (ts.isIdentifier(declaration.name) && declaration.initializer
55
+ && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) variables.set(declaration.name.text, declaration.initializer);
56
+ }
57
+ }
58
+
59
+ function recordRouteStatement(ts, statement, state) {
60
+ if (ts.isVariableStatement(statement)) addRouteVariableFunctions(ts, statement, state.variables);
61
+ if (ts.isFunctionDeclaration(statement) && statement.name) state.functions.set(statement.name.text, statement);
62
+ if (ts.isFunctionDeclaration(statement) && statement.modifiers?.some(item => item.kind === ts.SyntaxKind.DefaultKeyword)) state.directDefault = statement;
63
+ if (ts.isExportAssignment(statement) && !statement.isExportEquals) state.exportNode = statement;
64
+ }
65
+
66
+ function routeFunction(ts, sourceFile) {
67
+ const state = { variables: new Map(), functions: new Map(), directDefault: null, exportNode: null };
68
+ for (const statement of sourceFile.statements) recordRouteStatement(ts, statement, state);
69
+ if (state.directDefault) return { fn: state.directDefault, exportNode: state.directDefault };
70
+ if (!state.exportNode || !ts.isIdentifier(state.exportNode.expression)) return { fn: null, exportNode: state.exportNode };
71
+ return { fn: state.variables.get(state.exportNode.expression.text) ?? state.functions.get(state.exportNode.expression.text) ?? null, exportNode: state.exportNode };
72
+ }
73
+
74
+ function routePageRoots(context, sourceFile, featureRoots) {
75
+ const workspace = context.workspaceOf(sourceFile.fileName);
76
+ if (!workspace) return featureRoots.map(root => path.join(root, 'pages'));
77
+ const local = featureRoots.filter(root => context.workspaceOf(root) === workspace);
78
+ return (local.length ? local : featureRoots).map(root => path.join(root, 'pages'));
79
+ }
80
+
81
+ function routingTermination(ts, statement, navigation) {
82
+ if (ts.isThrowStatement(statement)) return true;
83
+ if (ts.isBlock(statement)) return statement.statements.length > 0 && routingTermination(ts, statement.statements.at(-1), navigation);
84
+ const expression = ts.isExpressionStatement(statement) ? statement.expression : (ts.isReturnStatement(statement) && statement.expression) || null;
85
+ return Boolean(expression && ts.isCallExpression(expression) && ts.isIdentifier(expression.expression)
86
+ && ['redirect', 'notFound'].includes(navigation.get(expression.expression.text)));
87
+ }
88
+
89
+ function routingGuard(ts, node, navigation) {
90
+ return ts.isIfStatement(node) && !node.elseStatement && routingTermination(ts, node.thenStatement, navigation);
91
+ }
92
+
93
+ function routeNavigationCall(ts, node, navigation) {
94
+ return ts.isCallExpression(node) && ts.isIdentifier(node.expression)
95
+ && ['redirect', 'notFound'].includes(navigation.get(node.expression.text));
96
+ }
97
+
98
+ function recordRouteNode(ts, node, state, insideNavigationArgument) {
99
+ if (ts.isJsxSelfClosingElement(node) || ts.isJsxElement(node)) state.jsx.push(node);
100
+ if (ts.isIfStatement(node) || ts.isSwitchStatement(node) || (ts.isConditionalExpression(node) && !insideNavigationArgument)) state.decisions.push(node);
101
+ }
102
+
103
+ function visitRouteNode(ts, node, state, insideNavigationArgument = false) {
104
+ recordRouteNode(ts, node, state, insideNavigationArgument);
105
+ if (routeNavigationCall(ts, node, state.navigation)) {
106
+ state.adapterCalls.push(node);
107
+ for (const argument of node.arguments) visitRouteNode(ts, argument, state, true);
108
+ return;
109
+ }
110
+ ts.forEachChild(node, child => visitRouteNode(ts, child, state, insideNavigationArgument));
111
+ }
112
+
113
+ function collectRouteNodes(ts, routeFunction, navigation) {
114
+ const state = { jsx: [], adapterCalls: [], decisions: [], navigation };
115
+ visitRouteNode(ts, routeFunction, state);
116
+ return state;
117
+ }
118
+
119
+ function mountedRoutePages(ts, context, sourceFile, featureRoots, bindings, jsx, insideAny) {
120
+ const pages = routePageRoots(context, sourceFile, featureRoots);
121
+ return jsx.filter(node => {
122
+ const opening = ts.isJsxElement(node) ? node.openingElement : node;
123
+ const local = jsxRootName(ts, opening.tagName);
124
+ const edge = local ? bindings.get(local) : null;
125
+ return edge && Boolean(reachableViolation(context.edges, edge, target => insideAny(pages, target), { follow: candidate => candidate.reexport && candidate.runtime }));
126
+ });
127
+ }
128
+
129
+ function reportRouteComposition(at, route, scan, mountedPages) {
130
+ const { config, context, sourceFile, violations, violation } = at;
131
+ const navigationOnly = scan.jsx.length === 0 && scan.adapterCalls.length === 1;
132
+ if (!navigationOnly && (mountedPages.length !== 1 || scan.jsx.length !== 1)) {
133
+ violations.push(violation(config, sourceFile, route.fn, 'FE_ROUTE_ONE_PAGE', 'A page.tsx route must mount exactly one pages-tier component, or be a zero-JSX redirect/notFound adapter.', { ts: context.ts }));
134
+ }
135
+ return navigationOnly;
136
+ }
137
+
138
+ function reportRouteDecisions(at, decisions, navigation, navigationOnly) {
139
+ if (navigationOnly) return;
140
+ const { config, context, sourceFile, violations, violation } = at;
141
+ for (const decision of decisions) if (!routingGuard(context.ts, decision, navigation)) {
142
+ violations.push(violation(config, sourceFile, decision, 'FE_ROUTE_DRAWING_DECISION', 'Visual route files may use terminal redirect/notFound guards but cannot select or omit visual composition.', { ts: context.ts }));
143
+ }
144
+ }
145
+
146
+ function reportClientDirective(config, context, sourceFile, violations, violation) {
147
+ const clientDirective = hasClientDirective(context.ts, sourceFile);
148
+ if (clientDirective) violations.push(violation(config, sourceFile, clientDirective, 'FE_ROUTE_CLIENT_BOUNDARY', 'A page.tsx route adapter cannot own the client boundary.', { ts: context.ts }));
149
+ }
150
+
151
+ function routeHookName(ts, sourceFile, local) {
152
+ return sourceFile.statements.flatMap(statement => ts.isImportDeclaration(statement) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings)
153
+ ? statement.importClause.namedBindings.elements.filter(item => item.name.text === local).map(item => item.name) : [])[0];
154
+ }
155
+
156
+ function reportForbiddenRouteHooks(config, context, sourceFile, navigation, violations, violation) {
157
+ const { ts } = context;
158
+ const forbiddenRouteHooks = new Set(['useRouter', 'usePathname', 'useSearchParams', 'useParams', 'useSelectedLayoutSegment', 'useSelectedLayoutSegments']);
159
+ for (const [local, imported] of navigation) if (forbiddenRouteHooks.has(imported)) {
160
+ const identifier = routeHookName(ts, sourceFile, local);
161
+ if (identifier) violations.push(violation(config, sourceFile, identifier, 'FE_ROUTE_CLIENT_HOOK', `Server route adapter imports ${imported}.`, { ts }));
162
+ }
163
+ }
164
+
165
+ export function checkRoute(config, context, sourceFile, roots, helpers) {
166
+ const { violation, insideAny, importsForSource } = helpers;
167
+ const { ts } = context;
168
+ const violations = [];
169
+ const edges = importsForSource(context, sourceFile.fileName);
170
+ const bindings = importBindings(ts, sourceFile, edges);
171
+ const route = routeFunction(ts, sourceFile);
172
+ if (!route.exportNode || !route.fn) {
173
+ violations.push({ ruleId: 'FE_ROUTE_DEFAULT_EXPORT', path: relativePath(config.root, sourceFile.fileName), line: 1, column: 1,
174
+ message: 'A page.tsx must default-export one local route function.' });
175
+ return violations;
176
+ }
177
+ const navigation = importedNames(ts, sourceFile, 'next/navigation');
178
+ const scan = collectRouteNodes(ts, route.fn, navigation);
179
+ const mountedPages = mountedRoutePages(ts, context, sourceFile, roots.features, bindings, scan.jsx, insideAny);
180
+ const at = { config, context, sourceFile, violations, violation };
181
+ const navigationOnly = reportRouteComposition(at, route, scan, mountedPages);
182
+ reportRouteDecisions(at, scan.decisions, navigation, navigationOnly);
183
+ reportClientDirective(config, context, sourceFile, violations, violation);
184
+ reportForbiddenRouteHooks(config, context, sourceFile, navigation, violations, violation);
185
+ return violations;
186
+ }