@ontrails/warden 1.0.0-beta.32 → 1.0.0-beta.39

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 (37) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +1 -1
  3. package/package.json +9 -9
  4. package/src/cli.ts +120 -10
  5. package/src/command.ts +25 -5
  6. package/src/drift.ts +116 -7
  7. package/src/fix.ts +8 -2
  8. package/src/formatters.ts +3 -8
  9. package/src/index.ts +30 -2
  10. package/src/project-context.ts +255 -1
  11. package/src/rules/ast.ts +4 -0
  12. package/src/rules/duplicate-exported-symbol.ts +211 -0
  13. package/src/rules/duplicate-public-contract.ts +2 -1
  14. package/src/rules/governed-symbol-residue.ts +131 -0
  15. package/src/rules/index.ts +43 -3
  16. package/src/rules/metadata.ts +105 -11
  17. package/src/rules/no-legacy-cli-alias-export.ts +247 -0
  18. package/src/rules/no-retired-cross-vocabulary.ts +19 -9
  19. package/src/rules/public-export-example-coverage.ts +5 -0
  20. package/src/rules/registry-names.ts +12 -2
  21. package/src/rules/retired-vocabulary.ts +396 -0
  22. package/src/rules/signal-graph-coaching.ts +32 -3
  23. package/src/rules/surface-overlay-coherence.ts +262 -0
  24. package/src/rules/{surface-facet-coherence.ts → surface-trailhead-coherence.ts} +45 -41
  25. package/src/rules/trailhead-override-divergence.ts +356 -0
  26. package/src/rules/types.ts +58 -1
  27. package/src/rules/webhook-route-collision.ts +64 -1
  28. package/src/trails/duplicate-exported-symbol.trail.ts +48 -0
  29. package/src/trails/duplicate-public-contract.trail.ts +1 -1
  30. package/src/trails/governed-symbol-residue.trail.ts +24 -0
  31. package/src/trails/index.ts +6 -1
  32. package/src/trails/no-legacy-cli-alias-export.trail.ts +41 -0
  33. package/src/trails/schema.ts +39 -0
  34. package/src/trails/surface-overlay-coherence.trail.ts +24 -0
  35. package/src/trails/{surface-facet-coherence.trail.ts → surface-trailhead-coherence.trail.ts} +4 -4
  36. package/src/trails/trailhead-override-divergence.trail.ts +47 -0
  37. package/src/trails/wrap-rule.ts +10 -0
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Coach the app-authored `surfaces` overlay toward coherent bindings.
3
+ *
4
+ * Reads the `surfaces` namespace from the serialized graph overlays and
5
+ * checks, per surface key, that every binding selector matches at least one
6
+ * trail, that grouped bindings do not overlap on expanded members, and that
7
+ * binding names do not shadow real surface entries (single-segment CLI
8
+ * routes, derived MCP tool names).
9
+ *
10
+ * Activation note: standard `trails warden` runs fire this rule. Warden's
11
+ * fresh topo loading collects the app-module overlays export through the
12
+ * shared `resolveTrailsOverlays` channel compile uses (TRL-1209 drift
13
+ * symmetry), so the topo-aware rule context graph carries the same overlays
14
+ * the committed lock embeds. Callers that supply a precomputed graph
15
+ * (committed locks, tests, the rule trail) keep working unchanged.
16
+ */
17
+
18
+ import {
19
+ SURFACES_OVERLAY_NAMESPACE,
20
+ classifySurfaceBinding,
21
+ deriveMcpToolName,
22
+ matchesTrailPattern,
23
+ surfaceBindingsFromLockOverlays,
24
+ } from '@ontrails/core';
25
+ import type {
26
+ SurfaceBindings,
27
+ SurfaceOverlayBindings,
28
+ Topo,
29
+ } from '@ontrails/core';
30
+ import type { TopoGraph } from '@ontrails/topographer';
31
+
32
+ import type { TopoAwareWardenRule, WardenDiagnostic } from './types.js';
33
+
34
+ const RULE_NAME = 'surface-overlay-coherence';
35
+ const TOPO_FILE = '<topo>';
36
+
37
+ const SURFACE_KEYS = ['cli', 'http', 'mcp', 'ws'] as const;
38
+ type SurfaceKey = (typeof SURFACE_KEYS)[number];
39
+
40
+ const warn = (message: string): WardenDiagnostic => ({
41
+ filePath: TOPO_FILE,
42
+ line: 1,
43
+ message,
44
+ rule: RULE_NAME,
45
+ severity: 'warn',
46
+ });
47
+
48
+ const memberRefs = (value: SurfaceBindings[string]): readonly string[] => {
49
+ const shape = classifySurfaceBinding(value);
50
+ return shape.kind === 'synonym' ? [shape.trail] : shape.members;
51
+ };
52
+
53
+ const expandRefs = (
54
+ refs: readonly string[],
55
+ trailIds: readonly string[]
56
+ ): ReadonlySet<string> => {
57
+ const matched = new Set<string>();
58
+ for (const ref of refs) {
59
+ for (const trailId of trailIds) {
60
+ if (matchesTrailPattern(trailId, ref)) {
61
+ matched.add(trailId);
62
+ }
63
+ }
64
+ }
65
+ return matched;
66
+ };
67
+
68
+ const zeroMatchDiagnostics = (
69
+ surfaceKey: SurfaceKey,
70
+ bindings: SurfaceBindings,
71
+ trailIds: readonly string[]
72
+ ): readonly WardenDiagnostic[] => {
73
+ const diagnostics: WardenDiagnostic[] = [];
74
+ for (const [name, value] of Object.entries(bindings)) {
75
+ for (const ref of memberRefs(value)) {
76
+ if (!trailIds.some((trailId) => matchesTrailPattern(trailId, ref))) {
77
+ diagnostics.push(
78
+ warn(
79
+ `Surface overlay binding "${name}" on "${surfaceKey}" references "${ref}", which matches no trails in the topo. Point the binding at an existing trail id or dotted trail-id glob.`
80
+ )
81
+ );
82
+ }
83
+ }
84
+ }
85
+ return diagnostics;
86
+ };
87
+
88
+ interface ExpandedGroup {
89
+ readonly name: string;
90
+ readonly trailIds: ReadonlySet<string>;
91
+ }
92
+
93
+ const groupOverlapDiagnostics = (
94
+ surfaceKey: SurfaceKey,
95
+ bindings: SurfaceBindings,
96
+ trailIds: readonly string[]
97
+ ): readonly WardenDiagnostic[] => {
98
+ const groups: ExpandedGroup[] = [];
99
+ for (const [name, value] of Object.entries(bindings)) {
100
+ const shape = classifySurfaceBinding(value);
101
+ if (shape.kind === 'group') {
102
+ groups.push({ name, trailIds: expandRefs(shape.members, trailIds) });
103
+ }
104
+ }
105
+
106
+ const diagnostics: WardenDiagnostic[] = [];
107
+ for (let i = 0; i < groups.length; i += 1) {
108
+ const first = groups[i];
109
+ if (!first) {
110
+ continue;
111
+ }
112
+ for (let j = i + 1; j < groups.length; j += 1) {
113
+ const second = groups[j];
114
+ if (!second) {
115
+ continue;
116
+ }
117
+ const [sharedId] = [...second.trailIds]
118
+ .filter((trailId) => first.trailIds.has(trailId))
119
+ .toSorted();
120
+ if (sharedId !== undefined) {
121
+ diagnostics.push(
122
+ warn(
123
+ `Surface overlay group "${second.name}" on "${surfaceKey}" overlaps group "${first.name}" on trail "${sharedId}". Narrow one group so each trail has one grouped owner per surface.`
124
+ )
125
+ );
126
+ }
127
+ }
128
+ }
129
+ return diagnostics;
130
+ };
131
+
132
+ const cliCollisionDiagnostics = (
133
+ bindings: SurfaceBindings,
134
+ graph: TopoGraph
135
+ ): readonly WardenDiagnostic[] => {
136
+ const routeByName = new Map<
137
+ string,
138
+ { readonly kind: string; readonly trailId: string }
139
+ >();
140
+ for (const entry of graph.entries) {
141
+ if (entry.kind !== 'trail') {
142
+ continue;
143
+ }
144
+ const path = entry.cli?.path;
145
+ const segment = path?.length === 1 ? path[0] : undefined;
146
+ if (segment !== undefined) {
147
+ routeByName.set(segment, { kind: 'canonical', trailId: entry.id });
148
+ }
149
+ for (const route of entry.cli?.routes ?? []) {
150
+ // Surface-sourced alias routes are the projections of the overlay
151
+ // bindings themselves (TRL-1207), so a binding can never "shadow"
152
+ // one — only canonical paths and trail-owned aliases are real
153
+ // entries a binding name could collide with.
154
+ if (route.source === 'surface') {
155
+ continue;
156
+ }
157
+ const routeSegment = route.path.length === 1 ? route.path[0] : undefined;
158
+ if (routeSegment !== undefined && !routeByName.has(routeSegment)) {
159
+ routeByName.set(routeSegment, {
160
+ kind: route.kind,
161
+ trailId: entry.id,
162
+ });
163
+ }
164
+ }
165
+ }
166
+
167
+ const diagnostics: WardenDiagnostic[] = [];
168
+ for (const name of Object.keys(bindings)) {
169
+ const route = routeByName.get(name);
170
+ if (route !== undefined) {
171
+ diagnostics.push(
172
+ warn(
173
+ `Surface overlay binding "${name}" on "cli" shadows the ${route.kind} CLI route "${name}" for trail "${route.trailId}". Rename the binding so it does not shadow a real entry.`
174
+ )
175
+ );
176
+ }
177
+ }
178
+ return diagnostics;
179
+ };
180
+
181
+ const mcpCollisionDiagnostics = (
182
+ bindings: SurfaceBindings,
183
+ appName: string,
184
+ graph: TopoGraph
185
+ ): readonly WardenDiagnostic[] => {
186
+ const trailIdByToolName = new Map<string, string>();
187
+ for (const entry of graph.entries) {
188
+ if (entry.kind === 'trail') {
189
+ trailIdByToolName.set(deriveMcpToolName(appName, entry.id), entry.id);
190
+ }
191
+ }
192
+
193
+ const diagnostics: WardenDiagnostic[] = [];
194
+ for (const name of Object.keys(bindings)) {
195
+ const trailId = trailIdByToolName.get(name);
196
+ if (trailId !== undefined) {
197
+ diagnostics.push(
198
+ warn(
199
+ `Surface overlay binding "${name}" on "mcp" shadows the derived MCP tool name "${name}" for trail "${trailId}". Rename the binding so it does not shadow a real entry.`
200
+ )
201
+ );
202
+ }
203
+ }
204
+ return diagnostics;
205
+ };
206
+
207
+ const collectSurfaceDiagnostics = (
208
+ topo: Topo,
209
+ graph: TopoGraph,
210
+ overlayBindings: SurfaceOverlayBindings
211
+ ): readonly WardenDiagnostic[] => {
212
+ const trailIds = [...topo.trails.keys()];
213
+ const diagnostics: WardenDiagnostic[] = [];
214
+ for (const surfaceKey of SURFACE_KEYS) {
215
+ const bindings = overlayBindings[surfaceKey];
216
+ if (bindings === undefined) {
217
+ continue;
218
+ }
219
+ diagnostics.push(
220
+ ...zeroMatchDiagnostics(surfaceKey, bindings, trailIds),
221
+ ...groupOverlapDiagnostics(surfaceKey, bindings, trailIds)
222
+ );
223
+ if (surfaceKey === 'cli') {
224
+ diagnostics.push(...cliCollisionDiagnostics(bindings, graph));
225
+ }
226
+ if (surfaceKey === 'mcp') {
227
+ diagnostics.push(...mcpCollisionDiagnostics(bindings, topo.name, graph));
228
+ }
229
+ }
230
+ return diagnostics;
231
+ };
232
+
233
+ export const surfaceOverlayCoherence: TopoAwareWardenRule = {
234
+ checkTopo(topo, context) {
235
+ const graph = context?.graph;
236
+ if (graph?.overlays === undefined) {
237
+ return [];
238
+ }
239
+
240
+ let overlayBindings: SurfaceOverlayBindings | undefined;
241
+ try {
242
+ overlayBindings = surfaceBindingsFromLockOverlays(graph.overlays);
243
+ } catch (error: unknown) {
244
+ return [
245
+ warn(
246
+ `The "${SURFACES_OVERLAY_NAMESPACE}" overlay in the serialized graph is invalid: ${
247
+ error instanceof Error ? error.message : String(error)
248
+ }`
249
+ ),
250
+ ];
251
+ }
252
+ if (overlayBindings === undefined) {
253
+ return [];
254
+ }
255
+
256
+ return collectSurfaceDiagnostics(topo, graph, overlayBindings);
257
+ },
258
+ description:
259
+ 'Keep app-authored surface overlay bindings pointed at real trails without group overlap or canonical-entry shadowing.',
260
+ name: RULE_NAME,
261
+ severity: 'warn',
262
+ };
@@ -22,10 +22,10 @@ import {
22
22
  import type { AstNode } from './ast.js';
23
23
  import type { WardenDiagnostic, WardenRule } from './types.js';
24
24
 
25
- const RULE_NAME = 'surface-facet-coherence';
25
+ const RULE_NAME = 'surface-trailhead-coherence';
26
26
 
27
- interface FacetSelector {
28
- readonly facetId: string;
27
+ interface TrailheadSelector {
28
+ readonly trailheadId: string;
29
29
  readonly node: AstNode;
30
30
  readonly value: string;
31
31
  }
@@ -68,28 +68,30 @@ const diagnostic = (
68
68
  severity: 'warn',
69
69
  });
70
70
 
71
- const isFacetDefinition = (node: AstNode): boolean =>
71
+ const isTrailheadDefinition = (node: AstNode): boolean =>
72
72
  node.type === 'ObjectExpression' &&
73
73
  findConfigProperty(node, 'trails') !== null;
74
74
 
75
- const isFacetMapCandidate = (node: AstNode): boolean =>
75
+ const isTrailheadMapCandidate = (node: AstNode): boolean =>
76
76
  objectProperties(node).some((property) => {
77
77
  const value = unwrapExpression(propertyValue(property));
78
- return value !== undefined && isFacetDefinition(value);
78
+ return value !== undefined && isTrailheadDefinition(value);
79
79
  });
80
80
 
81
- const isFacetMapBindingName = (name: string | null): boolean =>
81
+ const isTrailheadMapBindingName = (name: string | null): boolean =>
82
82
  name !== null &&
83
- (name === 'facets' || name.endsWith('Facets') || name.endsWith('FacetMap'));
83
+ (name === 'trailheads' ||
84
+ name.endsWith('Trailheads') ||
85
+ name.endsWith('TrailheadMap'));
84
86
 
85
- const hasFacetMapTypeAnnotation = (
87
+ const hasTrailheadMapTypeAnnotation = (
86
88
  sourceCode: string,
87
89
  node: AstNode
88
90
  ): boolean => {
89
91
  const typeAnnotation = getNodeTypeAnnotation(node);
90
92
  return (
91
93
  typeAnnotation !== undefined &&
92
- /\b(?:McpSurfaceFacetMap|TopoGraphFacetDeclaration|FacetMap)\b/.test(
94
+ /\b(?:McpSurfaceTrailheadMap|TrailheadMap)\b/.test(
93
95
  sourceCode.slice(typeAnnotation.start, typeAnnotation.end)
94
96
  )
95
97
  );
@@ -109,10 +111,10 @@ const selectorNodes = (trailsNode: AstNode): readonly AstNode[] | null => {
109
111
  const collectLiteralSelectors = (
110
112
  sourceCode: string,
111
113
  filePath: string,
112
- facetId: string,
114
+ trailheadId: string,
113
115
  trailsProp: AstNode,
114
116
  diagnostics: WardenDiagnostic[]
115
- ): readonly FacetSelector[] => {
117
+ ): readonly TrailheadSelector[] => {
116
118
  const trailsValue = propertyValue(trailsProp);
117
119
  const nodes = trailsValue ? selectorNodes(trailsValue) : null;
118
120
  if (nodes === null || nodes.length === 0) {
@@ -121,13 +123,13 @@ const collectLiteralSelectors = (
121
123
  sourceCode,
122
124
  filePath,
123
125
  trailsProp,
124
- `Surface facet "${facetId}" uses a dynamic trails selector. Keep facet selectors as string literals so Warden can check overlap and drift.`
126
+ `Surface trailhead "${trailheadId}" uses a dynamic trails selector. Keep trailhead selectors as string literals so Warden can check overlap and drift.`
125
127
  )
126
128
  );
127
129
  return [];
128
130
  }
129
131
 
130
- const selectors: FacetSelector[] = [];
132
+ const selectors: TrailheadSelector[] = [];
131
133
  for (const node of nodes) {
132
134
  const selectorValue = extractStringOrTemplateLiteral(node);
133
135
  if (selectorValue === null) {
@@ -136,12 +138,12 @@ const collectLiteralSelectors = (
136
138
  sourceCode,
137
139
  filePath,
138
140
  node,
139
- `Surface facet "${facetId}" uses a dynamic trails selector. Keep facet selectors as string literals so Warden can check overlap and drift.`
141
+ `Surface trailhead "${trailheadId}" uses a dynamic trails selector. Keep trailhead selectors as string literals so Warden can check overlap and drift.`
140
142
  )
141
143
  );
142
144
  continue;
143
145
  }
144
- selectors.push({ facetId, node, value: selectorValue });
146
+ selectors.push({ node, trailheadId, value: selectorValue });
145
147
  }
146
148
  return selectors;
147
149
  };
@@ -176,21 +178,21 @@ const literalBooleanProperty = (
176
178
  };
177
179
 
178
180
  const selectorsMayOverlap = (
179
- first: FacetSelector,
180
- second: FacetSelector
181
+ first: TrailheadSelector,
182
+ second: TrailheadSelector
181
183
  ): boolean =>
182
184
  first.value === second.value ||
183
185
  matchesTrailPattern(first.value, second.value) ||
184
186
  matchesTrailPattern(second.value, first.value);
185
187
 
186
- const diagnoseFacetDefinition = (
188
+ const diagnoseTrailheadDefinition = (
187
189
  sourceCode: string,
188
190
  filePath: string,
189
- facetId: string,
191
+ trailheadId: string,
190
192
  definition: AstNode
191
193
  ): {
192
194
  readonly diagnostics: readonly WardenDiagnostic[];
193
- readonly selectors: readonly FacetSelector[];
195
+ readonly selectors: readonly TrailheadSelector[];
194
196
  } => {
195
197
  const diagnostics: WardenDiagnostic[] = [];
196
198
  const trailsProp = findConfigProperty(definition, 'trails');
@@ -200,7 +202,7 @@ const diagnoseFacetDefinition = (
200
202
  : collectLiteralSelectors(
201
203
  sourceCode,
202
204
  filePath,
203
- facetId,
205
+ trailheadId,
204
206
  trailsProp,
205
207
  diagnostics
206
208
  );
@@ -211,7 +213,7 @@ const diagnoseFacetDefinition = (
211
213
  sourceCode,
212
214
  filePath,
213
215
  definition,
214
- `Surface facet "${facetId}" needs a non-empty description so MCP clients and agents can choose it without guessing.`
216
+ `Surface trailhead "${trailheadId}" needs a non-empty description so MCP clients and agents can choose it without guessing.`
215
217
  )
216
218
  );
217
219
  }
@@ -227,7 +229,7 @@ const diagnoseFacetDefinition = (
227
229
  sourceCode,
228
230
  filePath,
229
231
  definition,
230
- `Surface facet "${facetId}" explicitly sets public visibility without visibilityWideningAccepted: true. Facets must not accidentally widen hidden trails.`
232
+ `Surface trailhead "${trailheadId}" explicitly sets public visibility without visibilityWideningAccepted: true. Trailheads must not accidentally widen hidden trails.`
231
233
  )
232
234
  );
233
235
  }
@@ -241,7 +243,7 @@ const diagnoseFacetDefinition = (
241
243
  sourceCode,
242
244
  filePath,
243
245
  definition,
244
- `Surface facet "${facetId}" accepts visibility widening but does not record descriptionStableThrough review metadata.`
246
+ `Surface trailhead "${trailheadId}" accepts visibility widening but does not record descriptionStableThrough review metadata.`
245
247
  )
246
248
  );
247
249
  }
