@starci/hfs 1.0.1 → 2.0.1

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 (246) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +110 -12
  3. package/bin/hfs.mjs +119 -15
  4. package/emit/compiler.mjs +35 -0
  5. package/emit/contracts.mjs +97 -0
  6. package/emit/operations-worker.mjs +24 -0
  7. package/emit/operations.mjs +126 -0
  8. package/emit/schema-worker.mjs +117 -0
  9. package/emit/static-graph.mjs +670 -0
  10. package/emit/type-schema.mjs +145 -0
  11. package/package.json +4 -1
  12. package/report/sonar.mjs +180 -0
  13. package/runtime/engine/admission.mjs +284 -0
  14. package/runtime/engine/digest.mjs +10 -0
  15. package/runtime/engine/ledger-db.mjs +1245 -0
  16. package/runtime/engine/machine-db.mjs +1484 -0
  17. package/runtime/engine/migrations/machine/0001-init.sql +887 -0
  18. package/runtime/engine/migrations/runtime/0001-init.sql +1072 -0
  19. package/runtime/engine/migrations/runtime/0003-usage-unavailable.sql +13 -0
  20. package/runtime/engine/migrations/runtime/0004-attempt-why.sql +43 -0
  21. package/runtime/engine/plain-object.mjs +5 -0
  22. package/runtime/knowledge/hfs/canon-pins.yaml +16 -58
  23. package/runtime/knowledge/hfs/slots.yaml +405 -137
  24. package/runtime/knowledge/patterns/fe/folder.yaml +309 -0
  25. package/runtime/knowledge/sonar-gate.yaml +85 -0
  26. package/runtime/modules/kernel/failure-codes.yaml +1480 -16
  27. package/runtime/scripts/checks/architecture/backend.mjs +350 -0
  28. package/runtime/scripts/checks/architecture/background-unowned.mjs +107 -0
  29. package/runtime/scripts/checks/architecture/client-reaches-server.mjs +94 -0
  30. package/runtime/scripts/checks/architecture/clones.mjs +200 -0
  31. package/runtime/scripts/checks/architecture/config-unread.mjs +35 -0
  32. package/runtime/scripts/checks/architecture/config.mjs +310 -0
  33. package/runtime/scripts/checks/architecture/connection-map.mjs +208 -0
  34. package/runtime/scripts/checks/architecture/constructor-deps.mjs +100 -0
  35. package/runtime/scripts/checks/architecture/contract-fixture-guard.mjs +128 -0
  36. package/runtime/scripts/checks/architecture/contracts.mjs +792 -0
  37. package/runtime/scripts/checks/architecture/cross-app-duplicate.mjs +73 -0
  38. package/runtime/scripts/checks/architecture/dead-exports.mjs +265 -0
  39. package/runtime/scripts/checks/architecture/default-deny.mjs +129 -0
  40. package/runtime/scripts/checks/architecture/doc-language.mjs +39 -0
  41. package/runtime/scripts/checks/architecture/entrypoint.mjs +57 -0
  42. package/runtime/scripts/checks/architecture/error-codes.mjs +45 -0
  43. package/runtime/scripts/checks/architecture/error-masked.mjs +60 -0
  44. package/runtime/scripts/checks/architecture/fe-slot-allows.mjs +38 -0
  45. package/runtime/scripts/checks/architecture/feature-shape.mjs +49 -0
  46. package/runtime/scripts/checks/architecture/framework-pinned.mjs +83 -0
  47. package/runtime/scripts/checks/architecture/frontend.mjs +995 -0
  48. package/runtime/scripts/checks/architecture/hfs-graph.mjs +61 -0
  49. package/runtime/scripts/checks/architecture/hfs.mjs +521 -0
  50. package/runtime/scripts/checks/architecture/hooks-are-hooks.mjs +88 -0
  51. package/runtime/scripts/checks/architecture/i18n-keys.mjs +146 -0
  52. package/runtime/scripts/checks/architecture/index.mjs +316 -0
  53. package/runtime/scripts/checks/architecture/injection-token-exported.mjs +62 -0
  54. package/runtime/scripts/checks/architecture/machine-ast.mjs +138 -0
  55. package/runtime/scripts/checks/architecture/module-per-transport.mjs +130 -0
  56. package/runtime/scripts/checks/architecture/next-data.mjs +775 -0
  57. package/runtime/scripts/checks/architecture/owners.mjs +89 -0
  58. package/runtime/scripts/checks/architecture/package-shape.mjs +63 -0
  59. package/runtime/scripts/checks/architecture/reachability.mjs +233 -0
  60. package/runtime/scripts/checks/architecture/register-once.mjs +141 -0
  61. package/runtime/scripts/checks/architecture/registration.mjs +319 -0
  62. package/runtime/scripts/checks/architecture/required-files.mjs +172 -0
  63. package/runtime/scripts/checks/architecture/route-files-thin.mjs +97 -0
  64. package/runtime/scripts/checks/architecture/schema-owner.mjs +261 -0
  65. package/runtime/scripts/checks/architecture/size-growth.mjs +73 -0
  66. package/runtime/scripts/checks/architecture/source-names.mjs +607 -0
  67. package/runtime/scripts/checks/architecture/sql-owner.mjs +142 -0
  68. package/runtime/scripts/checks/architecture/sql-tokens.mjs +327 -0
  69. package/runtime/scripts/checks/architecture/symbols.mjs +193 -0
  70. package/runtime/scripts/checks/architecture/test-world-files.mjs +163 -0
  71. package/runtime/scripts/checks/architecture/tiers.mjs +130 -0
  72. package/runtime/scripts/checks/architecture/transport-owner.mjs +112 -0
  73. package/runtime/scripts/checks/architecture/typescript.mjs +500 -0
  74. package/runtime/scripts/checks/architecture/unit-spec-providers.mjs +122 -0
  75. package/runtime/scripts/checks/architecture.mjs +41 -0
  76. package/runtime/scripts/checks/common.mjs +37 -0
  77. package/runtime/scripts/checks/typescript-programs.mjs +82 -0
  78. package/runtime/scripts/lib/artifact-hold.mjs +89 -0
  79. package/runtime/scripts/lib/artifact-store.mjs +103 -0
  80. package/runtime/scripts/lib/fs-kind.mjs +10 -0
  81. package/runtime/scripts/lib/git.mjs +53 -0
  82. package/runtime/scripts/lib/hfs-allows.mjs +57 -0
  83. package/runtime/scripts/lib/hfs-check.mjs +254 -28
  84. package/runtime/scripts/lib/hfs-rules/contract.mjs +126 -0
  85. package/runtime/scripts/lib/hfs-rules/deps.mjs +63 -0
  86. package/runtime/scripts/lib/hfs-rules/fe-no-tests.mjs +47 -0
  87. package/runtime/scripts/lib/hfs-rules/frontend.mjs +124 -0
  88. package/runtime/scripts/lib/hfs-rules/lint-suppression.mjs +34 -0
  89. package/runtime/scripts/lib/hfs-rules/pipeline.mjs +51 -0
  90. package/runtime/scripts/lib/hfs-rules/proof-commands.mjs +64 -0
  91. package/runtime/scripts/lib/hfs-rules/read.mjs +28 -0
  92. package/runtime/scripts/lib/hfs-rules/repo-local-checks.mjs +32 -0
  93. package/runtime/scripts/lib/hfs-rules/secrets.mjs +54 -0
  94. package/runtime/scripts/lib/hfs-rules/spec-placement.mjs +31 -0
  95. package/runtime/scripts/lib/hfs-rules/stacks.mjs +54 -0
  96. package/runtime/scripts/lib/hfs-rules/test-topology.mjs +31 -0
  97. package/runtime/scripts/lib/hfs-slots.mjs +88 -40
  98. package/runtime/scripts/lib/hfs-tree.mjs +80 -0
  99. package/runtime/scripts/lib/hfs-view.mjs +68 -0
  100. package/runtime/scripts/lib/json.mjs +22 -0
  101. package/runtime/scripts/lib/language.mjs +107 -0
  102. package/runtime/scripts/lib/path-key.mjs +2 -0
  103. package/runtime/scripts/lib/redact.mjs +148 -0
  104. package/runtime/scripts/lib/repo-identity.mjs +50 -0
  105. package/runtime/scripts/lib/safe-remove.mjs +179 -0
  106. package/runtime/scripts/lib/secret-patterns.mjs +44 -0
  107. package/runtime/scripts/lib/sleep-sync.mjs +17 -0
  108. package/runtime/scripts/lib/stack-declaration.mjs +52 -0
  109. package/runtime/scripts/lib/stack-services.mjs +217 -0
  110. package/runtime/scripts/lib/test-secrets.mjs +120 -0
  111. package/scaffold/service.mjs +333 -0
  112. package/sync/format.mjs +46 -0
  113. package/sync/hygiene.mjs +56 -24
  114. package/sync/index.mjs +133 -41
  115. package/sync/managed.mjs +170 -0
  116. package/sync/skeleton.mjs +32 -10
  117. package/sync/sonar-key.mjs +13 -0
  118. package/sync/ts-strict.mjs +48 -0
  119. package/templates/{common → be/ci-workflows/github/workflows}/ci.yml +13 -13
  120. package/templates/be/{e2e.yml → ci-workflows/github/workflows/e2e.yml} +1 -0
  121. package/templates/be/hooks/husky/pre-commit +13 -0
  122. package/templates/be/hooks/husky/pre-push +7 -0
  123. package/templates/be/package-scripts/package.json +22 -0
  124. package/templates/be/{sonar-project.properties → quality-config/sonar-project.properties} +2 -3
  125. package/templates/be/skeleton/apps/__app__/src/app.module.ts +14 -4
  126. package/templates/be/skeleton/apps/__app__/src/main.ts +9 -6
  127. package/templates/be/skeleton/scripts/.gitkeep +0 -0
  128. package/templates/be/skeleton/src/features/system-health/application/check-liveness.contracts.ts +2 -0
  129. package/templates/be/skeleton/src/features/system-health/application/check-liveness.handler.ts +22 -0
  130. package/templates/be/skeleton/src/features/system-health/application/check-liveness.query.ts +11 -0
  131. package/templates/be/skeleton/src/features/system-health/index.ts +1 -1
  132. package/templates/be/skeleton/src/features/system-health/system-health.module.ts +3 -3
  133. package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.ts +8 -9
  134. package/templates/be/skeleton/src/features/system-health/transport/http/system-health-http.module.ts +3 -6
  135. package/templates/be/skeleton/src/modules/domain/liveness/index.ts +3 -0
  136. package/templates/be/skeleton/src/modules/domain/liveness/liveness.contracts.ts +9 -0
  137. package/templates/be/skeleton/src/modules/domain/liveness/liveness.module-definition.ts +7 -0
  138. package/templates/be/skeleton/src/modules/domain/liveness/liveness.module.ts +14 -0
  139. package/templates/be/skeleton/src/modules/domain/liveness/liveness.options.ts +2 -0
  140. package/templates/be/skeleton/src/modules/domain/liveness/liveness.service.spec.ts +40 -0
  141. package/templates/be/skeleton/src/modules/domain/liveness/liveness.service.ts +26 -0
  142. package/templates/be/skeleton/src/modules/platform/clock/clock.decorators.ts +9 -0
  143. package/templates/be/skeleton/src/modules/platform/clock/clock.module-definition.ts +7 -0
  144. package/templates/be/skeleton/src/modules/platform/clock/clock.module.ts +19 -0
  145. package/templates/be/skeleton/src/modules/platform/clock/clock.options.ts +2 -0
  146. package/templates/be/skeleton/src/modules/platform/clock/clock.port.ts +5 -0
  147. package/templates/be/skeleton/src/modules/platform/clock/index.ts +4 -0
  148. package/templates/be/skeleton/src/modules/platform/clock/system-clock.service.spec.ts +20 -0
  149. package/templates/be/skeleton/src/modules/platform/clock/system-clock.service.ts +9 -0
  150. package/templates/be/skeleton/src/modules/platform/composition/composition.decorators.ts +8 -0
  151. package/templates/be/skeleton/src/modules/platform/composition/index.ts +2 -0
  152. package/templates/be/skeleton/src/modules/platform/config/config.decorators.ts +9 -0
  153. package/templates/be/skeleton/src/modules/platform/config/index.ts +2 -3
  154. package/templates/be/skeleton/src/modules/platform/config/server.config.ts +1 -1
  155. package/templates/be/skeleton/src/modules/platform/config/server.options.ts +0 -3
  156. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.contracts.ts +5 -0
  157. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.decorators.ts +9 -0
  158. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.handler.ts +27 -0
  159. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.log-events.ts +5 -0
  160. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module-definition.ts +7 -0
  161. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module.ts +18 -0
  162. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.options.ts +2 -0
  163. package/templates/be/skeleton/src/modules/platform/cqrs/index.ts +4 -0
  164. package/templates/be/skeleton/src/modules/platform/errors/error.filter.ts +8 -12
  165. package/templates/be/skeleton/src/modules/platform/errors/errors.log-events.ts +5 -0
  166. package/templates/be/skeleton/src/modules/platform/errors/index.ts +1 -1
  167. package/templates/be/skeleton/src/modules/platform/logging/index.ts +4 -4
  168. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +99 -0
  169. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +38 -0
  170. package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +9 -0
  171. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +7 -0
  172. package/templates/be/skeleton/src/modules/platform/logging/logging.module-definition.ts +7 -0
  173. package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +23 -10
  174. package/templates/be/skeleton/src/modules/platform/logging/logging.options.ts +2 -0
  175. package/templates/be/skeleton/src/modules/platform/logging/logging.port.ts +15 -0
  176. package/templates/be/tool-config/eslint.config.mjs +3 -0
  177. package/templates/be/tool-config/jest.config.js +1 -0
  178. package/templates/be/tool-config/prettierignore +8 -0
  179. package/templates/be/tool-config/prettierrc +1 -0
  180. package/templates/be/tool-config/src/tests/tsconfig.json +5 -0
  181. package/templates/be/tool-config/tsconfig.build.json +5 -0
  182. package/templates/be/tool-config/tsconfig.json +11 -0
  183. package/templates/common/gitignore.base +1 -1
  184. package/templates/fe/ci-workflows/github/workflows/ci.yml +54 -0
  185. package/templates/fe/hooks/husky/pre-commit +16 -0
  186. package/templates/fe/hooks/husky/pre-push +6 -0
  187. package/templates/fe/package-scripts/package.json +17 -0
  188. package/templates/fe/parts/api-client.ts +44 -0
  189. package/templates/fe/parts/api-outcome.ts +7 -0
  190. package/templates/fe/quality-config/sonar-project.properties +8 -0
  191. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/layout.tsx +1 -1
  192. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/not-found.tsx +1 -1
  193. package/templates/fe/skeleton/apps/__app__/src/app/global-error.tsx +1 -1
  194. package/templates/fe/skeleton/scripts/.gitkeep +0 -0
  195. package/templates/fe/skeleton-app/apps/__app__/src/modules/api/client.ts +1 -0
  196. package/templates/fe/skeleton-app/apps/__app__/src/modules/api/index.ts +3 -0
  197. package/templates/fe/skeleton-app/apps/__app__/src/modules/api/outcome.ts +1 -0
  198. package/templates/fe/skeleton-app/apps/__app__/src/modules/i18n/index.ts +4 -0
  199. package/templates/fe/skeleton-shared/apps/__app__/next.config.ts +12 -0
  200. package/templates/fe/skeleton-shared/apps/__app__/src/modules/api/index.ts +2 -0
  201. package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/index.ts +9 -0
  202. package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/request.ts +5 -0
  203. package/templates/fe/skeleton-shared/apps/__app__/src/proxy.ts +5 -0
  204. package/templates/fe/skeleton-shared/packages/__family__-api/package.json +12 -0
  205. package/templates/fe/skeleton-shared/packages/__family__-api/src/client.ts +1 -0
  206. package/templates/fe/skeleton-shared/packages/__family__-api/src/index.ts +3 -0
  207. package/templates/fe/skeleton-shared/packages/__family__-api/src/outcome.ts +1 -0
  208. package/templates/fe/skeleton-shared/packages/__family__-api/tsconfig.json +5 -0
  209. package/templates/fe/skeleton-shared/packages/__family__-i18n/package.json +18 -0
  210. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/app.ts +19 -0
  211. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/index.ts +2 -0
  212. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/proxy.ts +12 -0
  213. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/request.ts +15 -0
  214. package/templates/fe/skeleton-shared/packages/__family__-i18n/tsconfig.json +5 -0
  215. package/templates/fe/tool-config/eslint.config.mjs +3 -0
  216. package/templates/fe/tool-config/prettierignore +10 -0
  217. package/templates/fe/tool-config/prettierrc +1 -0
  218. package/templates/fe/tool-config/stylelint.config.mjs +3 -0
  219. package/templates/fe/tool-config/tsconfig.json +4 -0
  220. package/templates/be/pre-commit +0 -8
  221. package/templates/be/skeleton/apps/__app__/src/__app__.composition.spec.ts +0 -31
  222. package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.spec.ts +0 -13
  223. package/templates/be/skeleton/src/modules/platform/config/env-source.spec.ts +0 -36
  224. package/templates/be/skeleton/src/modules/platform/config/server.config.spec.ts +0 -19
  225. package/templates/be/skeleton/src/modules/platform/errors/domain-error.spec.ts +0 -15
  226. package/templates/be/skeleton/src/modules/platform/errors/error.filter.spec.ts +0 -38
  227. package/templates/be/skeleton/src/modules/platform/logging/json-logger.spec.ts +0 -33
  228. package/templates/be/skeleton/src/modules/platform/logging/json-logger.ts +0 -35
  229. package/templates/be/skeleton/src/modules/platform/logging/log-id.ts +0 -9
  230. package/templates/be/skeleton/src/modules/platform/logging/logger.port.ts +0 -19
  231. package/templates/common/codecov.yml +0 -13
  232. package/templates/common/pre-push +0 -5
  233. package/templates/fe/e2e.yml +0 -22
  234. package/templates/fe/pre-commit +0 -7
  235. package/templates/fe/skeleton/apps/__app__/src/app/health/live/route.spec.ts +0 -10
  236. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages.spec.ts +0 -27
  237. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/routing.spec.ts +0 -10
  238. package/templates/fe/sonar-project.properties +0 -11
  239. /package/templates/be/skeleton/src/modules/platform/config/{env-source.ts → env-source.config.ts} +0 -0
  240. /package/templates/be/skeleton/src/modules/platform/errors/{domain-error.ts → domain.error.ts} +0 -0
  241. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/next.config.ts +0 -0
  242. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/config.ts +0 -0
  243. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/navigation.ts +0 -0
  244. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/request.ts +0 -0
  245. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/routing.ts +0 -0
  246. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/proxy.ts +0 -0
