@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,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[] {
@@ -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 === 'error' ? 'error' : 'warn',
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.endsWith('/**')) {
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 { 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,
@@ -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');