@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,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
+ };
@@ -8,17 +8,25 @@
8
8
  */
9
9
  import { resolve, sep } from 'node:path';
10
10
 
11
+ import { requireGovernedVocabularyTransition } from './retired-vocabulary.js';
11
12
  import type { WardenDiagnostic, WardenFix, WardenRule } from './types.js';
12
13
 
13
14
  const RULE_NAME = 'no-retired-cross-vocabulary';
14
15
 
15
- const SAFE_REWRITES = [
16
- { from: 'ctx.cross', to: 'ctx.compose' },
17
- { from: 'crossInput', to: 'composeInput' },
18
- { from: 'crosses', to: 'composes' },
19
- ] as const;
16
+ const CROSS_COMPOSE_TRANSITION =
17
+ requireGovernedVocabularyTransition('cross-compose');
20
18
 
21
- const REVIEW_TERMS = ['Cross', 'cross'] as const;
19
+ if (CROSS_COMPOSE_TRANSITION.target.kind !== 'single') {
20
+ throw new Error('cross-compose transition must have a single target.');
21
+ }
22
+
23
+ const COMPOSE_TARGET = CROSS_COMPOSE_TRANSITION.target.to;
24
+
25
+ const SAFE_REWRITES = Object.entries(
26
+ CROSS_COMPOSE_TRANSITION.safeRewriteForms
27
+ ).map(([from, to]) => ({ from, to }));
28
+
29
+ const REVIEW_TERMS = CROSS_COMPOSE_TRANSITION.reviewForms;
22
30
 
23
31
  const IDENTIFIER_CHAR = /[$0-9A-Z_a-z]/u;
24
32
 
@@ -28,6 +36,8 @@ const ALLOWED_PATH_SUFFIXES: readonly string[] = [
28
36
  '/docs/adr/0049-composition-is-compose-not-cross.md',
29
37
  '/packages/warden/src/rules/no-retired-cross-vocabulary.ts',
30
38
  '/packages/warden/src/rules/metadata.ts',
39
+ '/packages/warden/src/rules/retired-vocabulary.ts',
40
+ '/packages/warden/src/trails/governed-symbol-residue.trail.ts',
31
41
  '/packages/warden/src/trails/no-retired-cross-vocabulary.trail.ts',
32
42
  '/scripts/vocab-cutover-rewrite.ts',
33
43
  ];
