@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.
Files changed (249) hide show
  1. package/CHANGELOG.md +497 -6
  2. package/README.md +77 -26
  3. package/bin/warden.ts +50 -0
  4. package/package.json +27 -5
  5. package/src/adapter-check.ts +136 -0
  6. package/src/ast.ts +28 -0
  7. package/src/cli.ts +1374 -103
  8. package/src/command.ts +953 -0
  9. package/src/config.ts +184 -0
  10. package/src/draft.ts +22 -0
  11. package/src/drift.ts +106 -22
  12. package/src/fix.ts +120 -0
  13. package/src/formatters.ts +79 -9
  14. package/src/guide.ts +245 -0
  15. package/src/index.ts +206 -14
  16. package/src/project-context.ts +163 -0
  17. package/src/resolve.ts +530 -0
  18. package/src/rules/activation-orphan.ts +97 -0
  19. package/src/rules/ast.ts +3176 -85
  20. package/src/rules/circular-refs.ts +154 -0
  21. package/src/rules/composes-declarations.ts +704 -0
  22. package/src/rules/context-no-surface-types.ts +68 -8
  23. package/src/rules/contour-exists.ts +251 -0
  24. package/src/rules/contour-ids.ts +15 -0
  25. package/src/rules/dead-internal-trail.ts +154 -0
  26. package/src/rules/draft-file-marking.ts +160 -0
  27. package/src/rules/draft-visible-debt.ts +87 -0
  28. package/src/rules/error-mapping-completeness.ts +288 -0
  29. package/src/rules/example-valid.ts +401 -0
  30. package/src/rules/fires-declarations.ts +758 -0
  31. package/src/rules/implementation-returns-result.ts +1265 -95
  32. package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
  33. package/src/rules/incomplete-crud.ts +580 -0
  34. package/src/rules/index.ts +219 -18
  35. package/src/rules/intent-propagation.ts +127 -0
  36. package/src/rules/layer-field-name-drift.ts +96 -0
  37. package/src/rules/metadata.ts +654 -0
  38. package/src/rules/missing-reconcile.ts +98 -0
  39. package/src/rules/missing-visibility.ts +110 -0
  40. package/src/rules/no-destructured-compose.ts +192 -0
  41. package/src/rules/no-dev-permit-in-source.ts +99 -0
  42. package/src/rules/no-direct-implementation-call.ts +7 -7
  43. package/src/rules/no-legacy-layer-imports.ts +211 -0
  44. package/src/rules/no-native-error-result.ts +111 -0
  45. package/src/rules/no-redundant-result-error-wrap.ts +331 -0
  46. package/src/rules/no-retired-cross-vocabulary.ts +194 -0
  47. package/src/rules/no-sync-result-assumption.ts +1134 -99
  48. package/src/rules/no-throw-in-detour-recover.ts +225 -0
  49. package/src/rules/no-throw-in-implementation.ts +10 -9
  50. package/src/rules/no-top-level-surface.ts +389 -0
  51. package/src/rules/on-references-exist.ts +194 -0
  52. package/src/rules/orphaned-signal.ts +150 -0
  53. package/src/rules/owner-projection-parity.ts +146 -0
  54. package/src/rules/permit-governance.ts +25 -0
  55. package/src/rules/public-export-example-coverage.ts +553 -0
  56. package/src/rules/public-internal-deep-imports.ts +517 -0
  57. package/src/rules/public-output-schema.ts +29 -0
  58. package/src/rules/public-union-output-discriminants.ts +150 -0
  59. package/src/rules/read-intent-fires.ts +187 -0
  60. package/src/rules/reference-exists.ts +98 -0
  61. package/src/rules/registry-names.ts +145 -0
  62. package/src/rules/resolved-import-boundary.ts +146 -0
  63. package/src/rules/resource-declarations.ts +704 -0
  64. package/src/rules/resource-exists.ts +179 -0
  65. package/src/rules/resource-id-grammar.ts +65 -0
  66. package/src/rules/resource-mock-coverage.ts +115 -0
  67. package/src/rules/scan.ts +38 -25
  68. package/src/rules/scheduled-destroy-intent.ts +44 -0
  69. package/src/rules/signal-graph-coaching.ts +191 -0
  70. package/src/rules/specs.ts +9 -5
  71. package/src/rules/static-resource-accessor-preference.ts +657 -0
  72. package/src/rules/surface-facet-coherence.ts +370 -0
  73. package/src/rules/trail-versioning-source.ts +1094 -0
  74. package/src/rules/trail-versioning-topo.ts +172 -0
  75. package/src/rules/types.ts +270 -6
  76. package/src/rules/unmaterialized-activation-source.ts +84 -0
  77. package/src/rules/unreachable-detour-shadowing.ts +344 -0
  78. package/src/rules/valid-describe-refs.ts +160 -32
  79. package/src/rules/valid-detour-contract.ts +78 -0
  80. package/src/rules/warden-export-symmetry.ts +533 -0
  81. package/src/rules/warden-rules-use-ast.ts +996 -0
  82. package/src/rules/webhook-route-collision.ts +243 -0
  83. package/src/trails/activation-orphan.trail.ts +84 -0
  84. package/src/trails/circular-refs.trail.ts +29 -0
  85. package/src/trails/composes-declarations.trail.ts +22 -0
  86. package/src/trails/context-no-surface-types.trail.ts +21 -0
  87. package/src/trails/contour-exists.trail.ts +21 -0
  88. package/src/trails/dead-internal-trail.trail.ts +26 -0
  89. package/src/trails/deprecation-without-guidance.trail.ts +21 -0
  90. package/src/trails/draft-file-marking.trail.ts +16 -0
  91. package/src/trails/draft-visible-debt.trail.ts +16 -0
  92. package/src/trails/error-mapping-completeness.trail.ts +29 -0
  93. package/src/trails/example-valid.trail.ts +25 -0
  94. package/src/trails/fires-declarations.trail.ts +23 -0
  95. package/src/trails/fork-without-preserved-blaze.trail.ts +31 -0
  96. package/src/trails/implementation-returns-result.trail.ts +20 -0
  97. package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
  98. package/src/trails/incomplete-crud.trail.ts +39 -0
  99. package/src/trails/index.ts +78 -0
  100. package/src/trails/intent-propagation.trail.ts +30 -0
  101. package/src/trails/layer-field-name-drift.trail.ts +39 -0
  102. package/src/trails/marker-schema-unsupported.trail.ts +23 -0
  103. package/src/trails/missing-reconcile.trail.ts +33 -0
  104. package/src/trails/missing-visibility.trail.ts +22 -0
  105. package/src/trails/no-destructured-compose.trail.ts +44 -0
  106. package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
  107. package/src/trails/no-direct-implementation-call.trail.ts +16 -0
  108. package/src/trails/no-legacy-layer-imports.trail.ts +41 -0
  109. package/src/trails/no-native-error-result.trail.ts +18 -0
  110. package/src/trails/no-redundant-result-error-wrap.trail.ts +55 -0
  111. package/src/trails/no-retired-cross-vocabulary.trail.ts +42 -0
  112. package/src/trails/no-sync-result-assumption.trail.ts +19 -0
  113. package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
  114. package/src/trails/no-throw-in-implementation.trail.ts +20 -0
  115. package/src/trails/no-top-level-surface.trail.ts +43 -0
  116. package/src/trails/on-references-exist.trail.ts +21 -0
  117. package/src/trails/orphaned-signal.trail.ts +36 -0
  118. package/src/trails/owner-projection-parity.trail.ts +26 -0
  119. package/src/trails/pending-force.trail.ts +21 -0
  120. package/src/trails/permit-governance.trail.ts +51 -0
  121. package/src/trails/prefer-schema-inference.trail.ts +21 -0
  122. package/src/trails/public-export-example-coverage.trail.ts +16 -0
  123. package/src/trails/public-internal-deep-imports.trail.ts +94 -0
  124. package/src/trails/public-output-schema.trail.ts +55 -0
  125. package/src/trails/public-union-output-discriminants.trail.ts +33 -0
  126. package/src/trails/read-intent-fires.trail.ts +20 -0
  127. package/src/trails/reference-exists.trail.ts +25 -0
  128. package/src/trails/resolved-import-boundary.trail.ts +109 -0
  129. package/src/trails/resource-declarations.trail.ts +25 -0
  130. package/src/trails/resource-exists.trail.ts +27 -0
  131. package/src/trails/resource-id-grammar.trail.ts +39 -0
  132. package/src/trails/resource-mock-coverage.trail.ts +40 -0
  133. package/src/trails/run.ts +162 -0
  134. package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
  135. package/src/trails/schema.ts +194 -0
  136. package/src/trails/signal-graph-coaching.trail.ts +77 -0
  137. package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
  138. package/src/trails/surface-facet-coherence.trail.ts +25 -0
  139. package/src/trails/topo.ts +6 -0
  140. package/src/trails/unmaterialized-activation-source.trail.ts +72 -0
  141. package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
  142. package/src/trails/valid-describe-refs.trail.ts +18 -0
  143. package/src/trails/valid-detour-contract.trail.ts +71 -0
  144. package/src/trails/version-gap.trail.ts +35 -0
  145. package/src/trails/version-pinned-compose.trail.ts +23 -0
  146. package/src/trails/version-without-examples.trail.ts +38 -0
  147. package/src/trails/warden-export-symmetry.trail.ts +16 -0
  148. package/src/trails/warden-rules-use-ast.trail.ts +45 -0
  149. package/src/trails/webhook-route-collision.trail.ts +50 -0
  150. package/src/trails/wrap-rule.ts +213 -0
  151. package/src/workspaces.ts +238 -0
  152. package/.turbo/turbo-build.log +0 -1
  153. package/.turbo/turbo-lint.log +0 -3
  154. package/.turbo/turbo-typecheck.log +0 -1
  155. package/dist/cli.d.ts +0 -46
  156. package/dist/cli.d.ts.map +0 -1
  157. package/dist/cli.js +0 -221
  158. package/dist/cli.js.map +0 -1
  159. package/dist/drift.d.ts +0 -26
  160. package/dist/drift.d.ts.map +0 -1
  161. package/dist/drift.js +0 -27
  162. package/dist/drift.js.map +0 -1
  163. package/dist/formatters.d.ts +0 -29
  164. package/dist/formatters.d.ts.map +0 -1
  165. package/dist/formatters.js +0 -87
  166. package/dist/formatters.js.map +0 -1
  167. package/dist/index.d.ts +0 -26
  168. package/dist/index.d.ts.map +0 -1
  169. package/dist/index.js +0 -26
  170. package/dist/index.js.map +0 -1
  171. package/dist/rules/ast.d.ts +0 -41
  172. package/dist/rules/ast.d.ts.map +0 -1
  173. package/dist/rules/ast.js +0 -163
  174. package/dist/rules/ast.js.map +0 -1
  175. package/dist/rules/context-no-surface-types.d.ts +0 -12
  176. package/dist/rules/context-no-surface-types.d.ts.map +0 -1
  177. package/dist/rules/context-no-surface-types.js +0 -96
  178. package/dist/rules/context-no-surface-types.js.map +0 -1
  179. package/dist/rules/implementation-returns-result.d.ts +0 -13
  180. package/dist/rules/implementation-returns-result.d.ts.map +0 -1
  181. package/dist/rules/implementation-returns-result.js +0 -231
  182. package/dist/rules/implementation-returns-result.js.map +0 -1
  183. package/dist/rules/index.d.ts +0 -22
  184. package/dist/rules/index.d.ts.map +0 -1
  185. package/dist/rules/index.js +0 -41
  186. package/dist/rules/index.js.map +0 -1
  187. package/dist/rules/no-direct-impl-in-route.d.ts +0 -12
  188. package/dist/rules/no-direct-impl-in-route.d.ts.map +0 -1
  189. package/dist/rules/no-direct-impl-in-route.js +0 -46
  190. package/dist/rules/no-direct-impl-in-route.js.map +0 -1
  191. package/dist/rules/no-direct-implementation-call.d.ts +0 -12
  192. package/dist/rules/no-direct-implementation-call.d.ts.map +0 -1
  193. package/dist/rules/no-direct-implementation-call.js +0 -39
  194. package/dist/rules/no-direct-implementation-call.js.map +0 -1
  195. package/dist/rules/no-sync-result-assumption.d.ts +0 -6
  196. package/dist/rules/no-sync-result-assumption.d.ts.map +0 -1
  197. package/dist/rules/no-sync-result-assumption.js +0 -98
  198. package/dist/rules/no-sync-result-assumption.js.map +0 -1
  199. package/dist/rules/no-throw-in-detour-target.d.ts +0 -12
  200. package/dist/rules/no-throw-in-detour-target.d.ts.map +0 -1
  201. package/dist/rules/no-throw-in-detour-target.js +0 -87
  202. package/dist/rules/no-throw-in-detour-target.js.map +0 -1
  203. package/dist/rules/no-throw-in-implementation.d.ts +0 -9
  204. package/dist/rules/no-throw-in-implementation.d.ts.map +0 -1
  205. package/dist/rules/no-throw-in-implementation.js +0 -34
  206. package/dist/rules/no-throw-in-implementation.js.map +0 -1
  207. package/dist/rules/prefer-schema-inference.d.ts +0 -7
  208. package/dist/rules/prefer-schema-inference.d.ts.map +0 -1
  209. package/dist/rules/prefer-schema-inference.js +0 -86
  210. package/dist/rules/prefer-schema-inference.js.map +0 -1
  211. package/dist/rules/scan.d.ts +0 -8
  212. package/dist/rules/scan.d.ts.map +0 -1
  213. package/dist/rules/scan.js +0 -32
  214. package/dist/rules/scan.js.map +0 -1
  215. package/dist/rules/specs.d.ts +0 -29
  216. package/dist/rules/specs.d.ts.map +0 -1
  217. package/dist/rules/specs.js +0 -192
  218. package/dist/rules/specs.js.map +0 -1
  219. package/dist/rules/structure.d.ts +0 -13
  220. package/dist/rules/structure.d.ts.map +0 -1
  221. package/dist/rules/structure.js +0 -142
  222. package/dist/rules/structure.js.map +0 -1
  223. package/dist/rules/types.d.ts +0 -52
  224. package/dist/rules/types.d.ts.map +0 -1
  225. package/dist/rules/types.js +0 -2
  226. package/dist/rules/types.js.map +0 -1
  227. package/dist/rules/valid-describe-refs.d.ts +0 -7
  228. package/dist/rules/valid-describe-refs.d.ts.map +0 -1
  229. package/dist/rules/valid-describe-refs.js +0 -51
  230. package/dist/rules/valid-describe-refs.js.map +0 -1
  231. package/dist/rules/valid-detour-refs.d.ts +0 -6
  232. package/dist/rules/valid-detour-refs.d.ts.map +0 -1
  233. package/dist/rules/valid-detour-refs.js +0 -116
  234. package/dist/rules/valid-detour-refs.js.map +0 -1
  235. package/src/__tests__/cli.test.ts +0 -198
  236. package/src/__tests__/drift.test.ts +0 -74
  237. package/src/__tests__/formatters.test.ts +0 -157
  238. package/src/__tests__/implementation-returns-result.test.ts +0 -75
  239. package/src/__tests__/no-direct-implementation-call.test.ts +0 -83
  240. package/src/__tests__/no-sync-result-assumption.test.ts +0 -85
  241. package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
  242. package/src/__tests__/prefer-schema-inference.test.ts +0 -84
  243. package/src/__tests__/rules.test.ts +0 -188
  244. package/src/__tests__/valid-describe-refs.test.ts +0 -60
  245. package/src/rules/no-direct-impl-in-route.ts +0 -77
  246. package/src/rules/no-throw-in-detour-target.ts +0 -150
  247. package/src/rules/valid-detour-refs.ts +0 -187
  248. package/tsconfig.json +0 -9
  249. 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
+ };