@ontrails/warden 1.0.0-beta.3 → 1.0.0-beta.30

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