@ontrails/warden 1.0.0-beta.4 → 1.0.0-beta.41

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 (279) hide show
  1. package/CHANGELOG.md +835 -13
  2. package/README.md +133 -26
  3. package/bin/warden.ts +51 -0
  4. package/package.json +27 -6
  5. package/src/adapter-check.ts +136 -0
  6. package/src/cli.ts +1570 -105
  7. package/src/command.ts +986 -0
  8. package/src/config.ts +193 -0
  9. package/src/draft.ts +22 -0
  10. package/src/drift.ts +233 -23
  11. package/src/fix.ts +126 -0
  12. package/src/formatters.ts +78 -13
  13. package/src/guide.ts +245 -0
  14. package/src/index.ts +247 -15
  15. package/src/project-context.ts +446 -0
  16. package/src/project-rules.ts +290 -0
  17. package/src/resolve.ts +531 -0
  18. package/src/rules/activation-orphan.ts +97 -0
  19. package/src/rules/captured-kernel.ts +375 -0
  20. package/src/rules/circular-refs.ts +150 -0
  21. package/src/rules/cli-command-route-coherence.ts +177 -0
  22. package/src/rules/composes-declarations.ts +839 -0
  23. package/src/rules/context-no-surface-types.ts +79 -15
  24. package/src/rules/dead-internal-trail.ts +161 -0
  25. package/src/rules/dead-public-trail.ts +258 -0
  26. package/src/rules/draft-file-marking.ts +155 -0
  27. package/src/rules/draft-visible-debt.ts +82 -0
  28. package/src/rules/duplicate-exported-symbol.ts +211 -0
  29. package/src/rules/duplicate-public-contract.ts +137 -0
  30. package/src/rules/entity-exists.ts +254 -0
  31. package/src/rules/entity-ids.ts +15 -0
  32. package/src/rules/error-mapping-completeness.ts +290 -0
  33. package/src/rules/example-valid.ts +395 -0
  34. package/src/rules/fires-declarations.ts +740 -0
  35. package/src/rules/governed-symbol-residue.ts +438 -0
  36. package/src/rules/implementation-returns-result.ts +1409 -166
  37. package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
  38. package/src/rules/incomplete-crud.ts +583 -0
  39. package/src/rules/index.ts +277 -10
  40. package/src/rules/intent-propagation.ts +125 -0
  41. package/src/rules/layer-field-name-drift.ts +102 -0
  42. package/src/rules/library-projection-coherence.ts +100 -0
  43. package/src/rules/metadata.ts +871 -0
  44. package/src/rules/missing-reconcile.ts +97 -0
  45. package/src/rules/missing-visibility.ts +111 -0
  46. package/src/rules/no-destructured-compose.ts +196 -0
  47. package/src/rules/no-dev-permit-in-source.ts +99 -0
  48. package/src/rules/no-direct-implementation-call.ts +12 -7
  49. package/src/rules/no-legacy-cli-alias-export.ts +247 -0
  50. package/src/rules/no-legacy-layer-imports.ts +211 -0
  51. package/src/rules/no-native-error-result.ts +118 -0
  52. package/src/rules/no-redundant-result-error-wrap.ts +384 -0
  53. package/src/rules/no-retired-cross-vocabulary.ts +204 -0
  54. package/src/rules/no-sync-result-assumption.ts +1141 -98
  55. package/src/rules/no-throw-in-detour-recover.ts +225 -0
  56. package/src/rules/no-throw-in-implementation.ts +15 -8
  57. package/src/rules/no-top-level-surface.ts +371 -0
  58. package/src/rules/on-references-exist.ts +194 -0
  59. package/src/rules/orphaned-signal.ts +149 -0
  60. package/src/rules/owner-projection-parity.ts +143 -0
  61. package/src/rules/permit-governance.ts +25 -0
  62. package/src/rules/public-export-example-coverage.ts +573 -0
  63. package/src/rules/public-internal-deep-imports.ts +456 -0
  64. package/src/rules/public-output-schema.ts +29 -0
  65. package/src/rules/public-union-output-discriminants.ts +150 -0
  66. package/src/rules/read-intent-fires.ts +188 -0
  67. package/src/rules/reference-exists.ts +97 -0
  68. package/src/rules/registry-names.ts +167 -0
  69. package/src/rules/resolved-import-boundary.ts +146 -0
  70. package/src/rules/resource-declarations.ts +697 -0
  71. package/src/rules/resource-exists.ts +181 -0
  72. package/src/rules/resource-id-grammar.ts +65 -0
  73. package/src/rules/resource-mock-coverage.ts +115 -0
  74. package/src/rules/retired-vocabulary.ts +633 -0
  75. package/src/rules/scan.ts +38 -25
  76. package/src/rules/scheduled-destroy-intent.ts +44 -0
  77. package/src/rules/signal-graph-coaching.ts +220 -0
  78. package/src/rules/source/composition.ts +165 -0
  79. package/src/rules/source/drafts.ts +164 -0
  80. package/src/rules/source/entities.ts +618 -0
  81. package/src/rules/source/pragmas.ts +45 -0
  82. package/src/rules/source/resources.ts +64 -0
  83. package/src/rules/source/signals.ts +397 -0
  84. package/src/rules/source/stores.ts +310 -0
  85. package/src/rules/specs.ts +9 -5
  86. package/src/rules/static-resource-accessor-preference.ts +653 -0
  87. package/src/rules/surface-overlay-coherence.ts +262 -0
  88. package/src/rules/surface-trailhead-coherence.ts +366 -0
  89. package/src/rules/trail-fork-coaching.ts +625 -0
  90. package/src/rules/trail-versioning-source.ts +1076 -0
  91. package/src/rules/trail-versioning-topo.ts +172 -0
  92. package/src/rules/trailhead-override-divergence.ts +356 -0
  93. package/src/rules/types.ts +354 -8
  94. package/src/rules/unmaterialized-activation-source.ts +85 -0
  95. package/src/rules/unreachable-detour-shadowing.ts +339 -0
  96. package/src/rules/valid-describe-refs.ts +162 -32
  97. package/src/rules/valid-detour-contract.ts +78 -0
  98. package/src/rules/warden-export-symmetry.ts +540 -0
  99. package/src/rules/warden-rules-use-ast.ts +1109 -0
  100. package/src/rules/webhook-route-collision.ts +306 -0
  101. package/src/trails/activation-orphan.trail.ts +84 -0
  102. package/src/trails/captured-kernel.trail.ts +108 -0
  103. package/src/trails/circular-refs.trail.ts +29 -0
  104. package/src/trails/cli-command-route-coherence.trail.ts +47 -0
  105. package/src/trails/composes-declarations.trail.ts +22 -0
  106. package/src/trails/context-no-surface-types.trail.ts +21 -0
  107. package/src/trails/dead-internal-trail.trail.ts +26 -0
  108. package/src/trails/dead-public-trail.trail.ts +31 -0
  109. package/src/trails/deprecation-without-guidance.trail.ts +21 -0
  110. package/src/trails/draft-file-marking.trail.ts +16 -0
  111. package/src/trails/draft-visible-debt.trail.ts +16 -0
  112. package/src/trails/duplicate-exported-symbol.trail.ts +48 -0
  113. package/src/trails/duplicate-public-contract.trail.ts +47 -0
  114. package/src/trails/entity-exists.trail.ts +21 -0
  115. package/src/trails/error-mapping-completeness.trail.ts +30 -0
  116. package/src/trails/example-valid.trail.ts +25 -0
  117. package/src/trails/fires-declarations.trail.ts +23 -0
  118. package/src/trails/fork-without-preserved-implementation.trail.ts +31 -0
  119. package/src/trails/governed-symbol-residue.trail.ts +24 -0
  120. package/src/trails/implementation-returns-result.trail.ts +20 -0
  121. package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
  122. package/src/trails/incomplete-crud.trail.ts +39 -0
  123. package/src/trails/index.ts +89 -0
  124. package/src/trails/intent-propagation.trail.ts +30 -0
  125. package/src/trails/layer-field-name-drift.trail.ts +39 -0
  126. package/src/trails/library-projection-coherence.trail.ts +43 -0
  127. package/src/trails/marker-schema-unsupported.trail.ts +23 -0
  128. package/src/trails/missing-reconcile.trail.ts +33 -0
  129. package/src/trails/missing-visibility.trail.ts +22 -0
  130. package/src/trails/no-destructured-compose.trail.ts +44 -0
  131. package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
  132. package/src/trails/no-direct-implementation-call.trail.ts +16 -0
  133. package/src/trails/no-legacy-cli-alias-export.trail.ts +41 -0
  134. package/src/trails/no-legacy-layer-imports.trail.ts +41 -0
  135. package/src/trails/no-native-error-result.trail.ts +18 -0
  136. package/src/trails/no-redundant-result-error-wrap.trail.ts +55 -0
  137. package/src/trails/no-retired-cross-vocabulary.trail.ts +42 -0
  138. package/src/trails/no-sync-result-assumption.trail.ts +19 -0
  139. package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
  140. package/src/trails/no-throw-in-implementation.trail.ts +20 -0
  141. package/src/trails/no-top-level-surface.trail.ts +43 -0
  142. package/src/trails/on-references-exist.trail.ts +21 -0
  143. package/src/trails/orphaned-signal.trail.ts +36 -0
  144. package/src/trails/owner-projection-parity.trail.ts +26 -0
  145. package/src/trails/pending-force.trail.ts +21 -0
  146. package/src/trails/permit-governance.trail.ts +51 -0
  147. package/src/trails/prefer-schema-inference.trail.ts +21 -0
  148. package/src/trails/public-export-example-coverage.trail.ts +16 -0
  149. package/src/trails/public-internal-deep-imports.trail.ts +94 -0
  150. package/src/trails/public-output-schema.trail.ts +55 -0
  151. package/src/trails/public-union-output-discriminants.trail.ts +33 -0
  152. package/src/trails/read-intent-fires.trail.ts +20 -0
  153. package/src/trails/reference-exists.trail.ts +25 -0
  154. package/src/trails/resolved-import-boundary.trail.ts +109 -0
  155. package/src/trails/resource-declarations.trail.ts +25 -0
  156. package/src/trails/resource-exists.trail.ts +27 -0
  157. package/src/trails/resource-id-grammar.trail.ts +39 -0
  158. package/src/trails/resource-mock-coverage.trail.ts +40 -0
  159. package/src/trails/run.ts +160 -0
  160. package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
  161. package/src/trails/schema.ts +237 -0
  162. package/src/trails/signal-graph-coaching.trail.ts +77 -0
  163. package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
  164. package/src/trails/surface-overlay-coherence.trail.ts +24 -0
  165. package/src/trails/surface-trailhead-coherence.trail.ts +25 -0
  166. package/src/trails/topo.ts +6 -0
  167. package/src/trails/trail-fork-coaching.trail.ts +42 -0
  168. package/src/trails/trailhead-override-divergence.trail.ts +47 -0
  169. package/src/trails/unmaterialized-activation-source.trail.ts +72 -0
  170. package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
  171. package/src/trails/valid-describe-refs.trail.ts +18 -0
  172. package/src/trails/valid-detour-contract.trail.ts +71 -0
  173. package/src/trails/version-gap.trail.ts +35 -0
  174. package/src/trails/version-pinned-compose.trail.ts +23 -0
  175. package/src/trails/version-without-examples.trail.ts +38 -0
  176. package/src/trails/warden-export-symmetry.trail.ts +16 -0
  177. package/src/trails/warden-rules-use-ast.trail.ts +64 -0
  178. package/src/trails/webhook-route-collision.trail.ts +50 -0
  179. package/src/trails/wrap-rule.ts +224 -0
  180. package/src/workspaces.ts +199 -0
  181. package/.turbo/turbo-build.log +0 -1
  182. package/.turbo/turbo-lint.log +0 -3
  183. package/.turbo/turbo-typecheck.log +0 -1
  184. package/dist/cli.d.ts +0 -46
  185. package/dist/cli.d.ts.map +0 -1
  186. package/dist/cli.js +0 -218
  187. package/dist/cli.js.map +0 -1
  188. package/dist/drift.d.ts +0 -26
  189. package/dist/drift.d.ts.map +0 -1
  190. package/dist/drift.js +0 -27
  191. package/dist/drift.js.map +0 -1
  192. package/dist/formatters.d.ts +0 -29
  193. package/dist/formatters.d.ts.map +0 -1
  194. package/dist/formatters.js +0 -87
  195. package/dist/formatters.js.map +0 -1
  196. package/dist/index.d.ts +0 -26
  197. package/dist/index.d.ts.map +0 -1
  198. package/dist/index.js +0 -26
  199. package/dist/index.js.map +0 -1
  200. package/dist/rules/ast.d.ts +0 -41
  201. package/dist/rules/ast.d.ts.map +0 -1
  202. package/dist/rules/ast.js +0 -161
  203. package/dist/rules/ast.js.map +0 -1
  204. package/dist/rules/context-no-surface-types.d.ts +0 -12
  205. package/dist/rules/context-no-surface-types.d.ts.map +0 -1
  206. package/dist/rules/context-no-surface-types.js +0 -96
  207. package/dist/rules/context-no-surface-types.js.map +0 -1
  208. package/dist/rules/implementation-returns-result.d.ts +0 -13
  209. package/dist/rules/implementation-returns-result.d.ts.map +0 -1
  210. package/dist/rules/implementation-returns-result.js +0 -277
  211. package/dist/rules/implementation-returns-result.js.map +0 -1
  212. package/dist/rules/index.d.ts +0 -15
  213. package/dist/rules/index.d.ts.map +0 -1
  214. package/dist/rules/index.js +0 -34
  215. package/dist/rules/index.js.map +0 -1
  216. package/dist/rules/no-direct-impl-in-route.d.ts +0 -12
  217. package/dist/rules/no-direct-impl-in-route.d.ts.map +0 -1
  218. package/dist/rules/no-direct-impl-in-route.js +0 -47
  219. package/dist/rules/no-direct-impl-in-route.js.map +0 -1
  220. package/dist/rules/no-direct-implementation-call.d.ts +0 -12
  221. package/dist/rules/no-direct-implementation-call.d.ts.map +0 -1
  222. package/dist/rules/no-direct-implementation-call.js +0 -39
  223. package/dist/rules/no-direct-implementation-call.js.map +0 -1
  224. package/dist/rules/no-sync-result-assumption.d.ts +0 -6
  225. package/dist/rules/no-sync-result-assumption.d.ts.map +0 -1
  226. package/dist/rules/no-sync-result-assumption.js +0 -98
  227. package/dist/rules/no-sync-result-assumption.js.map +0 -1
  228. package/dist/rules/no-throw-in-detour-target.d.ts +0 -12
  229. package/dist/rules/no-throw-in-detour-target.d.ts.map +0 -1
  230. package/dist/rules/no-throw-in-detour-target.js +0 -87
  231. package/dist/rules/no-throw-in-detour-target.js.map +0 -1
  232. package/dist/rules/no-throw-in-implementation.d.ts +0 -9
  233. package/dist/rules/no-throw-in-implementation.d.ts.map +0 -1
  234. package/dist/rules/no-throw-in-implementation.js +0 -34
  235. package/dist/rules/no-throw-in-implementation.js.map +0 -1
  236. package/dist/rules/prefer-schema-inference.d.ts +0 -7
  237. package/dist/rules/prefer-schema-inference.d.ts.map +0 -1
  238. package/dist/rules/prefer-schema-inference.js +0 -86
  239. package/dist/rules/prefer-schema-inference.js.map +0 -1
  240. package/dist/rules/scan.d.ts +0 -8
  241. package/dist/rules/scan.d.ts.map +0 -1
  242. package/dist/rules/scan.js +0 -32
  243. package/dist/rules/scan.js.map +0 -1
  244. package/dist/rules/specs.d.ts +0 -29
  245. package/dist/rules/specs.d.ts.map +0 -1
  246. package/dist/rules/specs.js +0 -192
  247. package/dist/rules/specs.js.map +0 -1
  248. package/dist/rules/structure.d.ts +0 -13
  249. package/dist/rules/structure.d.ts.map +0 -1
  250. package/dist/rules/structure.js +0 -142
  251. package/dist/rules/structure.js.map +0 -1
  252. package/dist/rules/types.d.ts +0 -52
  253. package/dist/rules/types.d.ts.map +0 -1
  254. package/dist/rules/types.js +0 -2
  255. package/dist/rules/types.js.map +0 -1
  256. package/dist/rules/valid-describe-refs.d.ts +0 -7
  257. package/dist/rules/valid-describe-refs.d.ts.map +0 -1
  258. package/dist/rules/valid-describe-refs.js +0 -51
  259. package/dist/rules/valid-describe-refs.js.map +0 -1
  260. package/dist/rules/valid-detour-refs.d.ts +0 -6
  261. package/dist/rules/valid-detour-refs.d.ts.map +0 -1
  262. package/dist/rules/valid-detour-refs.js +0 -116
  263. package/dist/rules/valid-detour-refs.js.map +0 -1
  264. package/src/__tests__/cli.test.ts +0 -198
  265. package/src/__tests__/drift.test.ts +0 -74
  266. package/src/__tests__/formatters.test.ts +0 -157
  267. package/src/__tests__/implementation-returns-result.test.ts +0 -129
  268. package/src/__tests__/no-direct-implementation-call.test.ts +0 -83
  269. package/src/__tests__/no-sync-result-assumption.test.ts +0 -85
  270. package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
  271. package/src/__tests__/prefer-schema-inference.test.ts +0 -84
  272. package/src/__tests__/rules.test.ts +0 -227
  273. package/src/__tests__/valid-describe-refs.test.ts +0 -60
  274. package/src/rules/ast.ts +0 -213
  275. package/src/rules/no-direct-impl-in-route.ts +0 -81
  276. package/src/rules/no-throw-in-detour-target.ts +0 -150
  277. package/src/rules/valid-detour-refs.ts +0 -187
  278. package/tsconfig.json +0 -9
  279. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,181 @@
