@ontrails/warden 1.0.0-beta.41 → 1.0.0-beta.43

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 (34) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/package.json +10 -10
  3. package/src/adapter-check.ts +1 -1
  4. package/src/cli.ts +81 -5
  5. package/src/index.ts +17 -4
  6. package/src/regrade-history.ts +599 -0
  7. package/src/rules/cli-command-route-coherence.ts +6 -6
  8. package/src/rules/draft-visible-debt.ts +1 -1
  9. package/src/rules/duplicate-exported-symbol.ts +4 -43
  10. package/src/rules/governed-symbol-residue.ts +146 -53
  11. package/src/rules/governed-vocabulary-permutation-watch.ts +76 -0
  12. package/src/rules/index.ts +17 -6
  13. package/src/rules/layer-field-name-drift.ts +2 -2
  14. package/src/rules/{library-projection-coherence.ts → library-render-coherence.ts} +11 -14
  15. package/src/rules/metadata.ts +51 -14
  16. package/src/rules/no-retired-cross-vocabulary.ts +0 -1
  17. package/src/rules/{owner-projection-parity.ts → owner-render-parity.ts} +18 -18
  18. package/src/rules/public-internal-deep-imports.ts +1 -3
  19. package/src/rules/public-output-schema.ts +1 -1
  20. package/src/rules/registry-names.ts +6 -4
  21. package/src/rules/retired-vocabulary.ts +775 -41
  22. package/src/rules/surface-overlay-coherence.ts +1 -1
  23. package/src/rules/trail-versioning-source.ts +1 -1
  24. package/src/rules/trailhead-override-divergence.ts +4 -4
  25. package/src/rules/types.ts +51 -5
  26. package/src/trails/governed-vocabulary-permutation-watch.trail.ts +16 -0
  27. package/src/trails/index.ts +3 -2
  28. package/src/trails/layer-field-name-drift.trail.ts +1 -1
  29. package/src/trails/{library-projection-coherence.trail.ts → library-render-coherence.trail.ts} +6 -6
  30. package/src/trails/{owner-projection-parity.trail.ts → owner-render-parity.trail.ts} +4 -4
  31. package/src/trails/public-output-schema.trail.ts +1 -1
  32. package/src/trails/run.ts +44 -2
  33. package/src/trails/schema.ts +41 -0
  34. package/src/trails/wrap-rule.ts +44 -2
@@ -12,7 +12,13 @@ import type {
12
12
  GovernedVocabularySymbolRename,
13
13
  GovernedVocabularyTransition,
14
14
  } from './retired-vocabulary.js';
15
- import type { WardenDiagnostic, WardenFix, WardenRule } from './types.js';
15
+ import type {
16
+ GovernedVocabularyHistoryEvidence,
17
+ ProjectAwareWardenRule,
18
+ ProjectContext,
19
+ WardenDiagnostic,
20
+ WardenFix,
21
+ } from './types.js';
16
22
 
17
23
  const RULE_NAME = 'governed-symbol-residue';
18
24
 
@@ -37,11 +43,15 @@ const isAllowedSourcePath = (filePath: string): boolean =>
37
43
  );
38
44
 
39
45
  interface ActiveSymbolRename {
46
+ readonly history?: GovernedVocabularyHistoryEvidence;
40
47
  readonly rename: GovernedVocabularySymbolRename;
41
48
  readonly targetSegments: readonly string[];
49
+ readonly transition: GovernedVocabularyTransition;
42
50
  }
43
51
 
44
- const activeSymbolRenames = (): readonly ActiveSymbolRename[] =>
52
+ const activeSymbolRenames = (
53
+ context?: ProjectContext
54
+ ): readonly ActiveSymbolRename[] =>
45
55
  listGovernedVocabularyTransitions()
46
56
  .filter(
47
57
  (transition) =>
@@ -52,9 +62,14 @@ const activeSymbolRenames = (): readonly ActiveSymbolRename[] =>
52
62
  const targetSegments = [
53
63
  ...new Set(transition.symbolRenames.map((rename) => rename.to)),
54
64
  ];
65
+ const history = context?.governedVocabularyHistoryByTransitionId?.get(
66
+ transition.id
67
+ );
55
68
  return transition.symbolRenames.map((rename) => ({
69
+ ...(history === undefined ? {} : { history }),
56
70
  rename,
57
71
  targetSegments,
72
+ transition,
58
73
  }));
59
74
  });
60
75
 
@@ -353,12 +368,28 @@ const safeFixFor = (match: SymbolRenameMatch): WardenFix | undefined => {
353
368
  };
