@ontrails/warden 1.0.0-beta.4 → 1.0.0-beta.42

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 (279) hide show
  1. package/CHANGELOG.md +837 -13
  2. package/README.md +133 -26
  3. package/bin/warden.ts +51 -0
  4. package/package.json +27 -6
  5. package/src/adapter-check.ts +136 -0
  6. package/src/cli.ts +1570 -105
  7. package/src/command.ts +986 -0
  8. package/src/config.ts +193 -0
  9. package/src/draft.ts +22 -0
  10. package/src/drift.ts +233 -23
  11. package/src/fix.ts +126 -0
  12. package/src/formatters.ts +78 -13
  13. package/src/guide.ts +245 -0
  14. package/src/index.ts +247 -15
  15. package/src/project-context.ts +446 -0
  16. package/src/project-rules.ts +290 -0
  17. package/src/resolve.ts +531 -0
  18. package/src/rules/activation-orphan.ts +97 -0
  19. package/src/rules/captured-kernel.ts +375 -0
  20. package/src/rules/circular-refs.ts +150 -0
  21. package/src/rules/cli-command-route-coherence.ts +177 -0
  22. package/src/rules/composes-declarations.ts +839 -0
  23. package/src/rules/context-no-surface-types.ts +79 -15
  24. package/src/rules/dead-internal-trail.ts +161 -0
  25. package/src/rules/dead-public-trail.ts +258 -0
  26. package/src/rules/draft-file-marking.ts +155 -0
  27. package/src/rules/draft-visible-debt.ts +82 -0
  28. package/src/rules/duplicate-exported-symbol.ts +211 -0
  29. package/src/rules/duplicate-public-contract.ts +137 -0
  30. package/src/rules/entity-exists.ts +254 -0
  31. package/src/rules/entity-ids.ts +15 -0
  32. package/src/rules/error-mapping-completeness.ts +290 -0
  33. package/src/rules/example-valid.ts +395 -0
  34. package/src/rules/fires-declarations.ts +740 -0
  35. package/src/rules/governed-symbol-residue.ts +438 -0
  36. package/src/rules/implementation-returns-result.ts +1409 -166
  37. package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
  38. package/src/rules/incomplete-crud.ts +583 -0
  39. package/src/rules/index.ts +277 -10
  40. package/src/rules/intent-propagation.ts +125 -0
  41. package/src/rules/layer-field-name-drift.ts +102 -0
  42. package/src/rules/library-projection-coherence.ts +100 -0
  43. package/src/rules/metadata.ts +871 -0
  44. package/src/rules/missing-reconcile.ts +97 -0
  45. package/src/rules/missing-visibility.ts +111 -0
  46. package/src/rules/no-destructured-compose.ts +196 -0
  47. package/src/rules/no-dev-permit-in-source.ts +99 -0
  48. package/src/rules/no-direct-implementation-call.ts +12 -7
  49. package/src/rules/no-legacy-cli-alias-export.ts +247 -0
  50. package/src/rules/no-legacy-layer-imports.ts +211 -0
  51. package/src/rules/no-native-error-result.ts +118 -0
  52. package/src/rules/no-redundant-result-error-wrap.ts +384 -0
  53. package/src/rules/no-retired-cross-vocabulary.ts +204 -0
  54. package/src/rules/no-sync-result-assumption.ts +1141 -98
  55. package/src/rules/no-throw-in-detour-recover.ts +225 -0
  56. package/src/rules/no-throw-in-implementation.ts +15 -8
  57. package/src/rules/no-top-level-surface.ts +371 -0
  58. package/src/rules/on-references-exist.ts +194 -0
  59. package/src/rules/orphaned-signal.ts +149 -0
  60. package/src/rules/owner-projection-parity.ts +143 -0
  61. package/src/rules/permit-governance.ts +25 -0
  62. package/src/rules/public-export-example-coverage.ts +573 -0
  63. package/src/rules/public-internal-deep-imports.ts +456 -0
  64. package/src/rules/public-output-schema.ts +29 -0
  65. package/src/rules/public-union-output-discriminants.ts +150 -0
  66. package/src/rules/read-intent-fires.ts +188 -0
  67. package/src/rules/reference-exists.ts +97 -0
  68. package/src/rules/registry-names.ts +167 -0
  69. package/src/rules/resolved-import-boundary.ts +146 -0
  70. package/src/rules/resource-declarations.ts +697 -0
  71. package/src/rules/resource-exists.ts +181 -0
  72. package/src/rules/resource-id-grammar.ts +65 -0
  73. package/src/rules/resource-mock-coverage.ts +115 -0
  74. package/src/rules/retired-vocabulary.ts +633 -0
  75. package/src/rules/scan.ts +38 -25
  76. package/src/rules/scheduled-destroy-intent.ts +44 -0
  77. package/src/rules/signal-graph-coaching.ts +220 -0
  78. package/src/rules/source/composition.ts +165 -0
  79. package/src/rules/source/drafts.ts +164 -0
  80. package/src/rules/source/entities.ts +618 -0
  81. package/src/rules/source/pragmas.ts +45 -0
  82. package/src/rules/source/resources.ts +64 -0
  83. package/src/rules/source/signals.ts +397 -0
  84. package/src/rules/source/stores.ts +310 -0
  85. package/src/rules/specs.ts +9 -5
  86. package/src/rules/static-resource-accessor-preference.ts +653 -0
  87. package/src/rules/surface-overlay-coherence.ts +262 -0
  88. package/src/rules/surface-trailhead-coherence.ts +366 -0
  89. package/src/rules/trail-fork-coaching.ts +625 -0
  90. package/src/rules/trail-versioning-source.ts +1076 -0
  91. package/src/rules/trail-versioning-topo.ts +172 -0
  92. package/src/rules/trailhead-override-divergence.ts +356 -0
  93. package/src/rules/types.ts +354 -8
  94. package/src/rules/unmaterialized-activation-source.ts +85 -0
  95. package/src/rules/unreachable-detour-shadowing.ts +339 -0
  96. package/src/rules/valid-describe-refs.ts +162 -32
  97. package/src/rules/valid-detour-contract.ts +78 -0
  98. package/src/rules/warden-export-symmetry.ts +540 -0
  99. package/src/rules/warden-rules-use-ast.ts +1109 -0
  100. package/src/rules/webhook-route-collision.ts +306 -0
  101. package/src/trails/activation-orphan.trail.ts +84 -0
  102. package/src/trails/captured-kernel.trail.ts +108 -0
  103. package/src/trails/circular-refs.trail.ts +29 -0
  104. package/src/trails/cli-command-route-coherence.trail.ts +47 -0
  105. package/src/trails/composes-declarations.trail.ts +22 -0
  106. package/src/trails/context-no-surface-types.trail.ts +21 -0
  107. package/src/trails/dead-internal-trail.trail.ts +26 -0
  108. package/src/trails/dead-public-trail.trail.ts +31 -0
  109. package/src/trails/deprecation-without-guidance.trail.ts +21 -0
  110. package/src/trails/draft-file-marking.trail.ts +16 -0
  111. package/src/trails/draft-visible-debt.trail.ts +16 -0
  112. package/src/trails/duplicate-exported-symbol.trail.ts +48 -0
  113. package/src/trails/duplicate-public-contract.trail.ts +47 -0
  114. package/src/trails/entity-exists.trail.ts +21 -0
  115. package/src/trails/error-mapping-completeness.trail.ts +30 -0
  116. package/src/trails/example-valid.trail.ts +25 -0
  117. package/src/trails/fires-declarations.trail.ts +23 -0
  118. package/src/trails/fork-without-preserved-implementation.trail.ts +31 -0
  119. package/src/trails/governed-symbol-residue.trail.ts +24 -0
  120. package/src/trails/implementation-returns-result.trail.ts +20 -0
  121. package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
  122. package/src/trails/incomplete-crud.trail.ts +39 -0
  123. package/src/trails/index.ts +89 -0
  124. package/src/trails/intent-propagation.trail.ts +30 -0
  125. package/src/trails/layer-field-name-drift.trail.ts +39 -0
  126. package/src/trails/library-projection-coherence.trail.ts +43 -0
  127. package/src/trails/marker-schema-unsupported.trail.ts +23 -0
  128. package/src/trails/missing-reconcile.trail.ts +33 -0
  129. package/src/trails/missing-visibility.trail.ts +22 -0
  130. package/src/trails/no-destructured-compose.trail.ts +44 -0
  131. package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
  132. package/src/trails/no-direct-implementation-call.trail.ts +16 -0
  133. package/src/trails/no-legacy-cli-alias-export.trail.ts +41 -0
  134. package/src/trails/no-legacy-layer-imports.trail.ts +41 -0
  135. package/src/trails/no-native-error-result.trail.ts +18 -0
  136. package/src/trails/no-redundant-result-error-wrap.trail.ts +55 -0
  137. package/src/trails/no-retired-cross-vocabulary.trail.ts +42 -0
  138. package/src/trails/no-sync-result-assumption.trail.ts +19 -0
  139. package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
  140. package/src/trails/no-throw-in-implementation.trail.ts +20 -0
  141. package/src/trails/no-top-level-surface.trail.ts +43 -0
  142. package/src/trails/on-references-exist.trail.ts +21 -0
  143. package/src/trails/orphaned-signal.trail.ts +36 -0
  144. package/src/trails/owner-projection-parity.trail.ts +26 -0
  145. package/src/trails/pending-force.trail.ts +21 -0
  146. package/src/trails/permit-governance.trail.ts +51 -0
  147. package/src/trails/prefer-schema-inference.trail.ts +21 -0
  148. package/src/trails/public-export-example-coverage.trail.ts +16 -0
  149. package/src/trails/public-internal-deep-imports.trail.ts +94 -0
  150. package/src/trails/public-output-schema.trail.ts +55 -0
  151. package/src/trails/public-union-output-discriminants.trail.ts +33 -0
  152. package/src/trails/read-intent-fires.trail.ts +20 -0
  153. package/src/trails/reference-exists.trail.ts +25 -0
  154. package/src/trails/resolved-import-boundary.trail.ts +109 -0
  155. package/src/trails/resource-declarations.trail.ts +25 -0
  156. package/src/trails/resource-exists.trail.ts +27 -0
  157. package/src/trails/resource-id-grammar.trail.ts +39 -0
  158. package/src/trails/resource-mock-coverage.trail.ts +40 -0
  159. package/src/trails/run.ts +160 -0
  160. package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
  161. package/src/trails/schema.ts +237 -0
  162. package/src/trails/signal-graph-coaching.trail.ts +77 -0
  163. package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
  164. package/src/trails/surface-overlay-coherence.trail.ts +24 -0
  165. package/src/trails/surface-trailhead-coherence.trail.ts +25 -0
  166. package/src/trails/topo.ts +6 -0
  167. package/src/trails/trail-fork-coaching.trail.ts +42 -0
  168. package/src/trails/trailhead-override-divergence.trail.ts +47 -0
  169. package/src/trails/unmaterialized-activation-source.trail.ts +72 -0
  170. package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
  171. package/src/trails/valid-describe-refs.trail.ts +18 -0
  172. package/src/trails/valid-detour-contract.trail.ts +71 -0
  173. package/src/trails/version-gap.trail.ts +35 -0
  174. package/src/trails/version-pinned-compose.trail.ts +23 -0
  175. package/src/trails/version-without-examples.trail.ts +38 -0
  176. package/src/trails/warden-export-symmetry.trail.ts +16 -0
  177. package/src/trails/warden-rules-use-ast.trail.ts +64 -0
  178. package/src/trails/webhook-route-collision.trail.ts +50 -0
  179. package/src/trails/wrap-rule.ts +224 -0
  180. package/src/workspaces.ts +199 -0
  181. package/.turbo/turbo-build.log +0 -1
  182. package/.turbo/turbo-lint.log +0 -3
  183. package/.turbo/turbo-typecheck.log +0 -1
  184. package/dist/cli.d.ts +0 -46
  185. package/dist/cli.d.ts.map +0 -1
  186. package/dist/cli.js +0 -218
  187. package/dist/cli.js.map +0 -1
  188. package/dist/drift.d.ts +0 -26
  189. package/dist/drift.d.ts.map +0 -1
  190. package/dist/drift.js +0 -27
  191. package/dist/drift.js.map +0 -1
  192. package/dist/formatters.d.ts +0 -29
  193. package/dist/formatters.d.ts.map +0 -1
  194. package/dist/formatters.js +0 -87
  195. package/dist/formatters.js.map +0 -1
  196. package/dist/index.d.ts +0 -26
  197. package/dist/index.d.ts.map +0 -1
  198. package/dist/index.js +0 -26
  199. package/dist/index.js.map +0 -1
  200. package/dist/rules/ast.d.ts +0 -41
  201. package/dist/rules/ast.d.ts.map +0 -1
  202. package/dist/rules/ast.js +0 -161
  203. package/dist/rules/ast.js.map +0 -1
  204. package/dist/rules/context-no-surface-types.d.ts +0 -12
  205. package/dist/rules/context-no-surface-types.d.ts.map +0 -1
  206. package/dist/rules/context-no-surface-types.js +0 -96
  207. package/dist/rules/context-no-surface-types.js.map +0 -1
  208. package/dist/rules/implementation-returns-result.d.ts +0 -13
  209. package/dist/rules/implementation-returns-result.d.ts.map +0 -1
  210. package/dist/rules/implementation-returns-result.js +0 -277
  211. package/dist/rules/implementation-returns-result.js.map +0 -1
  212. package/dist/rules/index.d.ts +0 -15
  213. package/dist/rules/index.d.ts.map +0 -1
  214. package/dist/rules/index.js +0 -34
  215. package/dist/rules/index.js.map +0 -1
  216. package/dist/rules/no-direct-impl-in-route.d.ts +0 -12
  217. package/dist/rules/no-direct-impl-in-route.d.ts.map +0 -1
  218. package/dist/rules/no-direct-impl-in-route.js +0 -47
  219. package/dist/rules/no-direct-impl-in-route.js.map +0 -1
  220. package/dist/rules/no-direct-implementation-call.d.ts +0 -12
  221. package/dist/rules/no-direct-implementation-call.d.ts.map +0 -1
  222. package/dist/rules/no-direct-implementation-call.js +0 -39
  223. package/dist/rules/no-direct-implementation-call.js.map +0 -1
  224. package/dist/rules/no-sync-result-assumption.d.ts +0 -6
  225. package/dist/rules/no-sync-result-assumption.d.ts.map +0 -1
  226. package/dist/rules/no-sync-result-assumption.js +0 -98
  227. package/dist/rules/no-sync-result-assumption.js.map +0 -1
  228. package/dist/rules/no-throw-in-detour-target.d.ts +0 -12
  229. package/dist/rules/no-throw-in-detour-target.d.ts.map +0 -1
  230. package/dist/rules/no-throw-in-detour-target.js +0 -87
  231. package/dist/rules/no-throw-in-detour-target.js.map +0 -1
  232. package/dist/rules/no-throw-in-implementation.d.ts +0 -9
  233. package/dist/rules/no-throw-in-implementation.d.ts.map +0 -1
  234. package/dist/rules/no-throw-in-implementation.js +0 -34
  235. package/dist/rules/no-throw-in-implementation.js.map +0 -1
  236. package/dist/rules/prefer-schema-inference.d.ts +0 -7
  237. package/dist/rules/prefer-schema-inference.d.ts.map +0 -1
  238. package/dist/rules/prefer-schema-inference.js +0 -86
  239. package/dist/rules/prefer-schema-inference.js.map +0 -1
  240. package/dist/rules/scan.d.ts +0 -8
  241. package/dist/rules/scan.d.ts.map +0 -1
  242. package/dist/rules/scan.js +0 -32
  243. package/dist/rules/scan.js.map +0 -1
  244. package/dist/rules/specs.d.ts +0 -29
  245. package/dist/rules/specs.d.ts.map +0 -1
  246. package/dist/rules/specs.js +0 -192
  247. package/dist/rules/specs.js.map +0 -1
  248. package/dist/rules/structure.d.ts +0 -13
  249. package/dist/rules/structure.d.ts.map +0 -1
  250. package/dist/rules/structure.js +0 -142
  251. package/dist/rules/structure.js.map +0 -1
  252. package/dist/rules/types.d.ts +0 -52
  253. package/dist/rules/types.d.ts.map +0 -1
  254. package/dist/rules/types.js +0 -2
  255. package/dist/rules/types.js.map +0 -1
  256. package/dist/rules/valid-describe-refs.d.ts +0 -7
  257. package/dist/rules/valid-describe-refs.d.ts.map +0 -1
  258. package/dist/rules/valid-describe-refs.js +0 -51
  259. package/dist/rules/valid-describe-refs.js.map +0 -1
  260. package/dist/rules/valid-detour-refs.d.ts +0 -6
  261. package/dist/rules/valid-detour-refs.d.ts.map +0 -1
  262. package/dist/rules/valid-detour-refs.js +0 -116
  263. package/dist/rules/valid-detour-refs.js.map +0 -1
  264. package/src/__tests__/cli.test.ts +0 -198
  265. package/src/__tests__/drift.test.ts +0 -74
  266. package/src/__tests__/formatters.test.ts +0 -157
  267. package/src/__tests__/implementation-returns-result.test.ts +0 -129
  268. package/src/__tests__/no-direct-implementation-call.test.ts +0 -83
  269. package/src/__tests__/no-sync-result-assumption.test.ts +0 -85
  270. package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
  271. package/src/__tests__/prefer-schema-inference.test.ts +0 -84
  272. package/src/__tests__/rules.test.ts +0 -227
  273. package/src/__tests__/valid-describe-refs.test.ts +0 -60
  274. package/src/rules/ast.ts +0 -213
  275. package/src/rules/no-direct-impl-in-route.ts +0 -81
  276. package/src/rules/no-throw-in-detour-target.ts +0 -150
  277. package/src/rules/valid-detour-refs.ts +0 -187
  278. package/tsconfig.json +0 -9
  279. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,438 @@
