@markjaquith/agency 2.20.0 → 2.21.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.
@@ -13,7 +13,6 @@ import {
13
13
  TaskFrontmatter,
14
14
  WorkbaseConfig,
15
15
  WorkbaseRegistry,
16
- type Dependency,
17
16
  type EpicFrontmatter as EpicData,
18
17
  type PhaseFrontmatter as PhaseData,
19
18
  type TaskFrontmatter as TaskData,
@@ -22,6 +21,7 @@ import {
22
21
  } from "../workbase/schemas"
23
22
  import { validateWorktreeCreateCommand } from "../workbase/worktree-command"
24
23
  import { validateRunners } from "../workbase/runner-command"
24
+ import { findDependencyCycles } from "../workbase/dependency-graph"
25
25
 
26
26
  class WorkbaseNotFoundError extends Data.TaggedError("WorkbaseNotFoundError")<{
27
27
  readonly message: string
@@ -145,40 +145,6 @@ const writeRegistry = (
145
145
  yield* fs.writeJSON(path, registry)
146
146
  })
147
147
 
148
- const findCycles = (nodes: readonly Dependency[]): readonly string[] => {
149
- const dependencies = new Map(
150
- nodes.map((node) => [node.id, [...(node.dependsOn ?? [])]]),
151
- )
152
- const visiting = new Set<string>()
153
- const visited = new Set<string>()
154
- const cycles = new Set<string>()
155
-
156
- const visit = (id: string) => {
157
- if (visiting.has(id)) {
158
- cycles.add(id)
159
- return
160
- }
161
- if (visited.has(id)) {
162
- return
163
- }
164
-
165
- visiting.add(id)
166
- for (const dependency of dependencies.get(id) ?? []) {
167
- if (dependencies.has(dependency)) {
168
- visit(dependency)
169
- }
170
- }
171
- visiting.delete(id)
172
- visited.add(id)
173
- }
174
-
175
- for (const id of dependencies.keys()) {
176
- visit(id)
177
- }
178
-
179
- return [...cycles].sort()
180
- }
181
-
182
148
  export class WorkbaseService extends Effect.Service<WorkbaseService>()(
183
149
  "WorkbaseService",
184
150
  {
@@ -693,7 +659,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
693
659
  }
694
660
  }
695
661
  }
696
- for (const cycle of findCycles(epic.data.tasks)) {
662
+ for (const cycle of findDependencyCycles(epic.data.tasks)) {
697
663
  issue(epic.path, `Task dependency cycle includes '${cycle}'`)
698
664
  }
699
665
  }
@@ -739,7 +705,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
739
705
  issue(task.path, `Unlisted phase '${phaseId}'`)
740
706
  }
741
707
  }
742
- for (const cycle of findCycles(task.data.phases)) {
708
+ for (const cycle of findDependencyCycles(task.data.phases)) {
743
709
  issue(task.path, `Phase dependency cycle includes '${cycle}'`)
744
710
  }
745
711
  } else if (actualPhaseIds.length > 0) {
package/src/test-utils.ts CHANGED
@@ -18,6 +18,7 @@ import { GraphService } from "./services/GraphService"
18
18
  import { ClaimService } from "./services/ClaimService"
19
19
  import { SyncService } from "./services/SyncService"
20
20
  import { ReadinessService } from "./services/ReadinessService"
21
+ import { GraphMutationService } from "./services/GraphMutationService"
21
22
 
22
23
  export const createTempDir = () => mkdtemp(join(tmpdir(), "agency-test-"))
23
24
 
@@ -40,6 +41,7 @@ const TestLayer = Layer.mergeAll(
40
41
  ClaimService.Default,
41
42
  SyncService.Default,
42
43
  ReadinessService.Default,
44
+ GraphMutationService.Default,
43
45
  )
44
46
 
45
47
  export async function runTestEffect<A, E>(
@@ -0,0 +1,51 @@
1
+ import type { Dependency } from "./schemas"
2
+
3
+ export const findDependencyCycles = (
4
+ nodes: readonly Dependency[],
5
+ ): readonly string[] => {
6
+ const dependencies = new Map(
7
+ nodes.map((node) => [node.id, [...(node.dependsOn ?? [])]]),
8
+ )
9
+ const visiting = new Set<string>()
10
+ const visited = new Set<string>()
11
+ const cycles = new Set<string>()
12
+
13
+ const visit = (id: string) => {
14
+ if (visiting.has(id)) {
15
+ cycles.add(id)
16
+ return
17
+ }
18
+ if (visited.has(id)) return
19
+
20
+ visiting.add(id)
21
+ for (const dependency of dependencies.get(id) ?? []) {
22
+ if (dependencies.has(dependency)) visit(dependency)
23
+ }
24
+ visiting.delete(id)
25
+ visited.add(id)
26
+ }
27
+
28
+ for (const id of dependencies.keys()) visit(id)
29
+ return [...cycles].sort()
30
+ }
31
+
32
+ export const validateDependencies = (
33
+ nodes: readonly Dependency[],
34
+ label: string,
35
+ ): string | undefined => {
36
+ const singular = label.endsWith("s") ? label.slice(0, -1) : label
37
+ const ids = new Set(nodes.map((node) => node.id))
38
+ if (ids.size !== nodes.length) return `${label} IDs must be unique`
39
+ for (const node of nodes) {
40
+ for (const dependency of node.dependsOn ?? []) {
41
+ if (dependency === node.id) {
42
+ return `${singular} '${node.id}' cannot depend on itself`
43
+ }
44
+ if (!ids.has(dependency)) {
45
+ return `Unknown ${singular.toLowerCase()} dependency '${dependency}'`
46
+ }
47
+ }
48
+ }
49
+ const cycle = findDependencyCycles(nodes)[0]
50
+ return cycle ? `${singular} dependency cycle includes '${cycle}'` : undefined
51
+ }