@ontrails/warden 1.0.0-beta.42 → 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.
- package/CHANGELOG.md +55 -0
- package/package.json +10 -10
- package/src/adapter-check.ts +1 -1
- package/src/cli.ts +81 -5
- package/src/index.ts +17 -4
- package/src/regrade-history.ts +599 -0
- package/src/rules/cli-command-route-coherence.ts +6 -6
- package/src/rules/draft-visible-debt.ts +1 -1
- package/src/rules/duplicate-exported-symbol.ts +4 -43
- package/src/rules/governed-symbol-residue.ts +146 -53
- package/src/rules/governed-vocabulary-permutation-watch.ts +76 -0
- package/src/rules/index.ts +17 -6
- package/src/rules/layer-field-name-drift.ts +2 -2
- package/src/rules/{library-projection-coherence.ts → library-render-coherence.ts} +11 -14
- package/src/rules/metadata.ts +51 -14
- package/src/rules/no-retired-cross-vocabulary.ts +0 -1
- package/src/rules/{owner-projection-parity.ts → owner-render-parity.ts} +18 -18
- package/src/rules/public-internal-deep-imports.ts +1 -3
- package/src/rules/public-output-schema.ts +1 -1
- package/src/rules/registry-names.ts +6 -4
- package/src/rules/retired-vocabulary.ts +775 -41
- package/src/rules/surface-overlay-coherence.ts +1 -1
- package/src/rules/trail-versioning-source.ts +1 -1
- package/src/rules/trailhead-override-divergence.ts +4 -4
- package/src/rules/types.ts +51 -5
- package/src/trails/governed-vocabulary-permutation-watch.trail.ts +16 -0
- package/src/trails/index.ts +3 -2
- package/src/trails/layer-field-name-drift.trail.ts +1 -1
- package/src/trails/{library-projection-coherence.trail.ts → library-render-coherence.trail.ts} +6 -6
- package/src/trails/{owner-projection-parity.trail.ts → owner-render-parity.trail.ts} +4 -4
- package/src/trails/public-output-schema.trail.ts +1 -1
- package/src/trails/run.ts +44 -2
- package/src/trails/schema.ts +41 -0
- 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 {
|
|
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 = (
|
|
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:
|
|
402
|
+
message: diagnosticMessageFor(match, transition, history),
|
|
372
403
|
rule: RULE_NAME,
|
|
373
404
|
severity: 'error',
|
|
374
405
|
};
|
|
375
406
|
};
|
|
376
407
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
452
|
+
const renames = activeSymbolRenames(projectContext);
|
|
453
|
+
if (renames.length === 0) {
|
|
454
|
+
return [];
|
|
455
|
+
}
|
|
392
456
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
501
|
+
shorthandIdentifiers,
|
|
502
|
+
targetSegments
|
|
503
|
+
),
|
|
504
|
+
transition,
|
|
505
|
+
history
|
|
506
|
+
)
|
|
507
|
+
);
|
|
508
|
+
});
|
|
431
509
|
|
|
432
|
-
|
|
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
|
-
'
|
|
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
|
+
};
|
package/src/rules/index.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
-
[
|
|
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
|
-
[
|
|
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
|
|
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
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
54
|
+
rendering: TopoGraphLibraryDerived
|
|
58
55
|
): readonly WardenDiagnostic[] => {
|
|
59
56
|
const trailIdsByExport = new Map<string, string[]>();
|
|
60
|
-
for (const entry of
|
|
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
|
-
|
|
70
|
+
rendering: TopoGraphLibraryDerived,
|
|
74
71
|
knownTrailIds: ReadonlySet<string>
|
|
75
72
|
): readonly WardenDiagnostic[] =>
|
|
76
|
-
|
|
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
|
|
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
|
|
94
|
+
'Ensure resolved library rendering exports are collision-free and target existing trails.',
|
|
98
95
|
name: RULE_NAME,
|
|
99
96
|
severity: 'error',
|
|
100
97
|
};
|
package/src/rules/metadata.ts
CHANGED
|
@@ -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-
|
|
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
|
-
'
|
|
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
|
|
383
|
+
'Layer input field reserved names are shared across surface renderings.',
|
|
347
384
|
tier: 'source-static',
|
|
348
385
|
},
|
|
349
|
-
'library-
|
|
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
|
|
365
|
-
'Run the generated-package smoke after repairing
|
|
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
|
|
405
|
+
'Keep resolved derived library exports collision-free and attached to one trail contract.',
|
|
369
406
|
},
|
|
370
407
|
invariant:
|
|
371
|
-
'Resolved library
|
|
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
|
|
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-
|
|
507
|
-
invariant: 'Framework
|
|
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
|
|
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
|
|
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
|
|
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.',
|