@ontrails/warden 1.0.0-beta.2 → 1.0.0-beta.22
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 +508 -6
- package/README.md +77 -26
- package/bin/warden.ts +50 -0
- package/package.json +27 -5
- package/src/adapter-check.ts +136 -0
- package/src/ast.ts +28 -0
- package/src/cli.ts +1374 -103
- package/src/command.ts +953 -0
- package/src/config.ts +184 -0
- package/src/draft.ts +22 -0
- package/src/drift.ts +106 -22
- package/src/fix.ts +120 -0
- package/src/formatters.ts +79 -9
- package/src/guide.ts +245 -0
- package/src/index.ts +206 -14
- package/src/project-context.ts +163 -0
- package/src/resolve.ts +530 -0
- package/src/rules/activation-orphan.ts +97 -0
- package/src/rules/ast.ts +3176 -85
- package/src/rules/circular-refs.ts +154 -0
- package/src/rules/composes-declarations.ts +704 -0
- package/src/rules/context-no-surface-types.ts +68 -8
- package/src/rules/contour-exists.ts +251 -0
- package/src/rules/contour-ids.ts +15 -0
- package/src/rules/dead-internal-trail.ts +154 -0
- package/src/rules/draft-file-marking.ts +160 -0
- package/src/rules/draft-visible-debt.ts +87 -0
- package/src/rules/error-mapping-completeness.ts +288 -0
- package/src/rules/example-valid.ts +401 -0
- package/src/rules/fires-declarations.ts +758 -0
- package/src/rules/implementation-returns-result.ts +1265 -95
- package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
- package/src/rules/incomplete-crud.ts +580 -0
- package/src/rules/index.ts +219 -18
- package/src/rules/intent-propagation.ts +127 -0
- package/src/rules/layer-field-name-drift.ts +96 -0
- package/src/rules/metadata.ts +654 -0
- package/src/rules/missing-reconcile.ts +98 -0
- package/src/rules/missing-visibility.ts +110 -0
- package/src/rules/no-destructured-compose.ts +192 -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 +111 -0
- package/src/rules/no-redundant-result-error-wrap.ts +331 -0
- package/src/rules/no-retired-cross-vocabulary.ts +194 -0
- package/src/rules/no-sync-result-assumption.ts +1134 -99
- 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 +389 -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 +146 -0
- package/src/rules/permit-governance.ts +25 -0
- package/src/rules/public-export-example-coverage.ts +553 -0
- package/src/rules/public-internal-deep-imports.ts +517 -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 +145 -0
- package/src/rules/resolved-import-boundary.ts +146 -0
- package/src/rules/resource-declarations.ts +704 -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 +657 -0
- package/src/rules/surface-facet-coherence.ts +370 -0
- package/src/rules/trail-versioning-source.ts +1094 -0
- package/src/rules/trail-versioning-topo.ts +172 -0
- package/src/rules/types.ts +270 -6
- package/src/rules/unmaterialized-activation-source.ts +84 -0
- package/src/rules/unreachable-detour-shadowing.ts +344 -0
- package/src/rules/valid-describe-refs.ts +160 -32
- package/src/rules/valid-detour-contract.ts +78 -0
- package/src/rules/warden-export-symmetry.ts +533 -0
- package/src/rules/warden-rules-use-ast.ts +996 -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/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/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/error-mapping-completeness.trail.ts +29 -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 +78 -0
- package/src/trails/intent-propagation.trail.ts +30 -0
- package/src/trails/layer-field-name-drift.trail.ts +39 -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 +194 -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/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 +45 -0
- package/src/trails/webhook-route-collision.trail.ts +50 -0
- package/src/trails/wrap-rule.ts +213 -0
- package/src/workspaces.ts +238 -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 -231
- 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 -75
- 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 -188
- 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,370 @@
|
|
|
1
|
+
import { matchesTrailPattern } from '@ontrails/core';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
extractStringOrTemplateLiteral,
|
|
5
|
+
findConfigProperty,
|
|
6
|
+
getPropertyName,
|
|
7
|
+
offsetToLine,
|
|
8
|
+
parse,
|
|
9
|
+
walk,
|
|
10
|
+
} from './ast.js';
|
|
11
|
+
import type { AstNode } from './ast.js';
|
|
12
|
+
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
13
|
+
|
|
14
|
+
const RULE_NAME = 'surface-facet-coherence';
|
|
15
|
+
|
|
16
|
+
interface FacetSelector {
|
|
17
|
+
readonly facetId: string;
|
|
18
|
+
readonly node: AstNode;
|
|
19
|
+
readonly value: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const unwrapExpression = (node: AstNode | undefined): AstNode | undefined => {
|
|
23
|
+
let current = node;
|
|
24
|
+
while (
|
|
25
|
+
current?.type === 'TSAsExpression' ||
|
|
26
|
+
current?.type === 'TSSatisfiesExpression'
|
|
27
|
+
) {
|
|
28
|
+
current =
|
|
29
|
+
(current as unknown as { expression?: AstNode }).expression ??
|
|
30
|
+
(current as unknown as { argument?: AstNode }).argument;
|
|
31
|
+
}
|
|
32
|
+
return current;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const objectProperties = (node: AstNode): readonly AstNode[] =>
|
|
36
|
+
node.type === 'ObjectExpression'
|
|
37
|
+
? ((node as unknown as { properties?: readonly AstNode[] }).properties ??
|
|
38
|
+
[])
|
|
39
|
+
: [];
|
|
40
|
+
|
|
41
|
+
const propertyValue = (property: AstNode): AstNode | undefined =>
|
|
42
|
+
property.type === 'Property'
|
|
43
|
+
? (property as unknown as { value?: AstNode }).value
|
|
44
|
+
: undefined;
|
|
45
|
+
|
|
46
|
+
const literalBooleanValue = (node: AstNode | undefined): boolean | null => {
|
|
47
|
+
if (node?.type !== 'Literal') {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const { value } = node as unknown as { value?: unknown };
|
|
51
|
+
return typeof value === 'boolean' ? value : null;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const diagnostic = (
|
|
55
|
+
sourceCode: string,
|
|
56
|
+
filePath: string,
|
|
57
|
+
node: AstNode,
|
|
58
|
+
message: string
|
|
59
|
+
): WardenDiagnostic => ({
|
|
60
|
+
filePath,
|
|
61
|
+
line: offsetToLine(sourceCode, node.start),
|
|
62
|
+
message,
|
|
63
|
+
rule: RULE_NAME,
|
|
64
|
+
severity: 'warn',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const isFacetDefinition = (node: AstNode): boolean =>
|
|
68
|
+
node.type === 'ObjectExpression' &&
|
|
69
|
+
findConfigProperty(node, 'trails') !== null;
|
|
70
|
+
|
|
71
|
+
const isFacetMapCandidate = (node: AstNode): boolean =>
|
|
72
|
+
objectProperties(node).some((property) => {
|
|
73
|
+
const value = unwrapExpression(propertyValue(property));
|
|
74
|
+
return value !== undefined && isFacetDefinition(value);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const isFacetMapBindingName = (name: string | null): boolean =>
|
|
78
|
+
name !== null &&
|
|
79
|
+
(name === 'facets' || name.endsWith('Facets') || name.endsWith('FacetMap'));
|
|
80
|
+
|
|
81
|
+
const hasFacetMapTypeAnnotation = (
|
|
82
|
+
sourceCode: string,
|
|
83
|
+
node: AstNode
|
|
84
|
+
): boolean => {
|
|
85
|
+
const { typeAnnotation } = node as unknown as {
|
|
86
|
+
readonly typeAnnotation?: AstNode;
|
|
87
|
+
};
|
|
88
|
+
return (
|
|
89
|
+
typeAnnotation !== undefined &&
|
|
90
|
+
/\b(?:McpSurfaceFacetMap|TopoGraphFacetDeclaration|FacetMap)\b/.test(
|
|
91
|
+
sourceCode.slice(typeAnnotation.start, typeAnnotation.end)
|
|
92
|
+
)
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const selectorNodes = (trailsNode: AstNode): readonly AstNode[] | null => {
|
|
97
|
+
const value = unwrapExpression(trailsNode);
|
|
98
|
+
if (!value) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
if (value.type === 'ArrayExpression') {
|
|
102
|
+
return (
|
|
103
|
+
(value as unknown as { elements?: readonly (AstNode | null)[] })
|
|
104
|
+
.elements ?? []
|
|
105
|
+
).filter((element) => element !== null);
|
|
106
|
+
}
|
|
107
|
+
return [value];
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const collectLiteralSelectors = (
|
|
111
|
+
sourceCode: string,
|
|
112
|
+
filePath: string,
|
|
113
|
+
facetId: string,
|
|
114
|
+
trailsProp: AstNode,
|
|
115
|
+
diagnostics: WardenDiagnostic[]
|
|
116
|
+
): readonly FacetSelector[] => {
|
|
117
|
+
const trailsValue = propertyValue(trailsProp);
|
|
118
|
+
const nodes = trailsValue ? selectorNodes(trailsValue) : null;
|
|
119
|
+
if (nodes === null || nodes.length === 0) {
|
|
120
|
+
diagnostics.push(
|
|
121
|
+
diagnostic(
|
|
122
|
+
sourceCode,
|
|
123
|
+
filePath,
|
|
124
|
+
trailsProp,
|
|
125
|
+
`Surface facet "${facetId}" uses a dynamic trails selector. Keep facet selectors as string literals so Warden can check overlap and drift.`
|
|
126
|
+
)
|
|
127
|
+
);
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const selectors: FacetSelector[] = [];
|
|
132
|
+
for (const node of nodes) {
|
|
133
|
+
const selectorValue = extractStringOrTemplateLiteral(node);
|
|
134
|
+
if (selectorValue === null) {
|
|
135
|
+
diagnostics.push(
|
|
136
|
+
diagnostic(
|
|
137
|
+
sourceCode,
|
|
138
|
+
filePath,
|
|
139
|
+
node,
|
|
140
|
+
`Surface facet "${facetId}" uses a dynamic trails selector. Keep facet selectors as string literals so Warden can check overlap and drift.`
|
|
141
|
+
)
|
|
142
|
+
);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
selectors.push({ facetId, node, value: selectorValue });
|
|
146
|
+
}
|
|
147
|
+
return selectors;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const hasNonEmptyDescription = (definition: AstNode): boolean => {
|
|
151
|
+
const descriptionProp = findConfigProperty(definition, 'description');
|
|
152
|
+
const value = unwrapExpression(propertyValue(descriptionProp ?? definition));
|
|
153
|
+
const description = extractStringOrTemplateLiteral(value);
|
|
154
|
+
return typeof description === 'string' && description.trim().length > 0;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const literalStringProperty = (
|
|
158
|
+
definition: AstNode,
|
|
159
|
+
propertyName: string
|
|
160
|
+
): string | null => {
|
|
161
|
+
const prop = findConfigProperty(definition, propertyName);
|
|
162
|
+
if (!prop) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
return extractStringOrTemplateLiteral(unwrapExpression(propertyValue(prop)));
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const literalBooleanProperty = (
|
|
169
|
+
definition: AstNode,
|
|
170
|
+
propertyName: string
|
|
171
|
+
): boolean | null => {
|
|
172
|
+
const prop = findConfigProperty(definition, propertyName);
|
|
173
|
+
if (!prop) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
return literalBooleanValue(unwrapExpression(propertyValue(prop)));
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const selectorsMayOverlap = (
|
|
180
|
+
first: FacetSelector,
|
|
181
|
+
second: FacetSelector
|
|
182
|
+
): boolean =>
|
|
183
|
+
first.value === second.value ||
|
|
184
|
+
matchesTrailPattern(first.value, second.value) ||
|
|
185
|
+
matchesTrailPattern(second.value, first.value);
|
|
186
|
+
|
|
187
|
+
const diagnoseFacetDefinition = (
|
|
188
|
+
sourceCode: string,
|
|
189
|
+
filePath: string,
|
|
190
|
+
facetId: string,
|
|
191
|
+
definition: AstNode
|
|
192
|
+
): {
|
|
193
|
+
readonly diagnostics: readonly WardenDiagnostic[];
|
|
194
|
+
readonly selectors: readonly FacetSelector[];
|
|
195
|
+
} => {
|
|
196
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
197
|
+
const trailsProp = findConfigProperty(definition, 'trails');
|
|
198
|
+
const selectors =
|
|
199
|
+
trailsProp === null
|
|
200
|
+
? []
|
|
201
|
+
: collectLiteralSelectors(
|
|
202
|
+
sourceCode,
|
|
203
|
+
filePath,
|
|
204
|
+
facetId,
|
|
205
|
+
trailsProp,
|
|
206
|
+
diagnostics
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
if (!hasNonEmptyDescription(definition)) {
|
|
210
|
+
diagnostics.push(
|
|
211
|
+
diagnostic(
|
|
212
|
+
sourceCode,
|
|
213
|
+
filePath,
|
|
214
|
+
definition,
|
|
215
|
+
`Surface facet "${facetId}" needs a non-empty description so MCP clients and agents can choose it without guessing.`
|
|
216
|
+
)
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const visibility = literalStringProperty(definition, 'visibility');
|
|
221
|
+
const wideningAccepted = literalBooleanProperty(
|
|
222
|
+
definition,
|
|
223
|
+
'visibilityWideningAccepted'
|
|
224
|
+
);
|
|
225
|
+
if (visibility === 'public' && wideningAccepted !== true) {
|
|
226
|
+
diagnostics.push(
|
|
227
|
+
diagnostic(
|
|
228
|
+
sourceCode,
|
|
229
|
+
filePath,
|
|
230
|
+
definition,
|
|
231
|
+
`Surface facet "${facetId}" explicitly sets public visibility without visibilityWideningAccepted: true. Facets must not accidentally widen hidden trails.`
|
|
232
|
+
)
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (
|
|
237
|
+
wideningAccepted === true &&
|
|
238
|
+
!literalStringProperty(definition, 'descriptionStableThrough')
|
|
239
|
+
) {
|
|
240
|
+
diagnostics.push(
|
|
241
|
+
diagnostic(
|
|
242
|
+
sourceCode,
|
|
243
|
+
filePath,
|
|
244
|
+
definition,
|
|
245
|
+
`Surface facet "${facetId}" accepts visibility widening but does not record descriptionStableThrough review metadata.`
|
|
246
|
+
)
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return { diagnostics, selectors };
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const diagnoseFacetMap = (
|
|
254
|
+
sourceCode: string,
|
|
255
|
+
filePath: string,
|
|
256
|
+
facetMap: AstNode
|
|
257
|
+
): readonly WardenDiagnostic[] => {
|
|
258
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
259
|
+
const selectors: FacetSelector[] = [];
|
|
260
|
+
|
|
261
|
+
for (const property of objectProperties(facetMap)) {
|
|
262
|
+
const facetId = getPropertyName(
|
|
263
|
+
(property as unknown as { key?: AstNode }).key
|
|
264
|
+
);
|
|
265
|
+
const value = unwrapExpression(propertyValue(property));
|
|
266
|
+
if (!facetId || value === undefined || !isFacetDefinition(value)) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const result = diagnoseFacetDefinition(
|
|
270
|
+
sourceCode,
|
|
271
|
+
filePath,
|
|
272
|
+
facetId,
|
|
273
|
+
value
|
|
274
|
+
);
|
|
275
|
+
diagnostics.push(...result.diagnostics);
|
|
276
|
+
selectors.push(...result.selectors);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
for (let i = 0; i < selectors.length; i += 1) {
|
|
280
|
+
const first = selectors[i];
|
|
281
|
+
if (!first) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
for (let j = i + 1; j < selectors.length; j += 1) {
|
|
285
|
+
const second = selectors[j];
|
|
286
|
+
if (
|
|
287
|
+
!second ||
|
|
288
|
+
first.facetId === second.facetId ||
|
|
289
|
+
!selectorsMayOverlap(first, second)
|
|
290
|
+
) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
diagnostics.push(
|
|
294
|
+
diagnostic(
|
|
295
|
+
sourceCode,
|
|
296
|
+
filePath,
|
|
297
|
+
second.node,
|
|
298
|
+
`Surface facet selector "${second.value}" in "${second.facetId}" overlaps selector "${first.value}" in "${first.facetId}". Narrow one facet so each public trail has one MCP owner.`
|
|
299
|
+
)
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return diagnostics;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
export const surfaceFacetCoherence: WardenRule = {
|
|
308
|
+
check(sourceCode, filePath) {
|
|
309
|
+
const ast = parse(filePath, sourceCode);
|
|
310
|
+
if (!ast) {
|
|
311
|
+
return [];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const seen = new Set<number>();
|
|
315
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
316
|
+
const diagnoseCandidate = (node: AstNode | undefined): void => {
|
|
317
|
+
const unwrapped = unwrapExpression(node);
|
|
318
|
+
if (
|
|
319
|
+
unwrapped === undefined ||
|
|
320
|
+
unwrapped.type !== 'ObjectExpression' ||
|
|
321
|
+
seen.has(unwrapped.start) ||
|
|
322
|
+
!isFacetMapCandidate(unwrapped)
|
|
323
|
+
) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
seen.add(unwrapped.start);
|
|
327
|
+
diagnostics.push(...diagnoseFacetMap(sourceCode, filePath, unwrapped));
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
walk(ast, (node) => {
|
|
331
|
+
if (node.type === 'Property') {
|
|
332
|
+
const propertyName = getPropertyName(
|
|
333
|
+
(node as unknown as { key?: AstNode }).key
|
|
334
|
+
);
|
|
335
|
+
if (propertyName === 'facets') {
|
|
336
|
+
diagnoseCandidate(propertyValue(node));
|
|
337
|
+
}
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (node.type === 'VariableDeclarator') {
|
|
342
|
+
const bindingName =
|
|
343
|
+
(node as unknown as { id?: { name?: unknown } }).id?.name ?? null;
|
|
344
|
+
if (
|
|
345
|
+
typeof bindingName === 'string' &&
|
|
346
|
+
isFacetMapBindingName(bindingName)
|
|
347
|
+
) {
|
|
348
|
+
diagnoseCandidate(
|
|
349
|
+
(node as unknown as { init?: AstNode }).init ?? undefined
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (
|
|
356
|
+
(node.type === 'TSAsExpression' ||
|
|
357
|
+
node.type === 'TSSatisfiesExpression') &&
|
|
358
|
+
hasFacetMapTypeAnnotation(sourceCode, node)
|
|
359
|
+
) {
|
|
360
|
+
diagnoseCandidate(node);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
return diagnostics;
|
|
365
|
+
},
|
|
366
|
+
description:
|
|
367
|
+
'Coach surface facet maps away from selector overlap, hidden visibility widening, and drift-prone dynamic selectors.',
|
|
368
|
+
name: RULE_NAME,
|
|
369
|
+
severity: 'warn',
|
|
370
|
+
};
|