@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
@@ -1,4 +1,21 @@
1
1
  import {
2
+ getNodeArgument,
3
+ getNodeBodyStatements,
4
+ getNodeCallee,
5
+ getNodeComputed,
6
+ getNodeDeclaration,
7
+ getNodeDeclarations,
8
+ getNodeExported,
9
+ getNodeExpression,
10
+ getNodeId,
11
+ getNodeImported,
12
+ getNodeInit,
13
+ getNodeLeft,
14
+ getNodeLocal,
15
+ getNodeObject,
16
+ getNodeProperty,
17
+ getNodeSource,
18
+ getNodeSpecifiers,
2
19
  getStringValue,
3
20
  identifierName,
4
21
  isStringLiteral,
@@ -31,7 +48,7 @@ const diagnosticMessage =
31
48
  const unwrapExportDeclaration = (node: AstNode): AstNode =>
32
49
  node.type === 'ExportNamedDeclaration' ||
33
50
  node.type === 'ExportDefaultDeclaration'
34
- ? ((node as unknown as { declaration?: AstNode }).declaration ?? node)
51
+ ? (getNodeDeclaration(node) ?? node)
35
52
  : node;
36
53
 
37
54
  interface ImportedBindings {
@@ -40,7 +57,7 @@ interface ImportedBindings {
40
57
  }
41
58
 
42
59
  const importSource = (node: AstNode): string | null => {
43
- const { source } = node as unknown as { readonly source?: AstNode };
60
+ const source = getNodeSource(node);
44
61
  return source && isStringLiteral(source) ? getStringValue(source) : null;
45
62
  };
46
63
 
@@ -55,7 +72,7 @@ const addFrameworkImportBindings = (
55
72
  ): ImportedBindings => {
56
73
  const named = new Map<string, string>();
57
74
  const namespaces = new Set<string>();
58
- const body = (ast as unknown as { body?: readonly AstNode[] }).body ?? [];
75
+ const body = getNodeBodyStatements(ast);
59
76
 
60
77
  for (const statement of body) {
61
78
  if (
@@ -65,14 +82,10 @@ const addFrameworkImportBindings = (
65
82
  continue;
66
83
  }
67
84
 
68
- const specifiers =
69
- (statement as unknown as { specifiers?: readonly AstNode[] })
70
- .specifiers ?? [];
85
+ const specifiers = getNodeSpecifiers(statement);
71
86
  for (const specifier of specifiers) {
72
87
  if (specifier.type === 'ImportNamespaceSpecifier') {
73
- const localName = identifierName(
74
- (specifier as unknown as { local?: AstNode }).local
75
- );
88
+ const localName = identifierName(getNodeLocal(specifier));
76
89
  if (localName) {
77
90
  namespaces.add(localName);
78
91
  }
@@ -82,10 +95,8 @@ const addFrameworkImportBindings = (
82
95
  continue;
83
96
  }
84
97
 
85
- const { imported, local } = specifier as unknown as {
86
- readonly imported?: AstNode;
87
- readonly local?: AstNode;
88
- };
98
+ const imported = getNodeImported(specifier);
99
+ const local = getNodeLocal(specifier);
89
100
  const importedName = importedSpecifierName(imported);
90
101
  const localName = identifierName(local);
91
102
  if (importedName && allowedImports.has(importedName) && localName) {
@@ -105,11 +116,7 @@ const unwrapExpression = (node: AstNode | undefined): AstNode | undefined => {
105
116
  current?.type === 'TSAsExpression' ||
106
117
  current?.type === 'TSSatisfiesExpression'
107
118
  ) {
108
- current =
109
- (current as unknown as { argument?: AstNode; expression?: AstNode })
110
- .expression ??
111
- (current as unknown as { argument?: AstNode; expression?: AstNode })
112
- .argument;
119
+ current = getNodeExpression(current) ?? getNodeArgument(current);
113
120
  }
114
121
  return current;
115
122
  };
@@ -120,15 +127,10 @@ const memberExpressionParts = (
120
127
  if (node?.type !== 'MemberExpression') {
121
128
  return { objectName: null, propertyName: null };
122
129
  }
123
- const { computed, property } = node as unknown as {
124
- readonly computed?: boolean;
125
- readonly object?: AstNode;
126
- readonly property?: AstNode;
127
- };
130
+ const computed = getNodeComputed(node);
131
+ const property = getNodeProperty(node);
128
132
  return {
129
- objectName: identifierName(
130
- (node as unknown as { object?: AstNode }).object
131
- ),
133
+ objectName: identifierName(getNodeObject(node)),
132
134
  propertyName: computed ? null : identifierName(property),
133
135
  };
134
136
  };
@@ -163,10 +165,7 @@ const isTopoCall = (
163
165
  node: AstNode | undefined,
164
166
  bindings: ImportedBindings
165
167
  ): boolean =>
166
- calleeName(
167
- (unwrapExpression(node) as unknown as { callee?: AstNode })?.callee,
168
- bindings
169
- ) === 'topo';
168
+ calleeName(getNodeCallee(unwrapExpression(node)), bindings) === 'topo';
170
169
 
171
170
  const isSurfaceOpenCall = (
172
171
  node: AstNode | undefined,
@@ -177,7 +176,7 @@ const isSurfaceOpenCall = (
177
176
  return false;
178
177
  }
179
178
 
180
- const { callee } = expression as unknown as { readonly callee?: AstNode };
179
+ const callee = getNodeCallee(expression);
181
180
  const directName = calleeName(callee, bindings);
182
181
  if (directName && SURFACE_OPEN_CALLEE_NAMES.has(directName)) {
183
182
  return true;
@@ -195,27 +194,22 @@ const isSurfaceOpenCall = (
195
194
  };
196
195
 
197
196
  const declarationIdName = (node: AstNode | undefined): string | null =>
198
- identifierName(node) ??
199
- identifierName((node as unknown as { left?: AstNode } | undefined)?.left);
197
+ identifierName(node) ?? identifierName(node ? getNodeLeft(node) : undefined);
200
198
 
201
199
  const collectTopLevelTopoBindings = (
202
200
  ast: AstNode,
203
201
  topoBindings: ImportedBindings
204
202
  ): ReadonlySet<string> => {
205
203
  const bindings = new Set<string>();
206
- const body = (ast as unknown as { body?: readonly AstNode[] }).body ?? [];
204
+ const body = getNodeBodyStatements(ast);
207
205
 
208
206
  for (const statement of body) {
209
207
  const declaration = unwrapExportDeclaration(statement);
210
208
  if (declaration.type === 'VariableDeclaration') {
211
- const declarations =
212
- (declaration as unknown as { declarations?: readonly AstNode[] })
213
- .declarations ?? [];
209
+ const declarations = getNodeDeclarations(declaration);
214
210
  for (const item of declarations) {
215
- const { id, init } = item as unknown as {
216
- readonly id?: AstNode;
217
- readonly init?: AstNode;
218
- };
211
+ const id = getNodeId(item);
212
+ const init = getNodeInit(item);
219
213
  const name = declarationIdName(id);
220
214
  if (name && isTopoCall(init, topoBindings)) {
221
215
  bindings.add(name);
@@ -231,15 +225,11 @@ const namedExportCarriesTopo = (
231
225
  statement: AstNode,
232
226
  topLevelTopoBindings: ReadonlySet<string>
233
227
  ): boolean => {
234
- const specifiers =
235
- (statement as unknown as { specifiers?: readonly AstNode[] }).specifiers ??
236
- [];
228
+ const specifiers = getNodeSpecifiers(statement) ?? [];
237
229
 
238
230
  for (const specifier of specifiers) {
239
- const { exported, local } = specifier as unknown as {
240
- readonly exported?: AstNode;
241
- readonly local?: AstNode;
242
- };
231
+ const exported = getNodeExported(specifier);
232
+ const local = getNodeLocal(specifier);
243
233
  const exportedName = identifierName(exported);
244
234
  const localName = identifierName(local);
245
235
  if (
@@ -260,7 +250,7 @@ const moduleExportsTopo = (
260
250
  topoBindings: ImportedBindings
261
251
  ): boolean => {
262
252
  const topLevelTopoBindings = collectTopLevelTopoBindings(ast, topoBindings);
263
- const body = (ast as unknown as { body?: readonly AstNode[] }).body ?? [];
253
+ const body = getNodeBodyStatements(ast);
264
254
 
265
255
  for (const statement of body) {
266
256
  if (statement.type === 'ExportDefaultDeclaration') {
@@ -282,14 +272,10 @@ const moduleExportsTopo = (
282
272
  if (declaration.type !== 'VariableDeclaration') {
283
273
  continue;
284
274
  }
285
- const declarations =
286
- (declaration as unknown as { declarations?: readonly AstNode[] })
287
- .declarations ?? [];
275
+ const declarations = getNodeDeclarations(declaration);
288
276
  for (const item of declarations) {
289
- const { id, init } = item as unknown as {
290
- readonly id?: AstNode;
291
- readonly init?: AstNode;
292
- };
277
+ const id = getNodeId(item);
278
+ const init = getNodeInit(item);
293
279
  const name = declarationIdName(id);
294
280
  if (
295
281
  name &&
@@ -311,9 +297,7 @@ const topLevelSurfaceOpen = (
311
297
  ): AstNode | null => {
312
298
  const declaration = unwrapExportDeclaration(statement);
313
299
  if (declaration.type === 'ExpressionStatement') {
314
- const { expression } = declaration as unknown as {
315
- readonly expression?: AstNode;
316
- };
300
+ const expression = getNodeExpression(declaration);
317
301
  const unwrapped = unwrapExpression(expression);
318
302
  return isSurfaceOpenCall(unwrapped, surfaceBindings)
319
303
  ? (unwrapped ?? null)
@@ -329,11 +313,9 @@ const topLevelSurfaceOpen = (
329
313
  return null;
330
314
  }
331
315
 
332
- const declarations =
333
- (declaration as unknown as { declarations?: readonly AstNode[] })
334
- .declarations ?? [];
316
+ const declarations = getNodeDeclarations(declaration);
335
317
  for (const item of declarations) {
336
- const { init } = item as unknown as { readonly init?: AstNode };
318
+ const init = getNodeInit(item);
337
319
  const unwrapped = unwrapExpression(init);
338
320
  if (isSurfaceOpenCall(unwrapped, surfaceBindings)) {
339
321
  return unwrapped ?? null;
@@ -365,7 +347,7 @@ export const noTopLevelSurface: WardenRule = {
365
347
  );
366
348
 
367
349
  const diagnostics: WardenDiagnostic[] = [];
368
- const body = (ast as unknown as { body?: readonly AstNode[] }).body ?? [];
350
+ const body = getNodeBodyStatements(ast);
369
351
  for (const statement of body) {
370
352
  const surfaceOpen = topLevelSurfaceOpen(statement, surfaceBindings);
371
353
  if (!surfaceOpen) {
@@ -11,6 +11,7 @@ import { isDraftId } from '@ontrails/core';
11
11
 
12
12
  import {
13
13
  collectSignalDefinitionIds,
14
+ deriveConstString,
14
15
  findConfigProperty,
15
16
  findTrailDefinitions,
16
17
  getStringValue,
@@ -18,7 +19,6 @@ import {
18
19
  isStringLiteral,
19
20
  offsetToLine,
20
21
  parse,
21
- deriveConstString,
22
22
  } from './ast.js';
23
23
  import type { AstNode } from './ast.js';
24
24
  import { isTestFile } from './scan.js';
@@ -4,6 +4,11 @@ import { fileURLToPath } from 'node:url';
4
4
  import { intentValues } from '@ontrails/core';
5
5
 
6
6
  import {
7
+ getNodeExpression,
8
+ getNodeId,
9
+ getNodeInit,
10
+ getNodeKey,
11
+ getNodeProperties,
7
12
  getPropertyName,
8
13
  identifierName,
9
14
  offsetToLine,
@@ -34,7 +39,7 @@ const unwrapExpression = (node: AstNode | undefined): AstNode | undefined => {
34
39
  'TSTypeAssertion',
35
40
  ].includes(current.type)
36
41
  ) {
37
- current = (current as unknown as { expression?: AstNode }).expression;
42
+ current = getNodeExpression(current);
38
43
  }
39
44
  return current;
40
45
  };
@@ -52,10 +57,8 @@ const findHttpMethodByIntentMap = (ast: AstNode): ProjectionMap | null => {
52
57
  return;
53
58
  }
54
59
 
55
- const { id, init } = node as unknown as {
56
- id?: AstNode;
57
- init?: AstNode;
58
- };
60
+ const id = getNodeId(node);
61
+ const init = getNodeInit(node);
59
62
  if (identifierName(id) !== 'httpMethodByIntent') {
60
63
  return;
61
64
  }
@@ -67,17 +70,11 @@ const findHttpMethodByIntentMap = (ast: AstNode): ProjectionMap | null => {
67
70
  }
68
71
 
69
72
  const keys = new Set<string>();
70
- for (const property of (
71
- objectExpression as unknown as {
72
- properties?: readonly AstNode[];
73
- }
74
- ).properties ?? []) {
73
+ for (const property of getNodeProperties(objectExpression) ?? []) {
75
74
  if (property.type !== 'Property') {
76
75
  continue;
77
76
  }
78
- const key = getPropertyName(
79
- (property as unknown as { key?: AstNode }).key
80
- );
77
+ const key = getPropertyName(getNodeKey(property));
81
78
  if (key) {
82
79
  keys.add(key);
83
80
  }
@@ -30,7 +30,21 @@ import { readFileSync } from 'node:fs';
30
30
  import { dirname, join, normalize, relative, resolve } from 'node:path';
31
31
  import { fileURLToPath } from 'node:url';
32
32
 
33
- import { offsetToLine, parse } from './ast.js';
33
+ import {
34
+ getNodeBodyStatements,
35
+ getNodeDeclaration,
36
+ getNodeDeclarations,
37
+ getNodeExportKind,
38
+ getNodeExported,
39
+ getNodeId,
40
+ getNodeLocal,
41
+ getNodeName,
42
+ getNodeSource,
43
+ getNodeSpecifiers,
44
+ getNodeValue,
45
+ offsetToLine,
46
+ parse,
47
+ } from './ast.js';
34
48
  import type { AstNode } from './ast.js';
35
49
  import type { WardenDiagnostic, WardenRule } from './types.js';
36
50
 
@@ -158,33 +172,33 @@ interface BarrelInventory {
158
172
  }
159
173
 
160
174
  const isTypeKind = (node: AstNode): boolean =>
161
- (node as unknown as { exportKind?: string }).exportKind === 'type';
175
+ getNodeExportKind(node) === 'type';
162
176
 
163
177
  const readNameNode = (node: AstNode | undefined): string | null => {
164
178
  if (!node) {
165
179
  return null;
166
180
  }
167
181
  if (node.type === 'Identifier') {
168
- return (node as unknown as { name?: string }).name ?? null;
182
+ return getNodeName(node) ?? null;
169
183
  }
170
184
  if (node.type === 'Literal' || node.type === 'StringLiteral') {
171
- const { value } = node as unknown as { value?: unknown };
185
+ const value = getNodeValue(node);
172
186
  return typeof value === 'string' ? value : null;
173
187
  }
174
188
  return null;
175
189
  };
176
190
 
177
191
  const moduleSpecifierValue = (node: AstNode): string | null => {
178
- const { source } = node as unknown as { source?: AstNode };
192
+ const source = getNodeSource(node);
179
193
  if (!source) {
180
194
  return null;
181
195
  }
182
- const { value } = source as unknown as { value?: unknown };
196
+ const value = getNodeValue(source);
183
197
  return typeof value === 'string' ? value : null;
184
198
  };
185
199
 
186
200
  const programBody = (ast: AstNode): readonly AstNode[] =>
187
- (ast as unknown as { body?: readonly AstNode[] }).body ?? [];
201
+ getNodeBodyStatements(ast);
188
202
 
189
203
  const diagnostic = (
190
204
  sourceCode: string,
@@ -204,16 +218,13 @@ const specifiersFromExportDeclaration = (
204
218
  node: AstNode,
205
219
  moduleSpecifier: string
206
220
  ): readonly PublicExportSpecifier[] => {
207
- const specifiers =
208
- (node as unknown as { specifiers?: readonly AstNode[] }).specifiers ?? [];
221
+ const specifiers = getNodeSpecifiers(node) ?? [];
209
222
  return specifiers.flatMap((specifier) => {
210
223
  if (specifier.type !== 'ExportSpecifier' || isTypeKind(specifier)) {
211
224
  return [];
212
225
  }
213
- const { exported, local } = specifier as unknown as {
214
- exported?: AstNode;
215
- local?: AstNode;
216
- };
226
+ const exported = getNodeExported(specifier);
227
+ const local = getNodeLocal(specifier);
217
228
  const exportName = readNameNode(exported);
218
229
  if (!exportName) {
219
230
  return [];
@@ -241,7 +252,7 @@ const inventoryNamedExport = (node: AstNode, ctx: InventoryContext): void => {
241
252
  if (isTypeKind(node)) {
242
253
  return;
243
254
  }
244
- const { declaration } = node as unknown as { declaration?: AstNode };
255
+ const declaration = getNodeDeclaration(node);
245
256
  if (declaration) {
246
257
  // Declaration-form exports (`export const foo = ...`) are not module
247
258
  // re-exports; the script's inventory skipped them the same way.
@@ -344,15 +355,13 @@ const declarationNameMatches = (
344
355
  declaration.type === 'TSInterfaceDeclaration' ||
345
356
  declaration.type === 'TSTypeAliasDeclaration'
346
357
  ) {
347
- const { id } = declaration as unknown as { id?: AstNode };
358
+ const id = getNodeId(declaration);
348
359
  return readNameNode(id) === exportName;
349
360
  }
350
361
  if (declaration.type === 'VariableDeclaration') {
351
- const declarations =
352
- (declaration as unknown as { declarations?: readonly AstNode[] })
353
- .declarations ?? [];
362
+ const declarations = getNodeDeclarations(declaration);
354
363
  return declarations.some((declarator) => {
355
- const { id } = declarator as unknown as { id?: AstNode };
364
+ const id = getNodeId(declarator);
356
365
  return readNameNode(id) === exportName;
357
366
  });
358
367
  }
@@ -404,7 +413,7 @@ const hasLeadingExampleForExport = (
404
413
  if (statement.type !== 'ExportNamedDeclaration') {
405
414
  continue;
406
415
  }
407
- const { declaration } = statement as unknown as { declaration?: AstNode };
416
+ const declaration = getNodeDeclaration(statement);
408
417
  if (!declaration || !declarationNameMatches(declaration, importedName)) {
409
418
  continue;
410
419
  }
@@ -3,13 +3,13 @@ import { resolve, sep } from 'node:path';
3
3
 
4
4
  import {
5
5
  extractStringLiteral,
6
+ getNodeSource,
6
7
  hasIgnoreCommentOnLine,
7
8
  offsetToLine,
8
9
  parse,
9
10
  splitSourceLines,
10
11
  walk,
11
12
  } from './ast.js';
12
- import type { AstNode } from './ast.js';
13
13
  import type {
14
14
  ProjectAwareWardenRule,
15
15
  ProjectContext,
@@ -223,7 +223,7 @@ const collectReExportSites = (
223
223
  return;
224
224
  }
225
225
 
226
- const { source } = node as unknown as { source?: AstNode };
226
+ const source = getNodeSource(node);
227
227
  const value = extractStringLiteral(source);
228
228
  if (typeof value !== 'string') {
229
229
  return;
@@ -4,6 +4,9 @@ import {
4
4
  extractStringLiteral,
5
5
  findConfigProperty,
6
6
  findTrailDefinitions,
7
+ getNodeElements,
8
+ getNodeId,
9
+ getNodeInit,
7
10
  getStringValue,
8
11
  identifierName,
9
12
  isStringLiteral,
@@ -34,10 +37,8 @@ const collectArrayBindings = (ast: AstNode): ReadonlyMap<string, AstNode> => {
34
37
  return;
35
38
  }
36
39
 
37
- const { id, init } = node as unknown as {
38
- readonly id?: AstNode;
39
- readonly init?: AstNode;
40
- };
40
+ const id = getNodeId(node);
41
+ const init = getNodeInit(node);
41
42
  const name = identifierName(id);
42
43
  if (name && init?.type === 'ArrayExpression') {
43
44
  bindings.set(name, init);
@@ -69,10 +70,9 @@ const getFiresElements = (
69
70
  return [];
70
71
  }
71
72
 
72
- return (
73
- (array as unknown as { readonly elements?: readonly (AstNode | null)[] })
74
- .elements ?? []
75
- ).filter((element): element is AstNode => element !== null);
73
+ return getNodeElements(array).filter(
74
+ (element): element is AstNode => element !== null
75
+ );
76
76
  };
77
77
 
78
78
  const resolveFireElementId = (
@@ -7,13 +7,16 @@
7
7
  * the `warden-export-symmetry` rule will fail the build if they drift.
8
8
  */
9
9
  import { activationOrphan } from './activation-orphan.js';
10
+ import { cliCommandRouteCoherence } from './cli-command-route-coherence.js';
10
11
  import { circularRefs } from './circular-refs.js';
11
12
  import { contourExists } from './contour-exists.js';
12
13
  import { contextNoSurfaceTypes } from './context-no-surface-types.js';
13
14
  import { composesDeclarations } from './composes-declarations.js';
14
15
  import { deadInternalTrail } from './dead-internal-trail.js';
16
+ import { deadPublicTrail } from './dead-public-trail.js';
15
17
  import { draftFileMarking } from './draft-file-marking.js';
16
18
  import { draftVisibleDebt } from './draft-visible-debt.js';
19
+ import { duplicatePublicContract } from './duplicate-public-contract.js';
17
20
  import { errorMappingCompleteness } from './error-mapping-completeness.js';
18
21
  import { exampleValid } from './example-valid.js';
19
22
  import { firesDeclarations } from './fires-declarations.js';
@@ -22,6 +25,7 @@ import { incompleteAccessorForStandardOp } from './incomplete-accessor-for-stand
22
25
  import { incompleteCrud } from './incomplete-crud.js';
23
26
  import { intentPropagation } from './intent-propagation.js';
24
27
  import { layerFieldNameDrift } from './layer-field-name-drift.js';
28
+ import { libraryProjectionCoherence } from './library-projection-coherence.js';
25
29
  import { missingReconcile } from './missing-reconcile.js';
26
30
  import { missingVisibility } from './missing-visibility.js';
27
31
  import { noDevPermitInSource } from './no-dev-permit-in-source.js';
@@ -55,6 +59,7 @@ import { scheduledDestroyIntent } from './scheduled-destroy-intent.js';
55
59
  import { signalGraphCoaching } from './signal-graph-coaching.js';
56
60
  import { staticResourceAccessorPreference } from './static-resource-accessor-preference.js';
57
61
  import { surfaceFacetCoherence } from './surface-facet-coherence.js';
62
+ import { trailForkCoaching } from './trail-fork-coaching.js';
58
63
  import {
59
64
  forkWithoutPreservedBlaze,
60
65
  markerSchemaUnsupported,
@@ -81,12 +86,15 @@ import { webhookRouteCollision } from './webhook-route-collision.js';
81
86
  */
82
87
  export const registeredRuleNames: readonly string[] = [
83
88
  activationOrphan.name,
89
+ cliCommandRouteCoherence.name,
84
90
  circularRefs.name,
85
91
  contextNoSurfaceTypes.name,
86
92
  contourExists.name,
87
93
  composesDeclarations.name,
88
94
  deadInternalTrail.name,
95
+ deadPublicTrail.name,
89
96
  deprecationWithoutGuidance.name,
97
+ duplicatePublicContract.name,
90
98
  draftFileMarking.name,
91
99
  draftVisibleDebt.name,
92
100
  errorMappingCompleteness.name,
@@ -98,6 +106,7 @@ export const registeredRuleNames: readonly string[] = [
98
106
  incompleteCrud.name,
99
107
  intentPropagation.name,
100
108
  layerFieldNameDrift.name,
109
+ libraryProjectionCoherence.name,
101
110
  markerSchemaUnsupported.name,
102
111
  missingReconcile.name,
103
112
  missingVisibility.name,
@@ -133,6 +142,7 @@ export const registeredRuleNames: readonly string[] = [
133
142
  signalGraphCoaching.name,
134
143
  staticResourceAccessorPreference.name,
135
144
  surfaceFacetCoherence.name,
145
+ trailForkCoaching.name,
136
146
  unmaterializedActivationSource.name,
137
147
  unreachableDetourShadowing.name,
138
148
  validDetourContract.name,
@@ -10,9 +10,17 @@
10
10
  import {
11
11
  collectNamedResourceIds,
12
12
  extractFirstStringArg,
13
- findConfigProperty,
14
13
  findBlazeBodies,
14
+ findConfigProperty,
15
15
  findTrailDefinitions,
16
+ getNodeCallee,
17
+ getNodeId,
18
+ getNodeInit,
19
+ getNodeKey,
20
+ getNodeLeft,
21
+ getNodeObject,
22
+ getNodeProperty,
23
+ getNodeValueNode,
16
24
  getStringValue,
17
25
  identifierName,
18
26
  isStringLiteral,
@@ -49,12 +57,8 @@ const extractMemberPair = (
49
57
  return null;
50
58
  }
51
59
 
52
- const objName = identifierName(
53
- (callee as unknown as { object?: AstNode }).object
54
- );
55
- const propName = identifierName(
56
- (callee as unknown as { property?: AstNode }).property
57
- );
60
+ const objName = identifierName(getNodeObject(callee));
61
+ const propName = identifierName(getNodeProperty(callee));
58
62
 
59
63
  return objName && propName ? { objName, propName } : null;
60
64
  };
@@ -64,10 +68,7 @@ const isInlineResourceCall = (node: AstNode): boolean => {
64
68
  if (node.type !== 'CallExpression') {
65
69
  return false;
66
70
  }
67
- return (
68
- identifierName((node as unknown as { callee?: AstNode }).callee) ===
69
- 'resource'
70
- );
71
+ return identifierName(getNodeCallee(node)) === 'resource';
71
72
  };
72
73
 
73
74
  /** Get `resources` array elements from a trail config. */
@@ -152,7 +153,7 @@ const extractContextParamName = (blazeBody: AstNode): string | null => {
152
153
  return null;
153
154
  }
154
155
  if (param.type === 'AssignmentPattern') {
155
- const { left } = param as unknown as { left?: AstNode };
156
+ const left = getNodeLeft(param);
156
157
  return identifierName(left);
157
158
  }
158
159
  return identifierName(param);
@@ -163,16 +164,11 @@ const extractResourceAlias = (property: AstNode): string | null => {
163
164
  if (property.type !== 'Property') {
164
165
  return null;
165
166
  }
166
- const keyName = identifierName(
167
- (property as unknown as { key?: AstNode }).key
168
- );
167
+ const keyName = identifierName(getNodeKey(property));
169
168
  if (keyName !== 'resource') {
170
169
  return null;
171
170
  }
172
- return (
173
- identifierName((property as unknown as { value?: AstNode }).value) ??
174
- keyName
175
- );
171
+ return identifierName(getNodeValueNode(property)) ?? keyName;
176
172
  };
177
173
 
178
174
  /**
@@ -224,8 +220,7 @@ const extractCallCallee = (node: AstNode): AstNode | null => {
224
220
  if (node.type !== 'CallExpression') {
225
221
  return null;
226
222
  }
227
- return ((node as unknown as { callee?: AstNode }).callee ??
228
- null) as AstNode | null;
223
+ return (getNodeCallee(node) ?? null) as AstNode | null;
229
224
  };
230
225
 
231
226
  /** Extract the first identifier argument from a CallExpression. */
@@ -338,16 +333,12 @@ const collectResourceAliases = (
338
333
  return [];
339
334
  }
340
335
 
341
- const keyName = identifierName(
342
- (property as unknown as { key?: AstNode }).key
343
- );
336
+ const keyName = identifierName(getNodeKey(property));
344
337
  if (keyName !== 'resource') {
345
338
  return [];
346
339
  }
347
340
 
348
- const alias =
349
- identifierName((property as unknown as { value?: AstNode }).value) ??
350
- keyName;
341
+ const alias = identifierName(getNodeValueNode(property)) ?? keyName;
351
342
  return [alias];
352
343
  });
353
344
  };
@@ -357,10 +348,8 @@ const collectResourceAliases = (
357
348
  return;
358
349
  }
359
350
 
360
- const { id, init } = node as unknown as {
361
- readonly id?: AstNode;
362
- readonly init?: AstNode;
363
- };
351
+ const id = getNodeId(node);
352
+ const init = getNodeInit(node);
364
353
  const initName = identifierName(init);
365
354
  if (!initName || !ctxNames.has(initName)) {
366
355
  return;
@@ -6,6 +6,7 @@ import {
6
6
  extractFirstStringArg,
7
7
  findConfigProperty,
8
8
  findTrailDefinitions,
9
+ getNodeCallee,
9
10
  getStringValue,
10
11
  identifierName,
11
12
  isStringLiteral,
@@ -22,8 +23,7 @@ import type {
22
23
 
23
24
  const isResourceCall = (node: AstNode): boolean =>
24
25
  node.type === 'CallExpression' &&
25
- identifierName((node as unknown as { callee?: AstNode }).callee) ===
26
- 'resource';
26
+ identifierName(getNodeCallee(node)) === 'resource';
27
27
 
28
28
  const getResourceElements = (config: AstNode): readonly AstNode[] => {
29
29
  const resourcesProp = findConfigProperty(config, 'resources');