@ontrails/warden 1.0.0-beta.4 → 1.0.0-beta.42
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 +837 -13
- package/README.md +133 -26
- package/bin/warden.ts +51 -0
- package/package.json +27 -6
- package/src/adapter-check.ts +136 -0
- package/src/cli.ts +1570 -105
- package/src/command.ts +986 -0
- package/src/config.ts +193 -0
- package/src/draft.ts +22 -0
- package/src/drift.ts +233 -23
- package/src/fix.ts +126 -0
- package/src/formatters.ts +78 -13
- package/src/guide.ts +245 -0
- package/src/index.ts +247 -15
- package/src/project-context.ts +446 -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/captured-kernel.ts +375 -0
- package/src/rules/circular-refs.ts +150 -0
- package/src/rules/cli-command-route-coherence.ts +177 -0
- package/src/rules/composes-declarations.ts +839 -0
- package/src/rules/context-no-surface-types.ts +79 -15
- 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 +155 -0
- package/src/rules/draft-visible-debt.ts +82 -0
- package/src/rules/duplicate-exported-symbol.ts +211 -0
- package/src/rules/duplicate-public-contract.ts +137 -0
- package/src/rules/entity-exists.ts +254 -0
- package/src/rules/entity-ids.ts +15 -0
- package/src/rules/error-mapping-completeness.ts +290 -0
- package/src/rules/example-valid.ts +395 -0
- package/src/rules/fires-declarations.ts +740 -0
- package/src/rules/governed-symbol-residue.ts +438 -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 +583 -0
- package/src/rules/index.ts +277 -10
- package/src/rules/intent-propagation.ts +125 -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 +871 -0
- package/src/rules/missing-reconcile.ts +97 -0
- package/src/rules/missing-visibility.ts +111 -0
- package/src/rules/no-destructured-compose.ts +196 -0
- package/src/rules/no-dev-permit-in-source.ts +99 -0
- package/src/rules/no-direct-implementation-call.ts +12 -7
- package/src/rules/no-legacy-cli-alias-export.ts +247 -0
- 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 +204 -0
- package/src/rules/no-sync-result-assumption.ts +1141 -98
- package/src/rules/no-throw-in-detour-recover.ts +225 -0
- package/src/rules/no-throw-in-implementation.ts +15 -8
- 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 +149 -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 +573 -0
- package/src/rules/public-internal-deep-imports.ts +456 -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 +188 -0
- package/src/rules/reference-exists.ts +97 -0
- package/src/rules/registry-names.ts +167 -0
- package/src/rules/resolved-import-boundary.ts +146 -0
- package/src/rules/resource-declarations.ts +697 -0
- package/src/rules/resource-exists.ts +181 -0
- package/src/rules/resource-id-grammar.ts +65 -0
- package/src/rules/resource-mock-coverage.ts +115 -0
- package/src/rules/retired-vocabulary.ts +633 -0
- package/src/rules/scan.ts +38 -25
- package/src/rules/scheduled-destroy-intent.ts +44 -0
- package/src/rules/signal-graph-coaching.ts +220 -0
- package/src/rules/source/composition.ts +165 -0
- package/src/rules/source/drafts.ts +164 -0
- package/src/rules/source/entities.ts +618 -0
- package/src/rules/source/pragmas.ts +45 -0
- package/src/rules/source/resources.ts +64 -0
- package/src/rules/source/signals.ts +397 -0
- package/src/rules/source/stores.ts +310 -0
- package/src/rules/specs.ts +9 -5
- package/src/rules/static-resource-accessor-preference.ts +653 -0
- package/src/rules/surface-overlay-coherence.ts +262 -0
- package/src/rules/surface-trailhead-coherence.ts +366 -0
- package/src/rules/trail-fork-coaching.ts +625 -0
- package/src/rules/trail-versioning-source.ts +1076 -0
- package/src/rules/trail-versioning-topo.ts +172 -0
- package/src/rules/trailhead-override-divergence.ts +356 -0
- package/src/rules/types.ts +354 -8
- package/src/rules/unmaterialized-activation-source.ts +85 -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 +1109 -0
- package/src/rules/webhook-route-collision.ts +306 -0
- package/src/trails/activation-orphan.trail.ts +84 -0
- package/src/trails/captured-kernel.trail.ts +108 -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/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-exported-symbol.trail.ts +48 -0
- package/src/trails/duplicate-public-contract.trail.ts +47 -0
- package/src/trails/entity-exists.trail.ts +21 -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-implementation.trail.ts +31 -0
- package/src/trails/governed-symbol-residue.trail.ts +24 -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 +89 -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-cli-alias-export.trail.ts +41 -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 +160 -0
- package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
- package/src/trails/schema.ts +237 -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-overlay-coherence.trail.ts +24 -0
- package/src/trails/surface-trailhead-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/trailhead-override-divergence.trail.ts +47 -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 +224 -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 -218
- 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 -161
- 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 -15
- package/dist/rules/index.d.ts.map +0 -1
- package/dist/rules/index.js +0 -34
- 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 -47
- 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 -129
- 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 -227
- package/src/__tests__/valid-describe-refs.test.ts +0 -60
- package/src/rules/ast.ts +0 -213
- package/src/rules/no-direct-impl-in-route.ts +0 -81
- 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,172 @@
|
|
|
1
|
+
import { deriveTopoGraph } from '@ontrails/topography';
|
|
2
|
+
import type {
|
|
3
|
+
TopoGraph,
|
|
4
|
+
TopoGraphEntry,
|
|
5
|
+
TopoGraphForceEntry,
|
|
6
|
+
TopoGraphVersionEntry,
|
|
7
|
+
} from '@ontrails/topography';
|
|
8
|
+
import type { Topo } from '@ontrails/core';
|
|
9
|
+
|
|
10
|
+
import type { TopoAwareWardenRule, WardenDiagnostic } from './types.js';
|
|
11
|
+
|
|
12
|
+
const TOPO_FILE = '<topo>';
|
|
13
|
+
|
|
14
|
+
const isArchived = (entry: TopoGraphVersionEntry): boolean =>
|
|
15
|
+
entry.status?.state === 'archived';
|
|
16
|
+
|
|
17
|
+
const isDeprecatedWithoutGuidance = (entry: TopoGraphVersionEntry): boolean =>
|
|
18
|
+
entry.status?.state === 'deprecated' &&
|
|
19
|
+
entry.status.successor === undefined &&
|
|
20
|
+
entry.status.note === undefined &&
|
|
21
|
+
(entry.status.migration === undefined || entry.status.migration.length === 0);
|
|
22
|
+
|
|
23
|
+
const versionNumbersFor = (entry: TopoGraphEntry): readonly number[] => [
|
|
24
|
+
...(entry.version === undefined ? [] : [entry.version]),
|
|
25
|
+
...Object.keys(entry.versions ?? {}).map(Number),
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const versionEntryName = (trailId: string, version: string): string =>
|
|
29
|
+
`${trailId}@${version}`;
|
|
30
|
+
|
|
31
|
+
const topoDiagnostic = (
|
|
32
|
+
rule: string,
|
|
33
|
+
severity: WardenDiagnostic['severity'],
|
|
34
|
+
message: string
|
|
35
|
+
): WardenDiagnostic => ({
|
|
36
|
+
filePath: TOPO_FILE,
|
|
37
|
+
line: 1,
|
|
38
|
+
message,
|
|
39
|
+
rule,
|
|
40
|
+
severity,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const trailEntries = (graph: TopoGraph): readonly TopoGraphEntry[] =>
|
|
44
|
+
graph.entries.filter((entry) => entry.kind === 'trail');
|
|
45
|
+
|
|
46
|
+
const graphFor = (
|
|
47
|
+
topo: Topo,
|
|
48
|
+
context: Parameters<TopoAwareWardenRule['checkTopo']>[1]
|
|
49
|
+
): TopoGraph => context?.graph ?? deriveTopoGraph(topo);
|
|
50
|
+
|
|
51
|
+
const forceKey = (force: TopoGraphForceEntry): string =>
|
|
52
|
+
JSON.stringify([
|
|
53
|
+
force.kind,
|
|
54
|
+
force.id,
|
|
55
|
+
force.change,
|
|
56
|
+
force.detail,
|
|
57
|
+
force.reason,
|
|
58
|
+
force.severity,
|
|
59
|
+
force.source,
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
const pendingForceDiagnostic = (force: TopoGraphForceEntry): WardenDiagnostic =>
|
|
63
|
+
topoDiagnostic(
|
|
64
|
+
'pending-force',
|
|
65
|
+
'warn',
|
|
66
|
+
`Trail "${force.id}" has a pending forced topo break (${force.change}: ${force.detail}). Resolve or document the force event before release.`
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
export const deprecationWithoutGuidance: TopoAwareWardenRule = {
|
|
70
|
+
checkTopo(topo, context) {
|
|
71
|
+
return trailEntries(graphFor(topo, context)).flatMap((entry) =>
|
|
72
|
+
Object.entries(entry.versions ?? {}).flatMap(([version, historical]) =>
|
|
73
|
+
isDeprecatedWithoutGuidance(historical)
|
|
74
|
+
? [
|
|
75
|
+
topoDiagnostic(
|
|
76
|
+
'deprecation-without-guidance',
|
|
77
|
+
'error',
|
|
78
|
+
`Trail "${versionEntryName(entry.id, version)}" is deprecated without successor, migration, or note guidance. Add at least one guidance field to the version status.`
|
|
79
|
+
),
|
|
80
|
+
]
|
|
81
|
+
: []
|
|
82
|
+
)
|
|
83
|
+
);
|
|
84
|
+
},
|
|
85
|
+
description:
|
|
86
|
+
'Require deprecated trail version entries to carry successor, migration, or note guidance.',
|
|
87
|
+
name: 'deprecation-without-guidance',
|
|
88
|
+
severity: 'error',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const versionGap: TopoAwareWardenRule = {
|
|
92
|
+
checkTopo(topo, context) {
|
|
93
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
94
|
+
for (const entry of trailEntries(graphFor(topo, context))) {
|
|
95
|
+
if (entry.version === undefined) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const historicalVersions = new Set(
|
|
99
|
+
Object.keys(entry.versions ?? {}).map(Number)
|
|
100
|
+
);
|
|
101
|
+
if (historicalVersions.has(entry.version)) {
|
|
102
|
+
diagnostics.push(
|
|
103
|
+
topoDiagnostic(
|
|
104
|
+
'version-gap',
|
|
105
|
+
'error',
|
|
106
|
+
`Trail "${entry.id}" declares current version ${entry.version} both as current and as a historical entry. Remove the duplicate historical entry.`
|
|
107
|
+
)
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const versions = new Set(versionNumbersFor(entry));
|
|
112
|
+
for (let version = 1; version <= entry.version; version += 1) {
|
|
113
|
+
if (!versions.has(version)) {
|
|
114
|
+
diagnostics.push(
|
|
115
|
+
topoDiagnostic(
|
|
116
|
+
'version-gap',
|
|
117
|
+
'error',
|
|
118
|
+
`Trail "${entry.id}" has a gap before current version ${entry.version}: missing version ${version}. Historical version coverage must be contiguous, including archived entries.`
|
|
119
|
+
)
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return diagnostics;
|
|
125
|
+
},
|
|
126
|
+
description:
|
|
127
|
+
'Require trail version coverage to stay contiguous from v1 through the current version.',
|
|
128
|
+
name: 'version-gap',
|
|
129
|
+
severity: 'error',
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const versionWithoutExamples: TopoAwareWardenRule = {
|
|
133
|
+
checkTopo(topo, context) {
|
|
134
|
+
return trailEntries(graphFor(topo, context)).flatMap((entry) =>
|
|
135
|
+
Object.entries(entry.versions ?? {}).flatMap(([version, historical]) =>
|
|
136
|
+
!isArchived(historical) && (historical.exampleCount ?? 0) === 0
|
|
137
|
+
? [
|
|
138
|
+
topoDiagnostic(
|
|
139
|
+
'version-without-examples',
|
|
140
|
+
'warn',
|
|
141
|
+
`Trail "${versionEntryName(entry.id, version)}" is a live historical version without examples. Add version-entry examples or archive the entry if it no longer participates in runtime negotiation.`
|
|
142
|
+
),
|
|
143
|
+
]
|
|
144
|
+
: []
|
|
145
|
+
)
|
|
146
|
+
);
|
|
147
|
+
},
|
|
148
|
+
description:
|
|
149
|
+
'Warn when live historical trail version entries do not include examples.',
|
|
150
|
+
name: 'version-without-examples',
|
|
151
|
+
severity: 'warn',
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export const pendingForce: TopoAwareWardenRule = {
|
|
155
|
+
checkTopo(topo, context) {
|
|
156
|
+
const graph = graphFor(topo, context);
|
|
157
|
+
const diagnostics = new Map<string, WardenDiagnostic>();
|
|
158
|
+
for (const entry of trailEntries(graph)) {
|
|
159
|
+
for (const force of entry.forces ?? []) {
|
|
160
|
+
diagnostics.set(forceKey(force), pendingForceDiagnostic(force));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
for (const force of graph.forces ?? []) {
|
|
164
|
+
diagnostics.set(forceKey(force), pendingForceDiagnostic(force));
|
|
165
|
+
}
|
|
166
|
+
return [...diagnostics.values()];
|
|
167
|
+
},
|
|
168
|
+
description:
|
|
169
|
+
'Warn when graph-only force audit events remain attached to trail entries or the topo graph.',
|
|
170
|
+
name: 'pending-force',
|
|
171
|
+
severity: 'warn',
|
|
172
|
+
};
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Warn when a call-site MCP trailhead map diverges from the app-authored
|
|
3
|
+
* `surfaces` overlay's `mcp` list bindings.
|
|
4
|
+
*
|
|
5
|
+
* The overlay is the authored, lockable default; a call-site trailhead map
|
|
6
|
+
* is a supported override-in-context that wins at runtime. Divergence between
|
|
7
|
+
* the two is legal but must be visible: an agent reading the committed lock
|
|
8
|
+
* would otherwise trust grouped entries the running surface does not project.
|
|
9
|
+
*
|
|
10
|
+
* Rule-kind note: no Warden rule kind currently sees both the serialized
|
|
11
|
+
* graph overlays and source ASTs directly. This rule is the closest honest
|
|
12
|
+
* shape — a project-aware source rule whose `ProjectContext` carries
|
|
13
|
+
* per-app authored `mcp` binding sets resolved from the run's topo targets
|
|
14
|
+
* (graph overlays when available, else app-module overlay registrations).
|
|
15
|
+
* A call-site map is attributed to an app only when one of its literal
|
|
16
|
+
* member selectors matches a trail id in that app's topo, so one app's
|
|
17
|
+
* authored bindings never flag another app's call-site map. Without
|
|
18
|
+
* context (`check` without context, or a run with no topo targets) the
|
|
19
|
+
* rule stays silent rather than guessing.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { classifySurfaceBinding, matchesTrailPattern } from '@ontrails/core';
|
|
23
|
+
import type { SurfaceBindings } from '@ontrails/core';
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
extractStringOrTemplateLiteral,
|
|
27
|
+
findConfigProperty,
|
|
28
|
+
getNodeArgument,
|
|
29
|
+
getNodeElements,
|
|
30
|
+
getNodeExpression,
|
|
31
|
+
getNodeId,
|
|
32
|
+
getNodeInit,
|
|
33
|
+
getNodeKey,
|
|
34
|
+
getNodeName,
|
|
35
|
+
getNodeProperties,
|
|
36
|
+
getNodeTypeAnnotation,
|
|
37
|
+
getNodeValueNode,
|
|
38
|
+
getPropertyName,
|
|
39
|
+
offsetToLine,
|
|
40
|
+
parse,
|
|
41
|
+
walk,
|
|
42
|
+
} from '@ontrails/source';
|
|
43
|
+
import type { AstNode } from '@ontrails/source';
|
|
44
|
+
import type {
|
|
45
|
+
AuthoredMcpSurfaceBindingSet,
|
|
46
|
+
ProjectAwareWardenRule,
|
|
47
|
+
ProjectContext,
|
|
48
|
+
WardenDiagnostic,
|
|
49
|
+
} from './types.js';
|
|
50
|
+
|
|
51
|
+
const RULE_NAME = 'trailhead-override-divergence';
|
|
52
|
+
|
|
53
|
+
const unwrapExpression = (node: AstNode | undefined): AstNode | undefined => {
|
|
54
|
+
let current = node;
|
|
55
|
+
while (
|
|
56
|
+
current?.type === 'TSAsExpression' ||
|
|
57
|
+
current?.type === 'TSSatisfiesExpression'
|
|
58
|
+
) {
|
|
59
|
+
current = getNodeExpression(current) ?? getNodeArgument(current);
|
|
60
|
+
}
|
|
61
|
+
return current;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const objectProperties = (node: AstNode): readonly AstNode[] =>
|
|
65
|
+
node.type === 'ObjectExpression' ? (getNodeProperties(node) ?? []) : [];
|
|
66
|
+
|
|
67
|
+
const propertyValue = (property: AstNode): AstNode | undefined =>
|
|
68
|
+
property.type === 'Property' ? getNodeValueNode(property) : undefined;
|
|
69
|
+
|
|
70
|
+
const isTrailheadDefinition = (node: AstNode): boolean =>
|
|
71
|
+
node.type === 'ObjectExpression' &&
|
|
72
|
+
findConfigProperty(node, 'trails') !== null;
|
|
73
|
+
|
|
74
|
+
const isTrailheadMapCandidate = (node: AstNode): boolean =>
|
|
75
|
+
objectProperties(node).some((property) => {
|
|
76
|
+
const value = unwrapExpression(propertyValue(property));
|
|
77
|
+
return value !== undefined && isTrailheadDefinition(value);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const isTrailheadMapBindingName = (name: string | null): boolean =>
|
|
81
|
+
name !== null &&
|
|
82
|
+
(name === 'trailheads' ||
|
|
83
|
+
name.endsWith('Trailheads') ||
|
|
84
|
+
name.endsWith('TrailheadMap'));
|
|
85
|
+
|
|
86
|
+
const hasTrailheadMapTypeAnnotation = (
|
|
87
|
+
sourceCode: string,
|
|
88
|
+
node: AstNode
|
|
89
|
+
): boolean => {
|
|
90
|
+
const typeAnnotation = getNodeTypeAnnotation(node);
|
|
91
|
+
return (
|
|
92
|
+
typeAnnotation !== undefined &&
|
|
93
|
+
/\b(?:McpSurfaceTrailheadMap|TrailheadMap)\b/.test(
|
|
94
|
+
sourceCode.slice(typeAnnotation.start, typeAnnotation.end)
|
|
95
|
+
)
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const diagnostic = (
|
|
100
|
+
sourceCode: string,
|
|
101
|
+
filePath: string,
|
|
102
|
+
node: AstNode,
|
|
103
|
+
message: string
|
|
104
|
+
): WardenDiagnostic => ({
|
|
105
|
+
filePath,
|
|
106
|
+
line: offsetToLine(sourceCode, node.start),
|
|
107
|
+
message,
|
|
108
|
+
rule: RULE_NAME,
|
|
109
|
+
severity: 'warn',
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Collect the literal selector strings of one trailhead definition. Returns
|
|
114
|
+
* `null` when any selector is dynamic — `surface-trailhead-coherence`
|
|
115
|
+
* already flags those, so this rule skips the comparison instead of
|
|
116
|
+
* double-reporting.
|
|
117
|
+
*/
|
|
118
|
+
const literalSelectors = (definition: AstNode): readonly string[] | null => {
|
|
119
|
+
const trailsProp = findConfigProperty(definition, 'trails');
|
|
120
|
+
const trailsValue = trailsProp ? propertyValue(trailsProp) : undefined;
|
|
121
|
+
const value = unwrapExpression(trailsValue);
|
|
122
|
+
if (!value) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const nodes =
|
|
126
|
+
value.type === 'ArrayExpression'
|
|
127
|
+
? getNodeElements(value).filter((element) => element !== null)
|
|
128
|
+
: [value];
|
|
129
|
+
const selectors: string[] = [];
|
|
130
|
+
for (const node of nodes) {
|
|
131
|
+
const selector = extractStringOrTemplateLiteral(node);
|
|
132
|
+
if (selector === null) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
selectors.push(selector);
|
|
136
|
+
}
|
|
137
|
+
return selectors;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const authoredGroupSelectors = (
|
|
141
|
+
bindings: SurfaceBindings
|
|
142
|
+
): ReadonlyMap<string, readonly string[]> => {
|
|
143
|
+
const groups = new Map<string, readonly string[]>();
|
|
144
|
+
for (const [name, value] of Object.entries(bindings)) {
|
|
145
|
+
const shape = classifySurfaceBinding(value);
|
|
146
|
+
if (shape.kind === 'group') {
|
|
147
|
+
groups.set(name, shape.members);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return groups;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const sameSelectorSet = (
|
|
154
|
+
first: readonly string[],
|
|
155
|
+
second: readonly string[]
|
|
156
|
+
): boolean => {
|
|
157
|
+
const firstSet = new Set(first);
|
|
158
|
+
const secondSet = new Set(second);
|
|
159
|
+
return (
|
|
160
|
+
firstSet.size === secondSet.size &&
|
|
161
|
+
[...firstSet].every((selector) => secondSet.has(selector))
|
|
162
|
+
);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const formatSelectors = (selectors: readonly string[]): string =>
|
|
166
|
+
[...new Set(selectors)]
|
|
167
|
+
.toSorted()
|
|
168
|
+
.map((selector) => `"${selector}"`)
|
|
169
|
+
.join(', ');
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Attribute a call-site trailhead map to a binding set: the map belongs to
|
|
173
|
+
* an app when at least one of its literal member selectors matches a trail
|
|
174
|
+
* id registered in that app's topo.
|
|
175
|
+
*/
|
|
176
|
+
const mapBelongsToSet = (
|
|
177
|
+
trailheadMap: AstNode,
|
|
178
|
+
set: AuthoredMcpSurfaceBindingSet
|
|
179
|
+
): boolean =>
|
|
180
|
+
objectProperties(trailheadMap).some((property) => {
|
|
181
|
+
const value = unwrapExpression(propertyValue(property));
|
|
182
|
+
if (value === undefined || !isTrailheadDefinition(value)) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
const selectors = literalSelectors(value);
|
|
186
|
+
return (
|
|
187
|
+
selectors !== null &&
|
|
188
|
+
selectors.some((selector) =>
|
|
189
|
+
set.trailIds.some((trailId) => matchesTrailPattern(trailId, selector))
|
|
190
|
+
)
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const diagnoseTrailheadMap = (
|
|
195
|
+
sourceCode: string,
|
|
196
|
+
filePath: string,
|
|
197
|
+
trailheadMap: AstNode,
|
|
198
|
+
set: AuthoredMcpSurfaceBindingSet,
|
|
199
|
+
groups: ReadonlyMap<string, readonly string[]>
|
|
200
|
+
): readonly WardenDiagnostic[] => {
|
|
201
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
202
|
+
const groupNames = [...groups.keys()].toSorted();
|
|
203
|
+
|
|
204
|
+
for (const property of objectProperties(trailheadMap)) {
|
|
205
|
+
const trailheadId = getPropertyName(getNodeKey(property));
|
|
206
|
+
const value = unwrapExpression(propertyValue(property));
|
|
207
|
+
if (!trailheadId || value === undefined || !isTrailheadDefinition(value)) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const authored = groups.get(trailheadId);
|
|
212
|
+
if (authored === undefined) {
|
|
213
|
+
diagnostics.push(
|
|
214
|
+
diagnostic(
|
|
215
|
+
sourceCode,
|
|
216
|
+
filePath,
|
|
217
|
+
property,
|
|
218
|
+
`Call-site MCP trailhead "${trailheadId}" has no matching mcp list binding in app "${set.appName}"'s surfaceOverlay (authored groups: ${groupNames.length > 0 ? groupNames.map((name) => `"${name}"`).join(', ') : 'none'}). The call-site map overrides the lockable default at runtime — author the same binding in surfaceOverlay({ mcp }) or rename one side.`
|
|
219
|
+
)
|
|
220
|
+
);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const selectors = literalSelectors(value);
|
|
225
|
+
if (selectors === null) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (!sameSelectorSet(selectors, authored)) {
|
|
229
|
+
diagnostics.push(
|
|
230
|
+
diagnostic(
|
|
231
|
+
sourceCode,
|
|
232
|
+
filePath,
|
|
233
|
+
property,
|
|
234
|
+
`Call-site MCP trailhead "${trailheadId}" selects [${formatSelectors(selectors)}] but app "${set.appName}"'s surfaceOverlay mcp binding "${trailheadId}" authors [${formatSelectors(authored)}]. The call-site map overrides the lockable default at runtime — align the member selectors or make the intentional divergence explicit by renaming one side.`
|
|
235
|
+
)
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// The override replaces the authored default whole-map at runtime, so an
|
|
241
|
+
// authored group missing from the call-site map is silently dropped while
|
|
242
|
+
// the committed lock still advertises it.
|
|
243
|
+
const callSiteNames = new Set(
|
|
244
|
+
objectProperties(trailheadMap).flatMap((property) => {
|
|
245
|
+
const name = getPropertyName(getNodeKey(property));
|
|
246
|
+
const value = unwrapExpression(propertyValue(property));
|
|
247
|
+
return name && value !== undefined && isTrailheadDefinition(value)
|
|
248
|
+
? [name]
|
|
249
|
+
: [];
|
|
250
|
+
})
|
|
251
|
+
);
|
|
252
|
+
for (const name of groupNames) {
|
|
253
|
+
if (!callSiteNames.has(name)) {
|
|
254
|
+
diagnostics.push(
|
|
255
|
+
diagnostic(
|
|
256
|
+
sourceCode,
|
|
257
|
+
filePath,
|
|
258
|
+
trailheadMap,
|
|
259
|
+
`App "${set.appName}"'s surfaceOverlay mcp binding "${name}" is not carried by this call-site trailhead map, so it will not be projected at runtime while the committed lock still advertises it. Add "${name}" to the call-site map or remove it from the authored overlay.`
|
|
260
|
+
)
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return diagnostics;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Warn when a source file's call-site MCP trailhead map diverges from the
|
|
270
|
+
* app-authored `surfaces` overlay's `mcp` list bindings.
|
|
271
|
+
*/
|
|
272
|
+
export const trailheadOverrideDivergence: ProjectAwareWardenRule = {
|
|
273
|
+
check(): readonly WardenDiagnostic[] {
|
|
274
|
+
// Without project context there are no authored bindings to compare
|
|
275
|
+
// against; stay silent instead of guessing.
|
|
276
|
+
return [];
|
|
277
|
+
},
|
|
278
|
+
checkWithContext(
|
|
279
|
+
sourceCode: string,
|
|
280
|
+
filePath: string,
|
|
281
|
+
context: ProjectContext
|
|
282
|
+
): readonly WardenDiagnostic[] {
|
|
283
|
+
const sets = (context.authoredMcpSurfaceBindingSets ?? [])
|
|
284
|
+
.map((set) => ({
|
|
285
|
+
groups: authoredGroupSelectors(set.bindings),
|
|
286
|
+
set,
|
|
287
|
+
}))
|
|
288
|
+
.filter(({ groups }) => groups.size > 0);
|
|
289
|
+
if (sets.length === 0) {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const ast = parse(filePath, sourceCode);
|
|
294
|
+
if (!ast) {
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const seen = new Set<number>();
|
|
299
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
300
|
+
const diagnoseCandidate = (node: AstNode | undefined): void => {
|
|
301
|
+
const unwrapped = unwrapExpression(node);
|
|
302
|
+
if (
|
|
303
|
+
unwrapped === undefined ||
|
|
304
|
+
unwrapped.type !== 'ObjectExpression' ||
|
|
305
|
+
seen.has(unwrapped.start) ||
|
|
306
|
+
!isTrailheadMapCandidate(unwrapped)
|
|
307
|
+
) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
seen.add(unwrapped.start);
|
|
311
|
+
for (const { set, groups } of sets) {
|
|
312
|
+
if (!mapBelongsToSet(unwrapped, set)) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
diagnostics.push(
|
|
316
|
+
...diagnoseTrailheadMap(sourceCode, filePath, unwrapped, set, groups)
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
walk(ast, (node) => {
|
|
322
|
+
if (node.type === 'Property') {
|
|
323
|
+
const propertyName = getPropertyName(getNodeKey(node));
|
|
324
|
+
if (propertyName === 'trailheads') {
|
|
325
|
+
diagnoseCandidate(propertyValue(node));
|
|
326
|
+
}
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (node.type === 'VariableDeclarator') {
|
|
331
|
+
const bindingName = getNodeName(getNodeId(node));
|
|
332
|
+
if (
|
|
333
|
+
typeof bindingName === 'string' &&
|
|
334
|
+
isTrailheadMapBindingName(bindingName)
|
|
335
|
+
) {
|
|
336
|
+
diagnoseCandidate(getNodeInit(node) ?? undefined);
|
|
337
|
+
}
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (
|
|
342
|
+
(node.type === 'TSAsExpression' ||
|
|
343
|
+
node.type === 'TSSatisfiesExpression') &&
|
|
344
|
+
hasTrailheadMapTypeAnnotation(sourceCode, node)
|
|
345
|
+
) {
|
|
346
|
+
diagnoseCandidate(node);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
return diagnostics;
|
|
351
|
+
},
|
|
352
|
+
description:
|
|
353
|
+
'Call-site MCP trailhead maps stay aligned with the app-authored surfaces overlay mcp bindings they override.',
|
|
354
|
+
name: RULE_NAME,
|
|
355
|
+
severity: 'warn',
|
|
356
|
+
};
|