@markjaquith/agency 3.2.12 → 3.2.13
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/package.json
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"
|
|
2
2
|
import { mkdir, readFile as nodeReadFile, rename, rm } from "node:fs/promises"
|
|
3
3
|
import { dirname, join } from "node:path"
|
|
4
|
+
import { Effect } from "effect"
|
|
4
5
|
import {
|
|
5
6
|
captureLogs,
|
|
6
7
|
cleanupTempDir,
|
|
7
8
|
createTempDir,
|
|
8
9
|
runTestEffect,
|
|
10
|
+
trackDocumentReadConcurrency,
|
|
9
11
|
} from "../test-utils"
|
|
12
|
+
import { documentLoadConcurrency } from "../workbase/document-loading"
|
|
13
|
+
import { ContextService } from "../services/ContextService"
|
|
14
|
+
import { FileSystemService } from "../services/FileSystemService"
|
|
10
15
|
import { context } from "./context"
|
|
11
16
|
|
|
12
17
|
const write = async (root: string, path: string, content: string) => {
|
|
@@ -157,6 +162,33 @@ Phase prose.
|
|
|
157
162
|
])
|
|
158
163
|
})
|
|
159
164
|
|
|
165
|
+
test("bounds document reads across nested task and phase traversal", async () => {
|
|
166
|
+
await Promise.all(
|
|
167
|
+
Array.from({ length: documentLoadConcurrency + 8 }, (_, index) =>
|
|
168
|
+
write(
|
|
169
|
+
root,
|
|
170
|
+
`tasks/agent-contract/phases/extra-${index}/PHASE.md`,
|
|
171
|
+
`---\nrepo: agency\nbranch: extra-${index}\nbase: main\npr: null\nstatus: open\n---\n`,
|
|
172
|
+
),
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
const fs = await Effect.runPromise(
|
|
176
|
+
FileSystemService.pipe(Effect.provide(FileSystemService.Default)),
|
|
177
|
+
)
|
|
178
|
+
const tracked = trackDocumentReadConcurrency(fs)
|
|
179
|
+
|
|
180
|
+
await runTestEffect(
|
|
181
|
+
ContextService.pipe(
|
|
182
|
+
Effect.flatMap((service) =>
|
|
183
|
+
service.get({ cwd: root, target: "foundations" }),
|
|
184
|
+
),
|
|
185
|
+
Effect.provideService(FileSystemService, tracked.fs),
|
|
186
|
+
),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
expect(tracked.maximum()).toBe(documentLoadConcurrency)
|
|
190
|
+
})
|
|
191
|
+
|
|
160
192
|
afterEach(async () => {
|
|
161
193
|
await cleanupTempDir(root)
|
|
162
194
|
})
|
|
@@ -4,7 +4,14 @@ import { Effect } from "effect"
|
|
|
4
4
|
import { mkdir } from "node:fs/promises"
|
|
5
5
|
import { dirname, join } from "node:path"
|
|
6
6
|
import { AgencyGraph } from "../graph-schema"
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
cleanupTempDir,
|
|
9
|
+
createTempDir,
|
|
10
|
+
runTestEffect,
|
|
11
|
+
trackDocumentReadConcurrency,
|
|
12
|
+
} from "../test-utils"
|
|
13
|
+
import { documentLoadConcurrency } from "../workbase/document-loading"
|
|
14
|
+
import { FileSystemService } from "./FileSystemService"
|
|
8
15
|
import { GraphService } from "./GraphService"
|
|
9
16
|
import {
|
|
10
17
|
VersionControlService,
|
|
@@ -209,6 +216,33 @@ describe("GraphService", () => {
|
|
|
209
216
|
expect(await getGraph(root)).toEqual(graph)
|
|
210
217
|
})
|
|
211
218
|
|
|
219
|
+
test("bounds document reads across nested task and phase traversal", async () => {
|
|
220
|
+
const root = await createWorkbase()
|
|
221
|
+
roots.push(root)
|
|
222
|
+
await Promise.all(
|
|
223
|
+
Array.from({ length: documentLoadConcurrency + 8 }, (_, index) =>
|
|
224
|
+
write(
|
|
225
|
+
root,
|
|
226
|
+
`tasks/ship/phases/extra-${index}/PHASE.md`,
|
|
227
|
+
`---\nrepo: agency\nbranch: extra-${index}\nbase: main\npr: null\nstatus: open\n---\n`,
|
|
228
|
+
),
|
|
229
|
+
),
|
|
230
|
+
)
|
|
231
|
+
const fs = await Effect.runPromise(
|
|
232
|
+
FileSystemService.pipe(Effect.provide(FileSystemService.Default)),
|
|
233
|
+
)
|
|
234
|
+
const tracked = trackDocumentReadConcurrency(fs)
|
|
235
|
+
|
|
236
|
+
await runTestEffect(
|
|
237
|
+
GraphService.pipe(
|
|
238
|
+
Effect.flatMap((service) => service.get({ cwd: root })),
|
|
239
|
+
Effect.provideService(FileSystemService, tracked.fs),
|
|
240
|
+
),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
expect(tracked.maximum()).toBe(documentLoadConcurrency)
|
|
244
|
+
})
|
|
245
|
+
|
|
212
246
|
test("does not resolve a VCS backend when git details are not requested", async () => {
|
|
213
247
|
const root = await createWorkbase()
|
|
214
248
|
roots.push(root)
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import { parseFrontmatter } from "../workbase/frontmatter"
|
|
23
23
|
import { normalizePullRequestRecord } from "../workbase/delivery-command"
|
|
24
24
|
import { documentRevision } from "../workbase/document-revision"
|
|
25
|
+
import { documentLoadConcurrency } from "../workbase/document-loading"
|
|
25
26
|
import {
|
|
26
27
|
EpicFrontmatter,
|
|
27
28
|
PhaseFrontmatter,
|
|
@@ -201,7 +202,7 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
201
202
|
)
|
|
202
203
|
}),
|
|
203
204
|
),
|
|
204
|
-
{ concurrency:
|
|
205
|
+
{ concurrency: documentLoadConcurrency },
|
|
205
206
|
)
|
|
206
207
|
for (const document of epicDocuments) {
|
|
207
208
|
if (document) epics.set(document.id, document)
|
|
@@ -224,40 +225,37 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
224
225
|
const phaseIds = yield* directories(
|
|
225
226
|
join(root, "tasks", id, "phases"),
|
|
226
227
|
)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
)
|
|
247
|
-
}),
|
|
228
|
+
return { id, task, phaseIds }
|
|
229
|
+
}),
|
|
230
|
+
),
|
|
231
|
+
{ concurrency: documentLoadConcurrency },
|
|
232
|
+
)
|
|
233
|
+
const phaseDocuments = yield* Effect.all(
|
|
234
|
+
taskDocuments.flatMap(({ id, phaseIds }) =>
|
|
235
|
+
phaseIds.map((phaseId) => {
|
|
236
|
+
const phasePath = join(
|
|
237
|
+
root,
|
|
238
|
+
"tasks",
|
|
239
|
+
id,
|
|
240
|
+
"phases",
|
|
241
|
+
phaseId,
|
|
242
|
+
"PHASE.md",
|
|
243
|
+
)
|
|
244
|
+
return readDocument(phaseId, phasePath, PhaseFrontmatter).pipe(
|
|
245
|
+
Effect.catchTag("FileNotFoundError", () =>
|
|
246
|
+
Effect.succeed(null),
|
|
248
247
|
),
|
|
249
|
-
{
|
|
248
|
+
Effect.map((document) => ({ taskId: id, document })),
|
|
250
249
|
)
|
|
251
|
-
return { id, task, phases: taskPhases }
|
|
252
250
|
}),
|
|
253
251
|
),
|
|
254
|
-
{ concurrency:
|
|
252
|
+
{ concurrency: documentLoadConcurrency },
|
|
255
253
|
)
|
|
256
254
|
for (const documents of taskDocuments) {
|
|
257
255
|
if (documents.task) tasks.set(documents.id, documents.task)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
256
|
+
}
|
|
257
|
+
for (const { taskId, document } of phaseDocuments) {
|
|
258
|
+
if (document) phases.set(`${taskId}/${document.id}`, document)
|
|
261
259
|
}
|
|
262
260
|
|
|
263
261
|
const repositoryRecords = new Map<string, RepositoryRecord>()
|
|
@@ -26,6 +26,7 @@ import { validateAgents } from "../workbase/agent-command"
|
|
|
26
26
|
import { findDependencyCycles } from "../workbase/dependency-graph"
|
|
27
27
|
import { validateDelivery } from "../workbase/delivery-command"
|
|
28
28
|
import { documentRevision } from "../workbase/document-revision"
|
|
29
|
+
import { documentLoadConcurrency } from "../workbase/document-loading"
|
|
29
30
|
|
|
30
31
|
class WorkbaseNotFoundError extends Data.TaggedError("WorkbaseNotFoundError")<{
|
|
31
32
|
readonly message: string
|
|
@@ -66,8 +67,6 @@ interface DocumentRecord<T> {
|
|
|
66
67
|
readonly data: T
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
const validationConcurrency = 32
|
|
70
|
-
|
|
71
70
|
interface ValidationDocuments {
|
|
72
71
|
readonly epics: readonly DocumentRecord<EpicData>[]
|
|
73
72
|
readonly tasks: readonly DocumentRecord<TaskData>[]
|
|
@@ -751,7 +750,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
751
750
|
return document ? { id, path, ...document } : null
|
|
752
751
|
}),
|
|
753
752
|
),
|
|
754
|
-
{ concurrency:
|
|
753
|
+
{ concurrency: documentLoadConcurrency },
|
|
755
754
|
)
|
|
756
755
|
for (const document of epicDocuments) {
|
|
757
756
|
if (document) epics.set(document.id, document)
|
|
@@ -793,7 +792,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
793
792
|
: null
|
|
794
793
|
}),
|
|
795
794
|
),
|
|
796
|
-
{ concurrency:
|
|
795
|
+
{ concurrency: documentLoadConcurrency },
|
|
797
796
|
)
|
|
798
797
|
return {
|
|
799
798
|
id,
|
|
@@ -802,7 +801,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
802
801
|
}
|
|
803
802
|
}),
|
|
804
803
|
),
|
|
805
|
-
{ concurrency:
|
|
804
|
+
{ concurrency: documentLoadConcurrency },
|
|
806
805
|
)
|
|
807
806
|
for (const documents of taskDocuments) {
|
|
808
807
|
if (documents.task) tasks.set(documents.id, documents.task)
|
package/src/test-utils.ts
CHANGED
|
@@ -31,6 +31,26 @@ export const createTempDir = () => mkdtemp(join(tmpdir(), "agency-test-"))
|
|
|
31
31
|
export const cleanupTempDir = (path: string) =>
|
|
32
32
|
rm(path, { recursive: true, force: true })
|
|
33
33
|
|
|
34
|
+
export const trackDocumentReadConcurrency = (fs: FileSystemService) => {
|
|
35
|
+
let active = 0
|
|
36
|
+
let maximum = 0
|
|
37
|
+
return {
|
|
38
|
+
fs: {
|
|
39
|
+
...fs,
|
|
40
|
+
readFile: (path: string) => {
|
|
41
|
+
if (!/(?:EPIC|TASK|PHASE)\.md$/.test(path)) return fs.readFile(path)
|
|
42
|
+
return Effect.gen(function* () {
|
|
43
|
+
active += 1
|
|
44
|
+
maximum = Math.max(maximum, active)
|
|
45
|
+
yield* Effect.sleep(5)
|
|
46
|
+
return yield* fs.readFile(path)
|
|
47
|
+
}).pipe(Effect.ensuring(Effect.sync(() => (active -= 1))))
|
|
48
|
+
},
|
|
49
|
+
} satisfies FileSystemService,
|
|
50
|
+
maximum: () => maximum,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
34
54
|
const TestLayer = Layer.mergeAll(
|
|
35
55
|
FileSystemService.Default,
|
|
36
56
|
WorkbaseService.Default,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const documentLoadConcurrency = 32
|