1
+ import { isDraftId } from '@ontrails/core';
2
+
3
+ import {
4
+ collectNamedResourceIds,
5
+ collectResourceDefinitionIds,
6
+ } from './source/resources.js';
7
+ import {
8
+ extractFirstStringArg,
9
+ findConfigProperty,
10
+ findTrailDefinitions,
11
+ getNodeCallee,
12
+ getStringValue,
13
+ identifierName,
14
+ isStringLiteral,
15
+ offsetToLine,
16
+ parse,
17
+ } from '@ontrails/source';
18
+ import type { AstNode } from '@ontrails/source';
19
+ import { isTestFile } from './scan.js';
20
+ import type {
21
+ ProjectAwareWardenRule,
22
+ ProjectContext,
23
+ WardenDiagnostic,
24
+ } from './types.js';
25
+
26
+ const isResourceCall = (node: AstNode): boolean =>
27
+ node.type === 'CallExpression' &&
28
+ identifierName(getNodeCallee(node)) === 'resource';
29
+
30
+ const getResourceElements = (config: AstNode): readonly AstNode[] => {
31
+ const resourcesProp = findConfigProperty(config, 'resources');
32
+ if (!resourcesProp) {
33
+ return [];
34
+ }
35
+
36
+ const arrayNode = resourcesProp.value;
37
+ if (!arrayNode || (arrayNode as AstNode).type !== 'ArrayExpression') {
38
+ return [];
39
+ }
40
+
41
+ const elements = (arrayNode as AstNode)['elements'] as
42
+ | readonly AstNode[]
43
+ | undefined;
44
+ return elements ?? [];
45
+ };
46
+
47
+ const extractDeclaredResourceId = (
48
+ element: AstNode,
49
+ resourceIdsByName: ReadonlyMap<string, string>
50
+ ): string | null => {
51
+ if (element.type === 'Identifier') {
52
+ const name = identifierName(element);
53
+ return name ? (resourceIdsByName.get(name) ?? null) : null;
54
+ }
55
+
56
+ if (isStringLiteral(element)) {
57
+ return getStringValue(element);
58
+ }
59
+
60
+ return isResourceCall(element) ? extractFirstStringArg(element) : null;
61
+ };
62
+
63
+ const extractDeclaredResourceIds = (
64
+ config: AstNode,
65
+ resourceIdsByName: ReadonlyMap<string, string>
66
+ ): readonly string[] => [
67
+ ...new Set(
68
+ getResourceElements(config).flatMap((element) => {
69
+ const id = extractDeclaredResourceId(element, resourceIdsByName);
70
+ return id ? [id] : [];
71
+ })
72
+ ),
73
+ ];
74
+
75
+ const buildMissingResourceDiagnostic = (
76
+ trailId: string,
77
+ resourceId: string,
78
+ filePath: string,
79
+ line: number
80
+ ): WardenDiagnostic => ({
81
+ filePath,
82
+ line,
83
+ message: `Trail "${trailId}" declares resource "${resourceId}" which is not defined in the project. Define it with resource('${resourceId}', ...) and ensure that definition is included in the topo.`,
84
+ rule: 'resource-exists',
85
+ severity: 'error',
86
+ });
87
+
88
+ const reportMissingResources = (
89
+ def: { id: string; config: AstNode; start: number },
90
+ sourceCode: string,
91
+ resourceIdsByName: ReadonlyMap<string, string>,
92
+ filePath: string,
93
+ knownResourceIds: ReadonlySet<string>,
94
+ diagnostics: WardenDiagnostic[]
95
+ ): void => {
96
+ const line = offsetToLine(sourceCode, def.start);
97
+ for (const resourceId of extractDeclaredResourceIds(
98
+ def.config,
99
+ resourceIdsByName
100
+ )) {
101
+ if (!knownResourceIds.has(resourceId) && !isDraftId(resourceId)) {
102
+ diagnostics.push(
103
+ buildMissingResourceDiagnostic(def.id, resourceId, filePath, line)
104
+ );
105
+ }
106
+ }
107
+ };
108
+
109
+ const buildResourceDiagnostics = (
110
+ ast: AstNode,
111
+ sourceCode: string,
112
+ filePath: string,
113
+ knownResourceIds: ReadonlySet<string>
114
+ ): readonly WardenDiagnostic[] => {
115
+ const diagnostics: WardenDiagnostic[] = [];
116
+ const resourceIdsByName = collectNamedResourceIds(ast);
117
+ for (const def of findTrailDefinitions(ast)) {
118
+ reportMissingResources(
119
+ def,
120
+ sourceCode,
121
+ resourceIdsByName,
122
+ filePath,
123
+ knownResourceIds,
124
+ diagnostics
125
+ );
126
+ }
127
+ return diagnostics;
128
+ };
129
+
130
+ const checkResourcesExist = (
131
+ sourceCode: string,
132
+ filePath: string,
133
+ knownResourceIds: ReadonlySet<string>
134
+ ): readonly WardenDiagnostic[] => {
135
+ if (isTestFile(filePath)) {
136
+ return [];
137
+ }
138
+
139
+ const ast = parse(filePath, sourceCode);
140
+ if (!ast) {
141
+ return [];
142
+ }
143
+
144
+ return buildResourceDiagnostics(ast, sourceCode, filePath, knownResourceIds);
145
+ };
146
+
147
+ /**
148
+ * Checks that all declared resources resolve to known resource definitions.
149
+ */
150
+ export const resourceExists: ProjectAwareWardenRule = {
151
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
152
+ const ast = parse(filePath, sourceCode);
153
+ if (!ast) {
154
+ return [];
155
+ }
156
+ return checkResourcesExist(
157
+ sourceCode,
158
+ filePath,
159
+ collectResourceDefinitionIds(ast)
160
+ );
161
+ },
162
+ checkWithContext(
163
+ sourceCode: string,
164
+ filePath: string,
165
+ context: ProjectContext
166
+ ): readonly WardenDiagnostic[] {
167
+ const ast = parse(filePath, sourceCode);
168
+ const localResourceIds = ast
169
+ ? collectResourceDefinitionIds(ast)
170
+ : new Set<string>();
171
+ return checkResourcesExist(
172
+ sourceCode,
173
+ filePath,
174
+ context.knownResourceIds ?? localResourceIds
175
+ );
176
+ },
177
+ description:
178
+ 'Ensure every resource declared on a trail resolves to a known resource definition.',
179
+ name: 'resource-exists',
180
+ severity: 'error',
181
+ };
@@ -0,0 +1,65 @@
1
+ import {
2
+ extractFirstStringArg,
3
+ getNodeCallee,
4
+ identifierName,
5
+ offsetToLine,
6
+ parse,
7
+ walk,
8
+ } from '@ontrails/source';
9
+ import type { AstNode } from '@ontrails/source';
10
+ import { isTestFile } from './scan.js';
11
+ import type { WardenDiagnostic, WardenRule } from './types.js';
12
+
13
+ const isResourceCall = (node: AstNode): boolean =>
14
+ node.type === 'CallExpression' &&
15
+ identifierName(getNodeCallee(node)) === 'resource';
16
+
17
+ const buildDiagnostic = (
18
+ resourceId: string,
19
+ filePath: string,
20
+ line: number
21
+ ): WardenDiagnostic => ({
22
+ filePath,
23
+ line,
24
+ message: `Resource "${resourceId}" is invalid because resource ids may not contain ":".`,
25
+ rule: 'resource-id-grammar',
26
+ severity: 'error',
27
+ });
28
+
29
+ export const resourceIdGrammar: WardenRule = {
30
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
31
+ if (isTestFile(filePath)) {
32
+ return [];
33
+ }
34
+
35
+ const ast = parse(filePath, sourceCode);
36
+ if (!ast) {
37
+ return [];
38
+ }
39
+
40
+ const diagnostics: WardenDiagnostic[] = [];
41
+ walk(ast, (node) => {
42
+ if (!isResourceCall(node)) {
43
+ return;
44
+ }
45
+
46
+ const resourceId = extractFirstStringArg(node);
47
+ if (!resourceId || !resourceId.includes(':')) {
48
+ return;
49
+ }
50
+
51
+ diagnostics.push(
52
+ buildDiagnostic(
53
+ resourceId,
54
+ filePath,
55
+ offsetToLine(sourceCode, node.start)
56
+ )
57
+ );
58
+ });
59
+
60
+ return diagnostics;
61
+ },
62
+ description: 'Ensure resource ids do not contain the ":" scope separator.',
63
+ name: 'resource-id-grammar',
64
+ severity: 'error',
65
+ };
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Warns when a `resource('id', { ... })` definition declares neither a `mock`
3
+ * factory nor an explicit `unmockable` reason.
4
+ *
5
+ * Every resource should declare its test posture: a `mock` factory so
6
+ * `testAll(app)` runs without production-like configuration (common pitfall
7
+ * #10), or an explicit `unmockable: { reason }` escape hatch when it genuinely
8
+ * cannot be mocked. The `mock?`/`unmockable?` fields are both optional in
9
+ * `ResourceSpec`, so the compiler does not enforce the choice — this rule does.
10
+ *
11
+ * Conservative by design (zero false positives over completeness): only flags
12
+ * a `resource()` call whose second argument is an inline object literal with no
13
+ * spread. A referenced spec variable, a spread spec, or a non-object second
14
+ * argument cannot be verified statically, so they are skipped.
15
+ */
16
+
17
+ import {
18
+ extractFirstStringArg,
19
+ findConfigProperty,
20
+ getNodeCallee,
21
+ identifierName,
22
+ offsetToLine,
23
+ parse,
24
+ walk,
25
+ } from '@ontrails/source';
26
+ import type { AstNode } from '@ontrails/source';
27
+ import { isFrameworkInternalFile, isTestFile } from './scan.js';
28
+ import type { WardenDiagnostic, WardenRule } from './types.js';
29
+
30
+ const isResourceCall = (node: AstNode): boolean =>
31
+ node.type === 'CallExpression' &&
32
+ identifierName(getNodeCallee(node)) === 'resource';
33
+
34
+ /**
35
+ * `.test-d.ts` type-fixture files are not matched by `isTestFile` (its pattern
36
+ * keys on `.test.`/`.spec.`), yet they hold type-inference probe resources that
37
+ * intentionally omit `mock`. Treat them as test fixtures here.
38
+ */
39
+ const isTypeFixtureFile = (filePath: string): boolean =>
40
+ filePath.endsWith('.test-d.ts') || filePath.endsWith('.test-d.tsx');
41
+
42
+ /**
43
+ * Framework-internal packages (`@ontrails/warden`, `@ontrails/testing`) define
44
+ * throwaway fixture resources to build example topos for other rules' tests
45
+ * (e.g. signal-graph-coaching's `invoiceStore`). Those scaffolding resources
46
+ * are not governed application resources, so skip them — consistent with how
47
+ * the rest of the framework treats `isFrameworkInternalFile` source.
48
+ */
49
+ const isExcludedFile = (filePath: string): boolean =>
50
+ isTestFile(filePath) ||
51
+ isTypeFixtureFile(filePath) ||
52
+ isFrameworkInternalFile(filePath);
53
+
54
+ /** A spec object literal we can analyze: an ObjectExpression with no spread. */
55
+ const isStaticallyAnalyzableSpec = (spec: AstNode | undefined): boolean => {
56
+ if (!spec || spec.type !== 'ObjectExpression') {
57
+ return false;
58
+ }
59
+ const properties = spec['properties'] as readonly AstNode[] | undefined;
60
+ if (!properties) {
61
+ return false;
62
+ }
63
+ // A spread (`...base`) could contribute `mock`/`unmockable` from elsewhere;
64
+ // we cannot prove its absence, so do not flag.
65
+ return properties.every((prop) => prop.type === 'Property');
66
+ };
67
+
68
+ const declaresTestPosture = (spec: AstNode): boolean =>
69
+ findConfigProperty(spec, 'mock') !== null ||
70
+ findConfigProperty(spec, 'unmockable') !== null;
71
+
72
+ export const resourceMockCoverage: WardenRule = {
73
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
74
+ if (isExcludedFile(filePath)) {
75
+ return [];
76
+ }
77
+
78
+ const ast = parse(filePath, sourceCode);
79
+ if (!ast) {
80
+ return [];
81
+ }
82
+
83
+ const diagnostics: WardenDiagnostic[] = [];
84
+ walk(ast, (node) => {
85
+ if (!isResourceCall(node)) {
86
+ return;
87
+ }
88
+
89
+ const args = node['arguments'] as readonly AstNode[] | undefined;
90
+ const spec = args?.[1];
91
+ if (!isStaticallyAnalyzableSpec(spec) || !spec) {
92
+ return;
93
+ }
94
+ if (declaresTestPosture(spec)) {
95
+ return;
96
+ }
97
+
98
+ const resourceId = extractFirstStringArg(node);
99
+ const subject = resourceId ? `Resource "${resourceId}"` : 'Resource';
100
+ diagnostics.push({
101
+ filePath,
102
+ line: offsetToLine(sourceCode, node.start),
103
+ message: `${subject} declares no mock factory. Add a mock() so testAll(app) runs without configuration, or declare unmockable: { reason } if it intentionally cannot be mocked.`,
104
+ rule: 'resource-mock-coverage',
105
+ severity: 'warn',
106
+ });
107
+ });
108
+
109
+ return diagnostics;
110
+ },
111
+ description:
112
+ 'Resource definitions declare a mock factory or an explicit unmockable reason.',
113
+ name: 'resource-mock-coverage',
114
+ severity: 'warn',
115
+ };