@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.
- package/CHANGELOG.md +105 -0
- package/README.md +23 -1
- package/bin/warden.ts +1 -0
- package/package.json +9 -9
- package/src/cli.ts +160 -14
- package/src/command.ts +48 -5
- package/src/config.ts +9 -0
- package/src/drift.ts +116 -7
- package/src/fix.ts +8 -2
- package/src/formatters.ts +3 -8
- package/src/index.ts +32 -2
- package/src/project-context.ts +286 -3
- package/src/rules/ast.ts +4 -0
- package/src/rules/duplicate-exported-symbol.ts +211 -0
- package/src/rules/duplicate-public-contract.ts +2 -1
- package/src/rules/governed-symbol-residue.ts +131 -0
- package/src/rules/implementation-returns-result.ts +1 -3
- package/src/rules/index.ts +44 -3
- package/src/rules/metadata.ts +108 -14
- package/src/rules/no-legacy-cli-alias-export.ts +247 -0
- package/src/rules/no-retired-cross-vocabulary.ts +19 -9
- package/src/rules/permit-governance.ts +1 -1
- package/src/rules/public-export-example-coverage.ts +5 -0
- package/src/rules/public-internal-deep-imports.ts +3 -63
- package/src/rules/registry-names.ts +12 -2
- package/src/rules/retired-vocabulary.ts +396 -0
- package/src/rules/signal-graph-coaching.ts +32 -3
- package/src/rules/surface-overlay-coherence.ts +262 -0
- package/src/rules/{surface-facet-coherence.ts → surface-trailhead-coherence.ts} +46 -42
- package/src/rules/trail-fork-coaching.ts +1 -1
- package/src/rules/trailhead-override-divergence.ts +356 -0
- package/src/rules/types.ts +84 -4
- package/src/rules/webhook-route-collision.ts +64 -1
- package/src/trails/duplicate-exported-symbol.trail.ts +48 -0
- package/src/trails/duplicate-public-contract.trail.ts +1 -1
- package/src/trails/governed-symbol-residue.trail.ts +24 -0
- package/src/trails/index.ts +6 -1
- package/src/trails/no-legacy-cli-alias-export.trail.ts +41 -0
- package/src/trails/schema.ts +39 -0
- package/src/trails/surface-overlay-coherence.trail.ts +24 -0
- package/src/trails/{surface-facet-coherence.trail.ts → surface-trailhead-coherence.trail.ts} +5 -5
- package/src/trails/trail-fork-coaching.trail.ts +1 -1
- package/src/trails/trailhead-override-divergence.trail.ts +47 -0
- package/src/trails/wrap-rule.ts +10 -0
- 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
|
package/src/rules/index.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
-
[
|
|
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],
|
package/src/rules/metadata.ts
CHANGED
|
@@ -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-
|
|
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: [
|
|
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-
|
|
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: '
|
|
614
|
-
path: 'docs/adr/drafts/20260603-surface-
|
|
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
|
|
619
|
-
'Ensure each public trail belongs to one
|
|
620
|
-
'Record explicit visibility-widening acceptance and stable-description metadata when a
|
|
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
|
|
690
|
+
'Keep trailhead maps reviewable before they reach MCP projection.',
|
|
624
691
|
},
|
|
625
692
|
invariant:
|
|
626
|
-
'
|
|
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: [
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
+
};
|