@ontrails/warden 1.0.0-beta.23 → 1.0.0-beta.29

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 (61) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/README.md +35 -1
  3. package/package.json +10 -9
  4. package/src/ast.ts +109 -0
  5. package/src/cli.ts +44 -4
  6. package/src/command.ts +24 -34
  7. package/src/drift.ts +24 -8
  8. package/src/index.ts +9 -0
  9. package/src/project-rules.ts +290 -0
  10. package/src/resolve.ts +8 -7
  11. package/src/rules/ast.ts +837 -6
  12. package/src/rules/cli-command-route-coherence.ts +177 -0
  13. package/src/rules/composes-declarations.ts +210 -77
  14. package/src/rules/context-no-surface-types.ts +15 -12
  15. package/src/rules/contour-exists.ts +7 -7
  16. package/src/rules/dead-internal-trail.ts +11 -4
  17. package/src/rules/dead-public-trail.ts +258 -0
  18. package/src/rules/duplicate-public-contract.ts +91 -0
  19. package/src/rules/error-mapping-completeness.ts +5 -3
  20. package/src/rules/example-valid.ts +6 -12
  21. package/src/rules/fires-declarations.ts +41 -64
  22. package/src/rules/implementation-returns-result.ts +208 -76
  23. package/src/rules/incomplete-crud.ts +20 -19
  24. package/src/rules/index.ts +15 -0
  25. package/src/rules/layer-field-name-drift.ts +12 -6
  26. package/src/rules/library-projection-coherence.ts +100 -0
  27. package/src/rules/metadata.ts +102 -1
  28. package/src/rules/no-destructured-compose.ts +16 -14
  29. package/src/rules/no-native-error-result.ts +15 -8
  30. package/src/rules/no-redundant-result-error-wrap.ts +82 -29
  31. package/src/rules/no-sync-result-assumption.ts +70 -68
  32. package/src/rules/no-top-level-surface.ts +46 -64
  33. package/src/rules/on-references-exist.ts +1 -1
  34. package/src/rules/owner-projection-parity.ts +10 -13
  35. package/src/rules/public-export-example-coverage.ts +29 -20
  36. package/src/rules/public-internal-deep-imports.ts +2 -2
  37. package/src/rules/read-intent-fires.ts +8 -8
  38. package/src/rules/registry-names.ts +10 -0
  39. package/src/rules/resource-declarations.ts +20 -31
  40. package/src/rules/resource-exists.ts +2 -2
  41. package/src/rules/resource-id-grammar.ts +2 -2
  42. package/src/rules/resource-mock-coverage.ts +2 -2
  43. package/src/rules/static-resource-accessor-preference.ts +26 -34
  44. package/src/rules/surface-facet-coherence.ts +21 -29
  45. package/src/rules/trail-fork-coaching.ts +616 -0
  46. package/src/rules/trail-versioning-source.ts +56 -78
  47. package/src/rules/types.ts +2 -0
  48. package/src/rules/unreachable-detour-shadowing.ts +16 -21
  49. package/src/rules/valid-describe-refs.ts +14 -12
  50. package/src/rules/warden-export-symmetry.ts +42 -35
  51. package/src/rules/warden-rules-use-ast.ts +168 -50
  52. package/src/trails/cli-command-route-coherence.trail.ts +47 -0
  53. package/src/trails/dead-public-trail.trail.ts +31 -0
  54. package/src/trails/duplicate-public-contract.trail.ts +47 -0
  55. package/src/trails/error-mapping-completeness.trail.ts +1 -0
  56. package/src/trails/index.ts +5 -0
  57. package/src/trails/library-projection-coherence.trail.ts +43 -0
  58. package/src/trails/schema.ts +4 -0
  59. package/src/trails/trail-fork-coaching.trail.ts +42 -0
  60. package/src/trails/warden-rules-use-ast.trail.ts +19 -0
  61. package/src/trails/wrap-rule.ts +1 -0
