@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,131 @@
1
+ import {
2
+ identifierName,
3
+ offsetToLine,
4
+ parseWithDiagnostics,
5
+ walk,
6
+ } from './ast.js';
7
+ import type { AstNode } from './ast.js';
8
+ import { listGovernedVocabularyTransitions } from './retired-vocabulary.js';
9
+ import type {
10
+ GovernedVocabularySymbolRename,
11
+ GovernedVocabularyTransition,
12
+ } from './retired-vocabulary.js';
13
+ import type { WardenDiagnostic, WardenFix, WardenRule } from './types.js';
14
+
15
+ const RULE_NAME = 'governed-symbol-residue';
16
+
17
+ const ACTIVE_STATUSES = new Set<GovernedVocabularyTransition['status']>([
18
+ 'active',
19
+ 'complete',
20
+ ]);
21
+
22
+ const FIX_OWNED_TRANSITION_IDS = new Set(['cross-compose']);
23
+
24
+ const ALLOWED_SOURCE_PATH_SUFFIXES = [
25
+ '/packages/warden/src/rules/governed-symbol-residue.ts',
26
+ '/packages/warden/src/rules/retired-vocabulary.ts',
27
+ ] as const;
28
+
29
+ const normalizePath = (filePath: string): string =>
30
+ filePath.replaceAll('\\', '/');
31
+
32
+ const isAllowedSourcePath = (filePath: string): boolean =>
33
+ ALLOWED_SOURCE_PATH_SUFFIXES.some((suffix) =>
34
+ normalizePath(filePath).endsWith(suffix)
35
+ );
36
+
37
+ const activeSymbolRenames = (): readonly GovernedVocabularySymbolRename[] =>
38
+ listGovernedVocabularyTransitions()
39
+ .filter(
40
+ (transition) =>
41
+ ACTIVE_STATUSES.has(transition.status) &&
42
+ !FIX_OWNED_TRANSITION_IDS.has(transition.id)
43
+ )
44
+ .flatMap((transition) => transition.symbolRenames);
45
+
46
+ const renameBySourceSymbol = (): ReadonlyMap<
47
+ string,
48
+ GovernedVocabularySymbolRename
49
+ > =>
50
+ new Map(
51
+ activeSymbolRenames().map((rename) => [rename.from, rename] as const)
52
+ );
53
+
54
+ const safeFixFor = (
55
+ sourceCode: string,
56
+ node: AstNode,
57
+ rename: GovernedVocabularySymbolRename
58
+ ): WardenFix | undefined => {
59
+ const end = node.start + rename.from.length;
60
+ if (sourceCode.slice(node.start, end) !== rename.from) {
61
+ return undefined;
62
+ }
63
+ return {
64
+ class: 'term-rewrite',
65
+ edits: [
66
+ {
67
+ end,
68
+ replacement: rename.to,
69
+ start: node.start,
70
+ },
71
+ ],
72
+ reason: `Retired governed symbol '${rename.from}' has a mechanical replacement '${rename.to}'.`,
73
+ safety: 'safe',
74
+ };
75
+ };
76
+
77
+ const diagnosticFor = (
78
+ sourceCode: string,
79
+ filePath: string,
80
+ node: AstNode,
81
+ rename: GovernedVocabularySymbolRename
82
+ ): WardenDiagnostic => {
83
+ const fix = safeFixFor(sourceCode, node, rename);
84
+ return {
85
+ filePath,
86
+ ...(fix === undefined ? {} : { fix }),
87
+ line: offsetToLine(sourceCode, node.start),
88
+ message: `Retired governed symbol '${rename.from}' should migrate to '${rename.to}'.`,
89
+ rule: RULE_NAME,
90
+ severity: 'error',
91
+ };
92
+ };
93
+
94
+ export const governedSymbolResidue: WardenRule = {
95
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
96
+ if (isAllowedSourcePath(filePath)) {
97
+ return [];
98
+ }
99
+
100
+ const renames = renameBySourceSymbol();
101
+ if (renames.size === 0) {
102
+ return [];
103
+ }
104
+
105
+ const parsed = parseWithDiagnostics(filePath, sourceCode);
106
+ if (!parsed.ast || parsed.diagnostics.length > 0) {
107
+ return [];
108
+ }
109
+
110
+ const diagnostics: WardenDiagnostic[] = [];
111
+ walk(parsed.ast, (node) => {
112
+ const name = identifierName(node);
113
+ if (name === null) {
114
+ return;
115
+ }
116
+
117
+ const rename = renames.get(name);
118
+ if (rename === undefined) {
119
+ return;
120
+ }
121
+
122
+ diagnostics.push(diagnosticFor(sourceCode, filePath, node, rename));
123
+ });
124
+
125
+ return diagnostics;
126
+ },
127
+ description:
128
+ 'Detect active governed vocabulary symbols that remain in source code.',
129
+ name: RULE_NAME,
130
+ severity: 'error',
131
+ };
@@ -8,10 +8,12 @@ import { deadInternalTrail } from './dead-internal-trail.js';
8
8
  import { deadPublicTrail } from './dead-public-trail.js';