354
369
  };
355
370
 
371
+ const diagnosticMessageFor = (
372
+ match: SymbolRenameMatch,
373
+ transition?: GovernedVocabularyTransition,
374
+ history?: GovernedVocabularyHistoryEvidence
375
+ ): string => {
376
+ if (history !== undefined) {
377
+ return `Retired governed symbol '${match.from}' was reintroduced after governed transition '${history.transitionId}' in '${history.path}' (${history.id}); migrate to '${match.to}'.`;
378
+ }
379
+ if (transition?.provenance.mode === 'regrade-history') {
380
+ return `Retired governed symbol '${match.from}' belongs to governed transition '${transition.id}', but no committed Regrade history proves the migration ran.`;
381
+ }
382
+ return `Retired governed symbol '${match.from}' should migrate to '${match.to}'.`;
383
+ };
384
+
356
385
  const diagnosticFor = (
357
386
  sourceCode: string,
358
387
  filePath: string,
359
388
  node: AstNode,
360
389
  match: SymbolRenameMatch,
361
- reviewReason: string | null
390
+ reviewReason: string | null,
391
+ transition?: GovernedVocabularyTransition,
392
+ history?: GovernedVocabularyHistoryEvidence
362
393
  ): WardenDiagnostic => {
363
394
  const fix =
364
395
  reviewReason === null
@@ -368,71 +399,133 @@ const diagnosticFor = (
368
399
  filePath,
369
400
  ...(fix === undefined ? {} : { fix }),
370
401
  line: offsetToLine(sourceCode, node.start),
371
- message: `Retired governed symbol '${match.from}' should migrate to '${match.to}'.`,
402
+ message: diagnosticMessageFor(match, transition, history),
372
403
  rule: RULE_NAME,
373
404
  severity: 'error',
374
405
  };
375
406
  };
376
407
 
