@ontrails/warden 1.0.0-beta.3 → 1.0.0-beta.32
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 +674 -9
- package/README.md +133 -26
- package/bin/warden.ts +51 -0
- package/package.json +28 -5
- package/src/adapter-check.ts +136 -0
- package/src/ast.ts +137 -0
- package/src/cli.ts +1451 -104
- package/src/command.ts +966 -0
- package/src/config.ts +193 -0
- package/src/draft.ts +22 -0
- package/src/drift.ts +122 -22
- package/src/fix.ts +120 -0
- package/src/formatters.ts +79 -9
- package/src/guide.ts +245 -0
- package/src/index.ts +217 -14
- package/src/project-context.ts +192 -0
- package/src/project-rules.ts +290 -0
- package/src/resolve.ts +531 -0
- package/src/rules/activation-orphan.ts +97 -0
- package/src/rules/ast.ts +4008 -86
- package/src/rules/circular-refs.ts +154 -0
- package/src/rules/cli-command-route-coherence.ts +177 -0
- package/src/rules/composes-declarations.ts +837 -0
- package/src/rules/context-no-surface-types.ts +78 -15
- package/src/rules/contour-exists.ts +251 -0
- package/src/rules/contour-ids.ts +15 -0
- package/src/rules/dead-internal-trail.ts +161 -0
- package/src/rules/dead-public-trail.ts +258 -0
- package/src/rules/draft-file-marking.ts +160 -0
- package/src/rules/draft-visible-debt.ts +87 -0
- package/src/rules/duplicate-public-contract.ts +91 -0
- package/src/rules/error-mapping-completeness.ts +290 -0
- package/src/rules/example-valid.ts +395 -0
- package/src/rules/fires-declarations.ts +735 -0
- package/src/rules/implementation-returns-result.ts +1409 -166
- package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
- package/src/rules/incomplete-crud.ts +581 -0
- package/src/rules/index.ts +235 -18
- package/src/rules/intent-propagation.ts +127 -0
- package/src/rules/layer-field-name-drift.ts +102 -0
- package/src/rules/library-projection-coherence.ts +100 -0
- package/src/rules/metadata.ts +755 -0
- package/src/rules/missing-reconcile.ts +98 -0
- package/src/rules/missing-visibility.ts +110 -0
- package/src/rules/no-destructured-compose.ts +194 -0
- package/src/rules/no-dev-permit-in-source.ts +99 -0
- package/src/rules/no-direct-implementation-call.ts +7 -7
- package/src/rules/no-legacy-layer-imports.ts +211 -0
- package/src/rules/no-native-error-result.ts +118 -0
- package/src/rules/no-redundant-result-error-wrap.ts +384 -0
- package/src/rules/no-retired-cross-vocabulary.ts +194 -0
- package/src/rules/no-sync-result-assumption.ts +1135 -98
- package/src/rules/no-throw-in-detour-recover.ts +225 -0
- package/src/rules/no-throw-in-implementation.ts +10 -9
- package/src/rules/no-top-level-surface.ts +371 -0
- package/src/rules/on-references-exist.ts +194 -0
- package/src/rules/orphaned-signal.ts +150 -0
- package/src/rules/owner-projection-parity.ts +143 -0
- package/src/rules/permit-governance.ts +25 -0
- package/src/rules/public-export-example-coverage.ts +562 -0
- package/src/rules/public-internal-deep-imports.ts +457 -0
- package/src/rules/public-output-schema.ts +29 -0
- package/src/rules/public-union-output-discriminants.ts +150 -0
- package/src/rules/read-intent-fires.ts +187 -0
- package/src/rules/reference-exists.ts +98 -0
- package/src/rules/registry-names.ts +155 -0
- package/src/rules/resolved-import-boundary.ts +146 -0
- package/src/rules/resource-declarations.ts +693 -0
- package/src/rules/resource-exists.ts +179 -0
- package/src/rules/resource-id-grammar.ts +65 -0
- package/src/rules/resource-mock-coverage.ts +115 -0
- package/src/rules/scan.ts +38 -25
- package/src/rules/scheduled-destroy-intent.ts +44 -0
- package/src/rules/signal-graph-coaching.ts +191 -0
- package/src/rules/specs.ts +9 -5
- package/src/rules/static-resource-accessor-preference.ts +649 -0
- package/src/rules/surface-facet-coherence.ts +362 -0
- package/src/rules/trail-fork-coaching.ts +616 -0
- package/src/rules/trail-versioning-source.ts +1072 -0
- package/src/rules/trail-versioning-topo.ts +172 -0
- package/src/rules/types.ts +297 -8
- package/src/rules/unmaterialized-activation-source.ts +84 -0
- package/src/rules/unreachable-detour-shadowing.ts +339 -0
- package/src/rules/valid-describe-refs.ts +162 -32
- package/src/rules/valid-detour-contract.ts +78 -0
- package/src/rules/warden-export-symmetry.ts +540 -0
- package/src/rules/warden-rules-use-ast.ts +1114 -0
- package/src/rules/webhook-route-collision.ts +243 -0
- package/src/trails/activation-orphan.trail.ts +84 -0
- package/src/trails/circular-refs.trail.ts +29 -0
- package/src/trails/cli-command-route-coherence.trail.ts +47 -0
- package/src/trails/composes-declarations.trail.ts +22 -0
- package/src/trails/context-no-surface-types.trail.ts +21 -0
- package/src/trails/contour-exists.trail.ts +21 -0
- package/src/trails/dead-internal-trail.trail.ts +26 -0
- package/src/trails/dead-public-trail.trail.ts +31 -0
- package/src/trails/deprecation-without-guidance.trail.ts +21 -0
- package/src/trails/draft-file-marking.trail.ts +16 -0
- package/src/trails/draft-visible-debt.trail.ts +16 -0
- package/src/trails/duplicate-public-contract.trail.ts +47 -0
- package/src/trails/error-mapping-completeness.trail.ts +30 -0
- package/src/trails/example-valid.trail.ts +25 -0
- package/src/trails/fires-declarations.trail.ts +23 -0
- package/src/trails/fork-without-preserved-blaze.trail.ts +31 -0
- package/src/trails/implementation-returns-result.trail.ts +20 -0
- package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
- package/src/trails/incomplete-crud.trail.ts +39 -0
- package/src/trails/index.ts +83 -0
- package/src/trails/intent-propagation.trail.ts +30 -0
- package/src/trails/layer-field-name-drift.trail.ts +39 -0
- package/src/trails/library-projection-coherence.trail.ts +43 -0
- package/src/trails/marker-schema-unsupported.trail.ts +23 -0
- package/src/trails/missing-reconcile.trail.ts +33 -0
- package/src/trails/missing-visibility.trail.ts +22 -0
- package/src/trails/no-destructured-compose.trail.ts +44 -0
- package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
- package/src/trails/no-direct-implementation-call.trail.ts +16 -0
- package/src/trails/no-legacy-layer-imports.trail.ts +41 -0
- package/src/trails/no-native-error-result.trail.ts +18 -0
- package/src/trails/no-redundant-result-error-wrap.trail.ts +55 -0
- package/src/trails/no-retired-cross-vocabulary.trail.ts +42 -0
- package/src/trails/no-sync-result-assumption.trail.ts +19 -0
- package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
- package/src/trails/no-throw-in-implementation.trail.ts +20 -0
- package/src/trails/no-top-level-surface.trail.ts +43 -0
- package/src/trails/on-references-exist.trail.ts +21 -0
- package/src/trails/orphaned-signal.trail.ts +36 -0
- package/src/trails/owner-projection-parity.trail.ts +26 -0
- package/src/trails/pending-force.trail.ts +21 -0
- package/src/trails/permit-governance.trail.ts +51 -0
- package/src/trails/prefer-schema-inference.trail.ts +21 -0
- package/src/trails/public-export-example-coverage.trail.ts +16 -0
- package/src/trails/public-internal-deep-imports.trail.ts +94 -0
- package/src/trails/public-output-schema.trail.ts +55 -0
- package/src/trails/public-union-output-discriminants.trail.ts +33 -0
- package/src/trails/read-intent-fires.trail.ts +20 -0
- package/src/trails/reference-exists.trail.ts +25 -0
- package/src/trails/resolved-import-boundary.trail.ts +109 -0
- package/src/trails/resource-declarations.trail.ts +25 -0
- package/src/trails/resource-exists.trail.ts +27 -0
- package/src/trails/resource-id-grammar.trail.ts +39 -0
- package/src/trails/resource-mock-coverage.trail.ts +40 -0
- package/src/trails/run.ts +162 -0
- package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
- package/src/trails/schema.ts +198 -0
- package/src/trails/signal-graph-coaching.trail.ts +77 -0
- package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
- package/src/trails/surface-facet-coherence.trail.ts +25 -0
- package/src/trails/topo.ts +6 -0
- package/src/trails/trail-fork-coaching.trail.ts +42 -0
- package/src/trails/unmaterialized-activation-source.trail.ts +72 -0
- package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
- package/src/trails/valid-describe-refs.trail.ts +18 -0
- package/src/trails/valid-detour-contract.trail.ts +71 -0
- package/src/trails/version-gap.trail.ts +35 -0
- package/src/trails/version-pinned-compose.trail.ts +23 -0
- package/src/trails/version-without-examples.trail.ts +38 -0
- package/src/trails/warden-export-symmetry.trail.ts +16 -0
- package/src/trails/warden-rules-use-ast.trail.ts +64 -0
- package/src/trails/webhook-route-collision.trail.ts +50 -0
- package/src/trails/wrap-rule.ts +214 -0
- package/src/workspaces.ts +199 -0
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/cli.d.ts +0 -46
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -221
- package/dist/cli.js.map +0 -1
- package/dist/drift.d.ts +0 -26
- package/dist/drift.d.ts.map +0 -1
- package/dist/drift.js +0 -27
- package/dist/drift.js.map +0 -1
- package/dist/formatters.d.ts +0 -29
- package/dist/formatters.d.ts.map +0 -1
- package/dist/formatters.js +0 -87
- package/dist/formatters.js.map +0 -1
- package/dist/index.d.ts +0 -26
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -26
- package/dist/index.js.map +0 -1
- package/dist/rules/ast.d.ts +0 -41
- package/dist/rules/ast.d.ts.map +0 -1
- package/dist/rules/ast.js +0 -163
- package/dist/rules/ast.js.map +0 -1
- package/dist/rules/context-no-surface-types.d.ts +0 -12
- package/dist/rules/context-no-surface-types.d.ts.map +0 -1
- package/dist/rules/context-no-surface-types.js +0 -96
- package/dist/rules/context-no-surface-types.js.map +0 -1
- package/dist/rules/implementation-returns-result.d.ts +0 -13
- package/dist/rules/implementation-returns-result.d.ts.map +0 -1
- package/dist/rules/implementation-returns-result.js +0 -277
- package/dist/rules/implementation-returns-result.js.map +0 -1
- package/dist/rules/index.d.ts +0 -22
- package/dist/rules/index.d.ts.map +0 -1
- package/dist/rules/index.js +0 -41
- package/dist/rules/index.js.map +0 -1
- package/dist/rules/no-direct-impl-in-route.d.ts +0 -12
- package/dist/rules/no-direct-impl-in-route.d.ts.map +0 -1
- package/dist/rules/no-direct-impl-in-route.js +0 -46
- package/dist/rules/no-direct-impl-in-route.js.map +0 -1
- package/dist/rules/no-direct-implementation-call.d.ts +0 -12
- package/dist/rules/no-direct-implementation-call.d.ts.map +0 -1
- package/dist/rules/no-direct-implementation-call.js +0 -39
- package/dist/rules/no-direct-implementation-call.js.map +0 -1
- package/dist/rules/no-sync-result-assumption.d.ts +0 -6
- package/dist/rules/no-sync-result-assumption.d.ts.map +0 -1
- package/dist/rules/no-sync-result-assumption.js +0 -98
- package/dist/rules/no-sync-result-assumption.js.map +0 -1
- package/dist/rules/no-throw-in-detour-target.d.ts +0 -12
- package/dist/rules/no-throw-in-detour-target.d.ts.map +0 -1
- package/dist/rules/no-throw-in-detour-target.js +0 -87
- package/dist/rules/no-throw-in-detour-target.js.map +0 -1
- package/dist/rules/no-throw-in-implementation.d.ts +0 -9
- package/dist/rules/no-throw-in-implementation.d.ts.map +0 -1
- package/dist/rules/no-throw-in-implementation.js +0 -34
- package/dist/rules/no-throw-in-implementation.js.map +0 -1
- package/dist/rules/prefer-schema-inference.d.ts +0 -7
- package/dist/rules/prefer-schema-inference.d.ts.map +0 -1
- package/dist/rules/prefer-schema-inference.js +0 -86
- package/dist/rules/prefer-schema-inference.js.map +0 -1
- package/dist/rules/scan.d.ts +0 -8
- package/dist/rules/scan.d.ts.map +0 -1
- package/dist/rules/scan.js +0 -32
- package/dist/rules/scan.js.map +0 -1
- package/dist/rules/specs.d.ts +0 -29
- package/dist/rules/specs.d.ts.map +0 -1
- package/dist/rules/specs.js +0 -192
- package/dist/rules/specs.js.map +0 -1
- package/dist/rules/structure.d.ts +0 -13
- package/dist/rules/structure.d.ts.map +0 -1
- package/dist/rules/structure.js +0 -142
- package/dist/rules/structure.js.map +0 -1
- package/dist/rules/types.d.ts +0 -52
- package/dist/rules/types.d.ts.map +0 -1
- package/dist/rules/types.js +0 -2
- package/dist/rules/types.js.map +0 -1
- package/dist/rules/valid-describe-refs.d.ts +0 -7
- package/dist/rules/valid-describe-refs.d.ts.map +0 -1
- package/dist/rules/valid-describe-refs.js +0 -51
- package/dist/rules/valid-describe-refs.js.map +0 -1
- package/dist/rules/valid-detour-refs.d.ts +0 -6
- package/dist/rules/valid-detour-refs.d.ts.map +0 -1
- package/dist/rules/valid-detour-refs.js +0 -116
- package/dist/rules/valid-detour-refs.js.map +0 -1
- package/src/__tests__/cli.test.ts +0 -198
- package/src/__tests__/drift.test.ts +0 -74
- package/src/__tests__/formatters.test.ts +0 -157
- package/src/__tests__/implementation-returns-result.test.ts +0 -128
- package/src/__tests__/no-direct-implementation-call.test.ts +0 -83
- package/src/__tests__/no-sync-result-assumption.test.ts +0 -85
- package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
- package/src/__tests__/prefer-schema-inference.test.ts +0 -84
- package/src/__tests__/rules.test.ts +0 -215
- package/src/__tests__/valid-describe-refs.test.ts +0 -60
- package/src/rules/no-direct-impl-in-route.ts +0 -77
- package/src/rules/no-throw-in-detour-target.ts +0 -150
- package/src/rules/valid-detour-refs.ts +0 -187
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collectContourReferenceTargetsByName,
|
|
3
|
+
findContourDefinitions,
|
|
4
|
+
offsetToLine,
|
|
5
|
+
parse,
|
|
6
|
+
} from './ast.js';
|
|
7
|
+
import type { AstNode } from './ast.js';
|
|
8
|
+
import { isTestFile } from './scan.js';
|
|
9
|
+
import type {
|
|
10
|
+
ProjectAwareWardenRule,
|
|
11
|
+
ProjectContext,
|
|
12
|
+
WardenDiagnostic,
|
|
13
|
+
} from './types.js';
|
|
14
|
+
|
|
15
|
+
const mergeReferenceGraphs = (
|
|
16
|
+
localGraph: ReadonlyMap<string, readonly string[]>,
|
|
17
|
+
contextGraph?: ReadonlyMap<string, readonly string[]>
|
|
18
|
+
): ReadonlyMap<string, readonly string[]> => {
|
|
19
|
+
const merged = new Map<string, Set<string>>();
|
|
20
|
+
|
|
21
|
+
const addTargets = (source: string, targets: readonly string[]): void => {
|
|
22
|
+
const existing = merged.get(source);
|
|
23
|
+
if (existing) {
|
|
24
|
+
for (const target of targets) {
|
|
25
|
+
existing.add(target);
|
|
26
|
+
}
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
merged.set(source, new Set(targets));
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
for (const [source, targets] of contextGraph ?? []) {
|
|
34
|
+
addTargets(source, targets);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const [source, targets] of localGraph) {
|
|
38
|
+
addTargets(source, targets);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return new Map(
|
|
42
|
+
[...merged.entries()].map(([source, targets]) => [source, [...targets]])
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const findCyclePath = (
|
|
47
|
+
start: string,
|
|
48
|
+
graph: ReadonlyMap<string, readonly string[]>,
|
|
49
|
+
current = start,
|
|
50
|
+
path: readonly string[] = [start],
|
|
51
|
+
active: ReadonlySet<string> = new Set([start])
|
|
52
|
+
): readonly string[] | null => {
|
|
53
|
+
for (const target of graph.get(current) ?? []) {
|
|
54
|
+
if (target === start) {
|
|
55
|
+
return [...path, target];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (active.has(target)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const cycle = findCyclePath(
|
|
63
|
+
start,
|
|
64
|
+
graph,
|
|
65
|
+
target,
|
|
66
|
+
[...path, target],
|
|
67
|
+
new Set([...active, target])
|
|
68
|
+
);
|
|
69
|
+
if (cycle) {
|
|
70
|
+
return cycle;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return null;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const buildCircularReferenceDiagnostic = (
|
|
78
|
+
contourName: string,
|
|
79
|
+
cyclePath: readonly string[],
|
|
80
|
+
filePath: string,
|
|
81
|
+
line: number
|
|
82
|
+
): WardenDiagnostic => ({
|
|
83
|
+
filePath,
|
|
84
|
+
line,
|
|
85
|
+
message: `Contour "${contourName}" participates in circular contour references: ${cyclePath.join(' -> ')}. Break the cycle by removing one contour reference, or extract the shared shape into a new contour neither side depends on.`,
|
|
86
|
+
rule: 'circular-refs',
|
|
87
|
+
severity: 'warn',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const checkCircularReferences = (
|
|
91
|
+
ast: AstNode,
|
|
92
|
+
sourceCode: string,
|
|
93
|
+
filePath: string,
|
|
94
|
+
graph: ReadonlyMap<string, readonly string[]>
|
|
95
|
+
): readonly WardenDiagnostic[] => {
|
|
96
|
+
if (isTestFile(filePath)) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return findContourDefinitions(ast).flatMap((definition) => {
|
|
101
|
+
const cyclePath = findCyclePath(definition.name, graph);
|
|
102
|
+
if (!cyclePath) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return [
|
|
107
|
+
buildCircularReferenceDiagnostic(
|
|
108
|
+
definition.name,
|
|
109
|
+
cyclePath,
|
|
110
|
+
filePath,
|
|
111
|
+
offsetToLine(sourceCode, definition.start)
|
|
112
|
+
),
|
|
113
|
+
];
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Warns when contour references form a direct or transitive cycle.
|
|
119
|
+
*/
|
|
120
|
+
export const circularRefs: ProjectAwareWardenRule = {
|
|
121
|
+
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
122
|
+
const ast = parse(filePath, sourceCode);
|
|
123
|
+
if (!ast) {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const graph = collectContourReferenceTargetsByName(ast);
|
|
128
|
+
return checkCircularReferences(ast, sourceCode, filePath, graph);
|
|
129
|
+
},
|
|
130
|
+
checkWithContext(
|
|
131
|
+
sourceCode: string,
|
|
132
|
+
filePath: string,
|
|
133
|
+
context: ProjectContext
|
|
134
|
+
): readonly WardenDiagnostic[] {
|
|
135
|
+
const ast = parse(filePath, sourceCode);
|
|
136
|
+
if (!ast) {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const localGraph = collectContourReferenceTargetsByName(
|
|
141
|
+
ast,
|
|
142
|
+
context.knownContourIds
|
|
143
|
+
);
|
|
144
|
+
return checkCircularReferences(
|
|
145
|
+
ast,
|
|
146
|
+
sourceCode,
|
|
147
|
+
filePath,
|
|
148
|
+
mergeReferenceGraphs(localGraph, context.contourReferencesByName)
|
|
149
|
+
);
|
|
150
|
+
},
|
|
151
|
+
description: 'Warn when contour references form direct or transitive cycles.',
|
|
152
|
+
name: 'circular-refs',
|
|
153
|
+
severity: 'warn',
|
|
154
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { deriveTrailCliCommandProjection } from '@ontrails/core';
|
|
2
|
+
import type { CliCommandRoute, Topo } from '@ontrails/core';
|
|
3
|
+
import type { TopoGraph } from '@ontrails/topographer';
|
|
4
|
+
|
|
5
|
+
import type { TopoAwareWardenRule, WardenDiagnostic } from './types.js';
|
|
6
|
+
|
|
7
|
+
const RULE_NAME = 'cli-command-route-coherence';
|
|
8
|
+
const TOPO_FILE = '<topo>';
|
|
9
|
+
|
|
10
|
+
interface RouteClaim {
|
|
11
|
+
readonly kind: CliCommandRoute['kind'];
|
|
12
|
+
readonly path: readonly string[];
|
|
13
|
+
readonly source: CliCommandRoute['source'];
|
|
14
|
+
readonly trailId: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const routeKey = (path: readonly string[]): string => path.join('\0');
|
|
18
|
+
|
|
19
|
+
const renderPath = (path: readonly string[]): string => path.join(' ');
|
|
20
|
+
|
|
21
|
+
const claimLabel = (claim: RouteClaim): string =>
|
|
22
|
+
`${claim.kind} route for trail "${claim.trailId}" (${claim.source})`;
|
|
23
|
+
|
|
24
|
+
const buildProjectionDiagnostic = (
|
|
25
|
+
trailId: string,
|
|
26
|
+
error: unknown
|
|
27
|
+
): WardenDiagnostic => ({
|
|
28
|
+
filePath: TOPO_FILE,
|
|
29
|
+
line: 1,
|
|
30
|
+
message: `CLI command route projection for trail "${trailId}" is invalid: ${
|
|
31
|
+
error instanceof Error ? error.message : String(error)
|
|
32
|
+
}`,
|
|
33
|
+
rule: RULE_NAME,
|
|
34
|
+
severity: 'error',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const buildCollisionDiagnostic = (
|
|
38
|
+
path: readonly string[],
|
|
39
|
+
claims: readonly RouteClaim[]
|
|
40
|
+
): WardenDiagnostic => ({
|
|
41
|
+
filePath: TOPO_FILE,
|
|
42
|
+
line: 1,
|
|
43
|
+
message: `CLI command route collision on "${renderPath(path)}": ${claims
|
|
44
|
+
.toSorted((a, b) => a.trailId.localeCompare(b.trailId))
|
|
45
|
+
.map(claimLabel)
|
|
46
|
+
.join(
|
|
47
|
+
', '
|
|
48
|
+
)}. Rename or remove one CLI alias so every accepted command path normalizes into exactly one trail contract.`,
|
|
49
|
+
rule: RULE_NAME,
|
|
50
|
+
severity: 'error',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const buildGraphTargetDiagnostic = (
|
|
54
|
+
entryId: string,
|
|
55
|
+
route: CliCommandRoute,
|
|
56
|
+
knownTrailIds: ReadonlySet<string>
|
|
57
|
+
): WardenDiagnostic => ({
|
|
58
|
+
filePath: TOPO_FILE,
|
|
59
|
+
line: 1,
|
|
60
|
+
message: knownTrailIds.has(route.target)
|
|
61
|
+
? `Serialized CLI command route "${renderPath(route.path)}" is stored under trail "${entryId}" but targets "${route.target}". Route facts must stay attached to their owning trail.`
|
|
62
|
+
: `Serialized CLI command route "${renderPath(route.path)}" targets unknown trail "${route.target}". Surface-owned aliases must target existing trail IDs.`,
|
|
63
|
+
rule: RULE_NAME,
|
|
64
|
+
severity: 'error',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const collectTopoClaims = (
|
|
68
|
+
topo: Topo
|
|
69
|
+
): {
|
|
70
|
+
readonly claims: readonly RouteClaim[];
|
|
71
|
+
readonly diagnostics: readonly WardenDiagnostic[];
|
|
72
|
+
} => {
|
|
73
|
+
const claims: RouteClaim[] = [];
|
|
74
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
75
|
+
|
|
76
|
+
for (const trail of topo.list()) {
|
|
77
|
+
try {
|
|
78
|
+
const projection = deriveTrailCliCommandProjection(trail);
|
|
79
|
+
for (const route of projection.routes) {
|
|
80
|
+
claims.push({
|
|
81
|
+
kind: route.kind,
|
|
82
|
+
path: route.path,
|
|
83
|
+
source: route.source,
|
|
84
|
+
trailId: trail.id,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
} catch (error: unknown) {
|
|
88
|
+
diagnostics.push(buildProjectionDiagnostic(trail.id, error));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { claims, diagnostics };
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const collectGraphClaims = (
|
|
96
|
+
graph: TopoGraph | undefined
|
|
97
|
+
): readonly RouteClaim[] => {
|
|
98
|
+
if (graph === undefined) {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const claims: RouteClaim[] = [];
|
|
103
|
+
for (const entry of graph.entries) {
|
|
104
|
+
if (entry.kind !== 'trail') {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
for (const route of entry.cli?.routes ?? []) {
|
|
108
|
+
claims.push({
|
|
109
|
+
kind: route.kind,
|
|
110
|
+
path: route.path,
|
|
111
|
+
source: route.source,
|
|
112
|
+
trailId: entry.id,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return claims;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const collectCollisionDiagnostics = (
|
|
120
|
+
claims: readonly RouteClaim[]
|
|
121
|
+
): readonly WardenDiagnostic[] => {
|
|
122
|
+
const claimsByRoute = new Map<string, RouteClaim[]>();
|
|
123
|
+
for (const claim of claims) {
|
|
124
|
+
const key = routeKey(claim.path);
|
|
125
|
+
const current = claimsByRoute.get(key) ?? [];
|
|
126
|
+
current.push(claim);
|
|
127
|
+
claimsByRoute.set(key, current);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return [...claimsByRoute.values()]
|
|
131
|
+
.filter((grouped) => grouped.length > 1)
|
|
132
|
+
.map((grouped) =>
|
|
133
|
+
buildCollisionDiagnostic(grouped[0]?.path ?? [], grouped)
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const collectGraphTargetDiagnostics = (
|
|
138
|
+
graph: TopoGraph | undefined,
|
|
139
|
+
knownTrailIds: ReadonlySet<string>
|
|
140
|
+
): readonly WardenDiagnostic[] => {
|
|
141
|
+
if (graph === undefined) {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
146
|
+
for (const entry of graph.entries) {
|
|
147
|
+
if (entry.kind !== 'trail') {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
for (const route of entry.cli?.routes ?? []) {
|
|
151
|
+
if (route.target !== entry.id || !knownTrailIds.has(route.target)) {
|
|
152
|
+
diagnostics.push(
|
|
153
|
+
buildGraphTargetDiagnostic(entry.id, route, knownTrailIds)
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return diagnostics;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export const cliCommandRouteCoherence: TopoAwareWardenRule = {
|
|
162
|
+
checkTopo(topo, context) {
|
|
163
|
+
const { claims, diagnostics } = collectTopoClaims(topo);
|
|
164
|
+
const knownTrailIds = new Set(topo.trails.keys());
|
|
165
|
+
const routeClaims =
|
|
166
|
+
context?.graph === undefined ? claims : collectGraphClaims(context.graph);
|
|
167
|
+
return [
|
|
168
|
+
...diagnostics,
|
|
169
|
+
...collectCollisionDiagnostics(routeClaims),
|
|
170
|
+
...collectGraphTargetDiagnostics(context?.graph, knownTrailIds),
|
|
171
|
+
];
|
|
172
|
+
},
|
|
173
|
+
description:
|
|
174
|
+
'Ensure CLI command routes and aliases resolve to one coherent trail contract.',
|
|
175
|
+
name: RULE_NAME,
|
|
176
|
+
severity: 'error',
|
|
177
|
+
};
|