@@ -249,24 +251,24 @@ const diagnoseFacetDefinition = (
249
251
  return { diagnostics, selectors };
250
252
  };
251
253
 
252
- const diagnoseFacetMap = (
254
+ const diagnoseTrailheadMap = (
253
255
  sourceCode: string,
254
256
  filePath: string,
255
- facetMap: AstNode
257
+ trailheadMap: AstNode
256
258
  ): readonly WardenDiagnostic[] => {
257
259
  const diagnostics: WardenDiagnostic[] = [];
258
- const selectors: FacetSelector[] = [];
260
+ const selectors: TrailheadSelector[] = [];
259
261
 
260
- for (const property of objectProperties(facetMap)) {
261
- const facetId = getPropertyName(getNodeKey(property));
262
+ for (const property of objectProperties(trailheadMap)) {
263
+ const trailheadId = getPropertyName(getNodeKey(property));
262
264
  const value = unwrapExpression(propertyValue(property));
263
- if (!facetId || value === undefined || !isFacetDefinition(value)) {
265
+ if (!trailheadId || value === undefined || !isTrailheadDefinition(value)) {
264
266
  continue;
265
267
  }
266
- const result = diagnoseFacetDefinition(
268
+ const result = diagnoseTrailheadDefinition(
267
269
  sourceCode,
268
270
  filePath,
269
- facetId,
271
+ trailheadId,
270
272
  value
271
273
  );
272
274
  diagnostics.push(...result.diagnostics);
@@ -282,7 +284,7 @@ const diagnoseFacetMap = (
282
284
  const second = selectors[j];
283
285
  if (
284
286
  !second ||
285
- first.facetId === second.facetId ||
287
+ first.trailheadId === second.trailheadId ||
286
288
  !selectorsMayOverlap(first, second)
287
289
  ) {
288
290
  continue;
@@ -292,7 +294,7 @@ const diagnoseFacetMap = (
292
294
  sourceCode,
293
295
  filePath,
294
296
  second.node,
295
- `Surface facet selector "${second.value}" in "${second.facetId}" overlaps selector "${first.value}" in "${first.facetId}". Narrow one facet so each public trail has one MCP owner.`
297
+ `Surface trailhead selector "${second.value}" in "${second.trailheadId}" overlaps selector "${first.value}" in "${first.trailheadId}". Narrow one trailhead so each public trail has one MCP owner.`
296
298
  )
297
299
  );
298
300
  }
@@ -301,7 +303,7 @@ const diagnoseFacetMap = (
301
303
  return diagnostics;
302
304
  };
303
305
 
304
- export const surfaceFacetCoherence: WardenRule = {
306
+ export const surfaceTrailheadCoherence: WardenRule = {
305
307
  check(sourceCode, filePath) {
306
308
  const ast = parse(filePath, sourceCode);
307
309
  if (!ast) {
@@ -316,18 +318,20 @@ export const surfaceFacetCoherence: WardenRule = {
316
318
  unwrapped === undefined ||
317
319
  unwrapped.type !== 'ObjectExpression' ||
318
320
  seen.has(unwrapped.start) ||
319
- !isFacetMapCandidate(unwrapped)
321
+ !isTrailheadMapCandidate(unwrapped)
320
322
  ) {
321
323
  return;
322
324
  }
323
325
  seen.add(unwrapped.start);
324
- diagnostics.push(...diagnoseFacetMap(sourceCode, filePath, unwrapped));
326
+ diagnostics.push(
327
+ ...diagnoseTrailheadMap(sourceCode, filePath, unwrapped)
328
+ );
325
329
  };
326
330
 
327
331
  walk(ast, (node) => {
328
332
  if (node.type === 'Property') {
329
333
  const propertyName = getPropertyName(getNodeKey(node));
330
- if (propertyName === 'facets') {
334
+ if (propertyName === 'trailheads') {
331
335
  diagnoseCandidate(propertyValue(node));
332
336
  }
333
337
  return;
@@ -337,7 +341,7 @@ export const surfaceFacetCoherence: WardenRule = {
337
341
  const bindingName = getNodeName(getNodeId(node));
338
342
  if (
339
343
  typeof bindingName === 'string' &&
340
- isFacetMapBindingName(bindingName)
344
+ isTrailheadMapBindingName(bindingName)
341
345
  ) {
342
346
  diagnoseCandidate(getNodeInit(node) ?? undefined);
343
347
  }
@@ -347,7 +351,7 @@ export const surfaceFacetCoherence: WardenRule = {
347
351
  if (
348
352
  (node.type === 'TSAsExpression' ||
349
353
  node.type === 'TSSatisfiesExpression') &&
350
- hasFacetMapTypeAnnotation(sourceCode, node)
354
+ hasTrailheadMapTypeAnnotation(sourceCode, node)
351
355
  ) {
352
356
  diagnoseCandidate(node);
353
357
  }