377
- export const governedSymbolResidue: WardenRule = {
378
- check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
379
- if (isAllowedSourcePath(filePath)) {
380
- return [];
381
- }
408
+ const provenanceDiagnostics = (
409
+ context: ProjectContext
410
+ ): readonly WardenDiagnostic[] => {
411
+ const historyIssues = context.governedVocabularyHistoryIssues ?? [];
412
+ const invalidTransitionIds = new Set(
413
+ historyIssues.flatMap((issue) =>
414
+ issue.transitionId === undefined ? [] : [issue.transitionId]
415
+ )
416
+ );
417
+ const issues = historyIssues.map((issue) => ({
418
+ filePath: issue.path,
419
+ line: 1,
420
+ message: issue.message,
421
+ rule: RULE_NAME,
422
+ severity: 'error' as const,
423
+ }));
424
+ const missing = listGovernedVocabularyTransitions()
425
+ .filter(
426
+ (transition) =>
427
+ context.governedVocabularyHistoryRequired === true &&
428
+ transition.status === 'complete' &&
429
+ transition.provenance.mode === 'regrade-history' &&
430
+ !invalidTransitionIds.has(transition.id) &&
431
+ !context.governedVocabularyHistoryByTransitionId?.has(transition.id)
432
+ )
433
+ .map((transition) => ({
434
+ filePath: '<governed-vocabulary-history>',
435
+ line: 1,
436
+ message: `Governed transition '${transition.id}' is complete but has no valid committed Regrade history provenance. Run Regrade as the primary migration path before manual review or cleanup.`,
437
+ rule: RULE_NAME,
438
+ severity: 'error' as const,
439
+ }));
440
+ return [...issues, ...missing];
441
+ };
382
442
 
383
- const renames = activeSymbolRenames();
384
- if (renames.length === 0) {
385
- return [];
386
- }
443
+ const checkSource = (
444
+ sourceCode: string,
445
+ filePath: string,
446
+ projectContext?: ProjectContext
447
+ ): readonly WardenDiagnostic[] => {
448
+ if (isAllowedSourcePath(filePath)) {
449
+ return [];
450
+ }
387
451
 
388
- const parsed = parseWithDiagnostics(filePath, sourceCode);
389
- if (!parsed.ast || parsed.diagnostics.length > 0) {
390
- return [];
391
- }
452
+ const renames = activeSymbolRenames(projectContext);
453
+ if (renames.length === 0) {
454
+ return [];
455
+ }
392
456
 
393
- const shorthandIdentifiers = new Set<string>();
394
- walkWithScopeContext(parsed.ast, (node, context) => {
395
- if (!isShorthandPropertyKeyVisit(node, context)) {
396
- return;
397
- }
398
- const name = identifierName(node);
399
- if (name !== null) {
400
- shorthandIdentifiers.add(name);
401
- }
402
- });
457
+ const parsed = parseWithDiagnostics(filePath, sourceCode);
458
+ if (!parsed.ast || parsed.diagnostics.length > 0) {
459
+ return [];
460
+ }
403
461
 
404
- const diagnostics: WardenDiagnostic[] = [];
405
- walkWithScopeContext(parsed.ast, (node, context) => {
406
- if (isDuplicateShorthandValueVisit(node, context)) {
407
- return;
408
- }
462
+ const shorthandIdentifiers = new Set<string>();
463
+ walkWithScopeContext(parsed.ast, (node, astContext) => {
464
+ if (!isShorthandPropertyKeyVisit(node, astContext)) {
465
+ return;
466
+ }
467
+ const name = identifierName(node);
468
+ if (name !== null) {
469
+ shorthandIdentifiers.add(name);
470
+ }
471
+ });
409
472
 
410
- const activeMatch = renames
411
- .map(({ rename, targetSegments }) => ({
412
- match: resolveSymbolRenameMatch(node, sourceCode, rename),
413
- targetSegments,
414
- }))
415
- .find((candidate) => candidate.match !== null);
416
- if (activeMatch === undefined || activeMatch.match === null) {
417
- return;
418
- }
419
- const { match, targetSegments } = activeMatch;
473
+ const diagnostics: WardenDiagnostic[] = [];
474
+ walkWithScopeContext(parsed.ast, (node, astContext) => {
475
+ if (isDuplicateShorthandValueVisit(node, astContext)) {
476
+ return;
477
+ }
420
478
 
421
- diagnostics.push(
422
- diagnosticFor(
423
- sourceCode,
424
- filePath,
425
- node,
479
+ const activeMatch = renames
480
+ .map(({ history, rename, targetSegments, transition }) => ({
481
+ history,
482
+ match: resolveSymbolRenameMatch(node, sourceCode, rename),
483
+ targetSegments,
484
+ transition,
485
+ }))
486
+ .find((candidate) => candidate.match !== null);
487
+ if (activeMatch === undefined || activeMatch.match === null) {
488
+ return;
489
+ }
490
+ const { history, match, targetSegments, transition } = activeMatch;
491
+
492
+ diagnostics.push(
493
+ diagnosticFor(
494
+ sourceCode,
495
+ filePath,
496
+ node,
497
+ match,
498
+ reviewReasonFor(
499
+ astContext,
426
500
  match,
427
- reviewReasonFor(context, match, shorthandIdentifiers, targetSegments)
428
- )
429
- );
430
- });
501
+ shorthandIdentifiers,
502
+ targetSegments
503
+ ),
504
+ transition,
505
+ history
506
+ )
507
+ );
508
+ });
431
509
 
432
- return diagnostics;
510
+ return diagnostics;
511
+ };
512
+
513
+ export const governedSymbolResidue: ProjectAwareWardenRule = {
514
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
515
+ return checkSource(sourceCode, filePath);
516
+ },
517
+ checkProject(context: ProjectContext): readonly WardenDiagnostic[] {
518
+ return provenanceDiagnostics(context);
519
+ },
520
+ checkWithContext(
521
+ sourceCode: string,
522
+ filePath: string,
523
+ context: ProjectContext
524
+ ): readonly WardenDiagnostic[] {
525
+ return checkSource(sourceCode, filePath, context);
433
526
  },
434
527
  description:
435
- 'Detect active governed vocabulary symbols that remain in source code.',
528
+ 'Require committed Regrade provenance for governed transitions and detect retired symbols that remain or return.',
436
529
  name: RULE_NAME,
437
530
  severity: 'error',
438
531
  };
