@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,87 @@
1
+ import { isDraftId } from '@ontrails/core';
2
+
3
+ import {
4
+ collectFrameworkDraftPrefixConstantOffsets,
5
+ findStringLiterals,
6
+ hasIgnoreCommentOnLine,
7
+ offsetToLine,
8
+ parse,
9
+ splitSourceLines,
10
+ } from './ast.js';
11
+ import type { WardenDiagnostic, WardenRule } from './types.js';
12
+
13
+ const createDiagnostic = (
14
+ sourceCode: string,
15
+ filePath: string,
16
+ match: { start: number; value: string }
17
+ ): WardenDiagnostic => ({
18
+ filePath,
19
+ line: offsetToLine(sourceCode, match.start),
20
+ message:
21
+ `Draft id "${match.value}" is still visible debt. ` +
22
+ 'Established surfaces, lock export, and OpenAPI generation will reject it until it is promoted.',
23
+ rule: 'draft-visible-debt',
24
+ severity: 'warn',
25
+ });
26
+
27
+ const isSuppressedMatch = (
28
+ match: { start: number },
29
+ sourceCode: string,
30
+ lines: readonly string[],
31
+ frameworkConstantOffsets: ReadonlySet<number>
32
+ ): boolean =>
33
+ frameworkConstantOffsets.has(match.start) ||
34
+ hasIgnoreCommentOnLine(lines, offsetToLine(sourceCode, match.start));
35
+
36
+ const collectDraftVisibleDebtDiagnostics = (
37
+ sourceCode: string,
38
+ filePath: string,
39
+ ast: NonNullable<ReturnType<typeof parse>>
40
+ ): WardenDiagnostic[] => {
41
+ const frameworkConstantOffsets = collectFrameworkDraftPrefixConstantOffsets(
42
+ ast,
43
+ filePath
44
+ );
45
+ const lines = splitSourceLines(sourceCode);
46
+ const seen = new Set<string>();
47
+
48
+ return findStringLiterals(ast, (value) => isDraftId(value)).flatMap(
49
+ (match) => {
50
+ if (
51
+ isSuppressedMatch(match, sourceCode, lines, frameworkConstantOffsets)
52
+ ) {
53
+ return [];
54
+ }
55
+ const key = `${match.value}:${String(match.start)}`;
56
+ if (seen.has(key)) {
57
+ return [];
58
+ }
59
+ seen.add(key);
60
+ return [createDiagnostic(sourceCode, filePath, match)];
61
+ }
62
+ );
63
+ };
64
+
65
+ /**
66
+ * Warns when draft ids are still present so the debt stays visible during
67
+ * review even when the file is correctly marked.
68
+ *
69
+ * Severity is intentionally `warn`, not `error`. The hard rejection layer for
70
+ * draft state leaking into established outputs is `validateEstablishedTopo` at
71
+ * runtime — it blocks topo compile, surface projection, and lockfile writes.
72
+ * This rule surfaces the debt for human reviewers without duplicating that layer.
73
+ */
74
+ export const draftVisibleDebt: WardenRule = {
75
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
76
+ const ast = parse(filePath, sourceCode);
77
+ if (!ast) {
78
+ return [];
79
+ }
80
+
81
+ return collectDraftVisibleDebtDiagnostics(sourceCode, filePath, ast);
82
+ },
83
+ description:
84
+ 'Warn when draft ids remain in source so the debt stays visible during review.',
85
+ name: 'draft-visible-debt',
86
+ severity: 'warn',
87
+ };
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Validates that registered surface error mappers cover every error category.
3
+ *
4
+ * Scans `createSurfaceErrorMapper(...)` calls, then resolves simple object
5
+ * literals, identifier bindings, and object-property references in the same
6
+ * file so incomplete mapper registrations are caught before they ship.
7
+ */
8
+
9
+ import { codesByCategory, errorClasses } from '@ontrails/core';
10
+ import type { ErrorCategory } from '@ontrails/core';
11
+
12
+ import {
13
+ getStringValue,
14
+ identifierName,
15
+ isStringLiteral,
16
+ offsetToLine,
17
+ parse,
18
+ walk,
19
+ } from './ast.js';
20
+ import type { AstNode } from './ast.js';
21
+ import { isTestFile } from './scan.js';
22
+ import type { WardenDiagnostic, WardenRule } from './types.js';
23
+
24
+ const MEMBER_EXPRESSION_TYPES = new Set([
25
+ 'MemberExpression',
26
+ 'StaticMemberExpression',
27
+ ]);
28
+
29
+ const MAPPER_FACTORY_NAMES = new Set(['createSurfaceErrorMapper']);
30
+
31
+ const mappedErrorClassCategories = new Set(
32
+ errorClasses.flatMap((entry) =>
33
+ entry.category === 'dynamic' ? [] : [entry.category]
34
+ )
35
+ );
36
+
37
+ const requiredErrorCategories = (
38
+ Object.keys(codesByCategory) as ErrorCategory[]
39
+ ).filter((category) => mappedErrorClassCategories.has(category));
40
+
41
+ const getPropertyName = (node: AstNode | undefined): string | null => {
42
+ if (!node) {
43
+ return null;
44
+ }
45
+
46
+ return (
47
+ identifierName(node) ??
48
+ (isStringLiteral(node) ? getStringValue(node) : null)
49
+ );
50
+ };
51
+
52
+ const collectObjectBindings = (ast: AstNode): ReadonlyMap<string, AstNode> => {
53
+ const bindings = new Map<string, AstNode>();
54
+
55
+ walk(ast, (node) => {
56
+ if (node.type !== 'VariableDeclarator') {
57
+ return;
58
+ }
59
+
60
+ const { id, init } = node as { id?: AstNode; init?: AstNode };
61
+ const bindingName = identifierName(id);
62
+
63
+ if (bindingName && init?.type === 'ObjectExpression') {
64
+ bindings.set(bindingName, init);
65
+ }
66
+ });
67
+
68
+ return bindings;
69
+ };
70
+
71
+ const getObjectProperties = (objectNode: AstNode): readonly AstNode[] =>
72
+ objectNode.type === 'ObjectExpression'
73
+ ? ((objectNode['properties'] as readonly AstNode[] | undefined) ?? [])
74
+ : [];
75
+
76
+ const findObjectPropertyValue = (
77
+ objectNode: AstNode,
78
+ propertyName: string
79
+ ): AstNode | null => {
80
+ for (const property of getObjectProperties(objectNode)) {
81
+ if (property.type !== 'Property') {
82
+ continue;
83
+ }
84
+
85
+ const key = getPropertyName((property as unknown as { key?: AstNode }).key);
86
+ if (key === propertyName) {
87
+ return (property as unknown as { value?: AstNode }).value ?? null;
88
+ }
89
+ }
90
+
91
+ return null;
92
+ };
93
+
94
+ const resolveIdentifierObject = (
95
+ node: AstNode,
96
+ bindings: ReadonlyMap<string, AstNode>
97
+ ): AstNode | null =>
98
+ bindings.get((node as { name?: string }).name ?? '') ?? null;
99
+
100
+ const resolveMemberObject = (
101
+ node: AstNode,
102
+ bindings: ReadonlyMap<string, AstNode>,
103
+ depth: number,
104
+ resolve: (
105
+ node: AstNode | undefined,
106
+ bindings: ReadonlyMap<string, AstNode>,
107
+ depth?: number
108
+ ) => AstNode | null
109
+ ): AstNode | null => {
110
+ const { object, property } = node as { object?: AstNode; property?: AstNode };
111
+ const propertyName = getPropertyName(property);
112
+ if (!propertyName) {
113
+ return null;
114
+ }
115
+
116
+ const objectNode = resolve(object, bindings, depth + 1);
117
+ return objectNode
118
+ ? resolve(
119
+ findObjectPropertyValue(objectNode, propertyName) ?? undefined,
120
+ bindings,
121
+ depth + 1
122
+ )
123
+ : null;
124
+ };
125
+
126
+ const resolveObjectExpression = function resolveObjectExpression(
127
+ node: AstNode | undefined,
128
+ bindings: ReadonlyMap<string, AstNode>,
129
+ depth = 0
130
+ ): AstNode | null {
131
+ if (!node || depth > 4) {
132
+ return null;
133
+ }
134
+
135
+ if (node.type === 'ObjectExpression') {
136
+ return node;
137
+ }
138
+
139
+ if (node.type === 'Identifier') {
140
+ return resolveIdentifierObject(node, bindings);
141
+ }
142
+
143
+ return MEMBER_EXPRESSION_TYPES.has(node.type)
144
+ ? resolveMemberObject(node, bindings, depth, resolveObjectExpression)
145
+ : null;
146
+ };
147
+
148
+ const addMappedCategory = (
149
+ categories: Set<string>,
150
+ property: AstNode
151
+ ): boolean => {
152
+ if (property.type === 'SpreadElement') {
153
+ return false;
154
+ }
155
+
156
+ if (property.type !== 'Property') {
157
+ return true;
158
+ }
159
+
160
+ const key = getPropertyName((property as unknown as { key?: AstNode }).key);
161
+ if (key) {
162
+ categories.add(key);
163
+ }
164
+
165
+ return true;
166
+ };
167
+
168
+ const collectMappedCategories = (
169
+ mapperObject: AstNode
170
+ ): ReadonlySet<string> | null => {
171
+ if (mapperObject.type !== 'ObjectExpression') {
172
+ return null;
173
+ }
174
+
175
+ const categories = new Set<string>();
176
+ for (const property of getObjectProperties(mapperObject)) {
177
+ if (!addMappedCategory(categories, property)) {
178
+ return null;
179
+ }
180
+ }
181
+
182
+ return categories;
183
+ };
184
+
185
+ const createDiagnostic = (
186
+ filePath: string,
187
+ line: number,
188
+ missingCategories: readonly string[]
189
+ ): WardenDiagnostic => ({
190
+ filePath,
191
+ line,
192
+ message: `Surface error mapper is missing mappings for: ${missingCategories.join(', ')}. Registered createSurfaceErrorMapper() calls must cover every ErrorCategory.`,
193
+ rule: 'error-mapping-completeness',
194
+ severity: 'error',
195
+ });
196
+
197
+ const getCallArgs = (node: AstNode): readonly AstNode[] =>
198
+ (node as { arguments?: readonly AstNode[] }).arguments ?? [];
199
+
200
+ const getCallCallee = (node: AstNode): AstNode | undefined =>
201
+ (node as { callee?: AstNode }).callee;
202
+
203
+ const isMapperFactoryCall = (node: AstNode): boolean =>
204
+ node.type === 'CallExpression' &&
205
+ MAPPER_FACTORY_NAMES.has(identifierName(getCallCallee(node)) ?? '');
206
+
207
+ const findMissingCategories = (
208
+ mappedCategories: ReadonlySet<string>
209
+ ): readonly string[] =>
210
+ requiredErrorCategories.filter((category) => !mappedCategories.has(category));
211
+
212
+ const resolveMappedCategories = (
213
+ node: AstNode,
214
+ bindings: ReadonlyMap<string, AstNode>
215
+ ): ReadonlySet<string> | null => {
216
+ const [firstArg] = getCallArgs(node);
217
+ const mapperObject = resolveObjectExpression(firstArg, bindings);
218
+ return mapperObject ? collectMappedCategories(mapperObject) : null;
219
+ };
220
+
221
+ const inspectMapperCall = (
222
+ node: AstNode,
223
+ bindings: ReadonlyMap<string, AstNode>,
224
+ filePath: string,
225
+ sourceCode: string
226
+ ): WardenDiagnostic | null => {
227
+ if (!isMapperFactoryCall(node)) {
228
+ return null;
229
+ }
230
+
231
+ const mappedCategories = resolveMappedCategories(node, bindings);
232
+ if (!mappedCategories) {
233
+ return null;
234
+ }
235
+
236
+ const missingCategories = findMissingCategories(mappedCategories);
237
+ if (missingCategories.length === 0) {
238
+ return null;
239
+ }
240
+
241
+ return createDiagnostic(
242
+ filePath,
243
+ offsetToLine(sourceCode, node.start),
244
+ missingCategories
245
+ );
246
+ };
247
+
248
+ /**
249
+ * Flags registered surface error mapper calls that omit error categories.
250
+ */
251
+ export const errorMappingCompleteness: WardenRule = {
252
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
253
+ if (
254
+ isTestFile(filePath) ||
255
+ ![...MAPPER_FACTORY_NAMES].some((factoryName) =>
256
+ sourceCode.includes(factoryName)
257
+ )
258
+ ) {
259
+ return [];
260
+ }
261
+
262
+ const ast = parse(filePath, sourceCode);
263
+ if (!ast) {
264
+ return [];
265
+ }
266
+
267
+ const bindings = collectObjectBindings(ast);
268
+ const diagnostics: WardenDiagnostic[] = [];
269
+
270
+ walk(ast, (node) => {
271
+ const diagnostic = inspectMapperCall(
272
+ node,
273
+ bindings,
274
+ filePath,
275
+ sourceCode
276
+ );
277
+ if (diagnostic) {
278
+ diagnostics.push(diagnostic);
279
+ }
280
+ });
281
+
282
+ return diagnostics;
283
+ },
284
+ description:
285
+ 'Require registered surface error mappers to cover every ErrorCategory.',
286
+ name: 'error-mapping-completeness',
287
+ severity: 'error',
288
+ };