@markjaquith/agency 1.11.0 → 2.0.0

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 (130) hide show
  1. package/README.md +302 -113
  2. package/cli.ts +123 -415
  3. package/index.ts +1 -1
  4. package/package.json +12 -17
  5. package/skills/agency/SKILL.md +258 -0
  6. package/src/cli.test.ts +290 -0
  7. package/src/commands/description.test.ts +103 -0
  8. package/src/commands/epic.test.ts +103 -0
  9. package/src/commands/epic.ts +101 -0
  10. package/src/commands/init.test.ts +42 -290
  11. package/src/commands/init.ts +15 -123
  12. package/src/commands/phase.ts +120 -0
  13. package/src/commands/pr.test.ts +19 -170
  14. package/src/commands/pr.ts +23 -179
  15. package/src/commands/repo.test.ts +80 -0
  16. package/src/commands/repo.ts +83 -0
  17. package/src/commands/status.test.ts +35 -477
  18. package/src/commands/status.ts +24 -213
  19. package/src/commands/task-phase.test.ts +206 -0
  20. package/src/commands/task.ts +83 -1285
  21. package/src/commands/validate.test.ts +91 -0
  22. package/src/commands/validate.ts +50 -0
  23. package/src/commands/work.test.ts +172 -419
  24. package/src/commands/work.ts +47 -166
  25. package/src/services/EpicService.test.ts +71 -0
  26. package/src/services/EpicService.ts +135 -0
  27. package/src/services/FileSystemService.ts +61 -92
  28. package/src/services/PhaseService.ts +323 -0
  29. package/src/services/PullRequestService.test.ts +386 -0
  30. package/src/services/PullRequestService.ts +143 -0
  31. package/src/services/RepositoryService.test.ts +106 -0
  32. package/src/services/RepositoryService.ts +159 -0
  33. package/src/services/TaskPhaseService.test.ts +217 -0
  34. package/src/services/TaskService.ts +192 -0
  35. package/src/services/WorkbaseService.test.ts +446 -0
  36. package/src/services/WorkbaseService.ts +493 -0
  37. package/src/services/WorktreeService.test.ts +599 -0
  38. package/src/services/WorktreeService.ts +357 -0
  39. package/src/test-utils.ts +41 -438
  40. package/src/types.ts +1 -215
  41. package/src/utils/command.ts +1 -5
  42. package/src/utils/effect.test.ts +24 -0
  43. package/src/utils/effect.ts +3 -273
  44. package/src/utils/process.test.ts +0 -15
  45. package/src/utils/process.ts +1 -60
  46. package/src/workbase/frontmatter.test.ts +50 -0
  47. package/src/workbase/frontmatter.ts +77 -0
  48. package/src/workbase/repository-reference.test.ts +25 -0
  49. package/src/workbase/repository-reference.ts +18 -0
  50. package/src/workbase/schemas.test.ts +146 -0
  51. package/src/workbase/schemas.ts +78 -0
  52. package/src/workbase/worktree-command.test.ts +61 -0
  53. package/src/workbase/worktree-command.ts +52 -0
  54. package/src/commands/base.test.ts +0 -198
  55. package/src/commands/base.ts +0 -198
  56. package/src/commands/clean.test.ts +0 -299
  57. package/src/commands/clean.ts +0 -274
  58. package/src/commands/completions.test.ts +0 -51
  59. package/src/commands/completions.ts +0 -308
  60. package/src/commands/emit.integration.test.ts +0 -549
  61. package/src/commands/emit.test.ts +0 -576
  62. package/src/commands/emit.ts +0 -700
  63. package/src/commands/emitted.test.ts +0 -226
  64. package/src/commands/emitted.ts +0 -57
  65. package/src/commands/loop.test.ts +0 -621
  66. package/src/commands/loop.ts +0 -406
  67. package/src/commands/merge.integration.test.ts +0 -195
  68. package/src/commands/merge.test.ts +0 -246
  69. package/src/commands/merge.ts +0 -244
  70. package/src/commands/pull.test.ts +0 -395
  71. package/src/commands/pull.ts +0 -205
  72. package/src/commands/push.test.ts +0 -518
  73. package/src/commands/push.ts +0 -434
  74. package/src/commands/rebase.test.ts +0 -521
  75. package/src/commands/rebase.ts +0 -232
  76. package/src/commands/save.test.ts +0 -247
  77. package/src/commands/save.ts +0 -162
  78. package/src/commands/source.test.ts +0 -195
  79. package/src/commands/source.ts +0 -72
  80. package/src/commands/switch.test.ts +0 -194
  81. package/src/commands/switch.ts +0 -84
  82. package/src/commands/task-branching.test.ts +0 -633
  83. package/src/commands/task-continue.test.ts +0 -311
  84. package/src/commands/task-edit.test.ts +0 -235
  85. package/src/commands/task-main.test.ts +0 -1066
  86. package/src/commands/task-squash.test.ts +0 -266
  87. package/src/commands/tasks.test.ts +0 -391
  88. package/src/commands/tasks.ts +0 -161
  89. package/src/commands/template-delete.test.ts +0 -178
  90. package/src/commands/template-delete.ts +0 -98
  91. package/src/commands/template-list.test.ts +0 -135
  92. package/src/commands/template-list.ts +0 -87
  93. package/src/commands/template-view.test.ts +0 -158
  94. package/src/commands/template-view.ts +0 -86
  95. package/src/commands/template.test.ts +0 -32
  96. package/src/commands/template.ts +0 -96
  97. package/src/commands/use.test.ts +0 -87
  98. package/src/commands/use.ts +0 -97
  99. package/src/constants.ts +0 -10
  100. package/src/errors.ts +0 -17
  101. package/src/schemas.ts +0 -33
  102. package/src/services/AgencyMetadataService.ts +0 -311
  103. package/src/services/ClaudeService.test.ts +0 -184
  104. package/src/services/ClaudeService.ts +0 -91
  105. package/src/services/ConfigService.ts +0 -115
  106. package/src/services/FilterRepoService.ts +0 -164
  107. package/src/services/FormatterService.test.ts +0 -432
  108. package/src/services/FormatterService.ts +0 -219
  109. package/src/services/GitService.ts +0 -1183
  110. package/src/services/MockFilterRepoService.ts +0 -140
  111. package/src/services/OpencodeService.ts +0 -263
  112. package/src/services/PromptService.ts +0 -183
  113. package/src/services/TemplateService.ts +0 -81
  114. package/src/types/native-exec.d.ts +0 -8
  115. package/src/utils/colors.ts +0 -178
  116. package/src/utils/git-path.ts +0 -6
  117. package/src/utils/glob.test.ts +0 -161
  118. package/src/utils/glob.ts +0 -78
  119. package/src/utils/paths.ts +0 -51
  120. package/src/utils/pr-branch.test.ts +0 -376
  121. package/src/utils/pr-branch.ts +0 -508
  122. package/src/utils/spinner.test.ts +0 -45
  123. package/src/utils/spinner.ts +0 -96
  124. package/src/utils/task-parser.test.ts +0 -172
  125. package/src/utils/task-parser.ts +0 -93
  126. package/templates/AGENCY.md +0 -22
  127. package/templates/AGENTS.md +0 -11
  128. package/templates/CLAUDE.md +0 -3
  129. package/templates/TASK.md +0 -5
  130. package/templates/opencode.json +0 -4
