@ontrails/warden 1.0.0-beta.2 → 1.0.0-beta.21
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 +497 -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,187 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildSignalIdentifierResolver,
|
|
3
|
+
deriveConstString,
|
|
4
|
+
extractStringLiteral,
|
|
5
|
+
findConfigProperty,
|
|
6
|
+
findTrailDefinitions,
|
|
7
|
+
getStringValue,
|
|
8
|
+
identifierName,
|
|
9
|
+
isStringLiteral,
|
|
10
|
+
offsetToLine,
|
|
11
|
+
parse,
|
|
12
|
+
walk,
|
|
13
|
+
} from './ast.js';
|
|
14
|
+
import type { AstNode, SignalIdentifierResolver } from './ast.js';
|
|
15
|
+
import { isTestFile } from './scan.js';
|
|
16
|
+
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
17
|
+
|
|
18
|
+
interface DeclaredFireSummary {
|
|
19
|
+
readonly count: number;
|
|
20
|
+
readonly ids: readonly string[];
|
|
21
|
+
readonly line: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const isReadIntent = (config: AstNode): boolean => {
|
|
25
|
+
const intentProp = findConfigProperty(config, 'intent');
|
|
26
|
+
const intentValue = intentProp?.value as AstNode | undefined;
|
|
27
|
+
return isStringLiteral(intentValue) && getStringValue(intentValue) === 'read';
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const collectArrayBindings = (ast: AstNode): ReadonlyMap<string, AstNode> => {
|
|
31
|
+
const bindings = new Map<string, AstNode>();
|
|
32
|
+
walk(ast, (node) => {
|
|
33
|
+
if (node.type !== 'VariableDeclarator') {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const { id, init } = node as unknown as {
|
|
38
|
+
readonly id?: AstNode;
|
|
39
|
+
readonly init?: AstNode;
|
|
40
|
+
};
|
|
41
|
+
const name = identifierName(id);
|
|
42
|
+
if (name && init?.type === 'ArrayExpression') {
|
|
43
|
+
bindings.set(name, init);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return bindings;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const getFiresArray = (
|
|
50
|
+
config: AstNode,
|
|
51
|
+
arrayBindings: ReadonlyMap<string, AstNode>
|
|
52
|
+
): AstNode | null => {
|
|
53
|
+
const firesProp = findConfigProperty(config, 'fires');
|
|
54
|
+
const value = firesProp?.value as AstNode | undefined;
|
|
55
|
+
if (value?.type === 'ArrayExpression') {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const name = identifierName(value);
|
|
60
|
+
return name ? (arrayBindings.get(name) ?? null) : null;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const getFiresElements = (
|
|
64
|
+
config: AstNode,
|
|
65
|
+
arrayBindings: ReadonlyMap<string, AstNode>
|
|
66
|
+
): readonly AstNode[] => {
|
|
67
|
+
const array = getFiresArray(config, arrayBindings);
|
|
68
|
+
if (!array) {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
(array as unknown as { readonly elements?: readonly (AstNode | null)[] })
|
|
74
|
+
.elements ?? []
|
|
75
|
+
).filter((element): element is AstNode => element !== null);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const resolveFireElementId = (
|
|
79
|
+
element: AstNode,
|
|
80
|
+
sourceCode: string,
|
|
81
|
+
signalIds: SignalIdentifierResolver
|
|
82
|
+
): string | null => {
|
|
83
|
+
const literalValue = extractStringLiteral(element);
|
|
84
|
+
if (literalValue !== null) {
|
|
85
|
+
return literalValue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (element.type !== 'Identifier') {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const resolved = signalIds.resolve(element);
|
|
93
|
+
if (resolved.kind === 'signal' || resolved.kind === 'string') {
|
|
94
|
+
return resolved.id;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const name = identifierName(element);
|
|
98
|
+
return name ? deriveConstString(name, sourceCode) : null;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const summarizeDeclaredFires = (
|
|
102
|
+
config: AstNode,
|
|
103
|
+
arrayBindings: ReadonlyMap<string, AstNode>,
|
|
104
|
+
sourceCode: string,
|
|
105
|
+
signalIds: SignalIdentifierResolver
|
|
106
|
+
): DeclaredFireSummary | null => {
|
|
107
|
+
const firesProp = findConfigProperty(config, 'fires');
|
|
108
|
+
const elements = getFiresElements(config, arrayBindings);
|
|
109
|
+
if (elements.length === 0) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const ids: string[] = [];
|
|
114
|
+
for (const element of elements) {
|
|
115
|
+
const resolved = resolveFireElementId(element, sourceCode, signalIds);
|
|
116
|
+
if (resolved) {
|
|
117
|
+
ids.push(resolved);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const [firstElement] = elements;
|
|
122
|
+
const lineNode = firesProp ?? firstElement;
|
|
123
|
+
return {
|
|
124
|
+
count: elements.length,
|
|
125
|
+
ids,
|
|
126
|
+
line: lineNode ? offsetToLine(sourceCode, lineNode.start) : 1,
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const formatSignalList = (summary: DeclaredFireSummary): string => {
|
|
131
|
+
const named = summary.ids.map((id) => `"${id}"`);
|
|
132
|
+
const unresolvedCount = summary.count - summary.ids.length;
|
|
133
|
+
const unresolved =
|
|
134
|
+
unresolvedCount > 0
|
|
135
|
+
? [
|
|
136
|
+
unresolvedCount === 1
|
|
137
|
+
? '1 unresolved signal reference'
|
|
138
|
+
: `${unresolvedCount} unresolved signal references`,
|
|
139
|
+
]
|
|
140
|
+
: [];
|
|
141
|
+
return [...named, ...unresolved].join(', ');
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const buildDiagnostic = (
|
|
145
|
+
trailId: string,
|
|
146
|
+
summary: DeclaredFireSummary,
|
|
147
|
+
filePath: string
|
|
148
|
+
): WardenDiagnostic => ({
|
|
149
|
+
filePath,
|
|
150
|
+
line: summary.line,
|
|
151
|
+
message: `Trail "${trailId}" declares intent: 'read' but also declares fires: [${formatSignalList(summary)}]. Read trails should remain side-effect-free; change the trail intent or move ctx.fire behavior to an appropriate write trail.`,
|
|
152
|
+
rule: 'read-intent-fires',
|
|
153
|
+
severity: 'warn',
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
export const readIntentFires: WardenRule = {
|
|
157
|
+
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
158
|
+
if (isTestFile(filePath)) {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const ast = parse(filePath, sourceCode);
|
|
163
|
+
if (!ast) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const signalIds = buildSignalIdentifierResolver(ast);
|
|
168
|
+
const arrayBindings = collectArrayBindings(ast);
|
|
169
|
+
return findTrailDefinitions(ast).flatMap((def) => {
|
|
170
|
+
if (def.kind !== 'trail' || !isReadIntent(def.config)) {
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const summary = summarizeDeclaredFires(
|
|
175
|
+
def.config,
|
|
176
|
+
arrayBindings,
|
|
177
|
+
sourceCode,
|
|
178
|
+
signalIds
|
|
179
|
+
);
|
|
180
|
+
return summary ? [buildDiagnostic(def.id, summary, filePath)] : [];
|
|
181
|
+
});
|
|
182
|
+
},
|
|
183
|
+
description:
|
|
184
|
+
'Warn when read-intent trails declare signal fires side effects.',
|
|
185
|
+
name: 'read-intent-fires',
|
|
186
|
+
severity: 'warn',
|
|
187
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collectContourDefinitionIds,
|
|
3
|
+
collectContourReferenceSites,
|
|
4
|
+
offsetToLine,
|
|
5
|
+
parse,
|
|
6
|
+
} from './ast.js';
|
|
7
|
+
import type { AstNode } from './ast.js';
|
|
8
|
+
import { mergeKnownContourIds } from './contour-ids.js';
|
|
9
|
+
import { isTestFile } from './scan.js';
|
|
10
|
+
import type {
|
|
11
|
+
ProjectAwareWardenRule,
|
|
12
|
+
ProjectContext,
|
|
13
|
+
WardenDiagnostic,
|
|
14
|
+
} from './types.js';
|
|
15
|
+
|
|
16
|
+
const buildMissingReferenceDiagnostic = (
|
|
17
|
+
sourceContour: string,
|
|
18
|
+
field: string,
|
|
19
|
+
targetContour: string,
|
|
20
|
+
filePath: string,
|
|
21
|
+
line: number
|
|
22
|
+
): WardenDiagnostic => ({
|
|
23
|
+
filePath,
|
|
24
|
+
line,
|
|
25
|
+
message: `Contour "${sourceContour}" field "${field}" references contour "${targetContour}" which is not defined in the project. Define it with contour('${targetContour}', ...) and include it in the topo, or fix the field reference if this is a typo.`,
|
|
26
|
+
rule: 'reference-exists',
|
|
27
|
+
severity: 'error',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const checkContourReferences = (
|
|
31
|
+
ast: AstNode,
|
|
32
|
+
sourceCode: string,
|
|
33
|
+
filePath: string,
|
|
34
|
+
knownContourIds: ReadonlySet<string>
|
|
35
|
+
): readonly WardenDiagnostic[] => {
|
|
36
|
+
if (isTestFile(filePath)) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return collectContourReferenceSites(ast, knownContourIds).flatMap(
|
|
41
|
+
(reference) => {
|
|
42
|
+
if (knownContourIds.has(reference.target)) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return [
|
|
47
|
+
buildMissingReferenceDiagnostic(
|
|
48
|
+
reference.source,
|
|
49
|
+
reference.field,
|
|
50
|
+
reference.target,
|
|
51
|
+
filePath,
|
|
52
|
+
offsetToLine(sourceCode, reference.start)
|
|
53
|
+
),
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Checks that every contour `.id()` reference resolves to a known contour.
|
|
61
|
+
*/
|
|
62
|
+
export const referenceExists: ProjectAwareWardenRule = {
|
|
63
|
+
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
64
|
+
const ast = parse(filePath, sourceCode);
|
|
65
|
+
if (!ast) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return checkContourReferences(
|
|
70
|
+
ast,
|
|
71
|
+
sourceCode,
|
|
72
|
+
filePath,
|
|
73
|
+
collectContourDefinitionIds(ast)
|
|
74
|
+
);
|
|
75
|
+
},
|
|
76
|
+
checkWithContext(
|
|
77
|
+
sourceCode: string,
|
|
78
|
+
filePath: string,
|
|
79
|
+
context: ProjectContext
|
|
80
|
+
): readonly WardenDiagnostic[] {
|
|
81
|
+
const ast = parse(filePath, sourceCode);
|
|
82
|
+
if (!ast) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const localContourIds = collectContourDefinitionIds(ast);
|
|
87
|
+
return checkContourReferences(
|
|
88
|
+
ast,
|
|
89
|
+
sourceCode,
|
|
90
|
+
filePath,
|
|
91
|
+
mergeKnownContourIds(localContourIds, context.knownContourIds)
|
|
92
|
+
);
|
|
93
|
+
},
|
|
94
|
+
description:
|
|
95
|
+
'Ensure every contour field declared via .id() resolves to a known contour.',
|
|
96
|
+
name: 'reference-exists',
|
|
97
|
+
severity: 'error',
|
|
98
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry name snapshot used by `warden-export-symmetry`.
|
|
3
|
+
*
|
|
4
|
+
* Imports each rule module directly to avoid a dependency cycle with
|
|
5
|
+
* `./index.ts`, which itself imports `warden-export-symmetry`. Keep this
|
|
6
|
+
* list in lockstep with `wardenRules` / `wardenTopoRules` in `./index.ts` —
|
|
7
|
+
* the `warden-export-symmetry` rule will fail the build if they drift.
|
|
8
|
+
*/
|
|
9
|
+
import { activationOrphan } from './activation-orphan.js';
|
|
10
|
+
import { circularRefs } from './circular-refs.js';
|
|
11
|
+
import { contourExists } from './contour-exists.js';
|
|
12
|
+
import { contextNoSurfaceTypes } from './context-no-surface-types.js';
|
|
13
|
+
import { composesDeclarations } from './composes-declarations.js';
|
|
14
|
+
import { deadInternalTrail } from './dead-internal-trail.js';
|
|
15
|
+
import { draftFileMarking } from './draft-file-marking.js';
|
|
16
|
+
import { draftVisibleDebt } from './draft-visible-debt.js';
|
|
17
|
+
import { errorMappingCompleteness } from './error-mapping-completeness.js';
|
|
18
|
+
import { exampleValid } from './example-valid.js';
|
|
19
|
+
import { firesDeclarations } from './fires-declarations.js';
|
|
20
|
+
import { implementationReturnsResult } from './implementation-returns-result.js';
|
|
21
|
+
import { incompleteAccessorForStandardOp } from './incomplete-accessor-for-standard-op.js';
|
|
22
|
+
import { incompleteCrud } from './incomplete-crud.js';
|
|
23
|
+
import { intentPropagation } from './intent-propagation.js';
|
|
24
|
+
import { layerFieldNameDrift } from './layer-field-name-drift.js';
|
|
25
|
+
import { missingReconcile } from './missing-reconcile.js';
|
|
26
|
+
import { missingVisibility } from './missing-visibility.js';
|
|
27
|
+
import { noDevPermitInSource } from './no-dev-permit-in-source.js';
|
|
28
|
+
import { noDestructuredCompose } from './no-destructured-compose.js';
|
|
29
|
+
import { noLegacyLayerImports } from './no-legacy-layer-imports.js';
|
|
30
|
+
import { noDirectImplementationCall } from './no-direct-implementation-call.js';
|
|
31
|
+
import { noNativeErrorResult } from './no-native-error-result.js';
|
|
32
|
+
import { noRedundantResultErrorWrap } from './no-redundant-result-error-wrap.js';
|
|
33
|
+
import { noRetiredCrossVocabulary } from './no-retired-cross-vocabulary.js';
|
|
34
|
+
import { noSyncResultAssumption } from './no-sync-result-assumption.js';
|
|
35
|
+
import { noThrowInDetourRecover } from './no-throw-in-detour-recover.js';
|
|
36
|
+
import { noThrowInImplementation } from './no-throw-in-implementation.js';
|
|
37
|
+
import { noTopLevelSurface } from './no-top-level-surface.js';
|
|
38
|
+
import { onReferencesExist } from './on-references-exist.js';
|
|
39
|
+
import { orphanedSignal } from './orphaned-signal.js';
|
|
40
|
+
import { ownerProjectionParity } from './owner-projection-parity.js';
|
|
41
|
+
import { permitGovernance } from './permit-governance.js';
|
|
42
|
+
import { preferSchemaInference } from './prefer-schema-inference.js';
|
|
43
|
+
import { publicExportExampleCoverage } from './public-export-example-coverage.js';
|
|
44
|
+
import { publicInternalDeepImports } from './public-internal-deep-imports.js';
|
|
45
|
+
import { publicOutputSchema } from './public-output-schema.js';
|
|
46
|
+
import { publicUnionOutputDiscriminants } from './public-union-output-discriminants.js';
|
|
47
|
+
import { readIntentFires } from './read-intent-fires.js';
|
|
48
|
+
import { referenceExists } from './reference-exists.js';
|
|
49
|
+
import { resolvedImportBoundary } from './resolved-import-boundary.js';
|
|
50
|
+
import { resourceDeclarations } from './resource-declarations.js';
|
|
51
|
+
import { resourceExists } from './resource-exists.js';
|
|
52
|
+
import { resourceIdGrammar } from './resource-id-grammar.js';
|
|
53
|
+
import { resourceMockCoverage } from './resource-mock-coverage.js';
|
|
54
|
+
import { scheduledDestroyIntent } from './scheduled-destroy-intent.js';
|
|
55
|
+
import { signalGraphCoaching } from './signal-graph-coaching.js';
|
|
56
|
+
import { staticResourceAccessorPreference } from './static-resource-accessor-preference.js';
|
|
57
|
+
import { surfaceFacetCoherence } from './surface-facet-coherence.js';
|
|
58
|
+
import {
|
|
59
|
+
forkWithoutPreservedBlaze,
|
|
60
|
+
markerSchemaUnsupported,
|
|
61
|
+
versionPinnedCompose,
|
|
62
|
+
} from './trail-versioning-source.js';
|
|
63
|
+
import {
|
|
64
|
+
deprecationWithoutGuidance,
|
|
65
|
+
pendingForce,
|
|
66
|
+
versionGap,
|
|
67
|
+
versionWithoutExamples,
|
|
68
|
+
} from './trail-versioning-topo.js';
|
|
69
|
+
import { unmaterializedActivationSource } from './unmaterialized-activation-source.js';
|
|
70
|
+
import { unreachableDetourShadowing } from './unreachable-detour-shadowing.js';
|
|
71
|
+
import { validDetourContract } from './valid-detour-contract.js';
|
|
72
|
+
import { validDescribeRefs } from './valid-describe-refs.js';
|
|
73
|
+
import { wardenRulesUseAst } from './warden-rules-use-ast.js';
|
|
74
|
+
import { webhookRouteCollision } from './webhook-route-collision.js';
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* All non-`warden-export-symmetry` rule identifiers registered in
|
|
78
|
+
* `wardenRules` / `wardenTopoRules`. Excludes the symmetry rule itself to
|
|
79
|
+
* avoid a self-referential check; the symmetry rule adds its own name back in
|
|
80
|
+
* when comparing against the public barrel.
|
|
81
|
+
*/
|
|
82
|
+
export const registeredRuleNames: readonly string[] = [
|
|
83
|
+
activationOrphan.name,
|
|
84
|
+
circularRefs.name,
|
|
85
|
+
contextNoSurfaceTypes.name,
|
|
86
|
+
contourExists.name,
|
|
87
|
+
composesDeclarations.name,
|
|
88
|
+
deadInternalTrail.name,
|
|
89
|
+
deprecationWithoutGuidance.name,
|
|
90
|
+
draftFileMarking.name,
|
|
91
|
+
draftVisibleDebt.name,
|
|
92
|
+
errorMappingCompleteness.name,
|
|
93
|
+
exampleValid.name,
|
|
94
|
+
firesDeclarations.name,
|
|
95
|
+
forkWithoutPreservedBlaze.name,
|
|
96
|
+
implementationReturnsResult.name,
|
|
97
|
+
incompleteAccessorForStandardOp.name,
|
|
98
|
+
incompleteCrud.name,
|
|
99
|
+
intentPropagation.name,
|
|
100
|
+
layerFieldNameDrift.name,
|
|
101
|
+
markerSchemaUnsupported.name,
|
|
102
|
+
missingReconcile.name,
|
|
103
|
+
missingVisibility.name,
|
|
104
|
+
noDevPermitInSource.name,
|
|
105
|
+
noDestructuredCompose.name,
|
|
106
|
+
noLegacyLayerImports.name,
|
|
107
|
+
noDirectImplementationCall.name,
|
|
108
|
+
noNativeErrorResult.name,
|
|
109
|
+
noRedundantResultErrorWrap.name,
|
|
110
|
+
noRetiredCrossVocabulary.name,
|
|
111
|
+
noSyncResultAssumption.name,
|
|
112
|
+
noThrowInDetourRecover.name,
|
|
113
|
+
noThrowInImplementation.name,
|
|
114
|
+
noTopLevelSurface.name,
|
|
115
|
+
onReferencesExist.name,
|
|
116
|
+
orphanedSignal.name,
|
|
117
|
+
ownerProjectionParity.name,
|
|
118
|
+
pendingForce.name,
|
|
119
|
+
permitGovernance.name,
|
|
120
|
+
preferSchemaInference.name,
|
|
121
|
+
publicExportExampleCoverage.name,
|
|
122
|
+
publicInternalDeepImports.name,
|
|
123
|
+
publicOutputSchema.name,
|
|
124
|
+
publicUnionOutputDiscriminants.name,
|
|
125
|
+
readIntentFires.name,
|
|
126
|
+
referenceExists.name,
|
|
127
|
+
resolvedImportBoundary.name,
|
|
128
|
+
resourceDeclarations.name,
|
|
129
|
+
resourceExists.name,
|
|
130
|
+
resourceIdGrammar.name,
|
|
131
|
+
resourceMockCoverage.name,
|
|
132
|
+
scheduledDestroyIntent.name,
|
|
133
|
+
signalGraphCoaching.name,
|
|
134
|
+
staticResourceAccessorPreference.name,
|
|
135
|
+
surfaceFacetCoherence.name,
|
|
136
|
+
unmaterializedActivationSource.name,
|
|
137
|
+
unreachableDetourShadowing.name,
|
|
138
|
+
validDetourContract.name,
|
|
139
|
+
validDescribeRefs.name,
|
|
140
|
+
versionGap.name,
|
|
141
|
+
versionPinnedCompose.name,
|
|
142
|
+
versionWithoutExamples.name,
|
|
143
|
+
wardenRulesUseAst.name,
|
|
144
|
+
webhookRouteCollision.name,
|
|
145
|
+
];
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { hasIgnoreCommentOnLine, splitSourceLines } from './ast.js';
|
|
2
|
+
import { isTestFile } from './scan.js';
|
|
3
|
+
import type { WardenImportResolution } from '../resolve.js';
|
|
4
|
+
import type {
|
|
5
|
+
ProjectAwareWardenRule,
|
|
6
|
+
ProjectContext,
|
|
7
|
+
WardenDiagnostic,
|
|
8
|
+
} from './types.js';
|
|
9
|
+
|
|
10
|
+
const RULE_NAME = 'resolved-import-boundary';
|
|
11
|
+
|
|
12
|
+
const normalizePath = (path: string): string => path.replaceAll('\\', '/');
|
|
13
|
+
|
|
14
|
+
const isLocalPathImport = (importSource: string): boolean =>
|
|
15
|
+
importSource.startsWith('.') || importSource.startsWith('/');
|
|
16
|
+
|
|
17
|
+
const isFixtureOrMigrationFile = (filePath: string): boolean => {
|
|
18
|
+
const normalized = normalizePath(filePath);
|
|
19
|
+
return /(?:^|\/)(?:__fixtures__|fixtures?|migrations?)(?:\/|$)/.test(
|
|
20
|
+
normalized
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const isAllowlistedFile = (filePath: string): boolean =>
|
|
25
|
+
isTestFile(filePath) || isFixtureOrMigrationFile(filePath);
|
|
26
|
+
|
|
27
|
+
const resolutionLabel = (resolution: {
|
|
28
|
+
readonly importSource: string;
|
|
29
|
+
readonly packageName?: string | undefined;
|
|
30
|
+
}): string => resolution.packageName ?? resolution.importSource;
|
|
31
|
+
|
|
32
|
+
const publicSurfaceMessage = (resolution: {
|
|
33
|
+
readonly importSource: string;
|
|
34
|
+
readonly packageName?: string | undefined;
|
|
35
|
+
}): string =>
|
|
36
|
+
`Import "${resolution.importSource}" is not exported by ${resolutionLabel(
|
|
37
|
+
resolution
|
|
38
|
+
)}. Import the package root or an exported subpath instead.`;
|
|
39
|
+
|
|
40
|
+
const localPathBoundaryMessage = (resolution: {
|
|
41
|
+
readonly importSource: string;
|
|
42
|
+
readonly packageName?: string | undefined;
|
|
43
|
+
}): string =>
|
|
44
|
+
`Local import "${resolution.importSource}" composes into ${resolutionLabel(
|
|
45
|
+
resolution
|
|
46
|
+
)}. Import the target package public surface instead.`;
|
|
47
|
+
|
|
48
|
+
const internalTargetMessage = (resolution: {
|
|
49
|
+
readonly importSource: string;
|
|
50
|
+
readonly packageName?: string | undefined;
|
|
51
|
+
}): string =>
|
|
52
|
+
`Import "${resolution.importSource}" targets internal/private files in ${resolutionLabel(
|
|
53
|
+
resolution
|
|
54
|
+
)}. Import the target package public surface instead.`;
|
|
55
|
+
|
|
56
|
+
const diagnosticForResolution = (
|
|
57
|
+
filePath: string,
|
|
58
|
+
resolution: WardenImportResolution
|
|
59
|
+
): WardenDiagnostic | null => {
|
|
60
|
+
if (!resolution.crossesPackageBoundary) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (resolution.isInternalTarget) {
|
|
65
|
+
return {
|
|
66
|
+
filePath,
|
|
67
|
+
line: resolution.line,
|
|
68
|
+
message: internalTargetMessage(resolution),
|
|
69
|
+
rule: RULE_NAME,
|
|
70
|
+
severity: 'error',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (isLocalPathImport(resolution.importSource)) {
|
|
75
|
+
return {
|
|
76
|
+
filePath,
|
|
77
|
+
line: resolution.line,
|
|
78
|
+
message: localPathBoundaryMessage(resolution),
|
|
79
|
+
rule: RULE_NAME,
|
|
80
|
+
severity: 'error',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (resolution.errorKind === 'package-path-not-exported') {
|
|
85
|
+
return {
|
|
86
|
+
filePath,
|
|
87
|
+
line: resolution.line,
|
|
88
|
+
message: publicSurfaceMessage(resolution),
|
|
89
|
+
rule: RULE_NAME,
|
|
90
|
+
severity: 'error',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (
|
|
95
|
+
resolution.errorKind &&
|
|
96
|
+
resolution.errorKind !== 'builtin' &&
|
|
97
|
+
resolution.errorKind !== 'ignored'
|
|
98
|
+
) {
|
|
99
|
+
return {
|
|
100
|
+
filePath,
|
|
101
|
+
line: resolution.line,
|
|
102
|
+
message: publicSurfaceMessage(resolution),
|
|
103
|
+
rule: RULE_NAME,
|
|
104
|
+
severity: 'error',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return null;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const importResolutionsForFile = (context: ProjectContext, filePath: string) =>
|
|
112
|
+
context.importResolutionsByFile?.get(filePath) ?? [];
|
|
113
|
+
|
|
114
|
+
export const resolvedImportBoundary: ProjectAwareWardenRule = {
|
|
115
|
+
check(): readonly WardenDiagnostic[] {
|
|
116
|
+
return [];
|
|
117
|
+
},
|
|
118
|
+
checkWithContext(
|
|
119
|
+
sourceCode: string,
|
|
120
|
+
filePath: string,
|
|
121
|
+
context: ProjectContext
|
|
122
|
+
): readonly WardenDiagnostic[] {
|
|
123
|
+
if (isAllowlistedFile(filePath)) {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const lines = splitSourceLines(sourceCode);
|
|
128
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
129
|
+
|
|
130
|
+
for (const resolution of importResolutionsForFile(context, filePath)) {
|
|
131
|
+
if (hasIgnoreCommentOnLine(lines, resolution.line)) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const diagnostic = diagnosticForResolution(filePath, resolution);
|
|
135
|
+
if (diagnostic) {
|
|
136
|
+
diagnostics.push(diagnostic);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return diagnostics;
|
|
141
|
+
},
|
|
142
|
+
description:
|
|
143
|
+
'Ensure compose-package imports resolve through package-owned public exports.',
|
|
144
|
+
name: RULE_NAME,
|
|
145
|
+
severity: 'error',
|
|
146
|
+
};
|