@@ -2,6 +2,22 @@ import {
2
2
  findBlazeBodies,
3
3
  findConfigProperty,
4
4
  findTrailDefinitions,
5
+ getNodeArgument,
6
+ getNodeArguments,
7
+ getNodeCallee,
8
+ getNodeElements,
9
+ getNodeExpression,
10
+ getNodeId,
11
+ getNodeInit,
12
+ getNodeKey,
13
+ getNodeLeft,
14
+ getNodeName,
15
+ getNodeObject,
16
+ getNodeParams,
17
+ getNodeProperties,
18
+ getNodeProperty,
19
+ getNodeValue,
20
+ getNodeValueNode,
5
21
  isMemberAccessNonComputed,
6
22
  offsetToLine,
7
23
  parse,
@@ -148,14 +164,14 @@ const diagnostic = (
148
164
 
149
165
  const staticPropertyKeyName = (node: AstNode | undefined): string | null => {
150
166
  if (node?.type === 'Identifier') {
151
- return (node as unknown as { name?: string }).name ?? null;
167
+ return getNodeName(node) ?? null;
152
168
  }
153
169
  if (
154
170
  node?.type === 'Literal' ||
155
171
  node?.type === 'StringLiteral' ||
156
172
  node?.type === 'NumericLiteral'
157
173
  ) {
158
- const { value } = node as unknown as { value?: unknown };
174
+ const value = getNodeValue(node);
159
175
  return typeof value === 'string' || typeof value === 'number'
160
176
  ? String(value)
161
177
  : null;
@@ -164,22 +180,17 @@ const staticPropertyKeyName = (node: AstNode | undefined): string | null => {
164
180
  };
165
181
 
166
182
  const objectProperties = (node: AstNode | undefined): readonly AstNode[] =>
167
- node?.type === 'ObjectExpression'
168
- ? ((node as unknown as { properties?: readonly AstNode[] }).properties ??
169
- [])
170
- : [];
183
+ node?.type === 'ObjectExpression' ? (getNodeProperties(node) ?? []) : [];
171
184
 
172
185
  const propertyName = (node: AstNode): string | null =>
173
- node.type === 'Property'
174
- ? staticPropertyKeyName((node as unknown as { key?: AstNode }).key)
175
- : null;
186
+ node.type === 'Property' ? staticPropertyKeyName(getNodeKey(node)) : null;
176
187
 
177
188
  const hasProperty = (node: AstNode, name: string): boolean =>
178
189
  objectProperties(node).some((property) => propertyName(property) === name);
179
190
 
180
191
  const propertyValue = (property: AstNode | null): AstNode | undefined =>
181
192
  property?.type === 'Property'
182
- ? ((property as unknown as { value?: AstNode }).value ?? undefined)
193
+ ? (getNodeValueNode(property) ?? undefined)
183
194
  : undefined;
184
195
 
185
196
  const trailIsVersioned = (config: AstNode): boolean =>
@@ -194,9 +205,7 @@ const versionEntries = (config: AstNode): readonly AstNode[] => {
194
205
  };
195
206
 
196
207
  const identifierName = (node: AstNode | undefined): string | undefined =>
197
- node?.type === 'Identifier'
198
- ? (node as unknown as { name?: string }).name
199
- : undefined;
208
+ node?.type === 'Identifier' ? getNodeName(node) : undefined;
200
209
 
201
210
  const schemaBindingInitializer = (
202
211
  schemaBindings: SchemaBindings,
@@ -221,18 +230,16 @@ const schemaBindingInitializer = (
221
230
 
222
231
  const memberObject = (node: AstNode | undefined): AstNode | undefined =>
223
232
  node !== undefined && isMemberAccessNonComputed(node)
224
- ? (node as unknown as { object?: AstNode }).object
233
+ ? getNodeObject(node)
225
234
  : undefined;
226
235
 
227
236
  const memberPropertyName = (node: AstNode | undefined): string | undefined =>
228
237
  node !== undefined && isMemberAccessNonComputed(node)
229
- ? identifierName((node as unknown as { property?: AstNode }).property)
238
+ ? identifierName(getNodeProperty(node))
230
239
  : undefined;
231
240
 
232
241
  const callCallee = (node: AstNode): AstNode | undefined =>
233
- node.type === 'CallExpression'
234
- ? (node as unknown as { callee?: AstNode }).callee
235
- : undefined;
242
+ node.type === 'CallExpression' ? getNodeCallee(node) : undefined;
236
243
 
237
244
  const isZodSchemaReceiver = (
238
245
  node: AstNode | undefined,
@@ -272,9 +279,7 @@ const isZodSchemaCallee = (
272
279
  node !== undefined && isZodSchemaReceiver(memberObject(node), schemaBindings);
273
280
 
274
281
  const callArguments = (node: AstNode): readonly AstNode[] =>
275
- node.type === 'CallExpression'
276
- ? ((node as unknown as { arguments?: readonly AstNode[] }).arguments ?? [])
277
- : [];
282
+ node.type === 'CallExpression' ? (getNodeArguments(node) ?? []) : [];
278
283
 
279
284
  const unwrapExpression = (node: AstNode | undefined): AstNode | undefined => {
280
285
  let current = node;
@@ -283,7 +288,7 @@ const unwrapExpression = (node: AstNode | undefined): AstNode | undefined => {
283
288
  current?.type === 'TSSatisfiesExpression' ||
284
289
  current?.type === 'TSNonNullExpression'
285
290
  ) {
286
- current = (current as unknown as { expression?: AstNode }).expression;
291
+ current = getNodeExpression(current);
287
292
  }
288
293
  return current;
289
294
  };
@@ -291,10 +296,7 @@ const unwrapExpression = (node: AstNode | undefined): AstNode | undefined => {
291
296
  const arrayExpressionLength = (node: AstNode | undefined): number => {
292
297
  const expression = unwrapExpression(node);
293
298
  return expression?.type === 'ArrayExpression'
294
- ? (
295
- (expression as unknown as { elements?: readonly unknown[] }).elements ??
296
- []
297
- ).length
299
+ ? (getNodeElements(expression) ?? []).length
298
300
  : 0;
299
301
  };
300
302
 
@@ -320,18 +322,14 @@ const literalExpressionIsJsonLossy = (expression: AstNode): boolean => {
320
322
  ) {
321
323
  return true;
322
324
  }
323
- const literal = expression as unknown as {
324
- readonly bigint?: unknown;
325
- readonly regex?: unknown;
326
- readonly value?: unknown;
327
- };
328
- if (literal.bigint !== undefined || literal.regex !== undefined) {
325
+ if (expression['bigint'] !== undefined || expression['regex'] !== undefined) {
329
326
  return true;
330
327
  }
331
- if (literal.value instanceof RegExp) {
328
+ const value = getNodeValue(expression);
329
+ if (value instanceof RegExp) {
332
330
  return true;
333
331
  }
334
- return typeof literal.value === 'number' && !Number.isFinite(literal.value);
332
+ return typeof value === 'number' && !Number.isFinite(value);
335
333
  };
336
334
 
337
335
  const expressionIsJsonLossy = (node: AstNode | undefined): boolean => {
@@ -350,9 +348,7 @@ const expressionIsJsonLossy = (node: AstNode | undefined): boolean => {
350
348
  }
351
349
 
352
350
  if (expression.type === 'UnaryExpression') {
353
- return expressionIsJsonLossy(
354
- (expression as unknown as { argument?: AstNode }).argument
355
- );
351
+ return expressionIsJsonLossy(getNodeArgument(expression));
356
352
  }
357
353
 
358
354
  if (expression.type === 'Literal' || expression.type === 'NumericLiteral') {
@@ -360,9 +356,7 @@ const expressionIsJsonLossy = (node: AstNode | undefined): boolean => {
360
356
  }
361
357
 
362
358
  if (expression.type === 'ArrayExpression') {
363
- const elements =
364
- (expression as unknown as { elements?: readonly (AstNode | null)[] })
365
- .elements ?? [];
359
+ const elements = getNodeElements(expression);
366
360
  return elements.some((element) =>
367
361
  expressionIsJsonLossy(element ?? undefined)
368
362
  );
@@ -435,10 +429,9 @@ const isReferenceValuedLiteralCall = (
435
429
  if (value?.type !== 'ArrayExpression') {
436
430
  return false;
437
431
  }
438
- return (
439
- (value as unknown as { elements?: readonly (AstNode | null)[] }).elements ??
440
- []
441
- ).some((element) => expressionIsReferenceValued(element ?? undefined));
432
+ return (getNodeElements(value) ?? []).some((element) =>
433
+ expressionIsReferenceValued(element ?? undefined)
434
+ );
442
435
  };
443
436
 
444
437
  const isJsonLossyEnumCall = (
@@ -486,10 +479,9 @@ const isReferenceValuedEnumCall = (
486
479
  const [rawOptions] = callArguments(node);
487
480
  const options = unwrapExpression(rawOptions);
488
481
  if (options?.type === 'ArrayExpression') {
489
- return (
490
- (options as unknown as { elements?: readonly (AstNode | null)[] })
491
- .elements ?? []
492
- ).some((element) => expressionIsReferenceValued(element ?? undefined));
482
+ return getNodeElements(options).some((element) =>
483
+ expressionIsReferenceValued(element ?? undefined)
484
+ );
493
485
  }
494
486
  if (options?.type !== 'ObjectExpression') {
495
487
  return false;
@@ -583,10 +575,9 @@ const nestedSchemaArguments = (node: AstNode): readonly AstNode[] => {
583
575
  const [rawOptions] = callArguments(node);
584
576
  const options = unwrapExpression(rawOptions);
585
577
  return options?.type === 'ArrayExpression'
586
- ? (
587
- (options as unknown as { elements?: readonly (AstNode | null)[] })
588
- .elements ?? []
589
- ).filter((element): element is AstNode => element !== null)
578
+ ? getNodeElements(options).filter(
579
+ (element): element is AstNode => element !== null
580
+ )
590
581
  : [];
591
582
  };
592
583
 
@@ -689,7 +680,7 @@ const isMemberCallNamed = (
689
680
 
690
681
  const bindingName = (node: AstNode): string | undefined =>
691
682
  node.type === 'VariableDeclarator'
692
- ? identifierName((node as unknown as { id?: AstNode }).id)
683
+ ? identifierName(getNodeId(node))
693
684
  : undefined;
694
685
 
695
686
  const lexicalScopeTypes = new Set([
@@ -716,20 +707,15 @@ const addPatternBindingNames = (
716
707
  return;
717
708
  }
718
709
  if (node.type === 'AssignmentPattern') {
719
- addPatternBindingNames((node as unknown as { left?: AstNode }).left, into);
710
+ addPatternBindingNames(getNodeLeft(node), into);
720
711
  return;
721
712
  }
722
713
  if (node.type === 'RestElement') {
723
- addPatternBindingNames(
724
- (node as unknown as { argument?: AstNode }).argument,
725
- into
726
- );
714
+ addPatternBindingNames(getNodeArgument(node), into);
727
715
  return;
728
716
  }
729
717
  if (node.type === 'ArrayPattern') {
730
- const elements =
731
- (node as unknown as { elements?: readonly (AstNode | null)[] })
732
- .elements ?? [];
718
+ const elements = getNodeElements(node);
733
719
  for (const element of elements) {
734
720
  addPatternBindingNames(element ?? undefined, into);
735
721
  }
@@ -738,17 +724,13 @@ const addPatternBindingNames = (
738
724
  if (node.type !== 'ObjectPattern') {
739
725
  return;
740
726
  }
741
- const properties =
742
- (node as unknown as { properties?: readonly AstNode[] }).properties ?? [];
727
+ const properties = getNodeProperties(node) ?? [];
743
728
  for (const property of properties) {
744
729
  if (property.type === 'RestElement') {
745
730
  addPatternBindingNames(property, into);
746
731
  continue;
747
732
  }
748
- addPatternBindingNames(
749
- (property as unknown as { value?: AstNode }).value,
750
- into
751
- );
733
+ addPatternBindingNames(getNodeValueNode(property), into);
752
734
  }
753
735
  };
754
736
 
@@ -757,8 +739,7 @@ const parameterBindingNames = (node: AstNode): readonly string[] => {
757
739
  return [];
758
740
  }
759
741
  const names = new Set<string>();
760
- const params =
761
- (node as unknown as { params?: readonly AstNode[] }).params ?? [];
742
+ const params = getNodeParams(node) ?? [];
762
743
  for (const param of params) {
763
744
  addPatternBindingNames(param, names);
764
745
  }
@@ -767,7 +748,7 @@ const parameterBindingNames = (node: AstNode): readonly string[] => {
767
748
 
768
749
  const variableInitializer = (node: AstNode): AstNode | undefined =>
769
750
  node.type === 'VariableDeclarator'
770
- ? ((node as unknown as { init?: AstNode }).init ?? undefined)
751
+ ? (getNodeInit(node) ?? undefined)
771
752
  : undefined;
772
753
 
773
754
  const isZodSchemaExpression = (
@@ -887,22 +868,19 @@ const composeCallHasVersionPin = (node: AstNode): boolean => {
887
868
  if (node.type !== 'CallExpression') {
888
869
  return false;
889
870
  }
890
- const { arguments: args, callee } = node as unknown as {
891
- arguments?: readonly AstNode[];
892
- callee?: AstNode;
893
- };
871
+ const args = getNodeArguments(node);
872
+ const callee = getNodeCallee(node);
894
873
  if (!callee || !args) {
895
874
  return false;
896
875
  }
897
876
 
898
877
  const isComposeIdentifier =
899
- callee.type === 'Identifier' &&
900
- (callee as unknown as { name?: string }).name === 'compose';
901
- const { property } = callee as unknown as { property?: AstNode };
878
+ callee.type === 'Identifier' && getNodeName(callee) === 'compose';
879
+ const property = getNodeProperty(callee);
902
880
  const isComposeMember =
903
881
  isMemberAccessNonComputed(callee) &&
904
882
  property?.type === 'Identifier' &&
905
- (property as unknown as { name?: string }).name === 'compose';
883
+ getNodeName(property) === 'compose';
906
884
 
907
885
  return (isComposeIdentifier || isComposeMember) && hasVersionOption(args[2]);
908
886
  };
@@ -245,6 +245,8 @@ export interface ProjectContext {
245
245
  readonly crudTableIds?: ReadonlySet<string>;
246
246
  /** All known trail IDs in the project */
247
247
  readonly knownTrailIds: ReadonlySet<string>;
248
+ /** Trail IDs registered in configured app topo targets. */
249
+ readonly topoTrailIds?: ReadonlySet<string>;
248
250
  /** Declared contour references keyed by source contour name. */
249
251
  readonly contourReferencesByName?: ReadonlyMap<string, readonly string[]>;
250
252
  /** All known resource IDs in the project */
@@ -4,6 +4,13 @@ import {
4
4
  extractStringLiteral,
5
5
  findConfigProperty,
6
6
  findTrailDefinitions,
7
+ getNodeId,
8
+ getNodeImported,
9
+ getNodeInit,
10
+ getNodeLocal,
11
+ getNodeSource,
12
+ getNodeSpecifiers,
13
+ getNodeSuperClass,
7
14
  identifierName,
8
15
  offsetToLine,
9
16
  parse,
@@ -46,7 +53,7 @@ const resolveKnownErrorName = (
46
53
  ): string => aliases.get(name) ?? name;
47
54
 
48
55
  const coreImportSource = (node: AstNode): string | null =>
49
- extractStringLiteral((node as unknown as { source?: AstNode }).source);
56
+ extractStringLiteral(getNodeSource(node));
50
57
 
51
58
  const collectImportSpecifierAliases = (
52
59
  specifiers: readonly AstNode[] | undefined,
@@ -57,13 +64,9 @@ const collectImportSpecifierAliases = (
57
64
  continue;
58
65
  }
59
66
 
60
- const localName = identifierName(
61
- (specifier as unknown as { local?: AstNode }).local
62
- );
67
+ const localName = identifierName(getNodeLocal(specifier));
63
68
  const importedName =
64
- identifierName(
65
- (specifier as unknown as { imported?: AstNode }).imported
66
- ) ?? localName;
69
+ identifierName(getNodeImported(specifier)) ?? localName;
67
70
 
68
71
  if (localName && importedName && knownErrorConstructors.has(importedName)) {
69
72
  aliases.set(localName, importedName);
@@ -85,9 +88,7 @@ const collectKnownErrorAliases = (
85
88
  return;
86
89
  }
87
90
 
88
- const { specifiers } = node as unknown as {
89
- specifiers?: readonly AstNode[];
90
- };
91
+ const specifiers = getNodeSpecifiers(node);
91
92
  collectImportSpecifierAliases(specifiers, aliases);
92
93
  });
93
94
 
@@ -116,15 +117,13 @@ const collectClassExpressionParent = (
116
117
  return;
117
118
  }
118
119
 
119
- const { init } = node as unknown as { init?: AstNode };
120
+ const init = getNodeInit(node);
120
121
  if (!init || init.type !== 'ClassExpression') {
121
122
  return;
122
123
  }
123
124
 
124
- const className = identifierName((node as unknown as { id?: AstNode }).id);
125
- const parentName = identifierName(
126
- (init as unknown as { superClass?: AstNode }).superClass
127
- );
125
+ const className = identifierName(getNodeId(node));
126
+ const parentName = identifierName(getNodeSuperClass(init));
128
127
  recordLocalErrorParent(parents, aliases, className, parentName);
129
128
  };
130
129
 
@@ -136,12 +135,8 @@ const collectLocalErrorParents = (
136
135
 
137
136
  walk(ast, (node) => {
138
137
  if (node.type === 'ClassDeclaration') {
139
- const className = identifierName(
140
- (node as unknown as { id?: AstNode }).id
141
- );
142
- const parentName = identifierName(
143
- (node as unknown as { superClass?: AstNode }).superClass
144
- );
138
+ const className = identifierName(getNodeId(node));
139
+ const parentName = identifierName(getNodeSuperClass(node));
145
140
  recordLocalErrorParent(parents, aliases, className, parentName);
146
141
  return;
147
142
  }
@@ -1,5 +1,8 @@
1
1
  import {
2
2
  extractStringOrTemplateLiteral,
3
+ getNodeName,
4
+ getNodeProperty,
5
+ getNodeValue,
3
6
  offsetToLine,
4
7
  parse,
5
8
  walk,
@@ -35,11 +38,8 @@ const isDescribeMemberCallee = (callee: AstNode | undefined): boolean => {
35
38
  if (!callee || !MEMBER_CALLEE_TYPES.has(callee.type)) {
36
39
  return false;
37
40
  }
38
- const prop = (callee as unknown as { property?: AstNode }).property;
39
- return (
40
- prop?.type === 'Identifier' &&
41
- (prop as unknown as { name?: string }).name === 'describe'
42
- );
41
+ const prop = getNodeProperty(callee);
42
+ return prop?.type === 'Identifier' && getNodeName(prop) === 'describe';
43
43
  };
44
44
 
45
45
  const hasStringLiteralFirstArg = (node: AstNode): boolean => {
@@ -87,14 +87,16 @@ const isDescribeCall = (node: AstNode): boolean => {
87
87
  * missing an `@see` token.
88
88
  */
89
89
  const extractQuasiText = (quasi: AstNode): string | null => {
90
- const { value } = quasi as unknown as {
91
- value?: { cooked?: unknown; raw?: unknown };
92
- };
93
- if (typeof value?.cooked === 'string') {
94
- return value.cooked;
90
+ const value = getNodeValue(quasi);
91
+ if (!value || typeof value !== 'object') {
92
+ return null;
93
+ }
94
+ const record = value as Record<string, unknown>;
95
+ if (typeof record['cooked'] === 'string') {
96
+ return record['cooked'];
95
97
  }
96
- if (typeof value?.raw === 'string') {
97
- return value.raw;
98
+ if (typeof record['raw'] === 'string') {
99
+ return record['raw'];
98
100
  }
99
101
  return null;
100
102
  };
@@ -16,7 +16,26 @@
16
16
  */
17
17
  import { resolve } from 'node:path';
18
18
  import { fileURLToPath } from 'node:url';
19
- import { walk, offsetToLine, parse } from './ast.js';
19
+ import {
20
+ getNodeArgument,
21
+ getNodeDeclaration,
22
+ getNodeDeclarations,
23
+ getNodeElements,
24
+ getNodeExportKind,
25
+ getNodeExported,
26
+ getNodeId,
27
+ getNodeKey,
28
+ getNodeLeft,
29
+ getNodeLocal,
30
+ getNodeName,
31
+ getNodeProperties,
32
+ getNodeSource,
33
+ getNodeValue,
34
+ getNodeValueNode,
35
+ offsetToLine,
36
+ parse,
37
+ walk,
38
+ } from './ast.js';
20
39
  import type { AstNode } from './ast.js';
21
40
  import { registeredRuleNames } from './registry-names.js';
22
41
  import type { WardenDiagnostic, WardenRule } from './types.js';
@@ -61,10 +80,10 @@ const readIdentifierOrStringName = (
61
80
  return null;
62
81
  }
63
82
  if (node.type === 'Identifier') {
64
- return (node as unknown as { name?: string }).name ?? null;
83
+ return getNodeName(node) ?? null;
65
84
  }
66
85
  if (node.type === 'Literal' || node.type === 'StringLiteral') {
67
- const { value } = node as unknown as { value?: unknown };
86
+ const value = getNodeValue(node);
68
87
  return typeof value === 'string' ? value : null;
69
88
  }
70
89
  return null;
@@ -73,10 +92,8 @@ const readIdentifierOrStringName = (
73
92
  const extractSpecifierNames = (
74
93
  specifier: AstNode
75
94
  ): { readonly name: string; readonly localName: string } | null => {
76
- const { exported, local } = specifier as unknown as {
77
- exported?: AstNode;
78
- local?: AstNode;
79
- };
95
+ const exported = getNodeExported(specifier);
96
+ const local = getNodeLocal(specifier);
80
97
  const name = readIdentifierOrStringName(exported);
81
98
  if (!name) {
82
99
  return null;
@@ -86,7 +103,7 @@ const extractSpecifierNames = (
86
103
  };
87
104
 
88
105
  const isTypeExportSpecifier = (specifier: AstNode): boolean =>
89
- (specifier as unknown as { exportKind?: string }).exportKind === 'type';
106
+ getNodeExportKind(specifier) === 'type';
90
107
 
91
108
  const specifierSite = (specifier: AstNode): ExportSite | null => {
92
109
  if (
@@ -129,7 +146,7 @@ const siteFromSimpleBinding = (
129
146
  return name ? { localName: name, name, start } : null;
130
147
  }
131
148
  if (node.type === 'AssignmentPattern') {
132
- const { left } = node as unknown as { left?: AstNode };
149
+ const left = getNodeLeft(node);
133
150
  return left ? siteFromSimpleBinding(left, start) : null;
134
151
  }
135
152
  return null;
@@ -172,16 +189,14 @@ const sitesFromObjectProperty = (
172
189
  recurse: PatternSitesFn
173
190
  ): readonly ExportSite[] => {
174
191
  if (prop.type === 'RestElement') {
175
- const { argument } = prop as unknown as { argument?: AstNode };
192
+ const argument = getNodeArgument(prop);
176
193
  return recurse(argument, start);
177
194
  }
178
195
  if (prop.type !== 'Property') {
179
196
  return [];
180
197
  }
181
- const { key, value } = prop as unknown as {
182
- key?: AstNode;
183
- value?: AstNode;
184
- };
198
+ const key = getNodeKey(prop);
199
+ const value = getNodeValueNode(prop);
185
200
  if (isNestedPatternValue(value)) {
186
201
  return recurse(value, start);
187
202
  }
@@ -198,7 +213,7 @@ const sitesFromArrayElement = (
198
213
  return [];
199
214
  }
200
215
  if (element.type === 'RestElement') {
201
- const { argument } = element as unknown as { argument?: AstNode };
216
+ const argument = getNodeArgument(element);
202
217
  return recurse(argument, start);
203
218
  }
204
219
  return recurse(element, start);
@@ -209,9 +224,7 @@ const sitesFromObjectPattern = (
209
224
  start: number,
210
225
  recurse: PatternSitesFn
211
226
  ): readonly ExportSite[] => {
212
- const properties =
213
- (pattern as unknown as { properties?: readonly AstNode[] }).properties ??
214
- [];
227
+ const properties = getNodeProperties(pattern) ?? [];
215
228
  return properties.flatMap((prop) =>
216
229
  sitesFromObjectProperty(prop, start, recurse)
217
230
  );
@@ -222,9 +235,7 @@ const sitesFromArrayPattern = (
222
235
  start: number,
223
236
  recurse: PatternSitesFn
224
237
  ): readonly ExportSite[] => {
225
- const elements =
226
- (pattern as unknown as { elements?: readonly (AstNode | null)[] })
227
- .elements ?? [];
238
+ const elements = getNodeElements(pattern);
228
239
  return elements.flatMap((element) =>
229
240
  sitesFromArrayElement(element, start, recurse)
230
241
  );
@@ -262,16 +273,14 @@ const sitesForDeclaration = (declaration: AstNode): readonly ExportSite[] => {
262
273
  declaration.type === 'FunctionDeclaration' ||
263
274
  declaration.type === 'ClassDeclaration'
264
275
  ) {
265
- const { id } = declaration as unknown as { id?: AstNode };
276
+ const id = getNodeId(declaration);
266
277
  const site = namedSiteFromDeclId(id, declaration.start);
267
278
  return site ? [site] : [];
268
279
  }
269
280
  if (declaration.type === 'VariableDeclaration') {
270
- const declarations =
271
- (declaration as unknown as { declarations?: readonly AstNode[] })
272
- .declarations ?? [];
281
+ const declarations = getNodeDeclarations(declaration);
273
282
  return declarations.flatMap((declarator) => {
274
- const { id } = declarator as unknown as { id?: AstNode };
283
+ const id = getNodeId(declarator);
275
284
  return sitesFromPattern(id, declarator.start);
276
285
  });
277
286
  }
@@ -282,10 +291,10 @@ const sitesForExportNode = (node: AstNode): readonly ExportSite[] => {
282
291
  if (node.type !== 'ExportNamedDeclaration') {
283
292
  return [];
284
293
  }
285
- if ((node as unknown as { exportKind?: string }).exportKind === 'type') {
294
+ if (getNodeExportKind(node) === 'type') {
286
295
  return [];
287
296
  }
288
- const { declaration } = node as unknown as { declaration?: AstNode };
297
+ const declaration = getNodeDeclaration(node);
289
298
  if (declaration) {
290
299
  return sitesForDeclaration(declaration);
291
300
  }
@@ -324,15 +333,13 @@ const collectNamespaceReexports = (
324
333
  // Mirror the `ExportNamedDeclaration` guard: `export type * from ...` and
325
334
  // `export type * as ns from ...` propagate types only, never runtime
326
335
  // identifiers, so they cannot leak raw rule objects and must be allowed.
327
- if ((node as unknown as { exportKind?: string }).exportKind === 'type') {
336
+ if (getNodeExportKind(node) === 'type') {
328
337
  return;
329
338
  }
330
- const { source, exported } = node as unknown as {
331
- source?: { value?: unknown };
332
- exported?: AstNode;
333
- };
334
- const target =
335
- typeof source?.value === 'string' ? source.value : '<unknown>';
339
+ const source = getNodeSource(node);
340
+ const exported = getNodeExported(node);
341
+ const sourceValue = getNodeValue(source);
342
+ const target = typeof sourceValue === 'string' ? sourceValue : '<unknown>';
336
343
  // `export * as <alias> from '...'` exposes the alias as an
337
344
  // `IdentifierName` / string-literal node on `exported`. Bare `export *`
338
345
  // has `exported === null`.