@ontrails/warden 1.0.0-beta.30 → 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 (45) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +23 -1
  3. package/bin/warden.ts +1 -0
  4. package/package.json +9 -9
  5. package/src/cli.ts +160 -14
  6. package/src/command.ts +48 -5
  7. package/src/config.ts +9 -0
  8. package/src/drift.ts +116 -7
  9. package/src/fix.ts +8 -2
  10. package/src/formatters.ts +3 -8
  11. package/src/index.ts +32 -2
  12. package/src/project-context.ts +286 -3
  13. package/src/rules/ast.ts +4 -0
  14. package/src/rules/duplicate-exported-symbol.ts +211 -0
  15. package/src/rules/duplicate-public-contract.ts +2 -1
  16. package/src/rules/governed-symbol-residue.ts +131 -0
  17. package/src/rules/implementation-returns-result.ts +1 -3
  18. package/src/rules/index.ts +44 -3
  19. package/src/rules/metadata.ts +108 -14
  20. package/src/rules/no-legacy-cli-alias-export.ts +247 -0
  21. package/src/rules/no-retired-cross-vocabulary.ts +19 -9
  22. package/src/rules/permit-governance.ts +1 -1
  23. package/src/rules/public-export-example-coverage.ts +5 -0
  24. package/src/rules/public-internal-deep-imports.ts +3 -63
  25. package/src/rules/registry-names.ts +12 -2
  26. package/src/rules/retired-vocabulary.ts +396 -0
  27. package/src/rules/signal-graph-coaching.ts +32 -3
  28. package/src/rules/surface-overlay-coherence.ts +262 -0
  29. package/src/rules/{surface-facet-coherence.ts → surface-trailhead-coherence.ts} +46 -42
  30. package/src/rules/trail-fork-coaching.ts +1 -1
  31. package/src/rules/trailhead-override-divergence.ts +356 -0
  32. package/src/rules/types.ts +84 -4
  33. package/src/rules/webhook-route-collision.ts +64 -1
  34. package/src/trails/duplicate-exported-symbol.trail.ts +48 -0
  35. package/src/trails/duplicate-public-contract.trail.ts +1 -1
  36. package/src/trails/governed-symbol-residue.trail.ts +24 -0
  37. package/src/trails/index.ts +6 -1
  38. package/src/trails/no-legacy-cli-alias-export.trail.ts +41 -0
  39. package/src/trails/schema.ts +39 -0
  40. package/src/trails/surface-overlay-coherence.trail.ts +24 -0
  41. package/src/trails/{surface-facet-coherence.trail.ts → surface-trailhead-coherence.trail.ts} +5 -5
  42. package/src/trails/trail-fork-coaching.trail.ts +1 -1
  43. package/src/trails/trailhead-override-divergence.trail.ts +47 -0
  44. package/src/trails/wrap-rule.ts +10 -0
  45. package/src/workspaces.ts +30 -69
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { existsSync, readFileSync } from 'node:fs';
10
10
  import { dirname, isAbsolute, resolve } from 'node:path';
11
+ import { escapeRegExp } from '@ontrails/core';
11
12
  import type { AstNode } from './ast.js';
