@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,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `incomplete-accessor-for-standard-op` — flag standard-op CRUD trails whose
|
|
3
|
+
* backing resource accessor is missing the method the trail will call at
|
|
4
|
+
* runtime.
|
|
5
|
+
*
|
|
6
|
+
* The warden cannot invoke blazes directly, but most resources declare a
|
|
7
|
+
* `mock` factory that returns a structurally real accessor for testing. We
|
|
8
|
+
* exploit that: invoke the mock with no arguments, look up the accessor by
|
|
9
|
+
* contour name (which equals every CRUD-emitted trail ID's leading
|
|
10
|
+
* segments), and inspect the method keys.
|
|
11
|
+
*
|
|
12
|
+
* The rule is intentionally forgiving — if the mock factory is missing,
|
|
13
|
+
* throws, or the connection shape does not match what we expect, we skip
|
|
14
|
+
* the trail rather than produce a false positive. The runtime fallback in
|
|
15
|
+
* `derive-trail.ts` still surfaces genuine misuse at execution time.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { AnyResource, AnyTrail, Topo } from '@ontrails/core';
|
|
19
|
+
import { crudAccessorExpectations, crudOperations } from '@ontrails/store';
|
|
20
|
+
import type { CrudAccessorExpectation, CrudOperation } from '@ontrails/store';
|
|
21
|
+
|
|
22
|
+
import type { TopoAwareWardenRule, WardenDiagnostic } from './types.js';
|
|
23
|
+
|
|
24
|
+
type StandardOp = CrudOperation;
|
|
25
|
+
|
|
26
|
+
const STANDARD_OPS: ReadonlySet<StandardOp> = new Set(crudOperations);
|
|
27
|
+
|
|
28
|
+
const RULE_NAME = 'incomplete-accessor-for-standard-op';
|
|
29
|
+
|
|
30
|
+
const deriveOperation = (trailId: string): StandardOp | undefined => {
|
|
31
|
+
const lastDot = trailId.lastIndexOf('.');
|
|
32
|
+
if (lastDot === -1) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const tail = trailId.slice(lastDot + 1);
|
|
36
|
+
return STANDARD_OPS.has(tail as StandardOp)
|
|
37
|
+
? (tail as StandardOp)
|
|
38
|
+
: undefined;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const deriveContourName = (trailId: string): string | undefined => {
|
|
42
|
+
const lastDot = trailId.lastIndexOf('.');
|
|
43
|
+
if (lastDot <= 0) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
return trailId.slice(0, lastDot);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
|
|
50
|
+
if (value === null || typeof value !== 'object') {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return typeof (value as { then?: unknown }).then !== 'function';
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const collectMethodNames = (
|
|
57
|
+
accessor: Record<string, unknown>
|
|
58
|
+
): ReadonlySet<string> => {
|
|
59
|
+
const methods = new Set<string>();
|
|
60
|
+
let current: object | null = accessor;
|
|
61
|
+
while (current !== null && current !== Object.prototype) {
|
|
62
|
+
for (const key of Object.getOwnPropertyNames(current)) {
|
|
63
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, key);
|
|
64
|
+
if (
|
|
65
|
+
descriptor !== undefined &&
|
|
66
|
+
'value' in descriptor &&
|
|
67
|
+
typeof descriptor.value === 'function'
|
|
68
|
+
) {
|
|
69
|
+
methods.add(key);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
current = Object.getPrototypeOf(current);
|
|
73
|
+
}
|
|
74
|
+
return methods;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const invokeMockSafely = async (
|
|
78
|
+
resource: AnyResource
|
|
79
|
+
): Promise<unknown | undefined> => {
|
|
80
|
+
const { mock } = resource;
|
|
81
|
+
if (typeof mock !== 'function') {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
return await mock();
|
|
86
|
+
} catch {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const disposeMockConnection = async (
|
|
92
|
+
resource: AnyResource,
|
|
93
|
+
connection: unknown
|
|
94
|
+
): Promise<void> => {
|
|
95
|
+
const { dispose } = resource;
|
|
96
|
+
if (typeof dispose !== 'function') {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
await dispose(connection);
|
|
101
|
+
} catch {
|
|
102
|
+
// Cleanup failures should not turn best-effort inspection into a false positive.
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const resolveAccessor = (
|
|
107
|
+
connection: unknown,
|
|
108
|
+
contourName: string
|
|
109
|
+
): Record<string, unknown> | undefined => {
|
|
110
|
+
if (!isPlainObject(connection)) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
const accessor = connection[contourName];
|
|
114
|
+
return isPlainObject(accessor) ? accessor : undefined;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Attempt to resolve the accessor shape from the resource's mock factory.
|
|
119
|
+
*
|
|
120
|
+
* Returns `undefined` when the accessor cannot be inspected (no mock, mock
|
|
121
|
+
* throws, connection doesn't have the expected key, etc.). The rule treats
|
|
122
|
+
* `undefined` as "skip this trail" rather than as a violation — we should
|
|
123
|
+
* never false-positive on a shape we cannot see.
|
|
124
|
+
*/
|
|
125
|
+
const inspectAccessorMethods = async (
|
|
126
|
+
resource: AnyResource,
|
|
127
|
+
contourName: string
|
|
128
|
+
): Promise<ReadonlySet<string> | undefined> => {
|
|
129
|
+
const connection = await invokeMockSafely(resource);
|
|
130
|
+
if (connection === undefined) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const accessor = resolveAccessor(connection, contourName);
|
|
135
|
+
if (accessor === undefined) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
return collectMethodNames(accessor);
|
|
139
|
+
} finally {
|
|
140
|
+
await disposeMockConnection(resource, connection);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const formatDiagnostic = (
|
|
145
|
+
trailId: string,
|
|
146
|
+
operation: StandardOp,
|
|
147
|
+
message: string,
|
|
148
|
+
severity: 'warn' | 'error'
|
|
149
|
+
): WardenDiagnostic => ({
|
|
150
|
+
filePath: '<topo>',
|
|
151
|
+
line: 1,
|
|
152
|
+
message: `Trail "${trailId}" (crud.${operation}): ${message}`,
|
|
153
|
+
rule: RULE_NAME,
|
|
154
|
+
severity,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
interface StandardOpContext {
|
|
158
|
+
readonly trailId: string;
|
|
159
|
+
readonly operation: StandardOp;
|
|
160
|
+
readonly contourName: string;
|
|
161
|
+
readonly resource: AnyResource;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// CRUD trails synthesized by the store factory always declare one resource.
|
|
165
|
+
// For multi- or zero-resource trails, we cannot unambiguously pick one to
|
|
166
|
+
// inspect; skip rather than guess.
|
|
167
|
+
const extractSoleResource = (trail: AnyTrail): AnyResource | undefined => {
|
|
168
|
+
const resources = trail.resources ?? [];
|
|
169
|
+
if (resources.length !== 1) {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
const [resource] = resources;
|
|
173
|
+
return resource;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const extractStandardOpContext = (
|
|
177
|
+
trail: AnyTrail
|
|
178
|
+
): StandardOpContext | undefined => {
|
|
179
|
+
if (trail.pattern !== 'crud') {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
const operation = deriveOperation(trail.id);
|
|
183
|
+
const contourName = deriveContourName(trail.id);
|
|
184
|
+
const resource = extractSoleResource(trail);
|
|
185
|
+
if (
|
|
186
|
+
operation === undefined ||
|
|
187
|
+
contourName === undefined ||
|
|
188
|
+
resource === undefined
|
|
189
|
+
) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
return { contourName, operation, resource, trailId: trail.id };
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const diagnoseMissingMethod = (
|
|
196
|
+
ctx: StandardOpContext,
|
|
197
|
+
methods: ReadonlySet<string>,
|
|
198
|
+
expectation: CrudAccessorExpectation
|
|
199
|
+
): WardenDiagnostic | undefined => {
|
|
200
|
+
if (methods.has(expectation.preferred)) {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
const { fallback } = expectation;
|
|
204
|
+
const base = `resource "${ctx.resource.id}" accessor "${ctx.contourName}"`;
|
|
205
|
+
if (fallback === undefined) {
|
|
206
|
+
return formatDiagnostic(
|
|
207
|
+
ctx.trailId,
|
|
208
|
+
ctx.operation,
|
|
209
|
+
`${base} is missing required method "${expectation.preferred}"`,
|
|
210
|
+
expectation.severityWhenNoFallback
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (methods.has(fallback)) {
|
|
214
|
+
return formatDiagnostic(
|
|
215
|
+
ctx.trailId,
|
|
216
|
+
ctx.operation,
|
|
217
|
+
`${base} is missing preferred method "${expectation.preferred}"; falls back to "${fallback}"`,
|
|
218
|
+
expectation.severityWhenPreferredMissingWithFallback ??
|
|
219
|
+
expectation.severityWhenNoFallback
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return formatDiagnostic(
|
|
223
|
+
ctx.trailId,
|
|
224
|
+
ctx.operation,
|
|
225
|
+
`${base} is missing both "${expectation.preferred}" and fallback "${fallback}"`,
|
|
226
|
+
expectation.severityWhenNoFallback
|
|
227
|
+
);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const evaluateTrail = async (
|
|
231
|
+
trail: AnyTrail
|
|
232
|
+
): Promise<readonly WardenDiagnostic[]> => {
|
|
233
|
+
const ctx = extractStandardOpContext(trail);
|
|
234
|
+
if (ctx === undefined) {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
const methods = await inspectAccessorMethods(ctx.resource, ctx.contourName);
|
|
238
|
+
if (methods === undefined) {
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
241
|
+
const diagnostic = diagnoseMissingMethod(
|
|
242
|
+
ctx,
|
|
243
|
+
methods,
|
|
244
|
+
crudAccessorExpectations[ctx.operation]
|
|
245
|
+
);
|
|
246
|
+
return diagnostic === undefined ? [] : [diagnostic];
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Topo-aware rule that flags CRUD trails whose backing accessor is missing
|
|
251
|
+
* the method invoked by the synthesized blaze.
|
|
252
|
+
*
|
|
253
|
+
* @remarks
|
|
254
|
+
* Introspects each resource's `mock()` factory to determine the accessor
|
|
255
|
+
* shape. Trails whose resource has no mock, whose mock throws, or whose
|
|
256
|
+
* mock returns a shape the rule cannot interpret are silently skipped. The
|
|
257
|
+
* runtime fallback in `derive-trail.ts` remains the enforcement of last
|
|
258
|
+
* resort.
|
|
259
|
+
*/
|
|
260
|
+
export const incompleteAccessorForStandardOp: TopoAwareWardenRule = {
|
|
261
|
+
async checkTopo(topo: Topo): Promise<readonly WardenDiagnostic[]> {
|
|
262
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
263
|
+
for (const trail of topo.trails.values()) {
|
|
264
|
+
diagnostics.push(...(await evaluateTrail(trail)));
|
|
265
|
+
}
|
|
266
|
+
return diagnostics;
|
|
267
|
+
},
|
|
268
|
+
description:
|
|
269
|
+
'Flag CRUD-pattern trails whose resource accessor lacks the method the synthesized blaze will call at runtime.',
|
|
270
|
+
name: RULE_NAME,
|
|
271
|
+
severity: 'error',
|
|
272
|
+
};
|