@@ -40,7 +50,7 @@ interface SafeRewriteMatch {
40
50
 
41
51
  interface ReviewMatch {
42
52
  readonly index: number;
43
- readonly term: (typeof REVIEW_TERMS)[number];
53
+ readonly term: string;
44
54
  }
45
55
 
46
56
  const normalizePath = (filePath: string): string =>
@@ -150,7 +160,7 @@ const safeFix = (match: SafeRewriteMatch): WardenFix => ({
150
160
 
151
161
  const reviewFix = (term: string): WardenFix => ({
152
162
  class: 'term-rewrite',
153
- reason: `Retired composition vocabulary '${term}' appears in a larger or ambiguous form. Review before migrating to compose vocabulary.`,
163
+ reason: `Retired composition vocabulary '${term}' appears in a larger or ambiguous form. Review before migrating to ${COMPOSE_TARGET} vocabulary.`,
154
164
  safety: 'review',
155
165
  });
156
166
 
@@ -158,7 +168,7 @@ const safeMessage = (match: SafeRewriteMatch): string =>
158
168
  `Retired composition vocabulary '${match.from}' should be '${match.to}' after the beta.19 compose cutover.`;
159
169
 
160
170
  const reviewMessage = (term: string): string =>
161
- `Retired composition vocabulary '${term}' needs review before migrating to compose vocabulary.`;
171
+ `Retired composition vocabulary '${term}' needs review before migrating to ${COMPOSE_TARGET} vocabulary.`;
162
172
 
163
173
  export const noRetiredCrossVocabulary: WardenRule = {
164
174
  check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
@@ -113,6 +113,11 @@ export const PUBLIC_API_EXAMPLE_TARGETS: readonly PublicApiPackageTarget[] = [
113
113
  minimumExports: ['createApp', 'surface'],
114
114
  packageName: '@ontrails/hono',
115
115
  },
116
+ {
117
+ indexPath: 'adapters/cloudflare/src/index.ts',
118
+ minimumExports: ['createWorkersHandler', 'cloudflareKv'],
119
+ packageName: '@ontrails/cloudflare',
120
+ },
116
121
  ] as const;
117
122
 
118
123
  export interface ResolvedPublicApiTarget extends PublicApiPackageTarget {
@@ -16,10 +16,12 @@ import { deadInternalTrail } from './dead-internal-trail.js';
16
16
  import { deadPublicTrail } from './dead-public-trail.js';
17
17
  import { draftFileMarking } from './draft-file-marking.js';
18
18
  import { draftVisibleDebt } from './draft-visible-debt.js';
19
+ import { duplicateExportedSymbol } from './duplicate-exported-symbol.js';
19
20
  import { duplicatePublicContract } from './duplicate-public-contract.js';
20
21
  import { errorMappingCompleteness } from './error-mapping-completeness.js';
21
22
  import { exampleValid } from './example-valid.js';
22
23
  import { firesDeclarations } from './fires-declarations.js';
24
+ import { governedSymbolResidue } from './governed-symbol-residue.js';
23
25
  import { implementationReturnsResult } from './implementation-returns-result.js';
24
26
  import { incompleteAccessorForStandardOp } from './incomplete-accessor-for-standard-op.js';
25
27
  import { incompleteCrud } from './incomplete-crud.js';
@@ -30,6 +32,7 @@ import { missingReconcile } from './missing-reconcile.js';
30
32
  import { missingVisibility } from './missing-visibility.js';
31
33
  import { noDevPermitInSource } from './no-dev-permit-in-source.js';
32
34
  import { noDestructuredCompose } from './no-destructured-compose.js';
35
+ import { noLegacyCliAliasExport } from './no-legacy-cli-alias-export.js';
33
36
  import { noLegacyLayerImports } from './no-legacy-layer-imports.js';
34
37
  import { noDirectImplementationCall } from './no-direct-implementation-call.js';
35
38
  import { noNativeErrorResult } from './no-native-error-result.js';
@@ -58,8 +61,10 @@ import { resourceMockCoverage } from './resource-mock-coverage.js';
58
61
  import { scheduledDestroyIntent } from './scheduled-destroy-intent.js';
59
62
  import { signalGraphCoaching } from './signal-graph-coaching.js';
60
63
  import { staticResourceAccessorPreference } from './static-resource-accessor-preference.js';
61
- import { surfaceFacetCoherence } from './surface-facet-coherence.js';
64
+ import { surfaceOverlayCoherence } from './surface-overlay-coherence.js';
65
+ import { surfaceTrailheadCoherence } from './surface-trailhead-coherence.js';
62
66
  import { trailForkCoaching } from './trail-fork-coaching.js';
67
+ import { trailheadOverrideDivergence } from './trailhead-override-divergence.js';
63
68
  import {
64
69
  forkWithoutPreservedBlaze,
65
70
  markerSchemaUnsupported,
@@ -94,12 +99,14 @@ export const registeredRuleNames: readonly string[] = [
94
99
  deadInternalTrail.name,
95
100
  deadPublicTrail.name,
96
101
  deprecationWithoutGuidance.name,
102
+ duplicateExportedSymbol.name,
97
103
  duplicatePublicContract.name,
98
104
  draftFileMarking.name,
99
105
  draftVisibleDebt.name,
100
106
  errorMappingCompleteness.name,
101
107
  exampleValid.name,
102
108
  firesDeclarations.name,
109
+ governedSymbolResidue.name,
103
110
  forkWithoutPreservedBlaze.name,
104
111
  implementationReturnsResult.name,
105
112
  incompleteAccessorForStandardOp.name,
@@ -112,6 +119,7 @@ export const registeredRuleNames: readonly string[] = [
112
119
  missingVisibility.name,
113
120
  noDevPermitInSource.name,
114
121
  noDestructuredCompose.name,
122
+ noLegacyCliAliasExport.name,
115
123
  noLegacyLayerImports.name,
116
124
  noDirectImplementationCall.name,
117
125
  noNativeErrorResult.name,
@@ -141,8 +149,10 @@ export const registeredRuleNames: readonly string[] = [
141
149
  scheduledDestroyIntent.name,
142
150
  signalGraphCoaching.name,
143
151
  staticResourceAccessorPreference.name,
144
- surfaceFacetCoherence.name,
152
+ surfaceOverlayCoherence.name,
153
+ surfaceTrailheadCoherence.name,
145
154
  trailForkCoaching.name,
155
+ trailheadOverrideDivergence.name,
146
156
  unmaterializedActivationSource.name,
147
157
  unreachableDetourShadowing.name,
148
158
  validDetourContract.name,