9
9
  import { draftFileMarking } from './draft-file-marking.js';
10
10
  import { draftVisibleDebt } from './draft-visible-debt.js';
11
+ import { duplicateExportedSymbol } from './duplicate-exported-symbol.js';
11
12
  import { duplicatePublicContract } from './duplicate-public-contract.js';
12
13
  import { errorMappingCompleteness } from './error-mapping-completeness.js';
13
14
  import { exampleValid } from './example-valid.js';
14
15
  import { firesDeclarations } from './fires-declarations.js';
16
+ import { governedSymbolResidue } from './governed-symbol-residue.js';
15
17
  import { implementationReturnsResult } from './implementation-returns-result.js';
16
18
  import { incompleteAccessorForStandardOp } from './incomplete-accessor-for-standard-op.js';
17
19
  import { incompleteCrud } from './incomplete-crud.js';
@@ -22,6 +24,7 @@ import { missingVisibility } from './missing-visibility.js';
22
24
  import { missingReconcile } from './missing-reconcile.js';
23
25
  import { noDevPermitInSource } from './no-dev-permit-in-source.js';
24
26
  import { noDestructuredCompose } from './no-destructured-compose.js';
27
+ import { noLegacyCliAliasExport } from './no-legacy-cli-alias-export.js';
25
28
  import { noLegacyLayerImports } from './no-legacy-layer-imports.js';
26
29
  import { noDirectImplementationCall } from './no-direct-implementation-call.js';
27
30
  import { noNativeErrorResult } from './no-native-error-result.js';
@@ -50,8 +53,10 @@ import { resourceMockCoverage } from './resource-mock-coverage.js';
50
53
  import { scheduledDestroyIntent } from './scheduled-destroy-intent.js';
51
54
  import { signalGraphCoaching } from './signal-graph-coaching.js';
52
55
  import { staticResourceAccessorPreference } from './static-resource-accessor-preference.js';
53
- import { surfaceFacetCoherence } from './surface-facet-coherence.js';
56
+ import { surfaceOverlayCoherence } from './surface-overlay-coherence.js';
57
+ import { surfaceTrailheadCoherence } from './surface-trailhead-coherence.js';
54
58
  import { trailForkCoaching } from './trail-fork-coaching.js';
