@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.
- package/README.md +23 -0
- package/cli.ts +13 -0
- package/package.json +1 -1
- package/src/cli-parser.test.ts +45 -1
- package/src/cli-parser.ts +158 -5
- package/src/cli.test.ts +62 -0
- package/src/commands/epic.ts +51 -1
- package/src/commands/phase.ts +94 -1
- package/src/commands/task.ts +108 -1
- package/src/services/GraphMutationService.test.ts +336 -0
- package/src/services/GraphMutationService.ts +952 -0
- package/src/services/WorkbaseService.ts +3 -37
- package/src/test-utils.ts +2 -0
- package/src/workbase/dependency-graph.ts +51 -0
|
@@ -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
|
|
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
|
|
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
|
+
}
|