@@ -0,0 +1,76 @@
1
+ import type {
2
+ GovernedVocabularyHistoryEvidence,
3
+ GovernedVocabularyHistoryFormObservation,
4
+ ProjectAwareWardenRule,
5
+ ProjectContext,
6
+ WardenDiagnostic,
7
+ } from './types.js';
8
+
9
+ const RULE_NAME = 'governed-vocabulary-permutation-watch';
10
+
11
+ const formIdentity = (
12
+ form: string,
13
+ evidence: GovernedVocabularyHistoryEvidence
14
+ ): string => (evidence.caseSensitive ? form : form.toLowerCase());
15
+
16
+ const isUnknownPermutation = (
17
+ observation: GovernedVocabularyHistoryFormObservation
18
+ ): boolean =>
19
+ observation.reason === 'unclassified-neighbor' &&
20
+ observation.verdict === 'deferred' &&
21
+ (observation.scopeTier === undefined || observation.scopeTier === 'in-scope');
22
+
23
+ const compareObservation = (
24
+ left: GovernedVocabularyHistoryFormObservation,
25
+ right: GovernedVocabularyHistoryFormObservation
26
+ ): number =>
27
+ left.path.localeCompare(right.path) ||
28
+ left.line - right.line ||
29
+ left.form.localeCompare(right.form);
30
+
31
+ const diagnosticsForHistory = (
32
+ evidence: GovernedVocabularyHistoryEvidence
33
+ ): readonly WardenDiagnostic[] => {
34
+ const unknownByIdentity = new Map<
35
+ string,
36
+ GovernedVocabularyHistoryFormObservation
37
+ >();
38
+
39
+ for (const observation of evidence.latestFormObservations) {
40
+ if (!isUnknownPermutation(observation)) {
41
+ continue;
42
+ }
43
+ const identity = formIdentity(observation.form, evidence);
44
+ const current = unknownByIdentity.get(identity);
45
+ if (current === undefined || compareObservation(observation, current) < 0) {
46
+ unknownByIdentity.set(identity, observation);
47
+ }
48
+ }
49
+
50
+ return [...unknownByIdentity.entries()]
51
+ .toSorted(([left], [right]) => left.localeCompare(right))
52
+ .map(([, observation]) => ({
53
+ filePath: observation.path,
54
+ line: observation.line,
55
+ message: `Governed transition '${evidence.transitionId}' recorded unknown vocabulary form '${observation.form}' in committed Regrade history '${evidence.path}' (${evidence.id}). Add the form and run an incremental plan, or classify it as out-of-family or preserved.`,
56
+ rule: RULE_NAME,
57
+ severity: 'warn' as const,
58
+ }));
59
+ };
60
+
61
+ const checkProject = (context: ProjectContext): readonly WardenDiagnostic[] =>
62
+ [...(context.governedVocabularyHistoryByTransitionId?.values() ?? [])]
63
+ .toSorted((left, right) =>
64
+ left.transitionId.localeCompare(right.transitionId)
65
+ )
66
+ .flatMap(diagnosticsForHistory);
67
+
68
+ export const governedVocabularyPermutationWatch: ProjectAwareWardenRule = {
69
+ check: () => [],
70
+ checkProject,
71
+ checkWithContext: () => [],
72
+ description:
73
+ 'Advise when committed Regrade history records unclassified permutations of governed vocabulary.',
74
+ name: RULE_NAME,
75
+ severity: 'warn',
76
+ };
@@ -15,12 +15,13 @@ import { errorMappingCompleteness } from './error-mapping-completeness.js';
15
15
  import { exampleValid } from './example-valid.js';
16
16
  import { firesDeclarations } from './fires-declarations.js';
17
17
  import { governedSymbolResidue } from './governed-symbol-residue.js';
18
+ import { governedVocabularyPermutationWatch } from './governed-vocabulary-permutation-watch.js';
18
19
  import { implementationReturnsResult } from './implementation-returns-result.js';
19
20
  import { incompleteAccessorForStandardOp } from './incomplete-accessor-for-standard-op.js';
20
21
  import { incompleteCrud } from './incomplete-crud.js';
21
22
  import { intentPropagation } from './intent-propagation.js';
22
23
  import { layerFieldNameDrift } from './layer-field-name-drift.js';
23
- import { libraryProjectionCoherence } from './library-projection-coherence.js';
24
+ import { libraryRenderCoherence } from './library-render-coherence.js';
24
25
  import { missingVisibility } from './missing-visibility.js';
25
26
  import { missingReconcile } from './missing-reconcile.js';
26
27
  import { noDevPermitInSource } from './no-dev-permit-in-source.js';
@@ -37,7 +38,7 @@ import { noThrowInImplementation } from './no-throw-in-implementation.js';
37
38
  import { noTopLevelSurface } from './no-top-level-surface.js';
38
39
  import { onReferencesExist } from './on-references-exist.js';
39
40
  import { orphanedSignal } from './orphaned-signal.js';
40
- import { ownerProjectionParity } from './owner-projection-parity.js';
41
+ import { ownerRenderParity } from './owner-render-parity.js';
41
42
  import { permitGovernance } from './permit-governance.js';
42
43
  import { preferSchemaInference } from './prefer-schema-inference.js';
