@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,775 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createRequire } from 'node:module';
4
+ import { canonical, exactKeys, isInside, slash } from './config.mjs';
5
+ import { relativePath, sourceLocation, unwrapExpression } from './typescript.mjs';
6
+
7
+ export const SWR_KEY_RULE_ID = 'FE_SWR_KEY_IDENTITY';
8
+ export const SWR_MUTATION_RULE_ID = 'FE_SWR_MUTATION_RESOURCE_IDENTITY';
9
+ export const SWR_DATA_RULE_IDS = [SWR_KEY_RULE_ID, SWR_MUTATION_RULE_ID];
10
+
11
+ const CONTRACT_SCHEMA = 'starci/next-data-lifecycle@1';
12
+ const SOURCE = /\.[cm]?[jt]sx?$/i;
13
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
14
+ const BINDING = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/;
15
+ const TOP_KEYS = new Set(['schema', 'swr', 'hooks']);
16
+ const SWR_KEYS = new Set(['package', 'major']);
17
+ const HOOK_KEYS = new Set(['id', 'path', 'export', 'kind', 'resultBinding', 'identities']);
18
+ const IDENTITY_KEYS = new Set(['id', 'binding', 'gatesRequest', 'resource']);
19
+ const SWR_SPECIFIERS = new Set(['swr', 'swr/immutable', 'swr/mutation']);
20
+
21
+ function relativeSource(repository, value, label) {
22
+ repository = path.resolve(repository);
23
+ if (typeof value !== 'string' || !value.trim() || path.isAbsolute(value)) throw Error(`${label} must be a non-empty repository-relative source path.`);
24
+ const relative = slash(value.trim()).replace(/^\.\//, '');
25
+ if (relative.split('/').includes('..') || !SOURCE.test(relative) || /\.d\.[cm]?[jt]s$/i.test(relative)) {
26
+ throw Error(`${label} must be a production TypeScript or JavaScript source path.`);
27
+ }
28
+ const absolute = path.resolve(repository, ...relative.split('/'));
29
+ if (!isInside(repository, absolute)) throw Error(`${label} must stay inside the repository.`);
30
+ for (let current = absolute; current !== repository; current = path.dirname(current)) {
31
+ const stat = fs.lstatSync(current);
32
+ if (stat.isSymbolicLink()) throw Error(`${label} must not cross a symbolic-link or junction ancestor.`);
33
+ }
34
+ const stat = fs.lstatSync(absolute);
35
+ if (!stat.isFile() || !isInside(canonical(repository), canonical(absolute))) throw Error(`${label} must be a regular source file inside the repository.`);
36
+ return relative;
37
+ }
38
+
39
+ function parseContract(config) {
40
+ const manifest = JSON.parse(fs.readFileSync(path.join(config.root, 'package.json'), 'utf8'));
41
+ const next = manifest?.starci?.codePatterns?.next;
42
+ const value = next?.dataLifecycle;
43
+ if (value === undefined) return null;
44
+ if (next.schema !== 'starci/next-code-pattern-contract@1') throw Error('package.json#starci.codePatterns.next must use starci/next-code-pattern-contract@1.');
45
+ exactKeys(value, TOP_KEYS, 'package.json#starci.codePatterns.next.dataLifecycle');
46
+ if (value.schema !== CONTRACT_SCHEMA) throw Error(`dataLifecycle.schema must be ${CONTRACT_SCHEMA}.`);
47
+ exactKeys(value.swr, SWR_KEYS, 'dataLifecycle.swr');
48
+ if (value.swr.package !== 'swr' || !Number.isInteger(value.swr.major) || value.swr.major < 1) {
49
+ throw Error('dataLifecycle.swr must bind package swr and one positive integer major version.');
50
+ }
51
+ if (!Array.isArray(value.hooks) || value.hooks.length === 0) throw Error('dataLifecycle.hooks must contain at least one declared lifecycle call.');
52
+ const ids = new Set();
53
+ const hooks = value.hooks.map((hook, index) => {
54
+ const label = `dataLifecycle.hooks[${index}]`;
55
+ exactKeys(hook, HOOK_KEYS, label);
56
+ if (typeof hook.id !== 'string' || !hook.id.trim() || ids.has(hook.id.trim())) throw Error(`${label}.id must be unique and non-empty.`);
57
+ ids.add(hook.id.trim());
58
+ const source = relativeSource(config.root, hook.path, `${label}.path`);
59
+ if (typeof hook.export !== 'string' || !/^use[A-Z0-9_$][\w$]*$/.test(hook.export)) throw Error(`${label}.export must name one exported use* hook.`);
60
+ if (!['query', 'mutation'].includes(hook.kind)) throw Error(`${label}.kind must be query or mutation.`);
61
+ if (hook.resultBinding !== undefined && (typeof hook.resultBinding !== 'string' || !IDENTIFIER.test(hook.resultBinding))) {
62
+ throw Error(`${label}.resultBinding must be one local identifier.`);
63
+ }
64
+ if (!Array.isArray(hook.identities)) throw Error(`${label}.identities must be an array.`);
65
+ const identityIds = new Set(), bindings = new Set();
66
+ const identities = hook.identities.map((identity, identityIndex) => {
67
+ const identityLabel = `${label}.identities[${identityIndex}]`;
68
+ exactKeys(identity, IDENTITY_KEYS, identityLabel);
69
+ if (typeof identity.id !== 'string' || !identity.id.trim() || identityIds.has(identity.id.trim())) throw Error(`${identityLabel}.id must be unique and non-empty.`);
70
+ if (typeof identity.binding !== 'string' || !BINDING.test(identity.binding) || bindings.has(identity.binding)) throw Error(`${identityLabel}.binding must be a unique identifier or property path.`);
71
+ if (typeof identity.gatesRequest !== 'boolean' || typeof identity.resource !== 'boolean') throw Error(`${identityLabel} must explicitly declare gatesRequest and resource booleans.`);
72
+ identityIds.add(identity.id.trim());
73
+ bindings.add(identity.binding);
74
+ return { id: identity.id.trim(), binding: identity.binding, gatesRequest: identity.gatesRequest, resource: identity.resource };
75
+ });
76
+ if (hook.kind === 'mutation' && !identities.some(identity => identity.resource)) throw Error(`${label} mutation must declare at least one resource identity.`);
77
+ return { id: hook.id.trim(), path: source, export: hook.export, kind: hook.kind,
78
+ ...(hook.resultBinding === undefined ? {} : { resultBinding: hook.resultBinding }), identities };
79
+ });
80
+ return { schema: value.schema, swr: { package: 'swr', major: value.swr.major }, hooks };
81
+ }
82
+
83
+ function installedSWR(repository) {
84
+ const require = createRequire(path.join(repository, 'package.json'));
85
+ let manifest;
86
+ try { manifest = require.resolve('swr/package.json'); } catch {
87
+ let entry;
88
+ try { entry = require.resolve('swr'); } catch { throw Error('Installed swr package is unavailable.'); }
89
+ let current = path.dirname(entry);
90
+ while (isInside(repository, current) && current !== repository) {
91
+ const candidate = path.join(current, 'package.json');
92
+ if (fs.existsSync(candidate)) { manifest = candidate; break; }
93
+ current = path.dirname(current);
94
+ }
95
+ }
96
+ if (!manifest) throw Error('Installed swr package.json is unavailable.');
97
+ const actualManifest = canonical(manifest);
98
+ const parsed = JSON.parse(fs.readFileSync(actualManifest, 'utf8'));
99
+ const major = Number.parseInt(String(parsed.version ?? '').split('.')[0], 10);
100
+ if (parsed.name !== 'swr' || !Number.isInteger(major)) throw Error('Installed swr package identity or version is invalid.');
101
+ return { name: parsed.name, version: parsed.version, major, root: canonical(path.dirname(actualManifest)) };
102
+ }
103
+
104
+ function normalizedSymbol(ts, checker, value) {
105
+ let symbol = value ?? null;
106
+ const seen = new Set();
107
+ while (symbol && (symbol.flags & ts.SymbolFlags.Alias) && !seen.has(symbol)) {
108
+ seen.add(symbol);
109
+ const target = checker.getAliasedSymbol(symbol);
110
+ if (!target || target === symbol) break;
111
+ symbol = target;
112
+ }
113
+ return symbol;
114
+ }
115
+
116
+ function selectedNode(ts, expression) {
117
+ expression = unwrapExpression(ts, expression);
118
+ if (ts.isIdentifier(expression)) return expression;
119
+ if (ts.isPropertyAccessExpression(expression)) return expression.name;
120
+ if (ts.isElementAccessExpression(expression) && ts.isStringLiteralLike(expression.argumentExpression)) return expression.argumentExpression;
121
+ return null;
122
+ }
123
+
124
+ function symbolAt(ts, checker, node) {
125
+ return normalizedSymbol(ts, checker, checker.getSymbolAtLocation(node));
126
+ }
127
+
128
+ function valueSymbol(ts, checker, expression, seen = new Set()) {
129
+ const selected = selectedNode(ts, expression);
130
+ const symbol = selected ? symbolAt(ts, checker, selected) : null;
131
+ if (!symbol || seen.has(symbol)) return symbol;
132
+ const declarations = symbol.getDeclarations?.() ?? [];
133
+ if (declarations.length === 1 && ts.isVariableDeclaration(declarations[0]) && declarations[0].initializer
134
+ && (ts.getCombinedNodeFlags(declarations[0].parent) & ts.NodeFlags.Const)) {
135
+ return valueSymbol(ts, checker, declarations[0].initializer, new Set(seen).add(symbol));
136
+ }
137
+ return symbol;
138
+ }
139
+
140
+ function declarationsInside(symbol, root) {
141
+ const declarations = symbol?.getDeclarations?.() ?? [];
142
+ return declarations.length > 0 && declarations.every(declaration => isInside(root, canonical(declaration.getSourceFile().fileName)));
143
+ }
144
+
145
+ function officialTargets(config, context, installed, reasons) {
146
+ const wanted = new Map([
147
+ ['swr', new Map([['default', 'query'], ['mutate', 'global-mutate'], ['useSWRConfig', 'config']])],
148
+ ['swr/immutable', new Map([['default', 'query']])],
149
+ ['swr/mutation', new Map([['default', 'mutation'], ['useSWRMutation', 'mutation']])],
150
+ ]);
151
+ const targets = new Map();
152
+ let selectedBindings = 0;
153
+ for (const source of context.files) {
154
+ const checker = context.checkerFor(source.fileName);
155
+ for (const statement of source.statements) {
156
+ if (!context.ts.isImportDeclaration(statement) && !context.ts.isExportDeclaration(statement)) continue;
157
+ if (!statement.moduleSpecifier || !context.ts.isStringLiteralLike(statement.moduleSpecifier)) continue;
158
+ const selected = wanted.get(statement.moduleSpecifier.text);
159
+ if (!selected) continue;
160
+ const moduleSymbol = checker.getSymbolAtLocation(statement.moduleSpecifier);
161
+ if (!moduleSymbol) {
162
+ reasons.push(`${relativePath(config.root, source.fileName)} cannot resolve ${statement.moduleSpecifier.text} through the target TypeScript program`);
163
+ continue;
164
+ }
165
+ const exports = new Map(checker.getExportsOfModule(moduleSymbol).map(symbol => [symbol.getName(), normalizedSymbol(context.ts, checker, symbol)]));
166
+ for (const [name, kind] of selected) {
167
+ const target = exports.get(name);
168
+ if (!target) continue;
169
+ if (!declarationsInside(target, installed.root)) {
170
+ reasons.push(`${relativePath(config.root, source.fileName)} resolves ${statement.moduleSpecifier.text}#${name} outside installed swr ${installed.version}`);
171
+ continue;
172
+ }
173
+ targets.set(target, kind);
174
+ }
175
+ if (context.ts.isImportDeclaration(statement) && statement.importClause) {
176
+ const clause = statement.importClause;
177
+ if (clause.name && selected.has('default')) selectedBindings += 1;
178
+ const bindings = clause.namedBindings;
179
+ if (bindings && context.ts.isNamespaceImport(bindings)) selectedBindings += 1;
180
+ if (bindings && context.ts.isNamedImports(bindings)) selectedBindings += bindings.elements
181
+ .filter(element => selected.has(element.propertyName?.text ?? element.name.text)).length;
182
+ }
183
+ }
184
+ }
185
+ return { targets, selectedBindings };
186
+ }
187
+
188
+ function callKind(ts, checker, call, targets, configMutates = new Set()) {
189
+ const symbol = valueSymbol(ts, checker, call.expression);
190
+ return configMutates.has(symbol) ? 'global-mutate' : targets.get(symbol) ?? null;
191
+ }
192
+
193
+ function bindingNames(ts, name) {
194
+ if (ts.isIdentifier(name)) return [name.text];
195
+ if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) return name.elements.flatMap(element => {
196
+ if (!element || ts.isOmittedExpression(element)) return [];
197
+ return bindingNames(ts, element.name);
198
+ });
199
+ return [];
200
+ }
201
+
202
+ function resultBindings(ts, call) {
203
+ let current = call;
204
+ while (current.parent && (ts.isParenthesizedExpression(current.parent) || ts.isAsExpression(current.parent)
205
+ || ts.isTypeAssertionExpression(current.parent) || ts.isNonNullExpression(current.parent)
206
+ || (ts.isSatisfiesExpression?.(current.parent) ?? false)
207
+ || ((ts.isPropertyAccessExpression(current.parent) || ts.isElementAccessExpression(current.parent)) && current.parent.expression === current))) {
208
+ current = current.parent;
209
+ }
210
+ return current.parent && ts.isVariableDeclaration(current.parent) && current.parent.initializer === current
211
+ ? bindingNames(ts, current.parent.name) : [];
212
+ }
213
+
214
+ function nearestFunction(ts, node) {
215
+ for (let current = node.parent; current; current = current.parent) if (ts.isFunctionDeclaration(current)
216
+ || ts.isFunctionExpression(current) || ts.isArrowFunction(current) || ts.isMethodDeclaration(current)) return current;
217
+ return null;
218
+ }
219
+
220
+ function configMutateSymbols(config, context, targets) {
221
+ const symbols = new Set();
222
+ for (const source of context.files) {
223
+ const checker = context.checkerFor(source.fileName);
224
+ const visit = node => {
225
+ if (context.ts.isVariableDeclaration(node) && context.ts.isObjectBindingPattern(node.name) && node.initializer
226
+ && context.ts.isCallExpression(unwrapExpression(context.ts, node.initializer))
227
+ && callKind(context.ts, checker, unwrapExpression(context.ts, node.initializer), targets) === 'config') {
228
+ for (const element of node.name.elements) {
229
+ const property = element.propertyName && context.ts.isIdentifier(element.propertyName) ? element.propertyName.text
230
+ : context.ts.isIdentifier(element.name) ? element.name.text : null;
231
+ if (property === 'mutate' && context.ts.isIdentifier(element.name)) symbols.add(symbolAt(context.ts, checker, element.name));
232
+ }
233
+ }
234
+ context.ts.forEachChild(node, visit);
235
+ };
236
+ visit(source);
237
+ }
238
+ return symbols;
239
+ }
240
+
241
+ function discoverCalls(config, context, targets) {
242
+ const configMutates = configMutateSymbols(config, context, targets);
243
+ const calls = [];
244
+ for (const source of context.files) {
245
+ const checker = context.checkerFor(source.fileName);
246
+ const visit = node => {
247
+ if (context.ts.isCallExpression(node)) {
248
+ const kind = callKind(context.ts, checker, node, targets, configMutates);
249
+ if (kind && kind !== 'config') calls.push({ kind, node, source, checker, owner: nearestFunction(context.ts, node), bindings: resultBindings(context.ts, node) });
250
+ }
251
+ context.ts.forEachChild(node, visit);
252
+ };
253
+ visit(source);
254
+ }
255
+ return calls;
256
+ }
257
+
258
+ function functionNode(ts, declaration) {
259
+ if (ts.isFunctionDeclaration(declaration) && declaration.body) return declaration;
260
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
261
+ const value = unwrapExpression(ts, declaration.initializer);
262
+ if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) return value;
263
+ }
264
+ return null;
265
+ }
266
+
267
+ function exportedHook(config, context, entry, reasons) {
268
+ const source = context.files.find(candidate => canonical(candidate.fileName) === canonical(path.join(config.root, ...entry.path.split('/'))));
269
+ if (!source) {
270
+ reasons.push(`${entry.path} is outside the canonical production TypeScript program`);
271
+ return null;
272
+ }
273
+ const checker = context.checkerFor(source.fileName);
274
+ const moduleSymbol = checker.getSymbolAtLocation(source);
275
+ const exported = moduleSymbol ? checker.getExportsOfModule(moduleSymbol).find(symbol => symbol.getName() === entry.export) : null;
276
+ const symbol = normalizedSymbol(context.ts, checker, exported);
277
+ const declarations = (symbol?.getDeclarations?.() ?? []).filter(declaration => canonical(declaration.getSourceFile().fileName) === canonical(source.fileName));
278
+ const functions = declarations.map(declaration => functionNode(context.ts, declaration)).filter(Boolean);
279
+ if (functions.length !== 1) {
280
+ reasons.push(`${entry.path}#${entry.export} does not resolve to one local exported hook implementation`);
281
+ return null;
282
+ }
283
+ return { source, checker, fn: functions[0] };
284
+ }
285
+
286
+ function bindingSymbols(ts, checker, fn) {
287
+ const found = new Map();
288
+ const addName = name => {
289
+ if (ts.isIdentifier(name)) {
290
+ const symbol = symbolAt(ts, checker, name);
291
+ if (symbol) {
292
+ if (!found.has(name.text)) found.set(name.text, new Set());
293
+ found.get(name.text).add(symbol);
294
+ }
295
+ return;
296
+ }
297
+ if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) for (const element of name.elements) {
298
+ if (element && !ts.isOmittedExpression(element)) addName(element.name);
299
+ }
300
+ };
301
+ for (const parameter of fn.parameters) addName(parameter.name);
302
+ const visit = node => {
303
+ if (node !== fn && (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node))) return;
304
+ if (ts.isVariableDeclaration(node)) addName(node.name);
305
+ ts.forEachChild(node, visit);
306
+ };
307
+ if (fn.body) visit(fn.body);
308
+ return found;
309
+ }
310
+
311
+ function resolvedIdentities(ts, checker, fn, entry, reasons) {
312
+ const symbols = bindingSymbols(ts, checker, fn);
313
+ const identities = [];
314
+ for (const identity of entry.identities) {
315
+ const parts = identity.binding.split('.');
316
+ const candidates = symbols.get(parts[0]) ?? new Set();
317
+ if (candidates.size !== 1) {
318
+ reasons.push(`${entry.path}#${entry.export} cannot resolve one local binding for identity ${identity.id} (${identity.binding})`);
319
+ continue;
320
+ }
321
+ identities.push({ ...identity, parts, symbol: [...candidates][0] });
322
+ }
323
+ return identities.length === entry.identities.length ? identities : null;
324
+ }
325
+
326
+ function accessPath(ts, checker, node) {
327
+ node = unwrapExpression(ts, node);
328
+ if (ts.isIdentifier(node)) return { symbol: symbolAt(ts, checker, node), parts: [node.text] };
329
+ if (ts.isPropertyAccessExpression(node)) {
330
+ const base = accessPath(ts, checker, node.expression);
331
+ return base ? { symbol: base.symbol, parts: [...base.parts, node.name.text] } : null;
332
+ }
333
+ if (ts.isElementAccessExpression(node) && ts.isStringLiteralLike(node.argumentExpression)) {
334
+ const base = accessPath(ts, checker, node.expression);
335
+ return base ? { symbol: base.symbol, parts: [...base.parts, node.argumentExpression.text] } : null;
336
+ }
337
+ return null;
338
+ }
339
+
340
+ function expandedAccessPath(ts, checker, node, seen = new Set(), depth = 0) {
341
+ if (!node || depth > 10) return null;
342
+ node = unwrapExpression(ts, node);
343
+ if (ts.isIdentifier(node)) {
344
+ const symbol = symbolAt(ts, checker, node);
345
+ if (!symbol || seen.has(symbol)) return symbol ? { symbol, parts: [node.text] } : null;
346
+ const initializer = constInitializer(ts, symbol);
347
+ return initializer ? expandedAccessPath(ts, checker, initializer, new Set(seen).add(symbol), depth + 1)
348
+ ?? { symbol, parts: [node.text] } : { symbol, parts: [node.text] };
349
+ }
350
+ if (ts.isPropertyAccessExpression(node)) {
351
+ const base = expandedAccessPath(ts, checker, node.expression, seen, depth + 1);
352
+ return base ? { symbol: base.symbol, parts: [...base.parts, node.name.text] } : null;
353
+ }
354
+ if (ts.isElementAccessExpression(node) && ts.isStringLiteralLike(node.argumentExpression)) {
355
+ const base = expandedAccessPath(ts, checker, node.expression, seen, depth + 1);
356
+ return base ? { symbol: base.symbol, parts: [...base.parts, node.argumentExpression.text] } : null;
357
+ }
358
+ return null;
359
+ }
360
+
361
+ function matchesIdentityAccess(ts, checker, node, identity) {
362
+ const matches = access => access?.symbol === identity.symbol && access.parts.length >= identity.parts.length
363
+ && identity.parts.every((part, index) => access.parts[index] === part);
364
+ return matches(accessPath(ts, checker, node)) || matches(expandedAccessPath(ts, checker, node));
365
+ }
366
+
367
+ function expressionReferences(ts, checker, expression, identity) {
368
+ let found = false;
369
+ const visit = node => {
370
+ if (found) return;
371
+ if (matchesIdentityAccess(ts, checker, node, identity)) {
372
+ found = true;
373
+ return;
374
+ }
375
+ ts.forEachChild(node, visit);
376
+ };
377
+ visit(expression);
378
+ return found;
379
+ }
380
+
381
+ function constInitializer(ts, symbol) {
382
+ const declarations = symbol?.getDeclarations?.() ?? [];
383
+ if (declarations.length !== 1 || !ts.isVariableDeclaration(declarations[0]) || !declarations[0].initializer
384
+ || !(ts.getCombinedNodeFlags(declarations[0].parent) & ts.NodeFlags.Const)) return null;
385
+ return declarations[0].initializer;
386
+ }
387
+
388
+ function propertyKey(ts, name) {
389
+ if (ts.isIdentifier(name) || ts.isStringLiteralLike(name) || ts.isNumericLiteral(name)) return name.text;
390
+ return null;
391
+ }
392
+
393
+ function effectiveObjectValues(ts, expression) {
394
+ const selected = new Map();
395
+ for (let index = expression.properties.length - 1; index >= 0; index -= 1) {
396
+ const property = expression.properties[index];
397
+ if (ts.isSpreadAssignment(property) || ts.isMethodDeclaration(property) || ts.isGetAccessorDeclaration(property)
398
+ || ts.isSetAccessorDeclaration(property)) return null;
399
+ const key = propertyKey(ts, property.name);
400
+ if (key === null) return null;
401
+ if (!selected.has(key)) selected.set(key, ts.isShorthandPropertyAssignment(property) ? property.name : property.initializer);
402
+ }
403
+ return [...selected.entries()].sort(([left], [right]) => left.localeCompare(right));
404
+ }
405
+
406
+ function identityContribution(ts, checker, expression, identity, seen = new Set(), depth = 0) {
407
+ if (!expression || depth > 12) return 'unproven';
408
+ expression = unwrapExpression(ts, expression);
409
+ if (matchesIdentityAccess(ts, checker, expression, identity)) return 'yes';
410
+ if (ts.isIdentifier(expression)) {
411
+ const symbol = symbolAt(ts, checker, expression);
412
+ if (!symbol || seen.has(symbol)) return 'no';
413
+ const initializer = constInitializer(ts, symbol);
414
+ return initializer ? identityContribution(ts, checker, initializer, identity, new Set(seen).add(symbol), depth + 1) : 'no';
415
+ }
416
+ if (ts.isConditionalExpression(expression)) {
417
+ const whenTrue = identityContribution(ts, checker, expression.whenTrue, identity, seen, depth + 1);
418
+ const whenFalse = identityContribution(ts, checker, expression.whenFalse, identity, seen, depth + 1);
419
+ if (whenTrue === 'yes' && whenFalse === 'yes') return 'yes';
420
+ if (whenTrue === 'unproven' || whenFalse === 'unproven') return 'unproven';
421
+ return 'no';
422
+ }
423
+ if (ts.isBinaryExpression(expression)) {
424
+ if (expression.operatorToken.kind === ts.SyntaxKind.CommaToken) return identityContribution(ts, checker, expression.right, identity, seen, depth + 1);
425
+ if (expression.operatorToken.kind !== ts.SyntaxKind.QuestionQuestionToken) {
426
+ return expressionReferences(ts, checker, expression, identity) ? 'unproven' : 'no';
427
+ }
428
+ const left = identityContribution(ts, checker, expression.left, identity, seen, depth + 1);
429
+ const right = identityContribution(ts, checker, expression.right, identity, seen, depth + 1);
430
+ return left === 'yes' || right === 'yes' ? 'yes' : left === 'unproven' || right === 'unproven' ? 'unproven' : 'no';
431
+ }
432
+ if (ts.isPrefixUnaryExpression(expression)) return expressionReferences(ts, checker, expression, identity) ? 'unproven' : 'no';
433
+ if (ts.isTemplateExpression(expression)) {
434
+ const values = expression.templateSpans.map(span => identityContribution(ts, checker, span.expression, identity, seen, depth + 1));
435
+ return values.includes('yes') ? 'yes' : values.includes('unproven') ? 'unproven' : 'no';
436
+ }
437
+ if (ts.isArrayLiteralExpression(expression)) {
438
+ if (expression.elements.some(element => ts.isSpreadElement(element))) return 'unproven';
439
+ const values = expression.elements.map(element => identityContribution(ts, checker, element, identity, seen, depth + 1));
440
+ return values.includes('yes') ? 'yes' : values.includes('unproven') ? 'unproven' : 'no';
441
+ }
442
+ if (ts.isObjectLiteralExpression(expression)) {
443
+ const values = effectiveObjectValues(ts, expression);
444
+ if (!values || values.length !== expression.properties.length) return 'unproven';
445
+ const states = values.map(([, value]) => identityContribution(ts, checker, value, identity, seen, depth + 1));
446
+ return states.includes('yes') ? 'yes' : states.includes('unproven') ? 'unproven' : 'no';
447
+ }
448
+ if (ts.isPropertyAccessExpression(expression) || ts.isElementAccessExpression(expression)) return 'no';
449
+ if (ts.isCallExpression(expression) || ts.isNewExpression(expression) || ts.isAwaitExpression(expression)) return 'unproven';
450
+ return 'no';
451
+ }
452
+
453
+ function returnedExpression(ts, fn) {
454
+ if (ts.isArrowFunction(fn) && !ts.isBlock(fn.body)) return fn.body;
455
+ if (!fn.body || !ts.isBlock(fn.body)) return null;
456
+ const returns = [];
457
+ const visit = node => {
458
+ if (node !== fn.body && (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node))) return;
459
+ if (ts.isReturnStatement(node) && node.expression) returns.push(node.expression);
460
+ else ts.forEachChild(node, visit);
461
+ };
462
+ visit(fn.body);
463
+ return returns.length === 1 ? returns[0] : null;
464
+ }
465
+
466
+ function rootKeyExpression(ts, checker, expression, identities, seen = new Set(), depth = 0) {
467
+ if (!expression || depth > 10) return null;
468
+ expression = unwrapExpression(ts, expression);
469
+ if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)) {
470
+ const returned = returnedExpression(ts, expression);
471
+ return returned ? rootKeyExpression(ts, checker, returned, identities, seen, depth + 1) : null;
472
+ }
473
+ if (ts.isIdentifier(expression) && !identities.some(identity => identity.symbol === symbolAt(ts, checker, expression))) {
474
+ const symbol = symbolAt(ts, checker, expression);
475
+ if (symbol && !seen.has(symbol)) {
476
+ const declarations = symbol.getDeclarations?.() ?? [];
477
+ if (declarations.length === 1 && ts.isVariableDeclaration(declarations[0]) && declarations[0].initializer
478
+ && (ts.getCombinedNodeFlags(declarations[0].parent) & ts.NodeFlags.Const)) {
479
+ return rootKeyExpression(ts, checker, declarations[0].initializer, identities, new Set(seen).add(symbol), depth + 1);
480
+ }
481
+ }
482
+ }
483
+ return expression;
484
+ }
485
+
486
+ function staticKeyExpression(ts, checker, expression, identities, seen = new Set(), depth = 0) {
487
+ if (!expression || depth > 12) return false;
488
+ expression = unwrapExpression(ts, expression);
489
+ if (expression.kind === ts.SyntaxKind.NullKeyword || expression.kind === ts.SyntaxKind.TrueKeyword
490
+ || expression.kind === ts.SyntaxKind.FalseKeyword || ts.isStringLiteralLike(expression)
491
+ || ts.isNumericLiteral(expression) || ts.isBigIntLiteral(expression)) return true;
492
+ if (ts.isNoSubstitutionTemplateLiteral(expression)) return true;
493
+ if (ts.isTemplateExpression(expression)) return expression.templateSpans.every(span => staticKeyExpression(ts, checker, span.expression, identities, seen, depth + 1));
494
+ if (ts.isArrayLiteralExpression(expression)) return expression.elements.every(element => !ts.isSpreadElement(element)
495
+ && staticKeyExpression(ts, checker, element, identities, seen, depth + 1));
496
+ if (ts.isObjectLiteralExpression(expression)) {
497
+ const values = effectiveObjectValues(ts, expression);
498
+ return values !== null && values.length === expression.properties.length
499
+ && values.every(([, value]) => staticKeyExpression(ts, checker, value, identities, seen, depth + 1));
500
+ }
501
+ if (ts.isConditionalExpression(expression)) return staticKeyExpression(ts, checker, expression.condition, identities, seen, depth + 1)
502
+ && staticKeyExpression(ts, checker, expression.whenTrue, identities, seen, depth + 1)
503
+ && staticKeyExpression(ts, checker, expression.whenFalse, identities, seen, depth + 1);
504
+ if (ts.isBinaryExpression(expression)) return staticKeyExpression(ts, checker, expression.left, identities, seen, depth + 1)
505
+ && staticKeyExpression(ts, checker, expression.right, identities, seen, depth + 1);
506
+ if (ts.isPrefixUnaryExpression(expression)) return staticKeyExpression(ts, checker, expression.operand, identities, seen, depth + 1);
507
+ if (ts.isPropertyAccessExpression(expression) || ts.isElementAccessExpression(expression)) {
508
+ if (identities.some(identity => expressionReferences(ts, checker, expression, identity))) return true;
509
+ return false;
510
+ }
511
+ if (ts.isIdentifier(expression)) {
512
+ if (expression.text === 'undefined' || identities.some(identity => identity.symbol === symbolAt(ts, checker, expression))) return true;
513
+ const symbol = symbolAt(ts, checker, expression);
514
+ if (!symbol || seen.has(symbol)) return false;
515
+ const declarations = symbol.getDeclarations?.() ?? [];
516
+ if (declarations.length !== 1 || !ts.isVariableDeclaration(declarations[0]) || !declarations[0].initializer
517
+ || !(ts.getCombinedNodeFlags(declarations[0].parent) & ts.NodeFlags.Const)) return false;
518
+ return staticKeyExpression(ts, checker, declarations[0].initializer, identities, new Set(seen).add(symbol), depth + 1);
519
+ }
520
+ return false;
521
+ }
522
+
523
+ function keyLeaves(ts, checker, expression, identities, decisions = [], depth = 0) {
524
+ if (depth > 12) return null;
525
+ expression = rootKeyExpression(ts, checker, expression, identities);
526
+ if (!expression) return null;
527
+ if (ts.isConditionalExpression(expression)) {
528
+ if (!staticKeyExpression(ts, checker, expression.condition, identities)) return null;
529
+ const whenTrue = keyLeaves(ts, checker, expression.whenTrue, identities, [...decisions, { condition: expression.condition, branch: true }], depth + 1);
530
+ const whenFalse = keyLeaves(ts, checker, expression.whenFalse, identities, [...decisions, { condition: expression.condition, branch: false }], depth + 1);
531
+ return whenTrue && whenFalse ? [...whenTrue, ...whenFalse] : null;
532
+ }
533
+ if (expression.kind === ts.SyntaxKind.NullKeyword) return [{ kind: 'null', expression, decisions }];
534
+ return staticKeyExpression(ts, checker, expression, identities) ? [{ kind: 'active', expression, decisions }] : null;
535
+ }
536
+
537
+ function nullish(ts, checker, expression) {
538
+ expression = unwrapExpression(ts, expression);
539
+ if (expression.kind === ts.SyntaxKind.NullKeyword) return true;
540
+ if (!ts.isIdentifier(expression) || expression.text !== 'undefined') return false;
541
+ const symbol = checker.getSymbolAtLocation(expression);
542
+ return !symbol || (symbol.getDeclarations?.() ?? []).every(declaration => declaration.getSourceFile().isDeclarationFile);
543
+ }
544
+
545
+ function trueMeansAvailable(ts, checker, condition, identity) {
546
+ condition = unwrapExpression(ts, condition);
547
+ if (expressionReferences(ts, checker, condition, identity) && (ts.isIdentifier(condition)
548
+ || ts.isPropertyAccessExpression(condition) || ts.isElementAccessExpression(condition))) return true;
549
+ if (ts.isPrefixUnaryExpression(condition) && condition.operator === ts.SyntaxKind.ExclamationToken) {
550
+ const nested = trueMeansAvailable(ts, checker, condition.operand, identity);
551
+ return nested === null ? null : !nested;
552
+ }
553
+ if (ts.isBinaryExpression(condition)) {
554
+ const leftIdentity = expressionReferences(ts, checker, condition.left, identity) && nullish(ts, checker, condition.right);
555
+ const rightIdentity = expressionReferences(ts, checker, condition.right, identity) && nullish(ts, checker, condition.left);
556
+ if (!leftIdentity && !rightIdentity) return null;
557
+ if ([ts.SyntaxKind.ExclamationEqualsToken, ts.SyntaxKind.ExclamationEqualsEqualsToken].includes(condition.operatorToken.kind)) return true;
558
+ if ([ts.SyntaxKind.EqualsEqualsToken, ts.SyntaxKind.EqualsEqualsEqualsToken].includes(condition.operatorToken.kind)) return false;
559
+ }
560
+ return null;
561
+ }
562
+
563
+ function violation(config, call, ruleId, message, extra = {}) {
564
+ return { ruleId, path: relativePath(config.root, call.source.fileName), ...sourceLocation(call.source, call.node), message, ...extra };
565
+ }
566
+
567
+ function isUnshadowedCommonJsRequire(ts, checker, expression, seen = new Set(), depth = 0) {
568
+ expression = unwrapExpression(ts, expression);
569
+ if (!ts.isIdentifier(expression) || depth > 10) return false;
570
+ const symbol = checker.getSymbolAtLocation(expression);
571
+ const declarations = symbol?.getDeclarations?.() ?? [];
572
+ if (expression.text === 'require' && (!symbol || declarations.length === 0
573
+ || declarations.every(declaration => declaration.getSourceFile().isDeclarationFile))) return true;
574
+ const normalized = symbolOfRequireAlias(ts, checker, expression);
575
+ if (!normalized || seen.has(normalized)) return false;
576
+ const initializer = constInitializer(ts, normalized);
577
+ return initializer ? isUnshadowedCommonJsRequire(ts, checker, initializer, new Set(seen).add(normalized), depth + 1) : false;
578
+ }
579
+
580
+ function symbolOfRequireAlias(ts, checker, expression) {
581
+ const symbol = checker.getSymbolAtLocation(expression);
582
+ return normalizedSymbol(ts, checker, symbol);
583
+ }
584
+
585
+ function swrReferences(config, context) {
586
+ let found = false;
587
+ const unsupported = [];
588
+ for (const source of context.files) {
589
+ const checker = context.checkerFor(source.fileName);
590
+ const report = (node, kind) => {
591
+ found = true;
592
+ unsupported.push(`${relativePath(config.root, source.fileName)}:${sourceLocation(source, node).line} uses unsupported ${kind} for SWR`);
593
+ };
594
+ const visit = node => {
595
+ if ((context.ts.isImportDeclaration(node) || context.ts.isExportDeclaration(node)) && node.moduleSpecifier
596
+ && context.ts.isStringLiteralLike(node.moduleSpecifier) && SWR_SPECIFIERS.has(node.moduleSpecifier.text)) found = true;
597
+ if (context.ts.isImportEqualsDeclaration(node) && context.ts.isExternalModuleReference(node.moduleReference)
598
+ && node.moduleReference.expression && context.ts.isStringLiteralLike(node.moduleReference.expression)
599
+ && SWR_SPECIFIERS.has(node.moduleReference.expression.text)) report(node, 'import-equals binding');
600
+ if (context.ts.isCallExpression(node) && node.arguments.length >= 1 && context.ts.isStringLiteralLike(node.arguments[0])
601
+ && SWR_SPECIFIERS.has(node.arguments[0].text)) {
602
+ if (node.expression.kind === context.ts.SyntaxKind.ImportKeyword) report(node, 'dynamic import binding');
603
+ else if (isUnshadowedCommonJsRequire(context.ts, checker, node.expression)) report(node, 'CommonJS require binding');
604
+ }
605
+ context.ts.forEachChild(node, visit);
606
+ };
607
+ visit(source);
608
+ }
609
+ return { found, unsupported };
610
+ }
611
+
612
+ function nonNullFalsy(ts, expression) {
613
+ expression = unwrapExpression(ts, expression);
614
+ return expression.kind === ts.SyntaxKind.FalseKeyword || ts.isStringLiteralLike(expression) && expression.text === ''
615
+ || ts.isNumericLiteral(expression) && Number(expression.text) === 0
616
+ || ts.isIdentifier(expression) && expression.text === 'undefined';
617
+ }
618
+
619
+ function containerSymbolsFromKey(ts, checker, expression, found = new Set(), seen = new Set(), depth = 0) {
620
+ if (!expression || depth > 12) return found;
621
+ expression = unwrapExpression(ts, expression);
622
+ if (ts.isIdentifier(expression)) {
623
+ const symbol = symbolAt(ts, checker, expression);
624
+ if (!symbol || seen.has(symbol)) return found;
625
+ const initializer = constInitializer(ts, symbol);
626
+ if (!initializer) return found;
627
+ const value = unwrapExpression(ts, initializer);
628
+ if (ts.isArrayLiteralExpression(value) || ts.isObjectLiteralExpression(value)) found.add(symbol);
629
+ containerSymbolsFromKey(ts, checker, initializer, found, new Set(seen).add(symbol), depth + 1);
630
+ return found;
631
+ }
632
+ ts.forEachChild(expression, child => { containerSymbolsFromKey(ts, checker, child, found, seen, depth + 1); });
633
+ return found;
634
+ }
635
+
636
+ function keyContainerRisks(ts, checker, key, owner) {
637
+ const symbols = containerSymbolsFromKey(ts, checker, key);
638
+ if (!symbols.size || !owner?.body) return [];
639
+ const declarationNames = new Set([...symbols].flatMap(symbol => (symbol.getDeclarations?.() ?? [])
640
+ .filter(ts.isVariableDeclaration).map(declaration => declaration.name)));
641
+ const risks = [];
642
+ const visit = node => {
643
+ if (ts.isIdentifier(node) && symbols.has(symbolAt(ts, checker, node)) && !declarationNames.has(node)
644
+ && !(node.pos >= key.pos && node.end <= key.end)) risks.push(node);
645
+ ts.forEachChild(node, visit);
646
+ };
647
+ visit(owner.body);
648
+ return risks;
649
+ }
650
+
651
+ function inspectKey(config, context, entry, call, identities, violations, reasons) {
652
+ const key = call.node.arguments[0];
653
+ if (!key) {
654
+ reasons.push(`${entry.path}#${entry.export} has an SWR call without a key`);
655
+ return;
656
+ }
657
+ const containerRisks = keyContainerRisks(context.ts, call.checker, key, call.owner);
658
+ if (containerRisks.length) {
659
+ const locations = containerRisks.map(node => sourceLocation(call.source, node).line).filter((line, index, all) => all.indexOf(line) === index);
660
+ reasons.push(`${entry.path}#${entry.export} key container is referenced outside its declaration and selected SWR key${locations.length ? ` (line${locations.length === 1 ? '' : 's'} ${locations.join(', ')})` : ''}`);
661
+ return;
662
+ }
663
+ const leaves = keyLeaves(context.ts, call.checker, key, identities);
664
+ if (!leaves) {
665
+ reasons.push(`${entry.path}#${entry.export} uses a dynamic SWR key whose identity cannot be proved`);
666
+ return;
667
+ }
668
+ const active = leaves.filter(leaf => leaf.kind === 'active');
669
+ if (active.some(leaf => nonNullFalsy(context.ts, leaf.expression))) violations.push(violation(config, call, SWR_KEY_RULE_ID,
670
+ `${entry.id} uses explicit null, rather than another falsy value, for a disabled SWR key.`, { lifecycle: entry.id }));
671
+ for (const identity of identities) {
672
+ if (!active.length) {
673
+ violations.push(violation(config, call, entry.kind === 'mutation' && identity.resource ? SWR_MUTATION_RULE_ID : SWR_KEY_RULE_ID,
674
+ `${entry.id} has no active key path carrying declared identity ${identity.id} (${identity.binding}).`,
675
+ { lifecycle: entry.id, identity: identity.id }));
676
+ continue;
677
+ }
678
+ const contributions = active.map(leaf => identityContribution(context.ts, call.checker, leaf.expression, identity));
679
+ if (contributions.includes('unproven')) {
680
+ reasons.push(`${entry.path}#${entry.export} key value contribution for identity ${identity.id} (${identity.binding}) cannot be proved on every active path`);
681
+ } else if (contributions.includes('no')) {
682
+ const ruleId = entry.kind === 'mutation' && identity.resource ? SWR_MUTATION_RULE_ID : SWR_KEY_RULE_ID;
683
+ violations.push(violation(config, call, ruleId,
684
+ `${entry.id} key must include declared ${identity.resource ? 'resource ' : ''}identity ${identity.id} (${identity.binding}) on every active key path.`,
685
+ { lifecycle: entry.id, identity: identity.id }));
686
+ }
687
+ if (identity.gatesRequest) {
688
+ const gated = active.every(leaf => leaf.decisions.some(decision => {
689
+ const available = trueMeansAvailable(context.ts, call.checker, decision.condition, identity);
690
+ return available !== null && decision.branch === available;
691
+ }));
692
+ if (!gated) violations.push(violation(config, call, SWR_KEY_RULE_ID,
693
+ `${entry.id} must produce an explicit null key when ${identity.id} (${identity.binding}) is unavailable.`,
694
+ { lifecycle: entry.id, identity: identity.id }));
695
+ }
696
+ }
697
+ }
698
+
699
+ /** Check explicitly declared SWR lifecycle keys against the target program and installed SWR identity. */
700
+ export function checkFrontendDataLifecycle(config, context) {
701
+ const ruleIds = [...SWR_DATA_RULE_IDS];
702
+ const reasons = [];
703
+ const violations = [];
704
+ const references = swrReferences(config, context);
705
+ let contract;
706
+ try { contract = parseContract(config); } catch (error) {
707
+ return { violations, coverage: { status: 'unavailable', ruleIds, details: [String(error.message ?? error)] } };
708
+ }
709
+ if (!contract && !references.found) return { violations, coverage: { status: 'not-applicable', ruleIds } };
710
+ if (!contract) return { violations, coverage: { status: 'unavailable', ruleIds,
711
+ details: ['package.json#starci.codePatterns.next.dataLifecycle is required when production source selects SWR', ...references.unsupported] } };
712
+ reasons.push(...references.unsupported);
713
+ let installed;
714
+ try { installed = installedSWR(config.root); } catch (error) {
715
+ return { violations, coverage: { status: 'unavailable', ruleIds, details: [String(error.message ?? error)] } };
716
+ }
717
+ if (installed.major !== contract.swr.major) reasons.push(`installed swr ${installed.version} does not match declared major ${contract.swr.major}`);
718
+ const official = officialTargets(config, context, installed, reasons);
719
+ const calls = discoverCalls(config, context, official.targets);
720
+ const lifecycleCalls = calls.filter(call => call.kind === 'query' || call.kind === 'mutation');
721
+ const globalMutates = calls.filter(call => call.kind === 'global-mutate');
722
+ if (official.selectedBindings && lifecycleCalls.length === 0) reasons.push('selected SWR hook imports have no statically resolved lifecycle call');
723
+ for (const call of globalMutates) reasons.push(`${relativePath(config.root, call.source.fileName)}:${sourceLocation(call.source, call.node).line} uses global mutate; resource matching or filter semantics are not statically declared`);
724
+
725
+ const hookCache = new Map();
726
+ const entriesByHook = new Map();
727
+ for (const entry of contract.hooks) {
728
+ const key = `${entry.path}\0${entry.export}`;
729
+ if (!entriesByHook.has(key)) entriesByHook.set(key, []);
730
+ entriesByHook.get(key).push(entry);
731
+ if (!hookCache.has(key)) hookCache.set(key, exportedHook(config, context, entry, reasons));
732
+ }
733
+ const mapped = new Map();
734
+ for (const [key, entries] of entriesByHook) {
735
+ const hook = hookCache.get(key);
736
+ if (!hook) continue;
737
+ const owned = lifecycleCalls.filter(call => call.owner === hook.fn);
738
+ if (!owned.length) {
739
+ reasons.push(`${entries[0].path}#${entries[0].export} contains no statically resolved SWR lifecycle call`);
740
+ continue;
741
+ }
742
+ for (const entry of entries) {
743
+ let candidates;
744
+ if (entry.resultBinding !== undefined) candidates = owned.filter(call => call.bindings.includes(entry.resultBinding));
745
+ else candidates = owned.length === 1 && entries.length === 1 ? owned : [];
746
+ if (candidates.length !== 1) {
747
+ reasons.push(`${entry.path}#${entry.export} lifecycle ${entry.id} must select exactly one SWR call${owned.length > 1 ? ' with resultBinding' : ''}`);
748
+ continue;
749
+ }
750
+ const call = candidates[0];
751
+ mapped.set(call, (mapped.get(call) ?? 0) + 1);
752
+ if (call.kind !== entry.kind) {
753
+ reasons.push(`${entry.path}#${entry.export} lifecycle ${entry.id} declares ${entry.kind} but resolves ${call.kind}`);
754
+ continue;
755
+ }
756
+ const identities = resolvedIdentities(context.ts, hook.checker, hook.fn, entry, reasons);
757
+ if (identities) inspectKey(config, context, entry, call, identities, violations, reasons);
758
+ }
759
+ for (const call of owned) if ((mapped.get(call) ?? 0) !== 1) {
760
+ reasons.push(`${relativePath(config.root, call.source.fileName)}:${sourceLocation(call.source, call.node).line} SWR call is not matched by exactly one declared lifecycle`);
761
+ }
762
+ }
763
+ const declaredFunctions = new Set([...hookCache.values()].filter(Boolean).map(item => item.fn));
764
+ for (const call of lifecycleCalls) if (!declaredFunctions.has(call.owner)) {
765
+ reasons.push(`${relativePath(config.root, call.source.fileName)}:${sourceLocation(call.source, call.node).line} SWR call is outside every declared lifecycle hook`);
766
+ }
767
+ const details = [...new Set(reasons)].sort();
768
+ return {
769
+ violations,
770
+ coverage: details.length ? { status: 'unavailable', ruleIds, hooks: contract.hooks.length, calls: lifecycleCalls.length,
771
+ swr: { name: installed.name, version: installed.version, major: installed.major }, details }
772
+ : { status: 'checked', ruleIds, hooks: contract.hooks.length, calls: lifecycleCalls.length,
773
+ swr: { name: installed.name, version: installed.version, major: installed.major } },
774
+ };
775
+ }