@@ -0,0 +1,493 @@
1
+ import { Schema } from "@effect/schema"
2
+ import { TreeFormatter } from "@effect/schema"
3
+ import { Data, Effect, Either } from "effect"
4
+ import { dirname, join, relative, resolve } from "node:path"
5
+ import { FileSystemService } from "./FileSystemService"
6
+ import { parseFrontmatter } from "../workbase/frontmatter"
7
+ import {
8
+ EpicFrontmatter,
9
+ PhaseFrontmatter,
10
+ TaskFrontmatter,
11
+ WorkbaseConfig,
12
+ type Dependency,
13
+ type EpicFrontmatter as EpicData,
14
+ type PhaseFrontmatter as PhaseData,
15
+ type TaskFrontmatter as TaskData,
16
+ } from "../workbase/schemas"
17
+ import { validateWorktreeCreateCommand } from "../workbase/worktree-command"
18
+
19
+ class WorkbaseNotFoundError extends Data.TaggedError("WorkbaseNotFoundError")<{
20
+ readonly message: string
21
+ }> {}
22
+
23
+ class WorkbaseConfigError extends Data.TaggedError("WorkbaseConfigError")<{
24
+ readonly message: string
25
+ readonly path: string
26
+ readonly cause?: unknown
27
+ }> {}
28
+
29
+ interface ValidationIssue {
30
+ readonly path: string
31
+ readonly message: string
32
+ }
33
+
34
+ interface ValidationReport {
35
+ readonly root: string
36
+ readonly issues: readonly ValidationIssue[]
37
+ readonly epicCount: number
38
+ readonly taskCount: number
39
+ readonly phaseCount: number
40
+ readonly valid: boolean
41
+ }
42
+
43
+ interface DocumentRecord<T> {
44
+ readonly id: string
45
+ readonly path: string
46
+ readonly data: T
47
+ }
48
+
49
+ type DecodeResult<T> =
50
+ | { readonly success: true; readonly value: T }
51
+ | { readonly success: false; readonly error: string }
52
+
53
+ const decode = <S extends Schema.Schema.AnyNoContext>(
54
+ schema: S,
55
+ input: unknown,
56
+ ): DecodeResult<Schema.Schema.Type<S>> => {
57
+ const result = Schema.decodeUnknownEither(schema, {
58
+ errors: "all",
59
+ onExcessProperty: "error",
60
+ })(input)
61
+
62
+ return Either.isLeft(result)
63
+ ? { success: false, error: TreeFormatter.formatErrorSync(result.left) }
64
+ : { success: true, value: result.right }
65
+ }
66
+
67
+ const findCycles = (nodes: readonly Dependency[]): readonly string[] => {
68
+ const dependencies = new Map(
69
+ nodes.map((node) => [node.id, [...(node.dependsOn ?? [])]]),
70
+ )
71
+ const visiting = new Set<string>()
72
+ const visited = new Set<string>()
73
+ const cycles = new Set<string>()
74
+
75
+ const visit = (id: string) => {
76
+ if (visiting.has(id)) {
77
+ cycles.add(id)
78
+ return
79
+ }
80
+ if (visited.has(id)) {
81
+ return
82
+ }
83
+
84
+ visiting.add(id)
85
+ for (const dependency of dependencies.get(id) ?? []) {
86
+ if (dependencies.has(dependency)) {
87
+ visit(dependency)
88
+ }
89
+ }
90
+ visiting.delete(id)
91
+ visited.add(id)
92
+ }
93
+
94
+ for (const id of dependencies.keys()) {
95
+ visit(id)
96
+ }
97
+
98
+ return [...cycles].sort()
99
+ }
100
+
101
+ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
102
+ "WorkbaseService",
103
+ {
104
+ sync: () => ({
105
+ initialize: (path: string = process.cwd()) =>
106
+ Effect.gen(function* () {
107
+ const fs = yield* FileSystemService
108
+ const root = resolve(path)
109
+ const configPath = join(root, "agency.json")
110
+
111
+ if (yield* fs.exists(configPath)) {
112
+ return yield* new WorkbaseConfigError({
113
+ path: configPath,
114
+ message: `Agency configuration already exists: ${configPath}`,
115
+ })
116
+ }
117
+
118
+ yield* fs.createDirectory(root)
119
+ yield* fs.writeJSON(configPath, { version: 2 })
120
+ for (const directory of ["repos", "epics", "tasks"]) {
121
+ yield* fs.createDirectory(join(root, directory))
122
+ }
123
+
124
+ const ignorePath = join(root, ".gitignore")
125
+ const requiredPatterns = [
126
+ "/repos/",
127
+ "/tasks/*/code/",
128
+ "/tasks/*/phases/*/code/",
129
+ ]
130
+ const existing = (yield* fs.exists(ignorePath))
131
+ ? yield* fs.readFile(ignorePath)
132
+ : ""
133
+ const existingLines = new Set(existing.split(/\r?\n/))
134
+ const missing = requiredPatterns.filter(
135
+ (pattern) => !existingLines.has(pattern),
136
+ )
137
+ if (missing.length > 0) {
138
+ const prefix =
139
+ existing.length > 0 && !existing.endsWith("\n") ? "\n" : ""
140
+ yield* fs.writeFile(
141
+ ignorePath,
142
+ `${existing}${prefix}${missing.join("\n")}\n`,
143
+ )
144
+ }
145
+
146
+ return root
147
+ }),
148
+
149
+ discover: (startPath: string = process.cwd()) =>
150
+ Effect.gen(function* () {
151
+ const fs = yield* FileSystemService
152
+ let current = resolve(startPath)
153
+
154
+ if (!(yield* fs.isDirectory(current))) {
155
+ current = dirname(current)
156
+ }
157
+
158
+ while (true) {
159
+ const configPath = join(current, "agency.json")
160
+ if (yield* fs.exists(configPath)) {
161
+ const content = yield* fs.readFile(configPath)
162
+ let input: unknown
163
+ try {
164
+ input = JSON.parse(content)
165
+ } catch (cause) {
166
+ return yield* new WorkbaseConfigError({
167
+ path: configPath,
168
+ message: `Invalid JSON in ${configPath}`,
169
+ cause,
170
+ })
171
+ }
172
+
173
+ if (
174
+ typeof input === "object" &&
175
+ input !== null &&
176
+ "version" in input &&
177
+ input.version === 2
178
+ ) {
179
+ const decoded = decode(WorkbaseConfig, input)
180
+ if (!decoded.success) {
181
+ return yield* new WorkbaseConfigError({
182
+ path: configPath,
183
+ message: `Invalid workbase configuration in ${configPath}:\n${decoded.error}`,
184
+ })
185
+ }
186
+ if (decoded.value.worktreeCreateCommand) {
187
+ try {
188
+ validateWorktreeCreateCommand(
189
+ decoded.value.worktreeCreateCommand,
190
+ )
191
+ } catch (cause) {
192
+ return yield* new WorkbaseConfigError({
193
+ path: configPath,
194
+ message:
195
+ cause instanceof Error
196
+ ? cause.message
197
+ : "Invalid worktreeCreateCommand",
198
+ })
199
+ }
200
+ }
201
+ return current
202
+ }
203
+ }
204
+
205
+ const parent = dirname(current)
206
+ if (parent === current) {
207
+ return yield* new WorkbaseNotFoundError({
208
+ message: `No Agency workbase found from ${resolve(startPath)}`,
209
+ })
210
+ }
211
+ current = parent
212
+ }
213
+ }),
214
+
215
+ loadConfig: (startPath: string = process.cwd()) =>
216
+ Effect.gen(function* () {
217
+ const service = yield* WorkbaseService
218
+ const fs = yield* FileSystemService
219
+ const root = yield* service.discover(startPath)
220
+ const configPath = join(root, "agency.json")
221
+ const content = yield* fs.readFile(configPath)
222
+ let input: unknown
223
+ try {
224
+ input = JSON.parse(content)
225
+ } catch (cause) {
226
+ return yield* new WorkbaseConfigError({
227
+ path: configPath,
228
+ message: `Invalid JSON in ${configPath}`,
229
+ cause,
230
+ })
231
+ }
232
+ const decoded = decode(WorkbaseConfig, input)
233
+ if (!decoded.success) {
234
+ return yield* new WorkbaseConfigError({
235
+ path: configPath,
236
+ message: `Invalid workbase configuration in ${configPath}:\n${decoded.error}`,
237
+ })
238
+ }
239
+ return { root, config: decoded.value }
240
+ }),
241
+
242
+ validate: (startPath: string = process.cwd()) =>
243
+ Effect.gen(function* () {
244
+ const service = yield* WorkbaseService
245
+ const fs = yield* FileSystemService
246
+ const root = yield* service.discover(startPath)
247
+ const issues: ValidationIssue[] = []
248
+ const epics = new Map<string, DocumentRecord<EpicData>>()
249
+ const tasks = new Map<string, DocumentRecord<TaskData>>()
250
+ const phases = new Map<string, DocumentRecord<PhaseData>>()
251
+
252
+ const issue = (path: string, message: string) => {
253
+ issues.push({ path: relative(root, path) || ".", message })
254
+ }
255
+
256
+ const readDirectories = (path: string) =>
257
+ Effect.gen(function* () {
258
+ if (!(yield* fs.isDirectory(path))) {
259
+ return []
260
+ }
261
+ const entries = yield* fs.readDirectory(path)
262
+ return entries
263
+ .filter((entry) => entry.isDirectory)
264
+ .map((entry) => entry.name)
265
+ .sort()
266
+ })
267
+
268
+ const readAliases = Effect.gen(function* () {
269
+ const reposPath = join(root, "repos")
270
+ if (!(yield* fs.isDirectory(reposPath))) {
271
+ return new Set<string>()
272
+ }
273
+ const entries = yield* fs.readDirectory(reposPath)
274
+ return new Set(
275
+ entries
276
+ .filter((entry) => entry.isDirectory || entry.isSymlink)
277
+ .map((entry) => entry.name),
278
+ )
279
+ })
280
+
281
+ const aliases = yield* readAliases
282
+
283
+ const readDocument = <S extends Schema.Schema.AnyNoContext>(
284
+ path: string,
285
+ schema: S,
286
+ ) =>
287
+ Effect.gen(function* () {
288
+ if (!(yield* fs.exists(path))) {
289
+ issue(path, "Required document is missing")
290
+ return null
291
+ }
292
+
293
+ const content = yield* fs.readFile(path)
294
+ const parsed = yield* Effect.either(
295
+ parseFrontmatter(content, path),
296
+ )
297
+ if (Either.isLeft(parsed)) {
298
+ issue(path, parsed.left.message)
299
+ return null
300
+ }
301
+
302
+ const decoded = decode(schema, parsed.right.data)
303
+ if (!decoded.success) {
304
+ issue(path, decoded.error)
305
+ return null
306
+ }
307
+ return decoded.value
308
+ })
309
+
310
+ for (const id of yield* readDirectories(join(root, "epics"))) {
311
+ const path = join(root, "epics", id, "EPIC.md")
312
+ const data = yield* readDocument(path, EpicFrontmatter)
313
+ if (data) {
314
+ epics.set(id, { id, path, data })
315
+ }
316
+ }
317
+
318
+ for (const id of yield* readDirectories(join(root, "tasks"))) {
319
+ const taskPath = join(root, "tasks", id)
320
+ const path = join(taskPath, "TASK.md")
321
+ const data = yield* readDocument(path, TaskFrontmatter)
322
+ if (data) {
323
+ tasks.set(id, { id, path, data })
324
+ }
325
+
326
+ for (const phaseId of yield* readDirectories(
327
+ join(taskPath, "phases"),
328
+ )) {
329
+ const phasePath = join(taskPath, "phases", phaseId, "PHASE.md")
330
+ const phase = yield* readDocument(phasePath, PhaseFrontmatter)
331
+ if (phase) {
332
+ phases.set(`${id}/${phaseId}`, {
333
+ id: phaseId,
334
+ path: phasePath,
335
+ data: phase,
336
+ })
337
+ }
338
+ }
339
+ }
340
+
341
+ const validateRepositories = (
342
+ record: DocumentRecord<EpicData | TaskData | PhaseData>,
343
+ ) => {
344
+ const data = record.data
345
+ const writable = "repo" in data ? data.repo : undefined
346
+ const references = "repos" in data ? (data.repos ?? []) : []
347
+ const referenceAliases = references.map(
348
+ (reference) => reference.repo,
349
+ )
350
+
351
+ if (writable && referenceAliases.includes(writable)) {
352
+ issue(
353
+ record.path,
354
+ `Repository '${writable}' cannot also be a reference`,
355
+ )
356
+ }
357
+
358
+ const all = [writable, ...referenceAliases].filter(
359
+ (alias): alias is string => alias !== undefined,
360
+ )
361
+ for (const alias of new Set(all)) {
362
+ if (!aliases.has(alias)) {
363
+ issue(record.path, `Unknown repository alias '${alias}'`)
364
+ }
365
+ }
366
+
367
+ if (new Set(referenceAliases).size !== referenceAliases.length) {
368
+ issue(record.path, "Repository references must be unique")
369
+ }
370
+ }
371
+
372
+ const branchOwners = new Map<
373
+ string,
374
+ DocumentRecord<TaskData | PhaseData>
375
+ >()
376
+ const validateBranchOwnership = (
377
+ record: DocumentRecord<TaskData | PhaseData>,
378
+ ) => {
379
+ if (!("repo" in record.data)) return
380
+ const key = `${record.data.repo}\u0000${record.data.branch}`
381
+ const owner = branchOwners.get(key)
382
+ if (owner) {
383
+ issue(
384
+ record.path,
385
+ `Writable branch '${record.data.branch}' for repository '${record.data.repo}' is also owned by ${relative(root, owner.path)}`,
386
+ )
387
+ } else {
388
+ branchOwners.set(key, record)
389
+ }
390
+ }
391
+
392
+ for (const epic of epics.values()) {
393
+ validateRepositories(epic)
394
+ const ids = new Set(epic.data.tasks.map((task) => task.id))
395
+ if (ids.size !== epic.data.tasks.length) {
396
+ issue(epic.path, "Epic task IDs must be unique")
397
+ }
398
+ for (const task of epic.data.tasks) {
399
+ const child = tasks.get(task.id)
400
+ if (!child) {
401
+ issue(epic.path, `Unknown child task '${task.id}'`)
402
+ } else if (child.data.epic !== epic.id) {
403
+ issue(
404
+ child.path,
405
+ `Task must reference parent epic '${epic.id}'`,
406
+ )
407
+ }
408
+ for (const dependency of task.dependsOn ?? []) {
409
+ if (!ids.has(dependency)) {
410
+ issue(epic.path, `Unknown task dependency '${dependency}'`)
411
+ }
412
+ }
413
+ }
414
+ for (const cycle of findCycles(epic.data.tasks)) {
415
+ issue(epic.path, `Task dependency cycle includes '${cycle}'`)
416
+ }
417
+ }
418
+
419
+ for (const task of tasks.values()) {
420
+ validateRepositories(task)
421
+ validateBranchOwnership(task)
422
+ if (task.data.epic) {
423
+ const parent = epics.get(task.data.epic)
424
+ if (!parent) {
425
+ issue(task.path, `Unknown parent epic '${task.data.epic}'`)
426
+ } else if (
427
+ !parent.data.tasks.some((child) => child.id === task.id)
428
+ ) {
429
+ issue(parent.path, `Epic does not list child task '${task.id}'`)
430
+ }
431
+ }
432
+
433
+ const phasePrefix = `${task.id}/`
434
+ const actualPhaseIds = [...phases.keys()]
435
+ .filter((key) => key.startsWith(phasePrefix))
436
+ .map((key) => key.slice(phasePrefix.length))
437
+
438
+ if ("phases" in task.data) {
439
+ const declaredIds = new Set(
440
+ task.data.phases.map((phase) => phase.id),
441
+ )
442
+ if (declaredIds.size !== task.data.phases.length) {
443
+ issue(task.path, "Task phase IDs must be unique")
444
+ }
445
+ for (const phase of task.data.phases) {
446
+ if (!phases.has(`${task.id}/${phase.id}`)) {
447
+ issue(task.path, `Missing phase '${phase.id}'`)
448
+ }
449
+ for (const dependency of phase.dependsOn ?? []) {
450
+ if (!declaredIds.has(dependency)) {
451
+ issue(task.path, `Unknown phase dependency '${dependency}'`)
452
+ }
453
+ }
454
+ }
455
+ for (const phaseId of actualPhaseIds) {
456
+ if (!declaredIds.has(phaseId)) {
457
+ issue(task.path, `Unlisted phase '${phaseId}'`)
458
+ }
459
+ }
460
+ for (const cycle of findCycles(task.data.phases)) {
461
+ issue(task.path, `Phase dependency cycle includes '${cycle}'`)
462
+ }
463
+ } else if (actualPhaseIds.length > 0) {
464
+ issue(
465
+ task.path,
466
+ "Single-phase task cannot contain phase directories",
467
+ )
468
+ }
469
+ }
470
+
471
+ for (const phase of phases.values()) {
472
+ validateRepositories(phase)
473
+ validateBranchOwnership(phase)
474
+ }
475
+
476
+ issues.sort((a, b) =>
477
+ a.path === b.path
478
+ ? a.message.localeCompare(b.message)
479
+ : a.path.localeCompare(b.path),
480
+ )
481
+
482
+ return {
483
+ root,
484
+ issues,
485
+ epicCount: epics.size,
486
+ taskCount: tasks.size,
487
+ phaseCount: phases.size,
488
+ valid: issues.length === 0,
489
+ } satisfies ValidationReport
490
+ }),
491
+ }),
492
+ },
493
+ ) {}