59
+ import { trailheadOverrideDivergence } from './trailhead-override-divergence.js';
55
60
  import {
56
61
  forkWithoutPreservedBlaze,
57
62
  markerSchemaUnsupported,
@@ -81,6 +86,7 @@ export type {
81
86
  WardenFixSafety,
82
87
  WardenGuidance,
83
88
  WardenGuidanceLink,
89
+ WardenExportedSymbolDefinition,
84
90
  WardenRuleLifecycle,
85
91
  WardenRuleLifecycleState,
86
92
  WardenRuleConcern,
@@ -95,6 +101,30 @@ export type {
95
101
  WardenSeverity,
96
102
  } from './types.js';
97
103
 
104
+ export {
105
+ formatGovernedVocabularyTransitionGuide,
106
+ getGovernedVocabularyTransition,
107
+ governedVocabularyLiteralRenameSchema,
108
+ governedVocabularyPreserveRuleSchema,
109
+ governedVocabularyRegistrySchema,
110
+ governedVocabularyScopeSchema,
111
+ governedVocabularySymbolRenameSchema,
112
+ governedVocabularyTargetSchema,
113
+ governedVocabularyTransitionSchema,
114
+ governedVocabularyTransitionStatuses,
115
+ governedVocabularyTransitions,
116
+ listGovernedVocabularyTransitions,
117
+ requireGovernedVocabularyTransition,
118
+ } from './retired-vocabulary.js';
119
+ export type {
120
+ GovernedVocabularyLiteralRename,
121
+ GovernedVocabularyPreserveRule,
122
+ GovernedVocabularyScope,
123
+ GovernedVocabularySymbolRename,
124
+ GovernedVocabularyTarget,
125
+ GovernedVocabularyTransition,
126
+ GovernedVocabularyTransitionInput,
127
+ } from './retired-vocabulary.js';
98
128
  export {
99
129
  builtinWardenRuleMetadata,
100
130
  getWardenRuleMetadata,
@@ -119,10 +149,12 @@ export { deadInternalTrail } from './dead-internal-trail.js';
119
149
  export { deadPublicTrail } from './dead-public-trail.js';
120
150
  export { draftFileMarking } from './draft-file-marking.js';
121
151
  export { draftVisibleDebt } from './draft-visible-debt.js';
152
+ export { duplicateExportedSymbol } from './duplicate-exported-symbol.js';
122
153
  export { duplicatePublicContract } from './duplicate-public-contract.js';
123
154
  export { errorMappingCompleteness } from './error-mapping-completeness.js';
124
155
  export { exampleValid } from './example-valid.js';
125
156
  export { firesDeclarations } from './fires-declarations.js';
157
+ export { governedSymbolResidue } from './governed-symbol-residue.js';
126
158
  export { incompleteAccessorForStandardOp } from './incomplete-accessor-for-standard-op.js';
127
159
  export { incompleteCrud } from './incomplete-crud.js';
128
160
  export { intentPropagation } from './intent-propagation.js';
@@ -133,6 +165,7 @@ export { missingReconcile } from './missing-reconcile.js';
133
165
  export { onReferencesExist } from './on-references-exist.js';
134
166
  export { noDevPermitInSource } from './no-dev-permit-in-source.js';
135
167
  export { noDestructuredCompose } from './no-destructured-compose.js';
168
+ export { noLegacyCliAliasExport } from './no-legacy-cli-alias-export.js';
136
169
  export { noLegacyLayerImports } from './no-legacy-layer-imports.js';
137
170
  export { noDirectImplementationCall } from './no-direct-implementation-call.js';
138
171
  export { noNativeErrorResult } from './no-native-error-result.js';
@@ -159,8 +192,10 @@ export { resourceMockCoverage } from './resource-mock-coverage.js';
159
192
  export { scheduledDestroyIntent } from './scheduled-destroy-intent.js';
160
193
  export { signalGraphCoaching } from './signal-graph-coaching.js';
161
194
  export { staticResourceAccessorPreference } from './static-resource-accessor-preference.js';
162
- export { surfaceFacetCoherence } from './surface-facet-coherence.js';
195
+ export { surfaceOverlayCoherence } from './surface-overlay-coherence.js';
196
+ export { surfaceTrailheadCoherence } from './surface-trailhead-coherence.js';
163
197
  export { trailForkCoaching } from './trail-fork-coaching.js';
198
+ export { trailheadOverrideDivergence } from './trailhead-override-divergence.js';
164
199
  export {
165
200
  forkWithoutPreservedBlaze,
166
201
  markerSchemaUnsupported,
@@ -192,9 +227,11 @@ export const wardenRules: ReadonlyMap<string, WardenRule> = new Map<
192
227
  [deadPublicTrail.name, deadPublicTrail],
193
228
  [draftFileMarking.name, draftFileMarking],
194
229
  [draftVisibleDebt.name, draftVisibleDebt],
230
+ [duplicateExportedSymbol.name, duplicateExportedSymbol],
195
231
  [errorMappingCompleteness.name, errorMappingCompleteness],
196
232
  [exampleValid.name, exampleValid],
197
233
  [firesDeclarations.name, firesDeclarations],
234
+ [governedSymbolResidue.name, governedSymbolResidue],
198
235
  [incompleteCrud.name, incompleteCrud],
199
236
  [intentPropagation.name, intentPropagation],
200
237
  [layerFieldNameDrift.name, layerFieldNameDrift],
@@ -214,12 +251,14 @@ export const wardenRules: ReadonlyMap<string, WardenRule> = new Map<
214
251
  [resourceMockCoverage.name, resourceMockCoverage],
215
252
  [preferSchemaInference.name, preferSchemaInference],
216
253
  [staticResourceAccessorPreference.name, staticResourceAccessorPreference],
217
- [surfaceFacetCoherence.name, surfaceFacetCoherence],
254
+ [surfaceTrailheadCoherence.name, surfaceTrailheadCoherence],
218
255
  [trailForkCoaching.name, trailForkCoaching],
256
+ [trailheadOverrideDivergence.name, trailheadOverrideDivergence],
219
257
  [validDescribeRefs.name, validDescribeRefs],
220
258
  [noDevPermitInSource.name, noDevPermitInSource],
221
259
  [noDestructuredCompose.name, noDestructuredCompose],
222
260
  [noDirectImplementationCall.name, noDirectImplementationCall],
261
+ [noLegacyCliAliasExport.name, noLegacyCliAliasExport],
223
262
  [noLegacyLayerImports.name, noLegacyLayerImports],
224
263
  [noNativeErrorResult.name, noNativeErrorResult],
225
264
  [noRedundantResultErrorWrap.name, noRedundantResultErrorWrap],
@@ -261,6 +300,7 @@ export const wardenTopoRules: ReadonlyMap<string, TopoAwareWardenRule> =
261
300
  [publicUnionOutputDiscriminants.name, publicUnionOutputDiscriminants],
262
301
  [scheduledDestroyIntent.name, scheduledDestroyIntent],
263
302
  [signalGraphCoaching.name, signalGraphCoaching],
303
+ [surfaceOverlayCoherence.name, surfaceOverlayCoherence],
264
304
  [unmaterializedActivationSource.name, unmaterializedActivationSource],
265
305
  [validDetourContract.name, validDetourContract],
266
306
  [deprecationWithoutGuidance.name, deprecationWithoutGuidance],
@@ -44,6 +44,7 @@ export const wardenRuleLifecycleStates = [
44
44
  ] as const satisfies readonly WardenRuleLifecycleState[];
45
45
 
46
46
  export const wardenFixClasses = [
47
+ 'export-restructure',
47
48
  'term-rewrite',
48
49
  ] as const satisfies readonly WardenFixClass[];
49
50
 
@@ -76,10 +77,12 @@ const concernByRuleName: Partial<Record<string, WardenRuleConcern>> = {
76
77
  'deprecation-without-guidance': 'lifecycle',
77
78
  'draft-file-marking': 'lifecycle',
78
79
  'draft-visible-debt': 'lifecycle',
80
+ 'duplicate-exported-symbol': 'general',
79
81
  'duplicate-public-contract': 'meta',
80
82
  'error-mapping-completeness': 'results',
81
83
  'fires-declarations': 'signals',
82
84
  'fork-without-preserved-blaze': 'lifecycle',
85
+ 'governed-symbol-residue': 'lifecycle',
83
86
  'implementation-returns-result': 'results',
84
87
  'intent-propagation': 'composition',
85
88
  'library-projection-coherence': 'meta',
@@ -89,6 +92,7 @@ const concernByRuleName: Partial<Record<string, WardenRuleConcern>> = {
89
92
  'no-destructured-compose': 'composition',
90
93
  'no-dev-permit-in-source': 'permits',
91
94
  'no-direct-implementation-call': 'composition',
95
+ 'no-legacy-cli-alias-export': 'general',
92
96
  'no-native-error-result': 'results',
93
97
  'no-redundant-result-error-wrap': 'results',
94
98
  'no-retired-cross-vocabulary': 'composition',
@@ -109,8 +113,10 @@ const concernByRuleName: Partial<Record<string, WardenRuleConcern>> = {
109
113
  'scheduled-destroy-intent': 'lifecycle',
110
114
  'signal-graph-coaching': 'signals',
111
115
  'static-resource-accessor-preference': 'resources',
112
- 'surface-facet-coherence': 'meta',
116
+ 'surface-overlay-coherence': 'meta',
117
+ 'surface-trailhead-coherence': 'meta',
113
118
  'trail-fork-coaching': 'meta',
119
+ 'trailhead-override-divergence': 'meta',
114
120
  'unmaterialized-activation-source': 'lifecycle',
115
121
  'valid-detour-contract': 'results',
116
122
  'version-gap': 'lifecycle',
@@ -220,6 +226,26 @@ const builtinWardenRuleMetadataInput = {
220
226
  invariant: 'Draft-authored IDs remain visible debt.',
221
227
  tier: 'source-static',
222
228
  },
229
+ 'duplicate-exported-symbol': {
230
+ ...durableRepoLocal,
231
+ guidance: {
232
+ docs: [
233
+ {
234
+ label: 'Package ownership',
235
+ path: 'docs/contributing/package-ownership.md',
236
+ },
237
+ ],
238
+ relatedRules: [
239
+ 'resolved-import-boundary',
240
+ 'public-internal-deep-imports',
241
+ ],
242
+ summary:
243
+ 'Keep exported symbol ownership from drifting across first-party packages.',
244
+ },
245
+ invariant:
246
+ 'First-party packages should not define the same exported symbol name in parallel.',
247
+ tier: 'project-static',
248
+ },
223
249
  'duplicate-public-contract': {
224
250
  ...durableExternal,
225
251
  guidance: {
@@ -265,6 +291,13 @@ const builtinWardenRuleMetadataInput = {
265
291
  invariant: 'Fork version entries preserve their historical blaze.',
266
292
  tier: 'source-static',
267
293
  },
294
+ 'governed-symbol-residue': {
295
+ ...durableExternal,
296
+ fix: { class: 'term-rewrite', safety: 'safe' },
297
+ invariant:
298
+ 'Active governed vocabulary symbol renames do not leave retired identifiers in source.',
299
+ tier: 'source-static',
300
+ },
268
301
  'implementation-returns-result': {
269
302
  ...durableExternal,
270
303
  invariant: 'Blazes return Result values.',
@@ -300,7 +333,10 @@ const builtinWardenRuleMetadataInput = {
300
333
  path: 'docs/adr/drafts/20260612-library-surface-and-compiler.md',
301
334
  },
302
335
  ],
303
- relatedRules: ['cli-command-route-coherence', 'surface-facet-coherence'],
336
+ relatedRules: [
337
+ 'cli-command-route-coherence',
338
+ 'surface-trailhead-coherence',
339
+ ],
304
340
  steps: [
305
341
  'Rename one source trail or add an explicit library export override before generating a package.',
306
342
  'Keep serialized library projection exports attached to existing trail IDs.',
@@ -346,6 +382,18 @@ const builtinWardenRuleMetadataInput = {
346
382
  invariant: 'Application code composes trails through ctx.compose().',
347
383
  tier: 'source-static',
348
384
  },
385
+ 'no-legacy-cli-alias-export': {
386
+ fix: { class: 'export-restructure', safety: 'review' },
387
+ invariant:
388
+ 'Legacy app-module CLI alias exports (cliAliases, trailsCliAliases) removed in the TRL-1207 surfaces-overlay cutover do not reappear in committed source.',
389
+ lifecycle: {
390
+ retireWhen:
391
+ 'TRL-1207 surfaces-overlay migration window closes and supported apps have moved CLI bindings into surfaceOverlay entries in their trailsOverlays exports.',
392
+ state: 'temporary',
393
+ },
394
+ scope: 'external',
395
+ tier: 'source-static',
396
+ },
349
397
  'no-legacy-layer-imports': {
350
398
  fix: { class: 'term-rewrite', safety: 'review' },
351
399
  invariant:
@@ -605,19 +653,38 @@ const builtinWardenRuleMetadataInput = {
605
653
  scope: 'advisory',
606
654
  tier: 'source-static',
607
655
  },
608
- 'surface-facet-coherence': {
656
+ 'surface-overlay-coherence': {
657
+ ...durableExternal,
658
+ guidance: {
659
+ relatedRules: [
660
+ 'cli-command-route-coherence',
661
+ 'surface-trailhead-coherence',
662
+ ],
663
+ steps: [
664
+ 'Point every binding selector at an existing trail id or dotted trail-id glob.',
665
+ 'Narrow overlapping grouped bindings so each trail has one grouped owner per surface.',
666
+ 'Rename bindings that shadow canonical CLI command paths or derived MCP tool names.',
667
+ ],
668
+ summary:
669
+ 'Keep surface overlay bindings pointed at real trails without shadowing canonical surface entries.',
670
+ },
671
+ invariant:
672
+ 'Surface overlay bindings resolve to real trails without group overlap or canonical-entry shadowing.',
673
+ tier: 'topo-aware',
674
+ },
675
+ 'surface-trailhead-coherence': {
609
676
  ...durableExternal,
610
677
  guidance: {
611
678
  docs: [
612
679
  {
613
680
  label: 'Trailheads ADR',
614
- path: 'docs/adr/drafts/20260603-surface-facets-shape-dense-topos.md',
681
+ path: 'docs/adr/drafts/20260603-surface-trailheads-shape-dense-topos.md',
615
682
  },
616
683
  ],
617
684
  steps: [
618
- 'Keep facet selectors as explicit string literals or literal arrays when possible.',
619
- 'Ensure each public trail belongs to one facet owner.',
620
- 'Record explicit visibility-widening acceptance and stable-description metadata when a facet intentionally widens visibility.',
685
+ 'Keep trailhead selectors as explicit string literals or literal arrays when possible.',
686
+ 'Ensure each public trail belongs to one trailhead owner.',
687
+ 'Record explicit visibility-widening acceptance and stable-description metadata when a trailhead intentionally widens visibility.',
621
688
  ],
622
689
  summary:
623
690
  'Keep trailhead maps reviewable before they reach MCP projection.',
@@ -639,12 +706,15 @@ const builtinWardenRuleMetadataInput = {
639
706
  path: 'docs/surfaces/surface-accommodations.md',
640
707
  },
641
708
  ],
642
- relatedRules: ['surface-facet-coherence', 'cli-command-route-coherence'],
709
+ relatedRules: [
710
+ 'surface-trailhead-coherence',
711
+ 'cli-command-route-coherence',
712
+ ],
643
713
  steps: [
644
714
  'Check the semantic fork boundary: intent, permits, outputs, errors, lifecycle, and side effects.',
645
715
  'Check the structural fork boundary: selected trail identity stays visible instead of hiding behind action vocabulary.',
646
716
  'Split real capability forks into distinct trails or a composing trail.',
647
- 'Use a facet only when one surface entry needs to group multiple trails while preserving selected member identity.',
717
+ 'Use a trailhead only when one surface entry needs to group multiple trails while preserving selected member identity.',
648
718
  ],
649
719
  summary:
650
720
  'Keep surface accommodations from hiding several capabilities behind one branching trail input.',
@@ -654,6 +724,30 @@ const builtinWardenRuleMetadataInput = {
654
724
  scope: 'advisory',
655
725
  tier: 'source-static',
656
726
  },
727
+ 'trailhead-override-divergence': {
728
+ ...durableExternal,
729
+ guidance: {
730
+ docs: [
731
+ {
732
+ label: 'Surface Accommodations',
733
+ path: 'docs/surfaces/surface-accommodations.md',
734
+ },
735
+ ],
736
+ relatedRules: [
737
+ 'surface-overlay-coherence',
738
+ 'surface-trailhead-coherence',
739
+ ],
740
+ steps: [
741
+ 'Author the grouped entry as an mcp list binding in surfaceOverlay({ mcp }) so the lock carries the default.',
742
+ 'Keep the call-site trailhead map aligned with the authored binding names and member selectors, or rename one side to make the intentional divergence explicit.',
743
+ ],
744
+ summary:
745
+ 'Keep call-site MCP trailhead overrides aligned with the authored overlay default.',
746
+ },
747
+ invariant:
748
+ 'Call-site MCP trailhead maps stay aligned with authored surfaces overlay mcp bindings.',
749
+ tier: 'project-static',
750
+ },
657
751
  'unmaterialized-activation-source': {
658
752
  ...durableExternal,
659
753
  invariant:
@@ -715,7 +809,7 @@ const builtinWardenRuleMetadataInput = {
715
809
 
716
810
  export type BuiltinWardenRuleName = keyof typeof builtinWardenRuleMetadataInput;
717
811
 
718
- const withFacetDefaults = (
812
+ const withRuleDefaults = (
719
813
  name: string,
720
814
  metadata: BuiltinWardenRuleMetadataInput
721
815
  ): WardenRuleMetadata => ({
@@ -727,7 +821,7 @@ const withFacetDefaults = (
727
821
  export const builtinWardenRuleMetadata = Object.fromEntries(
728
822
  Object.entries(builtinWardenRuleMetadataInput).map(([name, metadata]) => [
729
823
  name,
730
- withFacetDefaults(name, metadata),
824
+ withRuleDefaults(name, metadata),
731
825
  ])
732
826
  ) as Readonly<Record<BuiltinWardenRuleName, WardenRuleMetadata>>;
733
827