@@ -0,0 +1,200 @@
1
+ /**
2
+ * HFS check 7, R21 duplicate code (knowledge/hfs/slots.yaml ruleParams.<profile>.duplicateBlock = {lines: N, tokens: T},
3
+ * the one definition of the threshold): a token-normalised block of at least N source lines and T tokens that appears
4
+ * twice in the production program - in two owners, in two files of one owner, or twice in one file - is code copied
5
+ * instead of shared.
6
+ *
7
+ * Normalisation: every production file is flattened into its syntax-node sequence (the compiler's parsed tree, walked in
8
+ * source order; comments and whitespace do not exist in it), identifiers become one ID kind, string, template, numeric,
9
+ * bigint and regex literals become one LIT kind, keywords and operators keep their own kind. Leading import and
10
+ * export-from statements are dropped (imports repeat legitimately). A window is the run of tokens on N consecutive
11
+ * physical lines starting at a line start whose last token reaches line N; windows are hashed with a rolling polynomial
12
+ * hash (O(total tokens)), equal windows of two different owners are verified token by token, and overlapping hits on the
13
+ * same file pair and offset merge into one maximal block. A block whose lines are mostly type declarations
14
+ * (fewer than N/2 lines outside interface and type alias declarations) or of fewer than N/3 distinct line shapes
15
+ * (a decorated DTO field list repeating one shape) is ignored.
16
+ *
17
+ * Two kinds of file are uniform by design and are not compared, both found through the slot resolver: the entry main.ts of
18
+ * an app (slot appKind + its required main.ts) and the transport doors of a feature (a *.controller.ts, *.resolver.ts,
19
+ * *.gateway.ts, *.consumer.ts, *.job.ts or *.cli.ts of a slot that a protocol app composes). This is not a loophole: logic
20
+ * cannot live in either. A door body is dispatch only (eslint transport-is-thin, R88), and main.ts cannot share a
21
+ * helper because NestFactory is allowed only in main.ts (BE_ENTRYPOINT_ONLY_IN_APPS), so what repeats there is the
22
+ * canonical shape of section 5 of BE-CONVENTION. A door or main.ts that grows logic is refused by those enforcers, not
23
+ * excused here; every other file of the same slots (mappers, dto, app.module.ts) stays compared.
24
+ */
25
+ export const CLONE_RULE_IDS = ['HFS_DUPLICATE_CODE'];
26
+
27
+ const MAX_VIOLATIONS = 200;
28
+ const DOOR_ROLES = new Set(['controller', 'resolver', 'gateway', 'consumer', 'job', 'cli']);
29
+ const APP_ENTRY = 'main.ts';
30
+ const ID = -1;
31
+ const LIT = -2;
32
+
33
+ function tokenize(ts, sourceFile) {
34
+ const { SyntaxKind } = ts;
35
+ const literalKinds = new Set([SyntaxKind.StringLiteral, SyntaxKind.NumericLiteral, SyntaxKind.BigIntLiteral, SyntaxKind.RegularExpressionLiteral,
36
+ SyntaxKind.NoSubstitutionTemplateLiteral, SyntaxKind.TemplateHead, SyntaxKind.TemplateMiddle, SyntaxKind.TemplateTail, SyntaxKind.JsxText]);
37
+ const kinds = [];
38
+ const lines = [];
39
+ const types = [];
40
+ const lineStarts = sourceFile.getLineStarts();
41
+ const lineOf = (position) => {
42
+ let low = 0;
43
+ let high = lineStarts.length - 1;
44
+ while (low < high) {
45
+ const mid = (low + high + 1) >> 1;
46
+ if (lineStarts[mid] <= position) low = mid; else high = mid - 1;
47
+ }
48
+ return low + 1;
49
+ };
50
+ const visit = (node, inType) => {
51
+ const kind = node.kind;
52
+ if (kind >= SyntaxKind.FirstJSDocNode && kind <= SyntaxKind.LastJSDocNode) return;
53
+ if (kind === SyntaxKind.JsxText && node.containsOnlyTriviaWhiteSpaces) return;
54
+ const typeScope = inType || kind === SyntaxKind.InterfaceDeclaration || kind === SyntaxKind.TypeAliasDeclaration;
55
+ kinds.push(kind === SyntaxKind.Identifier || kind === SyntaxKind.PrivateIdentifier ? ID : literalKinds.has(kind) ? LIT : kind);
56
+ lines.push(lineOf(node.getStart(sourceFile)));
57
+ types.push(typeScope ? 1 : 0);
58
+ ts.forEachChild(node, (child) => { visit(child, typeScope); });
59
+ };
60
+ for (const statement of sourceFile.statements) {
61
+ if (statement.kind === SyntaxKind.ImportDeclaration || statement.kind === SyntaxKind.ImportEqualsDeclaration
62
+ || (statement.kind === SyntaxKind.ExportDeclaration && statement.moduleSpecifier)) continue;
63
+ visit(statement, false);
64
+ }
65
+ return { kinds: Int32Array.from(kinds), lines: Int32Array.from(lines), types: Uint8Array.from(types) };
66
+ }
67
+
68
+ /** The app a file belongs to, by the slot binding of the manifest; null for a file of no app. */
69
+ const appOfFile = (resolver, rel) => resolver.classifyPath(rel).bindings?.app ?? null;
70
+
71
+ /** True for the app entry and the transport doors: their shape is fixed by the convention and their body cannot hold logic. */
72
+ function uniformByDesign(resolver, rel) {
73
+ const classified = resolver.classifyPath(rel);
74
+ if (classified.status !== 'owned' || !classified.slot) return false;
75
+ const slot = resolver.slot(classified.slot);
76
+ const base = rel.slice(rel.lastIndexOf('/') + 1);
77
+ if (slot.appKind !== undefined) return base === APP_ENTRY && (slot.requires ?? []).includes(APP_ENTRY);
78
+ const role = base.split('.').at(-2);
79
+ return (slot.composedBy?.length ?? 0) > 0 && DOOR_ROLES.has(role) && base.endsWith('.ts');
80
+ }
81
+
82
+ function homeText(profile, sameOwner, crossApp) {
83
+ if (crossApp) return 'move it to a package (packages/<pkg>, the shared-code slot of the repository) and import it from both apps';
84
+ if (sameOwner) return 'extract it once inside the owner and call it from both places';
85
+ if (profile === 'fe') return 'extract it once inside the app (apps/<app>/src/modules/<capability>/ when pure, apps/<app>/src/hooks/<domain>/ for a React hook) and import it from both';
86
+ return "move the shared helper to src/modules/platform/primitives/ or into the owning capability's src/modules/domain/<capability>/ and import it from both";
87
+ }
88
+
89
+ export function checkClones({ config, context, graph } = {}) {
90
+ const { lines: N, tokens: T } = graph.resolver.ruleParams().duplicateBlock;
91
+ const ts = context.ts ?? context.loaded.ts;
92
+ const entries = [];
93
+ for (const rel of [...graph.files.keys()].sort()) {
94
+ if (/\.d\.[cm]?tsx?$/.test(rel) || uniformByDesign(graph.resolver, rel)) continue;
95
+ const unit = graph.unit(rel);
96
+ if (!unit) continue;
97
+ entries.push({ rel, unit, ...tokenize(ts, graph.files.get(rel).sourceFile) });
98
+ }
99
+ // Rolling hash: two independent 32-bit polynomial prefix hashes, verified by token comparison on every match.
100
+ const B1 = 1000003;
101
+ const B2 = 92821;
102
+ let maxTokens = 0;
103
+ for (const file of entries) maxTokens = Math.max(maxTokens, file.kinds.length);
104
+ const pow1 = new Int32Array(maxTokens + 1);
105
+ const pow2 = new Int32Array(maxTokens + 1);
106
+ pow1[0] = 1; pow2[0] = 1;
107
+ for (let i = 1; i <= maxTokens; i += 1) { pow1[i] = Math.imul(pow1[i - 1], B1); pow2[i] = Math.imul(pow2[i - 1], B2); }
108
+ const buckets = new Map();
109
+ entries.forEach((file, fileIndex) => {
110
+ const { kinds, lines } = file;
111
+ const count = kinds.length;
112
+ const pre1 = new Int32Array(count + 1);
113
+ const pre2 = new Int32Array(count + 1);
114
+ for (let i = 0; i < count; i += 1) {
115
+ pre1[i + 1] = (Math.imul(pre1[i], B1) + kinds[i] + 3) | 0;
116
+ pre2[i + 1] = (Math.imul(pre2[i], B2) + kinds[i] + 7) | 0;
117
+ }
118
+ let end = 0;
119
+ for (let start = 0; start < count; start += 1) {
120
+ if (start > 0 && lines[start] === lines[start - 1]) continue;
121
+ const limit = lines[start] + N;
122
+ if (end < start) end = start;
123
+ while (end < count && lines[end] < limit) end += 1;
124
+ if (end === start || lines[end - 1] - lines[start] + 1 < N) continue;
125
+ const length = end - start;
126
+ if (length < T) continue;
127
+ const key = `${(pre1[end] - Math.imul(pre1[start], pow1[length])) | 0}:${(pre2[end] - Math.imul(pre2[start], pow2[length])) | 0}:${length}`;
128
+ const list = buckets.get(key);
129
+ const item = [fileIndex, start, end];
130
+ if (list) list.push(item); else buckets.set(key, [item]);
131
+ }
132
+ });
133
+ const same = (a, b) => {
134
+ const ka = entries[a[0]].kinds;
135
+ const kb = entries[b[0]].kinds;
136
+ for (let i = 0; i < a[2] - a[1]; i += 1) if (ka[a[1] + i] !== kb[b[1] + i]) return false;
137
+ return true;
138
+ };
139
+ // Hits per (file a, file b, offset): merged into maximal blocks below.
140
+ const hits = new Map();
141
+ for (const list of buckets.values()) {
142
+ if (list.length < 2) continue;
143
+ for (const item of list) {
144
+ // The first other occurrence is the partner; overlapping windows of one file are one occurrence, not a copy.
145
+ const partner = list.find((other) => other !== item && (other[0] !== item[0] || other[1] >= item[2] || item[1] >= other[2]));
146
+ if (!partner || !same(item, partner)) continue;
147
+ const order = entries[item[0]].rel === entries[partner[0]].rel ? item[1] < partner[1] : entries[item[0]].rel < entries[partner[0]].rel;
148
+ const [a, b] = order ? [item, partner] : [partner, item];
149
+ const key = `${a[0]}|${b[0]}|${b[1] - a[1]}`;
150
+ const group = hits.get(key);
151
+ if (group) group.push([a[1], a[2]]); else hits.set(key, [[a[1], a[2]]]);
152
+ }
153
+ }
154
+ const blocks = [];
155
+ // Shape of a block: the lines outside type declarations and the number of distinct line shapes (a field list of
156
+ // one decorated DTO shape repeated N times is boilerplate, not a copied helper).
157
+ const blockShape = (file, start, end) => {
158
+ const nonType = new Set();
159
+ const shapes = new Map();
160
+ for (let i = start; i < end; i += 1) {
161
+ if (!file.types[i]) nonType.add(file.lines[i]);
162
+ shapes.set(file.lines[i], ((shapes.get(file.lines[i]) ?? 7) * 31 + file.kinds[i] + 5) | 0);
163
+ }
164
+ return { nonType: nonType.size, distinct: new Set(shapes.values()).size };
165
+ };
166
+ for (const [key, group] of hits) {
167
+ const [ai, bi, diagText] = key.split('|');
168
+ const fileA = entries[Number(ai)];
169
+ const fileB = entries[Number(bi)];
170
+ const diag = Number(diagText);
171
+ group.sort((x, y) => x[0] - y[0]);
172
+ let [start, end] = group[0];
173
+ const flush = () => {
174
+ const shape = blockShape(fileA, start, end);
175
+ if (shape.nonType < Math.ceil(N / 2) || shape.distinct < Math.ceil(N / 3)) return;
176
+ const lineA = fileA.lines[start];
177
+ const endA = fileA.lines[end - 1];
178
+ blocks.push({ a: fileA, b: fileB, line: lineA, endLine: endA, lines: endA - lineA + 1,
179
+ twin: { path: fileB.rel, line: fileB.lines[start + diag], endLine: fileB.lines[end - 1 + diag] } });
180
+ };
181
+ for (const [nextStart, nextEnd] of group.slice(1)) {
182
+ if (nextStart <= end) { end = Math.max(end, nextEnd); continue; }
183
+ flush();
184
+ start = nextStart; end = nextEnd;
185
+ }
186
+ flush();
187
+ }
188
+ blocks.sort((x, y) => x.a.rel.localeCompare(y.a.rel) || x.line - y.line || x.b.rel.localeCompare(y.b.rel));
189
+ const profile = graph.profile;
190
+ const differentApps = (a, b) => { const left = appOfFile(graph.resolver, a); const right = appOfFile(graph.resolver, b); return left !== null && right !== null && left !== right; };
191
+ const violations = blocks.slice(0, MAX_VIOLATIONS).map((block) => ({
192
+ ruleId: 'HFS_DUPLICATE_CODE', path: block.a.rel, line: block.line, endLine: block.endLine, twin: block.twin, lines: block.lines,
193
+ message: `${block.a.rel}:${block.line}-${block.endLine} duplicates ${block.twin.path}:${block.twin.line}-${block.twin.endLine} (${block.lines} lines, threshold ${N} lines / ${T} tokens, identifiers and literals ignored); ${homeText(profile, block.a.unit === block.b.unit, differentApps(block.a.rel, block.b.rel))}.`,
194
+ }));
195
+ const owners = new Set();
196
+ let duplicatedLines = 0;
197
+ for (const block of blocks) { owners.add(block.a.unit); owners.add(block.b.unit); duplicatedLines += block.lines; }
198
+ return { violations, coverage: { status: 'checked', minLines: N, minTokens: T, files: entries.length, cloneBlocks: blocks.length, ownersInvolved: owners.size, duplicatedLines,
199
+ ...(blocks.length > MAX_VIOLATIONS ? { truncated: true } : {}) } };
200
+ }
@@ -0,0 +1,35 @@
1
+ import path from 'node:path';
2
+ import { treeOf } from './required-files.mjs';
3
+
4
+ /**
5
+ * R24 `arch-config-unread` (HFS_ARCH_CONFIG_UNREAD). The machine reads `hfs.json` and nothing else; a config file it
6
+ * no longer reads is dead text that says a rule is tuned when it is not, and a declaration that yields no analysed file
7
+ * is a machine that judged nothing. Both are red:
8
+ *
9
+ * - a tracked `architecture.json` (the retired per-repository config; the declaration is hfs.json, the rest is derived
10
+ * from the slot manifest);
11
+ * - a repository whose declaration and tsconfig load but whose program holds no production source file.
12
+ */
13
+ export const CONFIG_UNREAD_RULE_IDS = ['HFS_ARCH_CONFIG_UNREAD'];
14
+
15
+ const RULE = 'HFS_ARCH_CONFIG_UNREAD';
16
+ /** Config file names an earlier machine read and this one never does. */
17
+ const RETIRED_CONFIG_NAMES = ['architecture.json'];
18
+
19
+ export function checkConfigUnread({ config, context }) {
20
+ const violations = [];
21
+ const tree = treeOf(config.root);
22
+ let retired = 0;
23
+ for (const file of [...tree.files].sort()) {
24
+ if (!RETIRED_CONFIG_NAMES.includes(path.posix.basename(file))) continue;
25
+ retired += 1;
26
+ violations.push({ ruleId: RULE, path: file, line: 1, column: 1,
27
+ message: `${file} is a config the architecture machine no longer reads. hfs.json declares the repository; owners, roots, tiers and budgets come from the slot manifest. Delete ${file}.` });
28
+ }
29
+ const analysed = context.files.length;
30
+ if (analysed === 0) {
31
+ violations.push({ ruleId: RULE, path: 'hfs.json', line: 1, column: 1,
32
+ message: 'hfs.json and the TypeScript projects load but the machine analysed no production source file, so it judged nothing. Declare the apps that hold source in hfs.json, or fix the tsconfig include so the source is in the program.' });
33
+ }
34
+ return { violations, coverage: { status: 'checked', retired, analysed } };
35
+ }
@@ -0,0 +1,310 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { slash } from '../../lib/path-key.mjs';
4
+ import { readJsonFile as readJson } from '../../lib/json.mjs';
5
+ import { isInside } from '../common.mjs';
6
+ import { openHfs } from '../../lib/hfs-slots.mjs';
7
+
8
+
9
+ /** A file's identity: its resolved real path, or the resolved path when it does not exist. */
10
+ function canonical(file) {
11
+ const absolute = path.resolve(file);
12
+ try { return path.resolve(fs.realpathSync(absolute)); } catch { return absolute; }
13
+ }
14
+
15
+ function exactKeys(value, allowed, label) {
16
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw Error(`${label} must be an object.`);
17
+ const unknown = Object.keys(value).filter(key => !allowed.has(key));
18
+ if (unknown.length) throw Error(`${label} has unsupported fields: ${unknown.sort().join(', ')}.`);
19
+ }
20
+
21
+ /**
22
+ * The git repository `root` sits in, found by walking up for a `.git` entry (a directory in a clone, a
23
+ * file in a worktree), or null when there is none. It is the outer boundary a `file:` dependency may
24
+ * resolve into: a checked project is routinely one package of a repository that also ships the packages
25
+ * it consumes, and `--root` is the project, not the repository.
26
+ */
27
+ function enclosingRepository(root) {
28
+ let cursor = path.resolve(root);
29
+ for (;;) {
30
+ if (fs.existsSync(path.join(cursor, '.git'))) return cursor;
31
+ const parent = path.dirname(cursor);
32
+ if (parent === cursor) return null;
33
+ cursor = parent;
34
+ }
35
+ }
36
+
37
+ function existingPackageDirectory(absolute) {
38
+ try {
39
+ if (!fs.lstatSync(absolute).isDirectory()) return false;
40
+ const manifest = fs.lstatSync(path.join(absolute, 'package.json'));
41
+ return manifest.isFile() && !manifest.isSymbolicLink();
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ function existingDirectory(root, relative) {
48
+ try {
49
+ return fs.lstatSync(path.join(root, relative)).isDirectory();
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ function existingRegularFile(root, relative) {
56
+ try {
57
+ const stat = fs.lstatSync(path.join(root, ...relative.split('/')));
58
+ return stat.isFile() && !stat.isSymbolicLink();
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ function workspaceDirectories(root) {
65
+ const directories = new Set();
66
+ const queue = [root];
67
+ const visited = new Set();
68
+ const repository = enclosingRepository(root);
69
+ const admit = (candidate, label = 'local package', strict = false) => {
70
+ const absolute = path.resolve(candidate);
71
+ if (!isInside(root, absolute) || absolute === root || !existingDirectory(root, slash(path.relative(root, absolute)))) {
72
+ if (strict) throw Error(`${label} must resolve to a package directory inside the repository.`);
73
+ return;
74
+ }
75
+ const manifest = path.join(absolute, 'package.json');
76
+ try {
77
+ if (!fs.lstatSync(manifest).isFile() || fs.lstatSync(manifest).isSymbolicLink()) {
78
+ if (strict) throw Error(`${label} must resolve to a regular package.json inside the repository.`);
79
+ return;
80
+ }
81
+ } catch (error) {
82
+ if (strict) throw Error(error.message.startsWith(label) ? error.message : `${label} must resolve to a regular package.json inside the repository.`);
83
+ return;
84
+ }
85
+ const relative = slash(path.relative(root, absolute));
86
+ if (!directories.has(relative)) { directories.add(relative); queue.push(absolute); }
87
+ };
88
+ while (queue.length) {
89
+ const packageRoot = queue.shift();
90
+ if (visited.has(packageRoot)) continue;
91
+ visited.add(packageRoot);
92
+ const pkg = readJson(path.join(packageRoot, 'package.json'));
93
+ const patterns = Array.isArray(pkg?.workspaces) ? pkg.workspaces : pkg?.workspaces?.packages;
94
+ for (const pattern of Array.isArray(patterns) ? patterns : []) {
95
+ if (typeof pattern !== 'string' || !pattern.trim()) throw Error('package.json workspace entries must be non-empty paths.');
96
+ const normalized = slash(pattern.trim()).replace(/^\.\//, '');
97
+ const segments = normalized.split('/');
98
+ if (path.isAbsolute(normalized) || segments.some(segment => segment === '..' || (segment.includes('*') && segment !== '*'))) {
99
+ throw Error(`Unsupported local workspace pattern: ${normalized}.`);
100
+ }
101
+ let candidates = [packageRoot];
102
+ for (const segment of segments) {
103
+ const next = [];
104
+ for (const base of candidates) {
105
+ if (segment === '*') {
106
+ if (!fs.existsSync(base) || !fs.lstatSync(base).isDirectory()) continue;
107
+ next.push(...fs.readdirSync(base, { withFileTypes: true })
108
+ .filter(item => item.isDirectory())
109
+ .sort((a, b) => a.name.localeCompare(b.name))
110
+ .map(item => path.join(base, item.name)));
111
+ } else next.push(path.join(base, segment));
112
+ }
113
+ candidates = next;
114
+ }
115
+ if (!candidates.length) throw Error(`Workspace pattern matched no package directories: ${normalized}.`);
116
+ // npm expands a `*` segment to the directories that hold a package.json and skips the rest (an empty or untracked folder is
117
+ // HFS_EMPTY_DIR / HFS_SLOT_UNDECLARED, never a reason to analyse nothing); a literal workspace path must resolve.
118
+ const wildcard = segments.includes('*');
119
+ for (const candidate of candidates) admit(candidate, `workspace ${normalized}`, !wildcard);
120
+ }
121
+ for (const section of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
122
+ for (const [name, value] of Object.entries(pkg?.[section] ?? {})) {
123
+ if (typeof value !== 'string' || !value.startsWith('file:')) continue;
124
+ const absolute = path.resolve(packageRoot, value.slice('file:'.length));
125
+ const label = `${section}.${name} file dependency`;
126
+ if (isInside(root, absolute)) { admit(absolute, label, true); continue; }
127
+ // A sibling package of the same repository: this project consumes it, so the path is real and
128
+ // resolvable, but it is not part of this project's own source layout and must not become one of
129
+ // its workspaces - collecting it would pull another package's src into these roots. Refusing it
130
+ // instead would fail the whole check closed over an ordinary monorepo shape.
131
+ if (!repository || !isInside(repository, absolute) || !existingPackageDirectory(absolute)) {
132
+ throw Error(`${label} must resolve to a package directory inside the repository.`);
133
+ }
134
+ }
135
+ }
136
+ }
137
+ return [...directories].sort();
138
+ }
139
+
140
+ function discoveredProjects(root, workspaces) {
141
+ const projects = [];
142
+ if (fs.existsSync(path.join(root, 'tsconfig.json'))) projects.push('tsconfig.json');
143
+ for (const workspace of workspaces) {
144
+ for (const name of ['tsconfig.json', 'tsconfig.app.json', 'tsconfig.build.json']) {
145
+ const candidate = `${workspace}/${name}`;
146
+ if (fs.existsSync(path.join(root, ...candidate.split('/')))) {
147
+ projects.push(candidate);
148
+ break;
149
+ }
150
+ }
151
+ }
152
+ return [...new Set(projects)];
153
+ }
154
+
155
+ function inferredLayout(root, workspaces) {
156
+ // HFS: frontend source roots live only under workspace packages (apps/<app>/, packages/<pkg>/);
157
+ // the repository root itself is never a frontend source root.
158
+ // The route, feature, component, hook and module roles are app slots (fe.route, fe.feature, fe.components, fe.hooks, fe.modules:
159
+ // apps/<app>/src/...); a workspace package is tier `package` and holds none of them - its own src/hooks is package source, and a
160
+ // grammar package's tier folders make it a component root below.
161
+ const appWorkspaces = workspaces.filter(prefix => prefix.startsWith('apps/'));
162
+ const collect = suffix => appWorkspaces.map(prefix => `${prefix}/${suffix}`).filter(relative => existingDirectory(root, relative));
163
+ // A design-system workspace whose src carries tier dirs (leaves/branches/…) IS a component root;
164
+ // collecting its src/components too would nest two roots in one role and fail the disjoint check.
165
+ const tierSources = new Set(workspaces.map(prefix => `${prefix}/src`)
166
+ .filter(source => ['leaves', 'branches', 'blocks', 'overlays', 'composites', 'layouts', 'product-shells', 'pages']
167
+ .some(tier => existingDirectory(root, `${source}/${tier}`))));
168
+ const components = collect('src/components')
169
+ .filter(relative => !tierSources.has(path.posix.dirname(relative)));
170
+ components.push(...tierSources);
171
+ return {
172
+ routes: collect('src/app'),
173
+ features: collect('src/features'),
174
+ components: [...new Set(components)],
175
+ hooks: collect('src/hooks'),
176
+ modules: collect('src/modules'),
177
+ transport: collect('src/modules/api'),
178
+ };
179
+ }
180
+
181
+ function assertFrontendRolesDisjoint(root, frontend) {
182
+ const roles = ['routes', 'features', 'components', 'hooks', 'modules'];
183
+ const entries = roles.flatMap(role => frontend[role].map(relative => ({
184
+ role,
185
+ relative,
186
+ absolute: path.join(root, ...relative.split('/')),
187
+ })));
188
+ for (let index = 0; index < entries.length; index += 1) for (let other = index + 1; other < entries.length; other += 1) {
189
+ const left = entries[index];
190
+ const right = entries[other];
191
+ if (isInside(left.absolute, right.absolute) || isInside(right.absolute, left.absolute)) {
192
+ throw Error(`Architecture frontend role roots must be disjoint; ${left.role} ${left.relative} overlaps ${right.role} ${right.relative}.`);
193
+ }
194
+ }
195
+ }
196
+
197
+ const SKIPPED_DIRECTORIES = new Set(['node_modules', '.git', 'dist', '.next', '.turbo', 'coverage', 'test-results', '.starciwork', '.starcistacks']);
198
+
199
+ function directoriesUnder(root, relative, depth) {
200
+ const out = [];
201
+ const visit = (dir, left) => {
202
+ let entries;
203
+ try { entries = fs.readdirSync(path.join(root, ...dir.split('/').filter(Boolean)), { withFileTypes: true }); } catch { return; }
204
+ for (const entry of entries) {
205
+ if (!entry.isDirectory() || SKIPPED_DIRECTORIES.has(entry.name)) continue;
206
+ const child = dir ? `${dir}/${entry.name}` : entry.name;
207
+ out.push(child);
208
+ if (left > 1) visit(child, left - 1);
209
+ }
210
+ };
211
+ visit(relative, depth);
212
+ return out;
213
+ }
214
+
215
+ /** The owner entry a slot instance exposes: index.ts/index.tsx, a package's src/index.ts, an app's app.module.ts. */
216
+ function ownerEntry(root, ownerRoot, tier) {
217
+ const candidates = tier === 'package' ? ['src/index.ts', 'src/index.tsx', 'index.ts', 'index.tsx']
218
+ : tier === 'app' ? ['app.module.ts'] : ['index.ts', 'index.tsx'];
219
+ for (const candidate of candidates) {
220
+ const relative = `${ownerRoot}/${candidate}`;
221
+ if (existingRegularFile(root, relative)) return relative;
222
+ }
223
+ return null;
224
+ }
225
+
226
+ /**
227
+ * Owners are derived from the slot manifest: every directory that an `owner: true` slot matches, with its entry file.
228
+ * An instance with no entry file is not listed: the required-files check reports the missing entry.
229
+ */
230
+ function derivedOwners(root, resolver) {
231
+ const seen = new Map();
232
+ for (const directory of directoriesUnder(root, '', 6)) {
233
+ const owner = resolver.ownerOf(directory);
234
+ if (!owner || owner.root !== directory || seen.has(owner.root)) continue;
235
+ const slot = resolver.slot(owner.slot);
236
+ const entry = ownerEntry(root, owner.root, slot.tier);
237
+ if (entry) seen.set(owner.root, { id: `${owner.slot}:${owner.root}`, root: owner.root, entry, slot: owner.slot, tier: slot.tier });
238
+ }
239
+ return [...seen.values()].sort((a, b) => a.root.localeCompare(b.root));
240
+ }
241
+
242
+ const DECLARED_HANDLER_DECORATORS = ['CommandHandler', 'QueryHandler'];
243
+
244
+ const GRAMMAR_PACKAGE = '@starci/grammar';
245
+
246
+ /**
247
+ * The Grammar contract of a frontend repository, derived rather than declared: every app's globals.css is a style
248
+ * source, every app and every workspace package that depends on the Grammar package is a consumer. null when no app has
249
+ * a globals.css to judge (the contract is then reported unavailable, never passed).
250
+ */
251
+ function derivedGrammar(root, apps, workspaces) {
252
+ const styleSources = apps.map(app => `apps/${app.name}/src/app/globals.css`).filter(relative => existingRegularFile(root, relative));
253
+ if (!styleSources.length) return null;
254
+ const consumerManifests = apps.map(app => `apps/${app.name}/package.json`).filter(relative => existingRegularFile(root, relative));
255
+ for (const workspace of workspaces) {
256
+ if (!workspace.startsWith('packages/')) continue;
257
+ const manifest = `${workspace}/package.json`;
258
+ const pkg = readJson(path.join(root, ...manifest.split('/')));
259
+ if (pkg && [pkg.dependencies, pkg.peerDependencies, pkg.devDependencies].some(section => section && Object.hasOwn(section, GRAMMAR_PACKAGE))) consumerManifests.push(manifest);
260
+ }
261
+ if (!consumerManifests.length) return null;
262
+ return { package: GRAMMAR_PACKAGE, entry: `${GRAMMAR_PACKAGE}/common`, styleEntry: `${GRAMMAR_PACKAGE}/common.css`, styleSources, consumerManifests, peers: ['react', '@heroui/react'] };
263
+ }
264
+
265
+ /**
266
+ * Resolve the layout contract from hfs.json and the slot manifest (knowledge/hfs/slots.yaml). architecture.json is
267
+ * retired: a repository carries only hfs.json. Owners, roots and the tier of every path come from slots; the contract
268
+ * has no ignore, waiver or baseline field.
269
+ */
270
+ export function loadArchitectureConfig(repositoryRoot, { hfs } = {}) {
271
+ const root = fs.realpathSync(path.resolve(repositoryRoot));
272
+ if (!fs.lstatSync(root).isDirectory()) throw Error('Repository root must be a directory.');
273
+ const opened = hfs ?? openHfs({ repoRoot: root });
274
+ const { profile, apps } = opened.repo;
275
+ const workspaces = workspaceDirectories(root);
276
+ const inferred = inferredLayout(root, [...new Set([...workspaces, ...apps.map(app => `apps/${app.name}`)])].sort());
277
+ const kinds = [profile === 'be' ? 'backend' : 'frontend'];
278
+ const projects = discoveredProjects(root, workspaces);
279
+ if (!projects.length) throw Error('The repository has no tsconfig.json to derive a TypeScript project from.');
280
+ const backend = {
281
+ modules: ['src/modules'],
282
+ features: ['src/features'],
283
+ apps: ['apps'],
284
+ moduleRegistration: { providerIdentity: 'exported-class-token', handlerDecorators: [...DECLARED_HANDLER_DECORATORS] },
285
+ };
286
+ const frontend = {
287
+ routes: inferred.routes,
288
+ features: inferred.features,
289
+ components: inferred.components,
290
+ hooks: inferred.hooks,
291
+ modules: inferred.modules,
292
+ transport: inferred.transport,
293
+ grammar: profile === 'fe' ? derivedGrammar(root, apps, workspaces) : null,
294
+ };
295
+ if (profile === 'fe') assertFrontendRolesDisjoint(root, frontend);
296
+ return {
297
+ root,
298
+ repository: enclosingRepository(root),
299
+ kinds,
300
+ projects,
301
+ workspaces,
302
+ apps: apps.map(app => ({ name: app.name, kind: app.kind })),
303
+ hfs: opened,
304
+ owners: derivedOwners(root, opened),
305
+ backend,
306
+ frontend,
307
+ };
308
+ }
309
+
310
+ export { canonical, enclosingRepository, exactKeys, isInside, slash };