@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,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
|
|
16
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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[] {
|
|
@@ -10,7 +10,7 @@ const toWardenDiagnostic = (
|
|
|
10
10
|
line: 1,
|
|
11
11
|
message: diagnostic.message,
|
|
12
12
|
rule: `permit.${diagnostic.rule}`,
|
|
13
|
-
severity: diagnostic.severity
|
|
13
|
+
severity: diagnostic.severity,
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
export const permitGovernance: TopoAwareWardenRule = {
|
|
@@ -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 {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, realpathSync } from 'node:fs';
|
|
2
2
|
import { resolve, sep } from 'node:path';
|
|
3
|
+
import { matchesPathGlob } from '@ontrails/core';
|
|
3
4
|
|
|
4
5
|
import {
|
|
5
6
|
extractStringLiteral,
|
|
@@ -303,50 +304,6 @@ const rootBarrelDiagnostics = (
|
|
|
303
304
|
const stripLeadingDotSlash = (path: string): string =>
|
|
304
305
|
path.startsWith('./') ? path.slice(2) : path;
|
|
305
306
|
|
|
306
|
-
const escapeRegExp = (value: string): string =>
|
|
307
|
-
value.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
308
|
-
|
|
309
|
-
const wildcardPatternSource = (pattern: string): string => {
|
|
310
|
-
let regexSource = '';
|
|
311
|
-
for (let index = 0; index < pattern.length; index += 1) {
|
|
312
|
-
const char = pattern[index];
|
|
313
|
-
if (char === '*') {
|
|
314
|
-
if (pattern[index + 1] === '*') {
|
|
315
|
-
regexSource += '.*';
|
|
316
|
-
index += 1;
|
|
317
|
-
} else {
|
|
318
|
-
regexSource += '[^/]*';
|
|
319
|
-
}
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
regexSource += escapeRegExp(char ?? '');
|
|
323
|
-
}
|
|
324
|
-
return regexSource;
|
|
325
|
-
};
|
|
326
|
-
|
|
327
|
-
const segmentWildcardPatternCovers = (
|
|
328
|
-
filePath: string,
|
|
329
|
-
pattern: string
|
|
330
|
-
): boolean => new RegExp(`^${wildcardPatternSource(pattern)}$`).test(filePath);
|
|
331
|
-
|
|
332
|
-
const deepWildcardPatternCovers = (
|
|
333
|
-
filePath: string,
|
|
334
|
-
pattern: string,
|
|
335
|
-
globIndex: number
|
|
336
|
-
): boolean => {
|
|
337
|
-
const prefix = pattern.slice(0, globIndex + 1);
|
|
338
|
-
if (!filePath.startsWith(prefix)) {
|
|
339
|
-
return false;
|
|
340
|
-
}
|
|
341
|
-
const suffixPattern = pattern.slice(globIndex + '/**/'.length);
|
|
342
|
-
if (suffixPattern.length === 0) {
|
|
343
|
-
return true;
|
|
344
|
-
}
|
|
345
|
-
const remainingPath = filePath.slice(prefix.length);
|
|
346
|
-
const regexSource = `^(?:.*/)?${wildcardPatternSource(suffixPattern)}$`;
|
|
347
|
-
return new RegExp(regexSource).test(remainingPath);
|
|
348
|
-
};
|
|
349
|
-
|
|
350
307
|
const filePatternCovers = (filePath: string, pattern: string): boolean => {
|
|
351
308
|
const normalizedFilePath = normalizePath(stripLeadingDotSlash(filePath));
|
|
352
309
|
const normalizedPattern = normalizePath(stripLeadingDotSlash(pattern));
|
|
@@ -359,27 +316,10 @@ const filePatternCovers = (filePath: string, pattern: string): boolean => {
|
|
|
359
316
|
if (normalizedPattern === '**') {
|
|
360
317
|
return true;
|
|
361
318
|
}
|
|
362
|
-
if (normalizedPattern.
|
|
363
|
-
const prefix = normalizedPattern.slice(0, -2);
|
|
364
|
-
return normalizedFilePath.startsWith(prefix);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const globIndex = normalizedPattern.indexOf('/**/');
|
|
368
|
-
if (globIndex === -1) {
|
|
369
|
-
if (normalizedPattern.includes('*')) {
|
|
370
|
-
return segmentWildcardPatternCovers(
|
|
371
|
-
normalizedFilePath,
|
|
372
|
-
normalizedPattern
|
|
373
|
-
);
|
|
374
|
-
}
|
|
319
|
+
if (!normalizedPattern.includes('*') && !normalizedPattern.includes('?')) {
|
|
375
320
|
return normalizedFilePath.startsWith(`${normalizedPattern}/`);
|
|
376
321
|
}
|
|
377
|
-
|
|
378
|
-
return deepWildcardPatternCovers(
|
|
379
|
-
normalizedFilePath,
|
|
380
|
-
normalizedPattern,
|
|
381
|
-
globIndex
|
|
382
|
-
);
|
|
322
|
+
return matchesPathGlob(normalizedFilePath, normalizedPattern);
|
|
383
323
|
};
|
|
384
324
|
|
|
385
325
|
const filesCoverTarget = (
|
|
@@ -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 {
|
|
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
|
-
|
|
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,
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const governedVocabularyTransitionStatuses = [
|
|
4
|
+
'planned',
|
|
5
|
+
'active',
|
|
6
|
+
'complete',
|
|
7
|
+
] as const;
|
|
8
|
+
|
|
9
|
+
const governedVocabularySingleTargetSchema = z.object({
|
|
10
|
+
kind: z.literal('single'),
|
|
11
|
+
to: z.string().min(1),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const governedVocabularyClassifiedTargetSchema = z.object({
|
|
15
|
+
guidance: z.string().min(1),
|
|
16
|
+
kind: z.literal('classified'),
|
|
17
|
+
options: z
|
|
18
|
+
.array(
|
|
19
|
+
z.object({
|
|
20
|
+
to: z.string().min(1),
|
|
21
|
+
when: z.string().min(1),
|
|
22
|
+
})
|
|
23
|
+
)
|
|
24
|
+
.min(1),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const governedVocabularyTargetSchema = z.discriminatedUnion('kind', [
|
|
28
|
+
governedVocabularySingleTargetSchema,
|
|
29
|
+
governedVocabularyClassifiedTargetSchema,
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
export const governedVocabularyPreserveRuleSchema = z.object({
|
|
33
|
+
paths: z.array(z.string().min(1)).optional(),
|
|
34
|
+
pattern: z.string().min(1),
|
|
35
|
+
reason: z.string().min(1),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const governedVocabularyScopeSchema = z.object({
|
|
39
|
+
exclude: z.array(z.string().min(1)).optional(),
|
|
40
|
+
extensions: z.array(z.string().min(1)).optional(),
|
|
41
|
+
ignoredDirectories: z.array(z.string().min(1)).optional(),
|
|
42
|
+
include: z.array(z.string().min(1)).optional(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const governedVocabularySymbolRenameSchema = z.object({
|
|
46
|
+
from: z.string().min(1),
|
|
47
|
+
reviewDeclarationTypes: z.array(z.string().min(1)).default([]),
|
|
48
|
+
to: z.string().min(1),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export const governedVocabularyLiteralRenameSchema = z.object({
|
|
52
|
+
from: z.string().min(1),
|
|
53
|
+
to: z.string().min(1),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export const governedVocabularyTransitionSchema = z.object({
|
|
57
|
+
codeIdentifiers: z.array(z.string().min(1)).default([]),
|
|
58
|
+
docs: z.object({
|
|
59
|
+
guidance: z.array(z.string().min(1)).default([]),
|
|
60
|
+
summary: z.string().min(1),
|
|
61
|
+
}),
|
|
62
|
+
from: z.string().min(1),
|
|
63
|
+
id: z.string().min(1),
|
|
64
|
+
intent: z.string().min(1),
|
|
65
|
+
kind: z.literal('vocabulary'),
|
|
66
|
+
oldForms: z.array(z.string().min(1)).min(1),
|
|
67
|
+
overrides: z.record(z.string().min(1), z.string().min(1)).default({}),
|
|
68
|
+
preserve: z.array(governedVocabularyPreserveRuleSchema).default([]),
|
|
69
|
+
reviewForms: z.array(z.string().min(1)).default([]),
|
|
70
|
+
safeRewriteForms: z.record(z.string().min(1), z.string().min(1)).default({}),
|
|
71
|
+
scope: governedVocabularyScopeSchema.optional(),
|
|
72
|
+
status: z.enum(governedVocabularyTransitionStatuses),
|
|
73
|
+
stringLiteralRenames: z
|
|
74
|
+
.array(governedVocabularyLiteralRenameSchema)
|
|
75
|
+
.default([]),
|
|
76
|
+
symbolRenames: z.array(governedVocabularySymbolRenameSchema).default([]),
|
|
77
|
+
target: governedVocabularyTargetSchema,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export const governedVocabularyRegistrySchema = z
|
|
81
|
+
.array(governedVocabularyTransitionSchema)
|
|
82
|
+
.superRefine((transitions, ctx) => {
|
|
83
|
+
const seenIds = new Set<string>();
|
|
84
|
+
const seenFrom = new Set<string>();
|
|
85
|
+
|
|
86
|
+
for (const [index, transition] of transitions.entries()) {
|
|
87
|
+
if (seenIds.has(transition.id)) {
|
|
88
|
+
ctx.addIssue({
|
|
89
|
+
code: 'custom',
|
|
90
|
+
message: `Duplicate governed vocabulary transition id "${transition.id}".`,
|
|
91
|
+
path: [index, 'id'],
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
seenIds.add(transition.id);
|
|
95
|
+
|
|
96
|
+
if (seenFrom.has(transition.from)) {
|
|
97
|
+
ctx.addIssue({
|
|
98
|
+
code: 'custom',
|
|
99
|
+
message: `Duplicate governed vocabulary source "${transition.from}".`,
|
|
100
|
+
path: [index, 'from'],
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
seenFrom.add(transition.from);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
export type GovernedVocabularyPreserveRule = z.output<
|
|
108
|
+
typeof governedVocabularyPreserveRuleSchema
|
|
109
|
+
>;
|
|
110
|
+
export type GovernedVocabularyScope = z.output<
|
|
111
|
+
typeof governedVocabularyScopeSchema
|
|
112
|
+
>;
|
|
113
|
+
export type GovernedVocabularyTarget = z.output<
|
|
114
|
+
typeof governedVocabularyTargetSchema
|
|
115
|
+
>;
|
|
116
|
+
export type GovernedVocabularySymbolRename = z.output<
|
|
117
|
+
typeof governedVocabularySymbolRenameSchema
|
|
118
|
+
>;
|
|
119
|
+
export type GovernedVocabularyLiteralRename = z.output<
|
|
120
|
+
typeof governedVocabularyLiteralRenameSchema
|
|
121
|
+
>;
|
|
122
|
+
export type GovernedVocabularyTransition = z.output<
|
|
123
|
+
typeof governedVocabularyTransitionSchema
|
|
124
|
+
>;
|
|
125
|
+
export type GovernedVocabularyTransitionInput = z.input<
|
|
126
|
+
typeof governedVocabularyTransitionSchema
|
|
127
|
+
>;
|
|
128
|
+
|
|
129
|
+
const defineTransition = (
|
|
130
|
+
input: GovernedVocabularyTransitionInput
|
|
131
|
+
): GovernedVocabularyTransition =>
|
|
132
|
+
governedVocabularyTransitionSchema.parse(input);
|
|
133
|
+
|
|
134
|
+
const reviewFunctionParamDeclarations = {
|
|
135
|
+
reviewDeclarationTypes: ['FunctionParam'],
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const v1VocabularyHistoricalExcludes = [
|
|
139
|
+
'.agents/memory/**',
|
|
140
|
+
'.agents/plans/archive/**',
|
|
141
|
+
'.changeset/**',
|
|
142
|
+
'**/CHANGELOG.md',
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
const v1VocabularySelfExcludes = [
|
|
146
|
+
'packages/warden/src/rules/retired-vocabulary.ts',
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
const unique = (values: readonly string[]): string[] => [...new Set(values)];
|
|
150
|
+
|
|
151
|
+
const defineV1Transition = (
|
|
152
|
+
input: GovernedVocabularyTransitionInput
|
|
153
|
+
): GovernedVocabularyTransition =>
|
|
154
|
+
defineTransition({
|
|
155
|
+
...input,
|
|
156
|
+
scope: {
|
|
157
|
+
...input.scope,
|
|
158
|
+
exclude: unique([
|
|
159
|
+
...v1VocabularyHistoricalExcludes,
|
|
160
|
+
...v1VocabularySelfExcludes,
|
|
161
|
+
...(input.scope?.exclude ?? []),
|
|
162
|
+
]),
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
export const governedVocabularyTransitions =
|
|
167
|
+
governedVocabularyRegistrySchema.parse([
|
|
168
|
+
defineTransition({
|
|
169
|
+
codeIdentifiers: ['ctx.cross', 'crossInput', 'crosses'],
|
|
170
|
+
docs: {
|
|
171
|
+
guidance: [
|
|
172
|
+
'Exact ctx.cross, crossInput, and crosses forms are mechanically rewritten.',
|
|
173
|
+
'Broader cross or Cross forms route to review because a text rule cannot prove the intended symbol boundary.',
|
|
174
|
+
],
|
|
175
|
+
summary:
|
|
176
|
+
'Beta.19 retired cross composition vocabulary in favor of compose.',
|
|
177
|
+
},
|
|
178
|
+
from: 'cross',
|
|
179
|
+
id: 'cross-compose',
|
|
180
|
+
intent:
|
|
181
|
+
'Retire beta.19 cross composition vocabulary in favor of compose.',
|
|
182
|
+
kind: 'vocabulary',
|
|
183
|
+
oldForms: ['cross', 'Cross', 'crosses', 'crossInput', 'ctx.cross'],
|
|
184
|
+
reviewForms: ['Cross', 'cross'],
|
|
185
|
+
safeRewriteForms: {
|
|
186
|
+
crossInput: 'composeInput',
|
|
187
|
+
crosses: 'composes',
|
|
188
|
+
'ctx.cross': 'ctx.compose',
|
|
189
|
+
},
|
|
190
|
+
status: 'complete',
|
|
191
|
+
symbolRenames: [
|
|
192
|
+
{ ...reviewFunctionParamDeclarations, from: 'cross', to: 'compose' },
|
|
193
|
+
{
|
|
194
|
+
...reviewFunctionParamDeclarations,
|
|
195
|
+
from: 'crossInput',
|
|
196
|
+
to: 'composeInput',
|
|
197
|
+
},
|
|
198
|
+
{ ...reviewFunctionParamDeclarations, from: 'crosses', to: 'composes' },
|
|
199
|
+
],
|
|
200
|
+
target: { kind: 'single', to: 'compose' },
|
|
201
|
+
}),
|
|
202
|
+
defineV1Transition({
|
|
203
|
+
codeIdentifiers: ['blaze', 'blazes'],
|
|
204
|
+
docs: {
|
|
205
|
+
guidance: [
|
|
206
|
+
'Treat code/API identifiers as governed symbols, not prose-only vocabulary.',
|
|
207
|
+
'Review inflected forms such as blazed or blazing instead of forcing a mechanical rewrite.',
|
|
208
|
+
],
|
|
209
|
+
summary:
|
|
210
|
+
'The authored trail behavior field is moving from blaze to implementation.',
|
|
211
|
+
},
|
|
212
|
+
from: 'blaze',
|
|
213
|
+
id: 'v1-blaze-implementation',
|
|
214
|
+
intent:
|
|
215
|
+
'Move the authored trail behavior field from blaze to implementation for v1.',
|
|
216
|
+
kind: 'vocabulary',
|
|
217
|
+
oldForms: ['blaze', 'blazes', 'Blaze'],
|
|
218
|
+
reviewForms: ['Blaze', 'blazing', 'blazed', 'trailblaze'],
|
|
219
|
+
safeRewriteForms: {
|
|
220
|
+
blaze: 'implementation',
|
|
221
|
+
blazes: 'implementations',
|
|
222
|
+
},
|
|
223
|
+
status: 'planned',
|
|
224
|
+
symbolRenames: [
|
|
225
|
+
{
|
|
226
|
+
...reviewFunctionParamDeclarations,
|
|
227
|
+
from: 'blaze',
|
|
228
|
+
to: 'implementation',
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
...reviewFunctionParamDeclarations,
|
|
232
|
+
from: 'blazes',
|
|
233
|
+
to: 'implementations',
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
target: { kind: 'single', to: 'implementation' },
|
|
237
|
+
}),
|
|
238
|
+
defineV1Transition({
|
|
239
|
+
codeIdentifiers: ['contour', 'contours'],
|
|
240
|
+
docs: {
|
|
241
|
+
guidance: [
|
|
242
|
+
'Keep domain-object semantics distinct from entities in app data until the occurrence is classified.',
|
|
243
|
+
],
|
|
244
|
+
summary:
|
|
245
|
+
'The domain object declaration term is moving from contour to entity.',
|
|
246
|
+
},
|
|
247
|
+
from: 'contour',
|
|
248
|
+
id: 'v1-contour-entity',
|
|
249
|
+
intent:
|
|
250
|
+
'Move the domain object declaration vocabulary from contour to entity for v1.',
|
|
251
|
+
kind: 'vocabulary',
|
|
252
|
+
oldForms: ['contour', 'contours', 'Contour'],
|
|
253
|
+
reviewForms: ['Contour'],
|
|
254
|
+
safeRewriteForms: {
|
|
255
|
+
contour: 'entity',
|
|
256
|
+
contours: 'entities',
|
|
257
|
+
},
|
|
258
|
+
status: 'planned',
|
|
259
|
+
symbolRenames: [
|
|
260
|
+
{ ...reviewFunctionParamDeclarations, from: 'contour', to: 'entity' },
|
|
261
|
+
{
|
|
262
|
+
...reviewFunctionParamDeclarations,
|
|
263
|
+
from: 'contours',
|
|
264
|
+
to: 'entities',
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
target: { kind: 'single', to: 'entity' },
|
|
268
|
+
}),
|
|
269
|
+
defineV1Transition({
|
|
270
|
+
codeIdentifiers: [
|
|
271
|
+
'facets',
|
|
272
|
+
'facetId',
|
|
273
|
+
'McpSurfaceFacetMap',
|
|
274
|
+
'surface-facet-coherence',
|
|
275
|
+
'wayfind.facets',
|
|
276
|
+
],
|
|
277
|
+
docs: {
|
|
278
|
+
guidance: [
|
|
279
|
+
'A trailhead is one grouped surface entry fronting several trails while preserving member identity.',
|
|
280
|
+
'Code/API identifiers remain governed symbols and need structured preservation before broad application.',
|
|
281
|
+
],
|
|
282
|
+
summary:
|
|
283
|
+
'The grouped surface entry term is moving from facet to trailhead.',
|
|
284
|
+
},
|
|
285
|
+
from: 'facet',
|
|
286
|
+
id: 'v1-facet-trailhead',
|
|
287
|
+
intent:
|
|
288
|
+
'Move grouped surface entry vocabulary from facet to trailhead for v1.',
|
|
289
|
+
kind: 'vocabulary',
|
|
290
|
+
oldForms: ['facet', 'facets', 'Facet'],
|
|
291
|
+
reviewForms: ['Facet'],
|
|
292
|
+
safeRewriteForms: {
|
|
293
|
+
facet: 'trailhead',
|
|
294
|
+
facets: 'trailheads',
|
|
295
|
+
},
|
|
296
|
+
status: 'complete',
|
|
297
|
+
stringLiteralRenames: [
|
|
298
|
+
{ from: 'surface-facet-coherence', to: 'surface-trailhead-coherence' },
|
|
299
|
+
{ from: 'wayfind.facets', to: 'wayfind.trailheads' },
|
|
300
|
+
],
|
|
301
|
+
symbolRenames: [
|
|
302
|
+
{ ...reviewFunctionParamDeclarations, from: 'facet', to: 'trailhead' },
|
|
303
|
+
{
|
|
304
|
+
...reviewFunctionParamDeclarations,
|
|
305
|
+
from: 'facets',
|
|
306
|
+
to: 'trailheads',
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
...reviewFunctionParamDeclarations,
|
|
310
|
+
from: 'facetId',
|
|
311
|
+
to: 'trailheadId',
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
...reviewFunctionParamDeclarations,
|
|
315
|
+
from: 'McpSurfaceFacetMap',
|
|
316
|
+
to: 'McpSurfaceTrailheadMap',
|
|
317
|
+
},
|
|
318
|
+
],
|
|
319
|
+
target: { kind: 'single', to: 'trailhead' },
|
|
320
|
+
}),
|
|
321
|
+
defineV1Transition({
|
|
322
|
+
codeIdentifiers: [],
|
|
323
|
+
docs: {
|
|
324
|
+
guidance: [
|
|
325
|
+
'Use derive for contract-owned fact production.',
|
|
326
|
+
'Use render for surface- or operator-facing presentation.',
|
|
327
|
+
'Route every occurrence to review until the stage is classified.',
|
|
328
|
+
],
|
|
329
|
+
summary: 'Projection vocabulary splits by stage into derive or render.',
|
|
330
|
+
},
|
|
331
|
+
from: 'projection',
|
|
332
|
+
id: 'v1-projection-derive-render',
|
|
333
|
+
intent:
|
|
334
|
+
'Split projection vocabulary into derive/render by lifecycle stage for v1.',
|
|
335
|
+
kind: 'vocabulary',
|
|
336
|
+
oldForms: ['projection', 'projections', 'project', 'projected'],
|
|
337
|
+
reviewForms: ['projection', 'projections', 'project', 'projected'],
|
|
338
|
+
safeRewriteForms: {},
|
|
339
|
+
status: 'planned',
|
|
340
|
+
target: {
|
|
341
|
+
guidance:
|
|
342
|
+
'No single replacement is safe. Classify by whether the occurrence produces contract facts or presents derived facts.',
|
|
343
|
+
kind: 'classified',
|
|
344
|
+
options: [
|
|
345
|
+
{
|
|
346
|
+
to: 'derive',
|
|
347
|
+
when: 'The occurrence describes producing contract-owned facts or inferred data from authored inputs.',
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
to: 'render',
|
|
351
|
+
when: 'The occurrence describes presenting derived facts through a surface, report, guide, or operator output.',
|
|
352
|
+
},
|
|
353
|
+
],
|
|
354
|
+
},
|
|
355
|
+
}),
|
|
356
|
+
]);
|
|
357
|
+
|
|
358
|
+
export const listGovernedVocabularyTransitions =
|
|
359
|
+
(): readonly GovernedVocabularyTransition[] => governedVocabularyTransitions;
|
|
360
|
+
|
|
361
|
+
export const getGovernedVocabularyTransition = (
|
|
362
|
+
id: string
|
|
363
|
+
): GovernedVocabularyTransition | undefined =>
|
|
364
|
+
governedVocabularyTransitions.find((transition) => transition.id === id);
|
|
365
|
+
|
|
366
|
+
export const requireGovernedVocabularyTransition = (
|
|
367
|
+
id: string
|
|
368
|
+
): GovernedVocabularyTransition => {
|
|
369
|
+
const transition = getGovernedVocabularyTransition(id);
|
|
370
|
+
if (transition === undefined) {
|
|
371
|
+
throw new Error(`Unknown governed vocabulary transition "${id}".`);
|
|
372
|
+
}
|
|
373
|
+
return transition;
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
export const formatGovernedVocabularyTransitionGuide = (
|
|
377
|
+
transitions: readonly GovernedVocabularyTransition[] = governedVocabularyTransitions
|
|
378
|
+
): string =>
|
|
379
|
+
transitions
|
|
380
|
+
.map((transition) => {
|
|
381
|
+
const target =
|
|
382
|
+
transition.target.kind === 'single'
|
|
383
|
+
? transition.target.to
|
|
384
|
+
: transition.target.options
|
|
385
|
+
.map((option) => `${option.to} (${option.when})`)
|
|
386
|
+
.join(' or ');
|
|
387
|
+
const lines = [
|
|
388
|
+
`- ${transition.id}: ${transition.from} -> ${target}`,
|
|
389
|
+
` - Status: ${transition.status}`,
|
|
390
|
+
` - Intent: ${transition.intent}`,
|
|
391
|
+
` - Safe rewrites: ${Object.keys(transition.safeRewriteForms).length}`,
|
|
392
|
+
` - Review forms: ${transition.reviewForms.join(', ') || 'none'}`,
|
|
393
|
+
];
|
|
394
|
+
return lines.join('\n');
|
|
395
|
+
})
|
|
396
|
+
.join('\n');
|