@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
package/src/command.ts ADDED
@@ -0,0 +1,953 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { isAbsolute, resolve } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { parseArgs } from 'node:util';
5
+
6
+ import {
7
+ deriveCliFlagValueAliases,
8
+ findAppModule,
9
+ findAppModuleCandidates,
10
+ } from '@ontrails/cli';
11
+ import type {
12
+ CliFlagValueAlias,
13
+ CliFlagValueAliasDeclaration,
14
+ } from '@ontrails/cli';
15
+ import type { Topo } from '@ontrails/core';
16
+ import { AmbiguousError, NotFoundError } from '@ontrails/core';
17
+
18
+ import type {
19
+ WardenConfigInput,
20
+ WardenConfigLayer,
21
+ WardenDepth,
22
+ WardenDraftsMode,
23
+ WardenFailOn,
24
+ WardenFormat,
25
+ WardenLockMode,
26
+ } from './config.js';
27
+ import {
28
+ resolveWardenConfig,
29
+ wardenDepthValues,
30
+ wardenDraftsValues,
31
+ wardenFailOnValues,
32
+ wardenFormatValues,
33
+ wardenLockValues,
34
+ } from './config.js';
35
+ import type {
36
+ WardenReport,
37
+ WardenRunOptions,
38
+ WardenTopoTarget,
39
+ } from './cli.js';
40
+ import { runWarden } from './cli.js';
41
+ import {
42
+ formatGitHubAnnotations,
43
+ formatJson,
44
+ formatSummary,
45
+ } from './formatters.js';
46
+ import type { WardenDiagnostic, WardenSeverity } from './rules/types.js';
47
+
48
+ type EnvRecord = Record<string, string | undefined>;
49
+
50
+ interface MutableWardenConfigLayer {
51
+ apps?: string[] | undefined;
52
+ depth?: WardenDepth | undefined;
53
+ drafts?: WardenDraftsMode | undefined;
54
+ failOn?: WardenFailOn | undefined;
55
+ format?: WardenFormat | undefined;
56
+ lock?: WardenLockMode | undefined;
57
+ noLockMutation?: boolean | undefined;
58
+ }
59
+
60
+ const CONFIG_CANDIDATES = [
61
+ 'trails.config.ts',
62
+ 'trails.config.mts',
63
+ 'trails.config.js',
64
+ 'trails.config.mjs',
65
+ ] as const;
66
+
67
+ const diagnostic = ({
68
+ filePath = '<warden-cli>',
69
+ message,
70
+ rule = 'warden-cli',
71
+ severity = 'error',
72
+ }: {
73
+ readonly filePath?: string | undefined;
74
+ readonly message: string;
75
+ readonly rule?: string | undefined;
76
+ readonly severity?: WardenSeverity | undefined;
77
+ }): WardenDiagnostic => ({
78
+ filePath,
79
+ line: 1,
80
+ message,
81
+ rule,
82
+ severity,
83
+ });
84
+
85
+ const errorMessage = (error: unknown): string =>
86
+ error instanceof Error ? error.message : String(error);
87
+
88
+ const cleanUndefined = <T extends Record<string, unknown>>(
89
+ value: T
90
+ ): Partial<T> =>
91
+ Object.fromEntries(
92
+ Object.entries(value).filter(([, entry]) => entry !== undefined)
93
+ ) as Partial<T>;
94
+
95
+ const splitApps = (value: string): readonly string[] =>
96
+ value
97
+ .split(',')
98
+ .map((entry) => entry.trim())
99
+ .filter((entry) => entry.length > 0);
100
+
101
+ const isAllowedValue = <T extends string>(
102
+ value: string,
103
+ allowed: readonly T[]
104
+ ): value is T => allowed.includes(value as T);
105
+
106
+ interface EnumReadOptions<T extends string> {
107
+ readonly allowed: readonly T[];
108
+ readonly diagnostics: WardenDiagnostic[];
109
+ readonly flag: string;
110
+ readonly value: string | undefined;
111
+ }
112
+
113
+ const readEnumValue = <T extends string>({
114
+ allowed,
115
+ diagnostics,
116
+ flag,
117
+ value,
118
+ }: EnumReadOptions<T>): T | undefined => {
119
+ if (value !== undefined && isAllowedValue(value, allowed)) {
120
+ return value;
121
+ }
122
+ diagnostics.push(
123
+ diagnostic({
124
+ message: `Invalid ${flag} value "${value ?? ''}". Expected one of: ${allowed.join(', ')}.`,
125
+ })
126
+ );
127
+ return undefined;
128
+ };
129
+
130
+ export interface ParsedWardenCommand {
131
+ readonly adapterCheck: boolean;
132
+ readonly ci: boolean;
133
+ readonly cli: WardenConfigLayer;
134
+ readonly configPath?: string | undefined;
135
+ readonly diagnostics: readonly WardenDiagnostic[];
136
+ readonly fix: boolean;
137
+ readonly prePush: boolean;
138
+ readonly rootDir?: string | undefined;
139
+ }
140
+
141
+ const createEmptyParsedCommand = (message: string): ParsedWardenCommand => ({
142
+ adapterCheck: false,
143
+ ci: false,
144
+ cli: {},
145
+ diagnostics: [diagnostic({ message })],
146
+ fix: false,
147
+ prePush: false,
148
+ });
149
+
150
+ const tokenValue = (token: {
151
+ readonly value?: string | boolean | undefined;
152
+ }): string | undefined =>
153
+ typeof token.value === 'string' ? token.value : undefined;
154
+
155
+ interface CommandParserState {
156
+ adapterCheck?: boolean | undefined;
157
+ readonly apps: string[];
158
+ readonly diagnostics: WardenDiagnostic[];
159
+ readonly cli: MutableWardenConfigLayer;
160
+ configPath?: string | undefined;
161
+ fix?: boolean | undefined;
162
+ rootDir?: string | undefined;
163
+ }
164
+
165
+ type AliasConfigKey = 'drafts' | 'format' | 'lock';
166
+
167
+ interface WardenAliasSpec {
168
+ readonly aliases: CliFlagValueAliasDeclaration;
169
+ readonly choices: readonly string[];
170
+ readonly configKey: AliasConfigKey;
171
+ readonly flagName: string;
172
+ }
173
+
174
+ interface WardenValueAliasTarget {
175
+ readonly alias: CliFlagValueAlias;
176
+ readonly configKey: AliasConfigKey;
177
+ }
178
+
179
+ const wardenAliasSpecs = [
180
+ {
181
+ aliases: true,
182
+ choices: wardenFormatValues,
183
+ configKey: 'format',
184
+ flagName: 'format',
185
+ },
186
+ {
187
+ aliases: {
188
+ cached: 'cached',
189
+ refresh: 'refresh',
190
+ skip: 'skip-lock',
191
+ },
192
+ choices: wardenLockValues,
193
+ configKey: 'lock',
194
+ flagName: 'lock',
195
+ },
196
+ {
197
+ aliases: {
198
+ exclude: 'exclude-drafts',
199
+ include: 'include-drafts',
200
+ only: 'only-drafts',
201
+ },
202
+ choices: wardenDraftsValues,
203
+ configKey: 'drafts',
204
+ flagName: 'drafts',
205
+ },
206
+ ] satisfies readonly WardenAliasSpec[];
207
+
208
+ const wardenValueAliasTargets: readonly WardenValueAliasTarget[] =
209
+ wardenAliasSpecs.flatMap((spec) =>
210
+ (
211
+ deriveCliFlagValueAliases({
212
+ aliases: spec.aliases,
213
+ choices: spec.choices,
214
+ flagName: spec.flagName,
215
+ }) ?? []
216
+ ).map((alias) => ({
217
+ alias,
218
+ configKey: spec.configKey,
219
+ }))
220
+ );
221
+
222
+ const wardenValueAliasTargetByName = new Map(
223
+ wardenValueAliasTargets.map((target) => [target.alias.name, target])
224
+ );
225
+
226
+ const valueAliasParseOptions = Object.fromEntries(
227
+ wardenValueAliasTargets.map((target) => [
228
+ target.alias.name,
229
+ { type: 'boolean' as const },
230
+ ])
231
+ );
232
+
233
+ const parseTokens = (
234
+ args: readonly string[]
235
+ ): ReturnType<typeof parseArgs> | { readonly error: string } => {
236
+ try {
237
+ return parseArgs({
238
+ allowPositionals: false,
239
+ args: [...args],
240
+ options: {
241
+ 'adapter-check': { type: 'boolean' },
242
+ apps: { multiple: true, short: 'a', type: 'string' },
243
+ ci: { type: 'boolean' },
244
+ 'config-path': { type: 'string' },
245
+ depth: { type: 'string' },
246
+ drafts: { type: 'string' },
247
+ 'fail-on': { type: 'string' },
248
+ fix: { type: 'boolean' },
249
+ format: { type: 'string' },
250
+ lock: { type: 'string' },
251
+ 'no-lock-mutation': { type: 'boolean' },
252
+ 'pre-push': { type: 'boolean' },
253
+ 'root-dir': { type: 'string' },
254
+ strict: { type: 'boolean' },
255
+ ...valueAliasParseOptions,
256
+ },
257
+ strict: true,
258
+ tokens: true,
259
+ });
260
+ } catch (error) {
261
+ return { error: errorMessage(error) };
262
+ }
263
+ };
264
+
265
+ const isParseError = (
266
+ value: ReturnType<typeof parseArgs> | { readonly error: string }
267
+ ): value is { readonly error: string } => 'error' in value;
268
+
269
+ const applyPresetToken = (
270
+ token: NonNullable<ReturnType<typeof parseArgs>['tokens']>[number],
271
+ cli: MutableWardenConfigLayer
272
+ ): { readonly ci: boolean; readonly prePush: boolean } => {
273
+ if (token.kind !== 'option') {
274
+ return { ci: false, prePush: false };
275
+ }
276
+ if (token.name === 'pre-push') {
277
+ Object.assign(cli, {
278
+ depth: 'project',
279
+ failOn: 'error',
280
+ lock: 'cached',
281
+ } satisfies WardenConfigLayer);
282
+ return { ci: false, prePush: true };
283
+ }
284
+ if (token.name === 'ci') {
285
+ Object.assign(cli, {
286
+ depth: 'all',
287
+ failOn: 'error',
288
+ format: 'github',
289
+ lock: 'auto',
290
+ noLockMutation: true,
291
+ } satisfies WardenConfigLayer);
292
+ return { ci: true, prePush: false };
293
+ }
294
+ return { ci: false, prePush: false };
295
+ };
296
+
297
+ const applyAliasOption = (name: string, state: CommandParserState): boolean => {
298
+ const target = wardenValueAliasTargetByName.get(name);
299
+ if (target === undefined) {
300
+ return false;
301
+ }
302
+ if (target.configKey === 'format') {
303
+ state.cli.format = target.alias.value as WardenFormat;
304
+ } else if (target.configKey === 'lock') {
305
+ state.cli.lock = target.alias.value as WardenLockMode;
306
+ } else {
307
+ state.cli.drafts = target.alias.value as WardenDraftsMode;
308
+ }
309
+ return true;
310
+ };
311
+
312
+ const applyEnumOption = (
313
+ name: string,
314
+ value: string | undefined,
315
+ state: CommandParserState
316
+ ): boolean => {
317
+ if (name === 'depth') {
318
+ state.cli.depth = readEnumValue<WardenDepth>({
319
+ allowed: wardenDepthValues,
320
+ diagnostics: state.diagnostics,
321
+ flag: '--depth',
322
+ value,
323
+ });
324
+ return true;
325
+ }
326
+ if (name === 'drafts') {
327
+ state.cli.drafts = readEnumValue<WardenDraftsMode>({
328
+ allowed: wardenDraftsValues,
329
+ diagnostics: state.diagnostics,
330
+ flag: '--drafts',
331
+ value,
332
+ });
333
+ return true;
334
+ }
335
+ if (name === 'fail-on') {
336
+ state.cli.failOn = readEnumValue<WardenFailOn>({
337
+ allowed: wardenFailOnValues,
338
+ diagnostics: state.diagnostics,
339
+ flag: '--fail-on',
340
+ value,
341
+ });
342
+ return true;
343
+ }
344
+ if (name === 'format') {
345
+ state.cli.format = readEnumValue<WardenFormat>({
346
+ allowed: wardenFormatValues,
347
+ diagnostics: state.diagnostics,
348
+ flag: '--format',
349
+ value,
350
+ });
351
+ return true;
352
+ }
353
+ if (name === 'lock') {
354
+ state.cli.lock = readEnumValue<WardenLockMode>({
355
+ allowed: wardenLockValues,
356
+ diagnostics: state.diagnostics,
357
+ flag: '--lock',
358
+ value,
359
+ });
360
+ return true;
361
+ }
362
+ return false;
363
+ };
364
+
365
+ const applyCommandOption = (
366
+ token: NonNullable<ReturnType<typeof parseArgs>['tokens']>[number],
367
+ state: CommandParserState
368
+ ): void => {
369
+ if (
370
+ token.kind !== 'option' ||
371
+ token.name === 'ci' ||
372
+ token.name === 'pre-push'
373
+ ) {
374
+ return;
375
+ }
376
+
377
+ const value = tokenValue(token);
378
+ if (
379
+ applyAliasOption(token.name, state) ||
380
+ applyEnumOption(token.name, value, state)
381
+ ) {
382
+ return;
383
+ }
384
+
385
+ if (token.name === 'apps') {
386
+ if (value === undefined) {
387
+ state.diagnostics.push(
388
+ diagnostic({ message: '--apps requires a comma-delimited value.' })
389
+ );
390
+ return;
391
+ }
392
+ state.apps.push(...splitApps(value));
393
+ return;
394
+ }
395
+ if (token.name === 'adapter-check') {
396
+ state.adapterCheck = true;
397
+ return;
398
+ }
399
+ if (token.name === 'config-path') {
400
+ state.configPath = value;
401
+ return;
402
+ }
403
+ if (token.name === 'fix') {
404
+ state.fix = true;
405
+ return;
406
+ }
407
+ if (token.name === 'no-lock-mutation') {
408
+ state.cli.noLockMutation = true;
409
+ return;
410
+ }
411
+ if (token.name === 'root-dir') {
412
+ state.rootDir = value;
413
+ return;
414
+ }
415
+ if (token.name === 'strict') {
416
+ state.cli.failOn = 'warning';
417
+ return;
418
+ }
419
+
420
+ state.diagnostics.push(
421
+ diagnostic({ message: `Unsupported Warden option: --${token.name}` })
422
+ );
423
+ };
424
+
425
+ export const parseWardenCommandArgs = (
426
+ args: readonly string[]
427
+ ): ParsedWardenCommand => {
428
+ const parsed = parseTokens(args);
429
+ if (isParseError(parsed)) {
430
+ return createEmptyParsedCommand(parsed.error);
431
+ }
432
+
433
+ const state: CommandParserState = {
434
+ apps: [],
435
+ cli: {},
436
+ diagnostics: [],
437
+ };
438
+ let ci = false;
439
+ let prePush = false;
440
+
441
+ for (const token of parsed.tokens ?? []) {
442
+ const preset = applyPresetToken(token, state.cli);
443
+ ci = ci || preset.ci;
444
+ prePush = prePush || preset.prePush;
445
+ }
446
+
447
+ for (const token of parsed.tokens ?? []) {
448
+ applyCommandOption(token, state);
449
+ }
450
+
451
+ if (state.apps.length > 0) {
452
+ state.cli.apps = state.apps;
453
+ }
454
+
455
+ return {
456
+ adapterCheck: state.adapterCheck ?? false,
457
+ ci,
458
+ cli: cleanUndefined(
459
+ state.cli as Record<string, unknown>
460
+ ) as WardenConfigLayer,
461
+ configPath: state.configPath,
462
+ diagnostics: state.diagnostics,
463
+ fix: state.fix ?? false,
464
+ prePush,
465
+ rootDir: state.rootDir,
466
+ };
467
+ };
468
+
469
+ interface WardenConfigLoadResult {
470
+ readonly config?: WardenConfigInput | undefined;
471
+ readonly configPath?: string | undefined;
472
+ readonly diagnostics: readonly WardenDiagnostic[];
473
+ }
474
+
475
+ const findConfigPath = (
476
+ rootDir: string,
477
+ configPath: string | undefined
478
+ ): WardenConfigLoadResult => {
479
+ if (configPath !== undefined) {
480
+ const resolvedPath = resolve(rootDir, configPath);
481
+ return existsSync(resolvedPath)
482
+ ? { configPath: resolvedPath, diagnostics: [] }
483
+ : {
484
+ diagnostics: [
485
+ diagnostic({
486
+ filePath: resolvedPath,
487
+ message: `Warden config file not found: ${resolvedPath}`,
488
+ rule: 'warden-config',
489
+ }),
490
+ ],
491
+ };
492
+ }
493
+
494
+ const candidate = CONFIG_CANDIDATES.map((entry) =>
495
+ resolve(rootDir, entry)
496
+ ).find((entry) => existsSync(entry));
497
+ return candidate === undefined
498
+ ? { diagnostics: [] }
499
+ : { configPath: candidate, diagnostics: [] };
500
+ };
501
+
502
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
503
+ typeof value === 'object' && value !== null;
504
+
505
+ interface ResultLike {
506
+ readonly error?: unknown;
507
+ readonly value?: unknown;
508
+ isErr(): boolean;
509
+ isOk(): boolean;
510
+ }
511
+
512
+ const isResultLike = (value: unknown): value is ResultLike =>
513
+ isRecord(value) &&
514
+ typeof value['isOk'] === 'function' &&
515
+ typeof value['isErr'] === 'function';
516
+
517
+ interface ResolvableConfig {
518
+ resolve(options: {
519
+ readonly cwd: string;
520
+ readonly env: EnvRecord;
521
+ }): Promise<unknown>;
522
+ }
523
+
524
+ const isResolvableConfig = (value: unknown): value is ResolvableConfig =>
525
+ isRecord(value) && typeof value['resolve'] === 'function';
526
+
527
+ const extractWardenConfig = (value: unknown): WardenConfigInput | undefined =>
528
+ isRecord(value) && 'warden' in value
529
+ ? (value['warden'] as WardenConfigInput)
530
+ : undefined;
531
+
532
+ const importConfigModule = async (
533
+ configPath: string
534
+ ): Promise<Record<string, unknown>> => {
535
+ const url = pathToFileURL(configPath);
536
+ url.searchParams.set('t', Date.now().toString());
537
+ return (await import(url.href)) as Record<string, unknown>;
538
+ };
539
+
540
+ export const loadWardenConfig = async ({
541
+ configPath,
542
+ env = {},
543
+ rootDir,
544
+ }: {
545
+ readonly configPath?: string | undefined;
546
+ readonly env?: EnvRecord | undefined;
547
+ readonly rootDir: string;
548
+ }): Promise<WardenConfigLoadResult> => {
549
+ const located = findConfigPath(rootDir, configPath);
550
+ if (located.configPath === undefined) {
551
+ return located;
552
+ }
553
+
554
+ try {
555
+ const mod = await importConfigModule(located.configPath);
556
+ const exported = mod['default'] ?? mod;
557
+ if (isResolvableConfig(exported)) {
558
+ const resolved = await exported.resolve({ cwd: rootDir, env });
559
+ if (isResultLike(resolved)) {
560
+ if (resolved.isOk()) {
561
+ return {
562
+ config: extractWardenConfig(resolved.value),
563
+ configPath: located.configPath,
564
+ diagnostics: located.diagnostics,
565
+ };
566
+ }
567
+ return {
568
+ configPath: located.configPath,
569
+ diagnostics: [
570
+ ...located.diagnostics,
571
+ diagnostic({
572
+ filePath: located.configPath,
573
+ message: `Failed to resolve Warden config: ${errorMessage(resolved.error)}`,
574
+ rule: 'warden-config',
575
+ }),
576
+ ],
577
+ };
578
+ }
579
+ return {
580
+ config: extractWardenConfig(resolved),
581
+ configPath: located.configPath,
582
+ diagnostics: located.diagnostics,
583
+ };
584
+ }
585
+ return {
586
+ config: extractWardenConfig(exported),
587
+ configPath: located.configPath,
588
+ diagnostics: located.diagnostics,
589
+ };
590
+ } catch (error) {
591
+ return {
592
+ configPath: located.configPath,
593
+ diagnostics: [
594
+ ...located.diagnostics,
595
+ diagnostic({
596
+ filePath: located.configPath,
597
+ message: `Failed to load Warden config: ${errorMessage(error)}`,
598
+ rule: 'warden-config',
599
+ }),
600
+ ],
601
+ };
602
+ }
603
+ };
604
+
605
+ const isTopo = (value: unknown): value is Topo => {
606
+ if (!isRecord(value)) {
607
+ return false;
608
+ }
609
+ return (
610
+ value['trails'] instanceof Map &&
611
+ value['signals'] instanceof Map &&
612
+ value['resources'] instanceof Map &&
613
+ value['contours'] instanceof Map &&
614
+ typeof value['get'] === 'function' &&
615
+ typeof value['list'] === 'function' &&
616
+ typeof value['name'] === 'string'
617
+ );
618
+ };
619
+
620
+ const TOPO_EXPORT_KEYS = ['default', 'graph', 'app'] as const;
621
+
622
+ const extractTopo = (
623
+ modulePath: string,
624
+ loaded: Record<string, unknown>
625
+ ): Topo => {
626
+ for (const key of TOPO_EXPORT_KEYS) {
627
+ const candidate = loaded[key];
628
+ if (isTopo(candidate)) {
629
+ return candidate;
630
+ }
631
+ }
632
+
633
+ throw new Error(
634
+ `Could not find a Topo export in "${modulePath}". Expected a default, "graph", or "app" export created with topo().`
635
+ );
636
+ };
637
+
638
+ const resolveFilesystemModulePath = (
639
+ rootDir: string,
640
+ modulePath: string
641
+ ): string => {
642
+ const absolutePath = isAbsolute(modulePath)
643
+ ? modulePath
644
+ : resolve(rootDir, modulePath);
645
+ if (!absolutePath.endsWith('.js') || existsSync(absolutePath)) {
646
+ return absolutePath;
647
+ }
648
+
649
+ const tsPath = absolutePath.replace(/\.js$/, '.ts');
650
+ return existsSync(tsPath) ? tsPath : absolutePath;
651
+ };
652
+
653
+ const resolveDiscoveredModulePath = (
654
+ rootDir: string,
655
+ explicit?: string | undefined
656
+ ): string =>
657
+ resolveFilesystemModulePath(rootDir, findAppModule(rootDir, explicit));
658
+
659
+ const appCandidateMatches = (candidate: string, appName: string): boolean =>
660
+ candidate === appName ||
661
+ candidate === `apps/${appName}/src/app.ts` ||
662
+ candidate.startsWith(`apps/${appName}/`);
663
+
664
+ const resolveNamedAppModulePath = (
665
+ rootDir: string,
666
+ appName: string
667
+ ): string => {
668
+ const matched = findAppModuleCandidates(rootDir).find((candidate) =>
669
+ appCandidateMatches(candidate, appName)
670
+ );
671
+ return matched === undefined
672
+ ? resolveDiscoveredModulePath(rootDir, appName)
673
+ : resolveFilesystemModulePath(rootDir, matched);
674
+ };
675
+
676
+ const importTopoFromModulePath = async (modulePath: string): Promise<Topo> => {
677
+ const loaded = (await import(pathToFileURL(modulePath).href)) as Record<
678
+ string,
679
+ unknown
680
+ >;
681
+ return extractTopo(modulePath, loaded);
682
+ };
683
+
684
+ const topoLoadDiagnostic = ({
685
+ filePath,
686
+ message,
687
+ severity,
688
+ }: {
689
+ readonly filePath: string;
690
+ readonly message: string;
691
+ readonly severity: WardenSeverity;
692
+ }): WardenDiagnostic =>
693
+ diagnostic({
694
+ filePath,
695
+ message,
696
+ rule: 'topo-load',
697
+ severity,
698
+ });
699
+
700
+ const WARDEN_TOPO_SELECTION_HINT =
701
+ 'Set warden.apps in trails.config.ts or pass --apps NAME,NAME.';
702
+
703
+ const cleanDiscoveryMessage = (message: string): string =>
704
+ message
705
+ .replaceAll('\n\nUse --module to select one explicitly.', '')
706
+ .replaceAll(' Use --module to specify the path.', '')
707
+ .trim();
708
+
709
+ const ambiguousTopoDiagnostic = (
710
+ rootDir: string,
711
+ message: string,
712
+ strict: boolean
713
+ ): WardenDiagnostic =>
714
+ topoLoadDiagnostic({
715
+ filePath: rootDir,
716
+ message: `Multiple Trails apps discovered; skipping topo-aware rules. ${WARDEN_TOPO_SELECTION_HINT} ${cleanDiscoveryMessage(message)}`,
717
+ severity: strict ? 'error' : 'warn',
718
+ });
719
+
720
+ const missingTopoDiagnostic = (
721
+ rootDir: string,
722
+ message: string
723
+ ): WardenDiagnostic =>
724
+ topoLoadDiagnostic({
725
+ filePath: rootDir,
726
+ message: `No Trails app could be loaded for topo-aware Warden checks. ${cleanDiscoveryMessage(message)} ${WARDEN_TOPO_SELECTION_HINT}`,
727
+ severity: 'error',
728
+ });
729
+
730
+ interface ResolveTopoTargetsOptions {
731
+ readonly apps?: readonly string[] | undefined;
732
+ readonly rootDir: string;
733
+ readonly strict: boolean;
734
+ }
735
+
736
+ interface ResolvedTopoTargets {
737
+ readonly diagnostics: readonly WardenDiagnostic[];
738
+ readonly topos: readonly WardenTopoTarget[];
739
+ }
740
+
741
+ export const resolveWardenTopoTargets = async ({
742
+ apps,
743
+ rootDir,
744
+ strict,
745
+ }: ResolveTopoTargetsOptions): Promise<ResolvedTopoTargets> => {
746
+ const diagnostics: WardenDiagnostic[] = [];
747
+ const topos: WardenTopoTarget[] = [];
748
+
749
+ if (apps !== undefined && apps.length > 0) {
750
+ for (const appName of apps) {
751
+ try {
752
+ const modulePath = resolveNamedAppModulePath(rootDir, appName);
753
+ topos.push({
754
+ name: appName,
755
+ topo: await importTopoFromModulePath(modulePath),
756
+ });
757
+ } catch (error) {
758
+ diagnostics.push(
759
+ topoLoadDiagnostic({
760
+ filePath: rootDir,
761
+ message: `Failed to load Trails app "${appName}" for Warden checks: ${errorMessage(error)}`,
762
+ severity: 'error',
763
+ })
764
+ );
765
+ }
766
+ }
767
+ return { diagnostics, topos };
768
+ }
769
+
770
+ try {
771
+ const modulePath = resolveDiscoveredModulePath(rootDir);
772
+ const topo = await importTopoFromModulePath(modulePath);
773
+ return {
774
+ diagnostics,
775
+ topos: [{ name: topo.name, topo }],
776
+ };
777
+ } catch (error) {
778
+ if (error instanceof NotFoundError) {
779
+ return {
780
+ diagnostics: strict
781
+ ? [missingTopoDiagnostic(rootDir, error.message)]
782
+ : diagnostics,
783
+ topos,
784
+ };
785
+ }
786
+ if (error instanceof AmbiguousError) {
787
+ return {
788
+ diagnostics: [ambiguousTopoDiagnostic(rootDir, error.message, strict)],
789
+ topos,
790
+ };
791
+ }
792
+ return {
793
+ diagnostics: [
794
+ topoLoadDiagnostic({
795
+ filePath: rootDir,
796
+ message: `Failed to load Trails app for Warden checks: ${errorMessage(error)}`,
797
+ severity: 'error',
798
+ }),
799
+ ],
800
+ topos,
801
+ };
802
+ }
803
+ };
804
+
805
+ const effectiveConfigNeedsTopo = (depth: WardenDepth): boolean =>
806
+ depth === 'topo' || depth === 'all';
807
+
808
+ const buildRunOptions = ({
809
+ adapterCheck,
810
+ cli,
811
+ config,
812
+ env,
813
+ fix,
814
+ rootDir,
815
+ topos,
816
+ }: {
817
+ readonly adapterCheck: boolean;
818
+ readonly cli: WardenConfigLayer;
819
+ readonly config?: WardenConfigInput | undefined;
820
+ readonly env: EnvRecord;
821
+ readonly fix: boolean;
822
+ readonly rootDir: string;
823
+ readonly topos: readonly WardenTopoTarget[];
824
+ }): WardenRunOptions => ({
825
+ ...cleanUndefined({
826
+ adapterCheck,
827
+ apps: cli.apps,
828
+ config,
829
+ depth: cli.depth,
830
+ drafts: cli.drafts,
831
+ failOn: cli.failOn,
832
+ fix,
833
+ format: cli.format,
834
+ lock: cli.lock,
835
+ noLockMutation: cli.noLockMutation,
836
+ rootDir,
837
+ topos,
838
+ }),
839
+ env,
840
+ });
841
+
842
+ const reportPassed = (report: WardenReport): boolean =>
843
+ report.errorCount === 0 &&
844
+ (report.effectiveConfig?.failOn !== 'warning' || report.warnCount === 0) &&
845
+ !(report.drift?.stale ?? false) &&
846
+ report.drift?.blockedReason === undefined;
847
+
848
+ const mergeDiagnosticsIntoReport = (
849
+ report: WardenReport,
850
+ diagnostics: readonly WardenDiagnostic[]
851
+ ): WardenReport => {
852
+ if (diagnostics.length === 0) {
853
+ return report;
854
+ }
855
+
856
+ const mergedDiagnostics = [...diagnostics, ...report.diagnostics];
857
+ const mergedReport = {
858
+ ...report,
859
+ diagnostics: mergedDiagnostics,
860
+ errorCount: mergedDiagnostics.filter((entry) => entry.severity === 'error')
861
+ .length,
862
+ warnCount: mergedDiagnostics.filter((entry) => entry.severity === 'warn')
863
+ .length,
864
+ };
865
+
866
+ return {
867
+ ...mergedReport,
868
+ passed: reportPassed(mergedReport),
869
+ };
870
+ };
871
+
872
+ export const formatWardenCommandOutput = (report: WardenReport): string => {
873
+ switch (report.effectiveConfig?.format ?? 'summary') {
874
+ case 'github': {
875
+ return formatGitHubAnnotations(report);
876
+ }
877
+ case 'json': {
878
+ return formatJson(report);
879
+ }
880
+ case 'summary': {
881
+ return formatSummary(report);
882
+ }
883
+ default: {
884
+ return formatSummary(report);
885
+ }
886
+ }
887
+ };
888
+
889
+ export interface WardenCommandResult {
890
+ readonly exitCode: 0 | 1;
891
+ readonly output: string;
892
+ readonly report: WardenReport;
893
+ readonly summary: string;
894
+ readonly writeStepSummary: boolean;
895
+ }
896
+
897
+ export interface RunWardenCommandOptions {
898
+ readonly args?: readonly string[] | undefined;
899
+ readonly cwd: string;
900
+ readonly env?: EnvRecord | undefined;
901
+ }
902
+
903
+ export const runWardenCommand = async ({
904
+ args = [],
905
+ cwd,
906
+ env = {},
907
+ }: RunWardenCommandOptions): Promise<WardenCommandResult> => {
908
+ const parsed = parseWardenCommandArgs(args);
909
+ const rootDir = resolve(cwd, parsed.rootDir ?? '.');
910
+ const loadedConfig = await loadWardenConfig({
911
+ configPath: parsed.configPath,
912
+ env,
913
+ rootDir,
914
+ });
915
+ const preflight = resolveWardenConfig({
916
+ cli: parsed.cli,
917
+ config: loadedConfig.config,
918
+ env,
919
+ });
920
+ const topoResolution = effectiveConfigNeedsTopo(
921
+ preflight.effectiveConfig.depth
922
+ )
923
+ ? await resolveWardenTopoTargets({
924
+ apps: preflight.effectiveConfig.apps,
925
+ rootDir,
926
+ strict: parsed.ci,
927
+ })
928
+ : { diagnostics: [], topos: [] };
929
+ const report = await runWarden(
930
+ buildRunOptions({
931
+ adapterCheck: parsed.adapterCheck,
932
+ cli: parsed.cli,
933
+ config: loadedConfig.config,
934
+ env,
935
+ fix: parsed.fix,
936
+ rootDir,
937
+ topos: topoResolution.topos,
938
+ })
939
+ );
940
+ const finalReport = mergeDiagnosticsIntoReport(report, [
941
+ ...parsed.diagnostics,
942
+ ...loadedConfig.diagnostics,
943
+ ...topoResolution.diagnostics,
944
+ ]);
945
+
946
+ return {
947
+ exitCode: finalReport.passed ? 0 : 1,
948
+ output: formatWardenCommandOutput(finalReport),
949
+ report: finalReport,
950
+ summary: formatSummary(finalReport),
951
+ writeStepSummary: parsed.ci,
952
+ };
953
+ };