1
+ import {
2
+ getNodeKey,
3
+ getNodeValueNode,
4
+ identifierName,
5
+ offsetToLine,
6
+ parseWithDiagnostics,
7
+ walkWithScopeContext,
8
+ } from '@ontrails/source';
9
+ import type { AstNode, AstScopeContext } from '@ontrails/source';
10
+ import { listGovernedVocabularyTransitions } from './retired-vocabulary.js';
11
+ import type {
12
+ GovernedVocabularySymbolRename,
13
+ GovernedVocabularyTransition,
14
+ } from './retired-vocabulary.js';
15
+ import type { WardenDiagnostic, WardenFix, WardenRule } from './types.js';
16
+
17
+ const RULE_NAME = 'governed-symbol-residue';
18
+
19
+ const ACTIVE_STATUSES = new Set<GovernedVocabularyTransition['status']>([
20
+ 'active',
21
+ 'complete',
22
+ ]);
23
+
24
+ const FIX_OWNED_TRANSITION_IDS = new Set(['cross-compose']);
25
+
26
+ const ALLOWED_SOURCE_PATH_SUFFIXES = [
27
+ '/packages/warden/src/rules/governed-symbol-residue.ts',
28
+ '/packages/warden/src/rules/retired-vocabulary.ts',
29
+ ] as const;
30
+
31
+ const normalizePath = (filePath: string): string =>
32
+ filePath.replaceAll('\\', '/');
33
+
34
+ const isAllowedSourcePath = (filePath: string): boolean =>
35
+ ALLOWED_SOURCE_PATH_SUFFIXES.some((suffix) =>
36
+ normalizePath(filePath).endsWith(suffix)
37
+ );
38
+
39
+ interface ActiveSymbolRename {
40
+ readonly rename: GovernedVocabularySymbolRename;
41
+ readonly targetSegments: readonly string[];
42
+ }
43
+
44
+ const activeSymbolRenames = (): readonly ActiveSymbolRename[] =>
45
+ listGovernedVocabularyTransitions()
46
+ .filter(
47
+ (transition) =>
48
+ ACTIVE_STATUSES.has(transition.status) &&
49
+ !FIX_OWNED_TRANSITION_IDS.has(transition.id)
50
+ )
51
+ .flatMap((transition) => {
52
+ const targetSegments = [
53
+ ...new Set(transition.symbolRenames.map((rename) => rename.to)),
54
+ ];
55
+ return transition.symbolRenames.map((rename) => ({
56
+ rename,
57
+ targetSegments,
58
+ }));
59
+ });
60
+
61
+ const isIdentifierNamed = (node: AstNode, name: string): boolean =>
62
+ node.type === 'Identifier' && identifierName(node) === name;
63
+
64
+ const identifierTokenSpan = (
65
+ node: AstNode,
66
+ sourceCode: string,
67
+ name: string
68
+ ): { readonly end: number; readonly start: number } | null => {
69
+ const end = node.start + name.length;
70
+ return sourceCode.slice(node.start, end) === name
71
+ ? { end, start: node.start }
72
+ : null;
73
+ };
74
+
75
+ const toPascalIdentifierSegment = (value: string): string =>
76
+ value.length === 0
77
+ ? value
78
+ : `${value[0]?.toUpperCase() ?? ''}${value.slice(1)}`;
79
+
80
+ const isScreamingSnakeIdentifier = (name: string): boolean =>
81
+ /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$/.test(name) && name.includes('_');
82
+
83
+ const replaceScreamingSnakeIdentifierSegment = (
84
+ name: string,
85
+ from: string,
86
+ to: string
87
+ ): string | null => {
88
+ if (!isScreamingSnakeIdentifier(name)) {
89
+ return null;
90
+ }
91
+
92
+ const fromSegment = from.toUpperCase();
93
+ const toSegment = to.toUpperCase();
94
+ let changed = false;
95
+ const nextName = name
96
+ .split('_')
97
+ .map((segment) => {
98
+ if (segment !== fromSegment) {
99
+ return segment;
100
+ }
101
+ changed = true;
102
+ return toSegment;
103
+ })
104
+ .join('_');
105
+
106
+ return changed ? nextName : null;
107
+ };
108
+
109
+ const replaceCamelOrPascalIdentifierSegment = (
110
+ name: string,
111
+ from: string,
112
+ to: string
113
+ ): string | null => {
114
+ if (!/^[A-Za-z][A-Za-z0-9]*$/.test(name)) {
115
+ return null;
116
+ }
117
+
118
+ const fromPascalSegment = toPascalIdentifierSegment(from);
119
+ const toPascalSegment = toPascalIdentifierSegment(to);
120
+ const segmentPattern = /[A-Z]+(?=[A-Z][a-z]|$)|[A-Z]?[a-z]+|[0-9]+/g;
121
+ let cursor = 0;
122
+ let changed = false;
123
+ let nextName = '';
124
+
125
+ for (const match of name.matchAll(segmentPattern)) {
126
+ const [segment] = match;
127
+ const { index } = match;
128
+ if (index === undefined || index !== cursor) {
129
+ return null;
130
+ }
131
+
132
+ if (segment === from) {
133
+ changed = true;
134
+ nextName += to;
135
+ } else if (segment === fromPascalSegment) {
136
+ changed = true;
137
+ nextName += toPascalSegment;
138
+ } else {
139
+ nextName += segment;
140
+ }
141
+
142
+ cursor = index + segment.length;
143
+ }
144
+
145
+ if (cursor !== name.length || !changed) {
146
+ return null;
147
+ }
148
+
149
+ return nextName;
150
+ };
151
+
152
+ const deriveIdentifierSegmentTarget = (
153
+ name: string,
154
+ from: string,
155
+ to: string
156
+ ): string | null => {
157
+ const leadingUnderscores = /^_+/.exec(name)?.[0] ?? '';
158
+ const coreName =
159
+ leadingUnderscores.length > 0
160
+ ? name.slice(leadingUnderscores.length)
161
+ : name;
162
+ if (coreName.length === 0) {
163
+ return null;
164
+ }
165
+
166
+ const screamingSnakeTarget = replaceScreamingSnakeIdentifierSegment(
167
+ coreName,
168
+ from,
169
+ to
170
+ );
171
+ if (screamingSnakeTarget !== null) {
172
+ return `${leadingUnderscores}${screamingSnakeTarget}`;
173
+ }
174
+
175
+ const camelOrPascalTarget = replaceCamelOrPascalIdentifierSegment(
176
+ coreName,
177
+ from,
178
+ to
179
+ );
180
+ return camelOrPascalTarget === null
181
+ ? null
182
+ : `${leadingUnderscores}${camelOrPascalTarget}`;
183
+ };
184
+
185
+ const hasIdentifierSegment = (name: string, segment: string): boolean =>
186
+ deriveIdentifierSegmentTarget(name, segment, segment) !== null;
187
+
188
+ interface SymbolRenameMatch {
189
+ readonly from: string;
190
+ readonly rename: GovernedVocabularySymbolRename;
191
+ readonly span: { readonly end: number; readonly start: number } | null;
192
+ readonly to: string;
193
+ }
194
+
195
+ const resolveSymbolRenameMatch = (
196
+ node: AstNode,
197
+ sourceCode: string,
198
+ rename: GovernedVocabularySymbolRename
199
+ ): SymbolRenameMatch | null => {
200
+ if (rename.match === 'exact') {
201
+ if (!isIdentifierNamed(node, rename.from)) {
202
+ return null;
203
+ }
204
+
205
+ return {
206
+ from: rename.from,
207
+ rename,
208
+ span: identifierTokenSpan(node, sourceCode, rename.from),
209
+ to: rename.to,
210
+ };
211
+ }
212
+
213
+ if (node.type !== 'Identifier') {
214
+ return null;
215
+ }
216
+
217
+ const name = identifierName(node);
218
+ if (name === null) {
219
+ return null;
220
+ }
221
+ const target = deriveIdentifierSegmentTarget(name, rename.from, rename.to);
222
+ if (target === null) {
223
+ return null;
224
+ }
225
+
226
+ return {
227
+ from: name,
228
+ rename,
229
+ span: identifierTokenSpan(node, sourceCode, name),
230
+ to: target,
231
+ };
232
+ };
233
+
234
+ const authoredPropertyKeyParentTypes = new Set([
235
+ 'AccessorProperty',
236
+ 'MethodDefinition',
237
+ 'Property',
238
+ 'PropertyDefinition',
239
+ 'TSAbstractMethodDefinition',
240
+ 'TSAbstractPropertyDefinition',
241
+ 'TSMethodSignature',
242
+ 'TSPropertySignature',
243
+ ]);
244
+
245
+ const isAuthoredMemberName = (context: AstScopeContext): boolean =>
246
+ (context.key === 'key' &&
247
+ context.parent !== null &&
248
+ context.parent !== undefined &&
249
+ authoredPropertyKeyParentTypes.has(context.parent.type)) ||
250
+ (context.key === 'parameter' &&
251
+ context.parent?.type === 'TSParameterProperty');
252
+
253
+ const isModuleBoundaryName = (context: AstScopeContext): boolean =>
254
+ context.parent?.type === 'ImportSpecifier' ||
255
+ context.parent?.type === 'ImportDefaultSpecifier' ||
256
+ context.parent?.type === 'ImportNamespaceSpecifier' ||
257
+ context.parent?.type === 'ExportSpecifier';
258
+
259
+ const reviewReasonFor = (
260
+ context: AstScopeContext,
261
+ match: SymbolRenameMatch,
262
+ shorthandIdentifiers: ReadonlySet<string>,
263
+ targetSegments: readonly string[]
264
+ ): string | null => {
265
+ if (match.rename.match === 'identifier-segment') {
266
+ const existingTarget = targetSegments.find((segment) =>
267
+ hasIdentifierSegment(match.from, segment)
268
+ );
269
+ if (existingTarget !== undefined) {
270
+ return `already contains target segment '${existingTarget}'`;
271
+ }
272
+ }
273
+
274
+ if (shorthandIdentifiers.has(match.from)) {
275
+ return 'participates in an authored shorthand property';
276
+ }
277
+
278
+ const declaration = context.getDeclaration(match.from);
279
+ if (
280
+ declaration &&
281
+ match.rename.reviewDeclarationTypes.includes(declaration.type)
282
+ ) {
283
+ return `resolves to ${declaration.type}`;
284
+ }
285
+
286
+ if (isModuleBoundaryName(context)) {
287
+ return 'names an import or export boundary';
288
+ }
289
+
290
+ if (
291
+ context.key === 'property' &&
292
+ (context.parent?.type === 'MemberExpression' ||
293
+ context.parent?.type === 'StaticMemberExpression')
294
+ ) {
295
+ return 'is a member property with no governed declaration';
296
+ }
297
+
298
+ if (isAuthoredMemberName(context)) {
299
+ return 'is an authored property key';
300
+ }
301
+
302
+ return null;
303
+ };
304
+
305
+ const isDuplicateShorthandValueVisit = (
306
+ node: AstNode,
307
+ context: AstScopeContext
308
+ ): boolean => {
309
+ if (context.key !== 'value' || context.parent?.type !== 'Property') {
310
+ return false;
311
+ }
312
+
313
+ const key = getNodeKey(context.parent);
314
+ return key?.start === node.start && key.end === node.end;
315
+ };
316
+
317
+ const isShorthandPropertyKeyVisit = (
318
+ node: AstNode,
319
+ context: AstScopeContext
320
+ ): boolean => {
321
+ if (context.key !== 'key' || context.parent?.type !== 'Property') {
322
+ return false;
323
+ }
324
+
325
+ const value = getNodeValueNode(context.parent);
326
+ return value?.start === node.start && value.end === node.end;
327
+ };
328
+
329
+ const reviewFixFor = (
330
+ match: SymbolRenameMatch,
331
+ reviewReason: string
332
+ ): WardenFix => ({
333
+ class: 'term-rewrite',
334
+ reason: `Retired governed symbol '${match.from}' ${reviewReason}; review before migrating to '${match.to}'.`,
335
+ safety: 'review',
336
+ });
337
+
338
+ const safeFixFor = (match: SymbolRenameMatch): WardenFix | undefined => {
339
+ if (match.span === null) {
340
+ return undefined;
341
+ }
342
+ return {
343
+ class: 'term-rewrite',
344
+ edits: [
345
+ {
346
+ end: match.span.end,
347
+ replacement: match.to,
348
+ start: match.span.start,
349
+ },
350
+ ],
351
+ reason: `Retired governed symbol '${match.from}' has a mechanical replacement '${match.to}'.`,
352
+ safety: 'safe',
353
+ };
354
+ };
355
+
356
+ const diagnosticFor = (
357
+ sourceCode: string,
358
+ filePath: string,
359
+ node: AstNode,
360
+ match: SymbolRenameMatch,
361
+ reviewReason: string | null
362
+ ): WardenDiagnostic => {
363
+ const fix =
364
+ reviewReason === null
365
+ ? safeFixFor(match)
366
+ : reviewFixFor(match, reviewReason);
367
+ return {
368
+ filePath,
369
+ ...(fix === undefined ? {} : { fix }),
370
+ line: offsetToLine(sourceCode, node.start),
371
+ message: `Retired governed symbol '${match.from}' should migrate to '${match.to}'.`,
372
+ rule: RULE_NAME,
373
+ severity: 'error',
374
+ };
375
+ };
376
+
377
+ export const governedSymbolResidue: WardenRule = {
378
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
379
+ if (isAllowedSourcePath(filePath)) {
380
+ return [];
381
+ }
382
+
383
+ const renames = activeSymbolRenames();
384
+ if (renames.length === 0) {
385
+ return [];
386
+ }
387
+
388
+ const parsed = parseWithDiagnostics(filePath, sourceCode);
389
+ if (!parsed.ast || parsed.diagnostics.length > 0) {
390
+ return [];
391
+ }
392
+
393
+ const shorthandIdentifiers = new Set<string>();
394
+ walkWithScopeContext(parsed.ast, (node, context) => {
395
+ if (!isShorthandPropertyKeyVisit(node, context)) {
396
+ return;
397
+ }
398
+ const name = identifierName(node);
399
+ if (name !== null) {
400
+ shorthandIdentifiers.add(name);
401
+ }
402
+ });
403
+
404
+ const diagnostics: WardenDiagnostic[] = [];
405
+ walkWithScopeContext(parsed.ast, (node, context) => {
406
+ if (isDuplicateShorthandValueVisit(node, context)) {
407
+ return;
408
+ }
409
+
410
+ const activeMatch = renames
411
+ .map(({ rename, targetSegments }) => ({
412
+ match: resolveSymbolRenameMatch(node, sourceCode, rename),
413
+ targetSegments,
414
+ }))
415
+ .find((candidate) => candidate.match !== null);
416
+ if (activeMatch === undefined || activeMatch.match === null) {
417
+ return;
418
+ }
419
+ const { match, targetSegments } = activeMatch;
420
+
421
+ diagnostics.push(
422
+ diagnosticFor(
423
+ sourceCode,
424
+ filePath,
425
+ node,
426
+ match,
427
+ reviewReasonFor(context, match, shorthandIdentifiers, targetSegments)
428
+ )
429
+ );
430
+ });
431
+
432
+ return diagnostics;
433
+ },
434
+ description:
435
+ 'Detect active governed vocabulary symbols that remain in source code.',
436
+ name: RULE_NAME,
437
+ severity: 'error',
438
+ };