12
13
  import {
13
14
  collectScopeFrameBindings,
@@ -288,9 +289,6 @@ const extractIdentifierName = (node: AstNode | undefined): string | null =>
288
289
 
289
290
  const DEFAULT_RESULT_TYPE_NAMES = new Set(['Result']);
290
291
 
291
- const escapeRegExp = (value: string): string =>
292
- value.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&');
293
-
294
292
  const hasGenericTypeReference = (
295
293
  annotationText: string,
296
294
  typeName: string
@@ -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,
@@ -77,9 +82,11 @@ export type {
77
82
  WardenFixCapability,
78
83
  WardenFixClass,
79
84
  WardenFixEdit,
85
+ WardenFixScanTargets,
80
86
  WardenFixSafety,
81
87
  WardenGuidance,
82
88
  WardenGuidanceLink,
89
+ WardenExportedSymbolDefinition,
83
90
  WardenRuleLifecycle,
84
91
  WardenRuleLifecycleState,
85
92
  WardenRuleConcern,
@@ -94,6 +101,30 @@ export type {
94
101
  WardenSeverity,
95
102
  } from './types.js';
96
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';
97
128
  export {
98
129
  builtinWardenRuleMetadata,
99
130
  getWardenRuleMetadata,
@@ -118,10 +149,12 @@ export { deadInternalTrail } from './dead-internal-trail.js';
118
149
  export { deadPublicTrail } from './dead-public-trail.js';
119
150
  export { draftFileMarking } from './draft-file-marking.js';
120
151
  export { draftVisibleDebt } from './draft-visible-debt.js';
152
+ export { duplicateExportedSymbol } from './duplicate-exported-symbol.js';
121
153
  export { duplicatePublicContract } from './duplicate-public-contract.js';
122
154
  export { errorMappingCompleteness } from './error-mapping-completeness.js';
123
155
  export { exampleValid } from './example-valid.js';
124
156
  export { firesDeclarations } from './fires-declarations.js';
157
+ export { governedSymbolResidue } from './governed-symbol-residue.js';
125
158
  export { incompleteAccessorForStandardOp } from './incomplete-accessor-for-standard-op.js';
126
159
  export { incompleteCrud } from './incomplete-crud.js';
127
160
  export { intentPropagation } from './intent-propagation.js';
@@ -132,6 +165,7 @@ export { missingReconcile } from './missing-reconcile.js';
132
165
  export { onReferencesExist } from './on-references-exist.js';
133
166
  export { noDevPermitInSource } from './no-dev-permit-in-source.js';
134
167
  export { noDestructuredCompose } from './no-destructured-compose.js';
168
+ export { noLegacyCliAliasExport } from './no-legacy-cli-alias-export.js';
135
169
  export { noLegacyLayerImports } from './no-legacy-layer-imports.js';
136
170
  export { noDirectImplementationCall } from './no-direct-implementation-call.js';
137
171
  export { noNativeErrorResult } from './no-native-error-result.js';
@@ -158,8 +192,10 @@ export { resourceMockCoverage } from './resource-mock-coverage.js';
158
192
  export { scheduledDestroyIntent } from './scheduled-destroy-intent.js';
159
193
  export { signalGraphCoaching } from './signal-graph-coaching.js';
160
194
  export { staticResourceAccessorPreference } from './static-resource-accessor-preference.js';
161
- export { surfaceFacetCoherence } from './surface-facet-coherence.js';
195
+ export { surfaceOverlayCoherence } from './surface-overlay-coherence.js';
196
+ export { surfaceTrailheadCoherence } from './surface-trailhead-coherence.js';
162
197
  export { trailForkCoaching } from './trail-fork-coaching.js';
198
+ export { trailheadOverrideDivergence } from './trailhead-override-divergence.js';
163
199
  export {
164
200
  forkWithoutPreservedBlaze,
165
201
  markerSchemaUnsupported,
@@ -191,9 +227,11 @@ export const wardenRules: ReadonlyMap<string, WardenRule> = new Map<
191
227
  [deadPublicTrail.name, deadPublicTrail],
192
228
  [draftFileMarking.name, draftFileMarking],
193
229
  [draftVisibleDebt.name, draftVisibleDebt],
230
+ [duplicateExportedSymbol.name, duplicateExportedSymbol],
194
231
  [errorMappingCompleteness.name, errorMappingCompleteness],
195
232
  [exampleValid.name, exampleValid],
196
233
  [firesDeclarations.name, firesDeclarations],
234
+ [governedSymbolResidue.name, governedSymbolResidue],
197
235
  [incompleteCrud.name, incompleteCrud],
198
236
  [intentPropagation.name, intentPropagation],
199
237
  [layerFieldNameDrift.name, layerFieldNameDrift],
@@ -213,12 +251,14 @@ export const wardenRules: ReadonlyMap<string, WardenRule> = new Map<
213
251
  [resourceMockCoverage.name, resourceMockCoverage],
214
252
  [preferSchemaInference.name, preferSchemaInference],
215
253
  [staticResourceAccessorPreference.name, staticResourceAccessorPreference],
216
- [surfaceFacetCoherence.name, surfaceFacetCoherence],
254
+ [surfaceTrailheadCoherence.name, surfaceTrailheadCoherence],
217
255
  [trailForkCoaching.name, trailForkCoaching],
256
+ [trailheadOverrideDivergence.name, trailheadOverrideDivergence],
218
257
  [validDescribeRefs.name, validDescribeRefs],
219
258
  [noDevPermitInSource.name, noDevPermitInSource],
220
259
  [noDestructuredCompose.name, noDestructuredCompose],
221
260
  [noDirectImplementationCall.name, noDirectImplementationCall],
261
+ [noLegacyCliAliasExport.name, noLegacyCliAliasExport],
222
262
  [noLegacyLayerImports.name, noLegacyLayerImports],
223
263
  [noNativeErrorResult.name, noNativeErrorResult],
224
264
  [noRedundantResultErrorWrap.name, noRedundantResultErrorWrap],
@@ -260,6 +300,7 @@ export const wardenTopoRules: ReadonlyMap<string, TopoAwareWardenRule> =
260
300
  [publicUnionOutputDiscriminants.name, publicUnionOutputDiscriminants],
261
301
  [scheduledDestroyIntent.name, scheduledDestroyIntent],
262
302
  [signalGraphCoaching.name, signalGraphCoaching],
303
+ [surfaceOverlayCoherence.name, surfaceOverlayCoherence],
263
304
  [unmaterializedActivationSource.name, unmaterializedActivationSource],
264
305
  [validDetourContract.name, validDetourContract],
265
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,25 +653,44 @@ 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
- label: 'Surface Facets ADR',
614
- path: 'docs/adr/drafts/20260603-surface-facets-shape-dense-topos.md',
680
+ label: 'Trailheads ADR',
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
- 'Keep surface facet maps reviewable before they reach MCP projection.',
690
+ 'Keep trailhead maps reviewable before they reach MCP projection.',
624
691
  },
625
692
  invariant:
626
- 'Surface facet maps avoid selector overlap, hidden visibility widening, and drift-prone dynamic selectors.',
693
+ 'Trailhead maps avoid selector overlap, hidden visibility widening, and drift-prone dynamic selectors.',
627
694
  tier: 'source-static',
628
695
  },
629
696
  'trail-fork-coaching': {
@@ -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
 
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Flags leftover legacy app-module CLI alias exports removed in TRL-1207.
3
+ *
4
+ * Before the surfaces-overlay cutover, apps published CLI alias maps through
5
+ * a module-level export named `cliAliases` or `trailsCliAliases`. TRL-1207 is
6
+ * a hard cutover: app-owned CLI bindings now live in a
7
+ * `surfaceOverlay({ cli: { ... } })` envelope inside the app module's
8
+ * `trailsOverlays` array export, and the legacy export convention was
9
+ * deleted. This rule makes a leftover legacy export an error so stale app
10
+ * modules get a fix-forward diagnostic instead of silently losing their
11
+ * bindings.
12
+ *
13
+ * Detection is AST-scoped and export-shaped:
14
+ *
15
+ * - `export const cliAliases = { ... }` (also `let`/`var`, destructured
16
+ * bindings, and function/class declarations) is flagged.
17
+ * - `export { cliAliases }` and `export { whatever as trailsCliAliases }`
18
+ * specifiers are flagged, including re-export statements such as
19
+ * `export { trailsCliAliases } from './app.js'`.
20
+ * - A non-exported local `const cliAliases = ...` is legal and never
21
+ * flagged, and the identifiers appearing only in strings or comments do
22
+ * not match because the rule inspects export bindings, not raw text.
23
+ */
24
+
25
+ import {
26
+ getNodeArgument,
27
+ getNodeBodyStatements,
28
+ getNodeDeclaration,
29
+ getNodeDeclarations,
30
+ getNodeElements,
31
+ getNodeExported,
32
+ getNodeExportKind,
33
+ getNodeId,
34
+ getNodeLeft,
35
+ getNodeProperties,
36
+ getNodeSpecifiers,
37
+ getNodeValueNode,
38
+ getStringValue,
39
+ identifierName,
40
+ isStringLiteral,
41
+ offsetToLine,
42
+ parse,
43
+ } from './ast.js';
44
+ import type { AstNode } from './ast.js';
45
+ import type { WardenDiagnostic, WardenFix, WardenRule } from './types.js';
46
+
47
+ const RULE_NAME = 'no-legacy-cli-alias-export';
48
+
49
+ /** Legacy app-module CLI alias export names removed in TRL-1207. */
50
+ const LEGACY_CLI_ALIAS_EXPORT_NAMES: ReadonlySet<string> = new Set([
51
+ 'cliAliases',
52
+ 'trailsCliAliases',
53
+ ]);
54
+
55
+ /** Declaration node types that only propagate types, never value bindings. */
56
+ const TYPE_ONLY_DECLARATION_TYPES: ReadonlySet<string> = new Set([
57
+ 'TSInterfaceDeclaration',
58
+ 'TSTypeAliasDeclaration',
59
+ ]);
60
+
61
+ interface ExportedBinding {
62
+ readonly name: string;
63
+ readonly start: number;
64
+ }
65
+
66
+ /** Exported name of a specifier: identifier or string-literal alias. */
67
+ const exportedSpecifierName = (node: AstNode | undefined): string | null =>
68
+ identifierName(node) ??
69
+ (node && isStringLiteral(node) ? getStringValue(node) : null);
70
+
71
+ /**
72
+ * Expand one binding-pattern node into legacy-named bindings, pushing child
73
+ * patterns onto the worklist. Destructured exports such as
74
+ * `export const { cliAliases } = mod` still introduce a module-level exported
75
+ * value binding, so patterns cannot be skipped.
76
+ */
77
+ const visitBindingPatternNode = (
78
+ node: AstNode,
79
+ into: ExportedBinding[],
80
+ worklist: AstNode[]
81
+ ): void => {
82
+ const name = identifierName(node);
83
+ if (name !== null) {
84
+ if (LEGACY_CLI_ALIAS_EXPORT_NAMES.has(name)) {
85
+ into.push({ name, start: node.start });
86
+ }
87
+ return;
88
+ }
89
+ if (node.type === 'AssignmentPattern') {
90
+ const left = getNodeLeft(node);
91
+ if (left) {
92
+ worklist.push(left);
93
+ }
94
+ return;
95
+ }
96
+ if (node.type === 'RestElement') {
97
+ const argument = getNodeArgument(node);
98
+ if (argument) {
99
+ worklist.push(argument);
100
+ }
101
+ return;
102
+ }
103
+ if (node.type === 'ObjectPattern') {
104
+ for (const property of getNodeProperties(node)) {
105
+ const value = getNodeValueNode(property) ?? getNodeArgument(property);
106
+ if (value) {
107
+ worklist.push(value);
108
+ }
109
+ }
110
+ return;
111
+ }
112
+ if (node.type === 'ArrayPattern') {
113
+ for (const element of getNodeElements(node)) {
114
+ if (element) {
115
+ worklist.push(element);
116
+ }
117
+ }
118
+ }
119
+ };
120
+
121
+ const collectLegacyPatternBindings = (
122
+ pattern: AstNode | undefined,
123
+ into: ExportedBinding[]
124
+ ): void => {
125
+ if (!pattern) {
126
+ return;
127
+ }
128
+ const worklist: AstNode[] = [pattern];
129
+ while (worklist.length > 0) {
130
+ const node = worklist.pop();
131
+ if (node) {
132
+ visitBindingPatternNode(node, into, worklist);
133
+ }
134
+ }
135
+ };
136
+
137
+ const collectLegacyDeclarationBindings = (
138
+ declaration: AstNode
139
+ ): readonly ExportedBinding[] => {
140
+ if (TYPE_ONLY_DECLARATION_TYPES.has(declaration.type)) {
141
+ return [];
142
+ }
143
+ const bindings: ExportedBinding[] = [];
144
+ if (declaration.type === 'VariableDeclaration') {
145
+ for (const declarator of getNodeDeclarations(declaration)) {
146
+ collectLegacyPatternBindings(getNodeId(declarator), bindings);
147
+ }
148
+ return bindings;
149
+ }
150
+ const name = identifierName(getNodeId(declaration));
151
+ if (name !== null && LEGACY_CLI_ALIAS_EXPORT_NAMES.has(name)) {
152
+ bindings.push({ name, start: declaration.start });
153
+ }
154
+ return bindings;
155
+ };
156
+
157
+ const collectLegacySpecifierBindings = (
158
+ statement: AstNode
159
+ ): readonly ExportedBinding[] => {
160
+ const bindings: ExportedBinding[] = [];
161
+ for (const specifier of getNodeSpecifiers(statement)) {
162
+ if (
163
+ specifier.type !== 'ExportSpecifier' ||
164
+ getNodeExportKind(specifier) === 'type'
165
+ ) {
166
+ continue;
167
+ }
168
+ const name = exportedSpecifierName(getNodeExported(specifier));
169
+ if (name !== null && LEGACY_CLI_ALIAS_EXPORT_NAMES.has(name)) {
170
+ bindings.push({ name, start: specifier.start });
171
+ }
172
+ }
173
+ return bindings;
174
+ };
175
+
176
+ const collectLegacyExportBindings = (
177
+ ast: AstNode
178
+ ): readonly ExportedBinding[] => {
179
+ const bindings: ExportedBinding[] = [];
180
+ for (const statement of getNodeBodyStatements(ast)) {
181
+ if (
182
+ statement.type !== 'ExportNamedDeclaration' ||
183
+ getNodeExportKind(statement) === 'type'
184
+ ) {
185
+ continue;
186
+ }
187
+ const declaration = getNodeDeclaration(statement);
188
+ if (declaration) {
189
+ bindings.push(...collectLegacyDeclarationBindings(declaration));
190
+ continue;
191
+ }
192
+ bindings.push(...collectLegacySpecifierBindings(statement));
193
+ }
194
+ return bindings;
195
+ };
196
+
197
+ const buildMessage = (name: string): string =>
198
+ `Legacy CLI alias export '${name}' was removed in the TRL-1207 surfaces-overlay cutover. Wrap the alias map into surfaceOverlay({ cli: { ... } }) from @ontrails/core inside the module's trailsOverlays array export.`;
199
+
200
+ /**
201
+ * Build the fix metadata for a legacy CLI alias export finding.
202
+ *
203
+ * The rewrite is an export restructure, not a rename, so there is no
204
+ * mechanical single-span replacement: the fix carries no edits and Warden
205
+ * never applies it. Downstream, the `export-restructure` fix class routes the
206
+ * finding to the Regrade `export-restructure:cli-aliases` class (TRL-1210),
207
+ * which inverts the alias map into `surfaceOverlay({ cli: { ... } })`
208
+ * bindings inside the module's `trailsOverlays` array export.
209
+ */
210
+ const buildFix = (name: string): WardenFix => ({
211
+ class: 'export-restructure',
212
+ reason: `Legacy CLI alias export '${name}' must be rewritten into a surfaceOverlay({ cli: { ... } }) entry inside the module's trailsOverlays array export; run the Regrade class export-restructure:cli-aliases (TRL-1210) to automate this restructure.`,
213
+ safety: 'review',
214
+ });
215
+
216
+ /**
217
+ * Flags exported bindings named `cliAliases` or `trailsCliAliases`, the
218
+ * legacy app-module CLI alias export convention deleted in TRL-1207.
219
+ */
220
+ export const noLegacyCliAliasExport: WardenRule = {
221
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
222
+ if (
223
+ !(
224
+ sourceCode.includes('cliAliases') ||
225
+ sourceCode.includes('trailsCliAliases')
226
+ )
227
+ ) {
228
+ return [];
229
+ }
230
+ const ast = parse(filePath, sourceCode);
231
+ if (!ast) {
232
+ return [];
233
+ }
234
+ return collectLegacyExportBindings(ast).map((binding) => ({
235
+ filePath,
236
+ fix: buildFix(binding.name),
237
+ line: offsetToLine(sourceCode, binding.start),
238
+ message: buildMessage(binding.name),
239
+ rule: RULE_NAME,
240
+ severity: 'error',
241
+ }));
242
+ },
243
+ description:
244
+ 'Disallow the legacy app-module CLI alias exports (cliAliases, trailsCliAliases) removed in the TRL-1207 surfaces-overlay cutover.',
245
+ name: RULE_NAME,
246
+ severity: 'error',
247
+ };