43
44
  import { publicExportExampleCoverage } from './public-export-example-coverage.js';
@@ -79,6 +80,9 @@ import { wardenRulesUseAst } from './warden-rules-use-ast.js';
79
80
  import { webhookRouteCollision } from './webhook-route-collision.js';
80
81
 
81
82
  export type {
83
+ GovernedVocabularyHistoryEvidence,
84
+ GovernedVocabularyHistoryFormObservation,
85
+ GovernedVocabularyHistoryIssue,
82
86
  WardenFix,
83
87
  WardenFixCapability,
84
88
  WardenFixClass,
@@ -106,7 +110,10 @@ export {
106
110
  formatGovernedVocabularyTransitionGuide,
107
111
  getGovernedVocabularyTransition,
108
112
  governedVocabularyLiteralRenameSchema,
113
+ governedVocabularyFileRenameSchema,
114
+ governedVocabularyHistoryProvenanceSchema,
109
115
  governedVocabularyPreserveRuleSchema,
116
+ governedVocabularyProvenancePolicySchema,
110
117
  governedVocabularyRegistrySchema,
111
118
  governedVocabularyScopeSchema,
112
119
  governedVocabularySymbolRenameSchema,
@@ -119,7 +126,9 @@ export {
119
126
  } from './retired-vocabulary.js';
120
127
  export type {
121
128
  GovernedVocabularyLiteralRename,
129
+ GovernedVocabularyHistoryProvenance,
122
130
  GovernedVocabularyPreserveRule,
131
+ GovernedVocabularyProvenancePolicy,
123
132
  GovernedVocabularyScope,
124
133
  GovernedVocabularySymbolRename,
125
134
  GovernedVocabularyTarget,
@@ -157,10 +166,11 @@ export { errorMappingCompleteness } from './error-mapping-completeness.js';
157
166
  export { exampleValid } from './example-valid.js';
158
167
  export { firesDeclarations } from './fires-declarations.js';
159
168
  export { governedSymbolResidue } from './governed-symbol-residue.js';
169
+ export { governedVocabularyPermutationWatch } from './governed-vocabulary-permutation-watch.js';
160
170
  export { incompleteAccessorForStandardOp } from './incomplete-accessor-for-standard-op.js';
161
171
  export { incompleteCrud } from './incomplete-crud.js';
162
172
  export { intentPropagation } from './intent-propagation.js';
163
- export { libraryProjectionCoherence } from './library-projection-coherence.js';
173
+ export { libraryRenderCoherence } from './library-render-coherence.js';
164
174
  export { layerFieldNameDrift } from './layer-field-name-drift.js';
165
175
  export { missingVisibility } from './missing-visibility.js';
166
176
  export { missingReconcile } from './missing-reconcile.js';
@@ -178,7 +188,7 @@ export { implementationReturnsResult } from './implementation-returns-result.js'
178
188
  export { noThrowInDetourRecover } from './no-throw-in-detour-recover.js';
179
189
  export { noTopLevelSurface } from './no-top-level-surface.js';
180
190
  export { orphanedSignal } from './orphaned-signal.js';
181
- export { ownerProjectionParity } from './owner-projection-parity.js';
191
+ export { ownerRenderParity } from './owner-render-parity.js';
182
192
  export { permitGovernance } from './permit-governance.js';
183
193
  export { preferSchemaInference } from './prefer-schema-inference.js';
184
194
  export { publicInternalDeepImports } from './public-internal-deep-imports.js';
@@ -235,6 +245,7 @@ export const wardenRules: ReadonlyMap<string, WardenRule> = new Map<
235
245
  [exampleValid.name, exampleValid],
236
246
  [firesDeclarations.name, firesDeclarations],
237
247
  [governedSymbolResidue.name, governedSymbolResidue],
248
+ [governedVocabularyPermutationWatch.name, governedVocabularyPermutationWatch],
238
249
  [incompleteCrud.name, incompleteCrud],
239
250
  [intentPropagation.name, intentPropagation],
240
251
  [layerFieldNameDrift.name, layerFieldNameDrift],
@@ -242,7 +253,7 @@ export const wardenRules: ReadonlyMap<string, WardenRule> = new Map<
242
253
  [missingReconcile.name, missingReconcile],
243
254
  [onReferencesExist.name, onReferencesExist],
244
255
  [orphanedSignal.name, orphanedSignal],
245
- [ownerProjectionParity.name, ownerProjectionParity],
256
+ [ownerRenderParity.name, ownerRenderParity],
246
257
  [publicExportExampleCoverage.name, publicExportExampleCoverage],
247
258
  [publicInternalDeepImports.name, publicInternalDeepImports],
248
259
  [resourceDeclarations.name, resourceDeclarations],
@@ -297,7 +308,7 @@ export const wardenTopoRules: ReadonlyMap<string, TopoAwareWardenRule> =
297
308
  [cliCommandRouteCoherence.name, cliCommandRouteCoherence],
298
309
  [duplicatePublicContract.name, duplicatePublicContract],
299
310
  [incompleteAccessorForStandardOp.name, incompleteAccessorForStandardOp],
300
- [libraryProjectionCoherence.name, libraryProjectionCoherence],
311
+ [libraryRenderCoherence.name, libraryRenderCoherence],
301
312
  [permitGovernance.name, permitGovernance],
302
313
  [publicOutputSchema.name, publicOutputSchema],
303
314
  [publicUnionOutputDiscriminants.name, publicUnionOutputDiscriminants],
@@ -60,7 +60,7 @@ const buildDiagnostic = (
60
60
  ): WardenDiagnostic => ({
61
61
  filePath,
62
62
  line: offsetToLine(sourceCode, node.start),
63
- message: `layer-field-name-drift: surface-local reserved name set "${name}" can make layer input fields project differently across surfaces. Import LAYER_FIELD_RESERVED_NAMES from @ontrails/core instead.`,
63
+ message: `layer-field-name-drift: surface-local reserved name set "${name}" can make layer input fields render differently across surfaces. Import LAYER_FIELD_RESERVED_NAMES from @ontrails/core instead.`,
64
64
  rule: RULE_NAME,
65
65
  severity: 'error',
66
66
  });
@@ -96,7 +96,7 @@ export const layerFieldNameDrift: WardenRule = {
96
96
  return diagnostics;
97
97
  },
98
98
  description:
99
- 'Prevent surface-local reserved-name sets from drifting layer input field projection across surfaces.',
99
+ 'Prevent surface-local reserved-name sets from drifting layer input field rendering across surfaces.',
100
100
  name: RULE_NAME,
101
101
  severity: 'error',
102
102
  };
@@ -1,12 +1,9 @@
1
1
  import { deriveTopoGraph } from '@ontrails/topography';
2
- import type {
3
- TopoGraph,
4
- TopoGraphLibraryProjection,
5
- } from '@ontrails/topography';
2
+ import type { TopoGraph, TopoGraphLibraryDerived } from '@ontrails/topography';
6
3
 
7
4
  import type { TopoAwareWardenRule, WardenDiagnostic } from './types.js';
8
5
 
9
- const RULE_NAME = 'library-projection-coherence';
6
+ const RULE_NAME = 'library-render-coherence';
10
7
  const TOPO_FILE = '<topo>';
11
8
 
12
9
  const resolveGraph = (
@@ -26,7 +23,7 @@ const collisionDiagnostic = (
26
23
  ): WardenDiagnostic => ({
27
24
  filePath: TOPO_FILE,
28
25
  line: 1,
29
- message: `Library projection export collision on "${exportName}": trails ${renderTrailIds(trailIds)} derive the same package export. Rename one trail or add a library export override before materializing the generated package.`,
26
+ message: `Library rendering export collision on "${exportName}": trails ${renderTrailIds(trailIds)} derive the same package export. Rename one trail or add a library export override before materializing the generated package.`,
30
27
  rule: RULE_NAME,
31
28
  severity: 'error',
32
29
  });
@@ -37,7 +34,7 @@ const missingTargetDiagnostic = (
37
34
  ): WardenDiagnostic => ({
38
35
  filePath: TOPO_FILE,
39
36
  line: 1,
40
- message: `Library projection export "${exportName}" targets unknown trail "${trailId}". Resolved library exports must stay attached to existing trail contracts.`,
37
+ message: `Library rendering export "${exportName}" targets unknown trail "${trailId}". Resolved library exports must stay attached to existing trail contracts.`,
41
38
  rule: RULE_NAME,
42
39
  severity: 'error',
43
40
  });
@@ -48,16 +45,16 @@ const duplicateExportDiagnostic = (
48
45
  ): WardenDiagnostic => ({
49
46
  filePath: TOPO_FILE,
50
47
  line: 1,
51
- message: `Library projection contains duplicate export "${exportName}" for trails ${renderTrailIds(trailIds)}. The resolved projection should record the collision and keep only one emitted export.`,
48
+ message: `Library rendering contains duplicate export "${exportName}" for trails ${renderTrailIds(trailIds)}. The resolved rendering should record the collision and keep only one emitted export.`,
52
49
  rule: RULE_NAME,
53
50
  severity: 'error',
54
51
  });
55
52
 
56
53
  const collectDuplicateExportDiagnostics = (
57
- projection: TopoGraphLibraryProjection
54
+ rendering: TopoGraphLibraryDerived
58
55
  ): readonly WardenDiagnostic[] => {
59
56
  const trailIdsByExport = new Map<string, string[]>();
60
- for (const entry of projection.exports) {
57
+ for (const entry of rendering.exports) {
61
58
  const trailIds = trailIdsByExport.get(entry.exportName) ?? [];
62
59
  trailIds.push(entry.trailId);
63
60
  trailIdsByExport.set(entry.exportName, trailIds);
@@ -70,14 +67,14 @@ const collectDuplicateExportDiagnostics = (
70
67
  };
71
68
 
72
69
  const collectMissingTargetDiagnostics = (
73
- projection: TopoGraphLibraryProjection,
70
+ rendering: TopoGraphLibraryDerived,
74
71
  knownTrailIds: ReadonlySet<string>
75
72
  ): readonly WardenDiagnostic[] =>
76
- projection.exports
73
+ rendering.exports
77
74
  .filter((entry) => !knownTrailIds.has(entry.trailId))
78
75
  .map((entry) => missingTargetDiagnostic(entry.exportName, entry.trailId));
79
76
 
80
- export const libraryProjectionCoherence: TopoAwareWardenRule = {
77
+ export const libraryRenderCoherence: TopoAwareWardenRule = {
81
78
  checkTopo(topo, context) {
82
79
  const graph = resolveGraph(topo, context?.graph);
83
80
  if (!graph.library) {
@@ -94,7 +91,7 @@ export const libraryProjectionCoherence: TopoAwareWardenRule = {
94
91
  ];
95
92
  },
96
93
  description:
97
- 'Ensure resolved library projection exports are collision-free and target existing trails.',
94
+ 'Ensure resolved library rendering exports are collision-free and target existing trails.',
98
95
  name: RULE_NAME,
99
96
  severity: 'error',
100
97
  };
@@ -84,9 +84,10 @@ const concernByRuleName: Partial<Record<string, WardenRuleConcern>> = {
84
84
  'fires-declarations': 'signals',
85
85
  'fork-without-preserved-implementation': 'lifecycle',
86
86
  'governed-symbol-residue': 'lifecycle',
87
+ 'governed-vocabulary-permutation-watch': 'lifecycle',
87
88
  'implementation-returns-result': 'results',
88
89
  'intent-propagation': 'composition',
89
- 'library-projection-coherence': 'meta',
90
+ 'library-render-coherence': 'meta',
90
91
  'marker-schema-unsupported': 'lifecycle',
91
92
  'missing-reconcile': 'resources',
92
93
  'missing-visibility': 'composition',
@@ -316,10 +317,46 @@ const builtinWardenRuleMetadataInput = {
316
317
  'governed-symbol-residue': {
317
318
  ...durableExternal,
318
319
  fix: { class: 'term-rewrite', safety: 'safe' },
320
+ guidance: {
321
+ docs: [
322
+ {
323
+ label: 'Vocabulary transition workflow',
324
+ path: 'docs/releases/v1-vocabulary-transition-workflow.md',
325
+ },
326
+ ],
327
+ steps: [
328
+ 'Run the governed transition through Regrade so committed history proves the primary migration.',
329
+ 'Use manual edits only for review or cleanup after Regrade exhausts the safe slice.',
330
+ 'Keep the transition registry and committed history evidence aligned.',
331
+ ],
332
+ summary:
333
+ 'Require committed Regrade evidence before completing a governed vocabulary migration.',
334
+ },
319
335
  invariant:
320
- 'Active governed vocabulary symbol renames do not leave retired identifiers in source.',
336
+ 'Governed vocabulary transitions carry committed Regrade provenance and do not leave or reintroduce retired identifiers.',
321
337
  tier: 'source-static',
322
338
  },
339
+ 'governed-vocabulary-permutation-watch': {
340
+ guidance: {
341
+ docs: [
342
+ {
343
+ label: 'Vocabulary transition workflow',
344
+ path: 'docs/releases/v1-vocabulary-transition-workflow.md',
345
+ },
346
+ ],
347
+ steps: [
348
+ 'Add the unknown form to the governed vocabulary plan and run an incremental Regrade plan.',
349
+ 'If the form is intentionally unrelated or preserved, classify it so later history suppresses the advisory.',
350
+ ],
351
+ summary:
352
+ 'Classify unknown governed-stem permutations recorded by committed Regrade history.',
353
+ },
354
+ invariant:
355
+ 'Committed Regrade history keeps unknown governed-stem permutations visible until they are classified.',
356
+ lifecycle: { state: 'durable' },
357
+ scope: 'advisory',
358
+ tier: 'project-static',
359
+ },
323
360
  'implementation-returns-result': {
324
361
  ...durableExternal,
325
362
  invariant: 'Implementations return Result values.',
@@ -343,10 +380,10 @@ const builtinWardenRuleMetadataInput = {
343
380
  'layer-field-name-drift': {
344
381
  ...durableExternal,
345
382
  invariant:
346
- 'Layer input field reserved names are shared across surface projections.',
383
+ 'Layer input field reserved names are shared across surface renderings.',
347
384
  tier: 'source-static',
348
385
  },
349
- 'library-projection-coherence': {
386
+ 'library-render-coherence': {
350
387
  ...durableExternal,
351
388
  guidance: {
352
389
  docs: [
@@ -361,20 +398,20 @@ const builtinWardenRuleMetadataInput = {
361
398
  ],
362
399
  steps: [
363
400
  'Rename one source trail or add an explicit library export override before generating a package.',
364
- 'Keep serialized library projection exports attached to existing trail IDs.',
365
- 'Run the generated-package smoke after repairing projection drift.',
401
+ 'Keep serialized derived library exports attached to existing trail IDs.',
402
+ 'Run the generated-package smoke after repairing rendering drift.',
366
403
  ],
367
404
  summary:
368
- 'Keep resolved library projection exports collision-free and attached to one trail contract.',
405
+ 'Keep resolved derived library exports collision-free and attached to one trail contract.',
369
406
  },
370
407
  invariant:
371
- 'Resolved library projection exports are collision-free and target existing trails.',
408
+ 'Resolved derived library exports are collision-free and target existing trails.',
372
409
  tier: 'topo-aware',
373
410
  },
374
411
  'marker-schema-unsupported': {
375
412
  ...durableExternal,
376
413
  invariant:
377
- 'Versioned schemas stay inside the supported marker projection subset.',
414
+ 'Versioned schemas stay inside the supported marker derivation subset.',
378
415
  tier: 'source-static',
379
416
  },
380
417
  'missing-reconcile': {
@@ -503,8 +540,8 @@ const builtinWardenRuleMetadataInput = {
503
540
  'Derived store signals are consumed by matching trail on: consumers.',
504
541
  tier: 'project-static',
505
542
  },
506
- 'owner-projection-parity': {
507
- invariant: 'Framework projections stay aligned with owner exports.',
543
+ 'owner-render-parity': {
544
+ invariant: 'Framework renderings stay aligned with owner exports.',
508
545
  lifecycle: { state: 'durable' },
509
546
  scope: 'internal',
510
547
  tier: 'source-static',
@@ -565,11 +602,11 @@ const builtinWardenRuleMetadataInput = {
565
602
  docs: [trailContractDocs, wardenDocs],
566
603
  relatedRules: ['public-union-output-discriminants'],
567
604
  steps: [
568
- 'Add an explicit output schema to public trails that can be projected onto MCP or HTTP surfaces.',
605
+ 'Add an explicit output schema to public trails that can be rendered onto MCP or HTTP surfaces.',
569
606
  'If the trail is composition-only, mark it visibility: "internal" instead of exposing it by default.',
570
607
  ],
571
608
  summary:
572
- 'Make public surface result contracts explicit before MCP/HTTP projection.',
609
+ 'Make public surface result contracts explicit before MCP/HTTP rendering.',
573
610
  },
574
611
  invariant: 'Public MCP/HTTP surface trails declare output schemas.',
575
612
  tier: 'topo-aware',
@@ -709,7 +746,7 @@ const builtinWardenRuleMetadataInput = {
709
746
  'Record explicit visibility-widening acceptance and stable-description metadata when a trailhead intentionally widens visibility.',
710
747
  ],
711
748
  summary:
712
- 'Keep trailhead maps reviewable before they reach MCP projection.',
749
+ 'Keep trailhead maps reviewable before they reach MCP rendering.',
713
750
  },
714
751
  invariant:
715
752
  'Trailhead maps avoid selector overlap, hidden visibility widening, and drift-prone dynamic selectors.',