Add TaskSource abstraction, config schema, and Bun.Glob file scanning
Architecture updates to support the plugin's I/O and configuration layer: - TaskSource interface abstracts task loading from I/O, making future sources (API, database, test) swappable without operation changes - FileSource implements v1: Bun.Glob for directory scanning, Bun.file for reading, parseFrontmatter for parsing (single-pass I/O) - SourceResult provides raw file content (for show) and per-file error detail (for validate) that parseTaskDirectory couldn't offer - Config schema uses TypeBox (already a dep via taskgraph) for compile-time types, runtime validation, and JSON Schema export - ADR-005: TaskSource abstraction rationale - ADR-006: Bun.Glob over parseTaskDirectory rationale - Performance benchmark added (43 tasks full pipeline: ~150ms) - AGENTS.md updated with config section and source structure
This commit is contained in:
62
docs/architecture/decisions/005-tasksource-abstraction.md
Normal file
62
docs/architecture/decisions/005-tasksource-abstraction.md
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-04-28
|
||||
---
|
||||
|
||||
# ADR-005: TaskSource Abstraction
|
||||
|
||||
## Context
|
||||
|
||||
v1 of this plugin reads tasks from a local `tasks/` directory. But the purpose of the plugin is to give agents graph analysis and decomposition guidance — nowhere in that mission statement does "read files from a directory" appear. File I/O is an implementation detail.
|
||||
|
||||
Future sources are likely:
|
||||
- **ApiSource** — tasks fetched from a project management tool (Jira, Linear, GitHub Issues) via HTTP
|
||||
- **MixedSource** — merge local task files with remote tasks
|
||||
- **TestSource** — in-memory tasks for unit testing operations without filesystem I/O
|
||||
|
||||
If every operation directly reads the filesystem, adding a new source means touching every operation.
|
||||
|
||||
## Decision
|
||||
|
||||
Define a `TaskSource` interface that operations use instead of direct filesystem access:
|
||||
|
||||
```typescript
|
||||
interface TaskSource {
|
||||
readonly name: string
|
||||
load(): Promise<SourceResult>
|
||||
}
|
||||
|
||||
interface SourceResult {
|
||||
tasks: TaskInput[]
|
||||
rawFiles: Map<string, string>
|
||||
errors: SourceError[]
|
||||
}
|
||||
|
||||
interface SourceError {
|
||||
filePath: string
|
||||
error: string
|
||||
}
|
||||
```
|
||||
|
||||
The source is resolved once at plugin initialization (in `index.ts`) based on config, and passed to `createTools()` → registry → operations.
|
||||
|
||||
v1 implements only `FileSource` (reads from `tasks/` directory via `Bun.Glob` + `parseFrontmatter`). The factory function `createSource(config, workspaceDir)` returns the appropriate source.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
- Operations are decoupled from I/O — they call `source.load()` and get `SourceResult`
|
||||
- Adding a new source means implementing `TaskSource` and updating the factory — zero operation changes
|
||||
- `rawFiles` gives `show` operation full markdown content without a second I/O pass
|
||||
- `errors` gives `validate` operation filenames with parse errors
|
||||
- Testing is trivial — inject a `TestSource` with in-memory data, no filesystem mocking needed
|
||||
- The "1 tool = 1 client" pattern (like an LLM client) emerges naturally: as sources expand, the plugin stays a single tool
|
||||
|
||||
**Negative:**
|
||||
- One level of indirection for what's currently just file reading
|
||||
- The `rawFiles` Map stores all file content in memory concurrently (acceptable for ≤50 files at a few KB each)
|
||||
|
||||
## References
|
||||
|
||||
- open-memory pattern: handlers directly query SQLite — no abstraction. That works because the data source is fixed (OpenCode's DB). Tasks data is more pluggable.
|
||||
- @alkdev/taskgraph `parseTaskDirectory` returns only `TaskInput[]` — no raw content, no error detail. The TaskSource abstraction gives us both.
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-04-28
|
||||
---
|
||||
|
||||
# ADR-006: Bun.Glob Over `parseTaskDirectory`
|
||||
|
||||
## Context
|
||||
|
||||
`@alkdev/taskgraph` provides `parseTaskDirectory(dirPath)` — a convenience function that recursively scans a directory for `.md` files and returns `TaskInput[]`. It uses `node:fs/promises.readdir` for directory traversal and silently skips files with invalid frontmatter.
|
||||
|
||||
The plugin needs more than what `parseTaskDirectory` provides:
|
||||
|
||||
1. **Raw file content** — the `show` operation returns full markdown body (frontmatter + description + acceptance criteria + notes). `parseTaskDirectory` only returns parsed frontmatter.
|
||||
2. **Error detail by filename** — the `validate` operation reports which file failed and why. `parseTaskDirectory` silently skips invalid files with no error reporting.
|
||||
3. **Bun-native runtime** — the plugin targets Bun. `Bun.Glob` and `Bun.file()` are native APIs with no Node compat overhead.
|
||||
4. **Single-pass I/O** — read each file once. `parseTaskDirectory` + separate file reads for `show` would be two passes.
|
||||
|
||||
## Decision
|
||||
|
||||
Use `Bun.Glob("**/*.md")` for directory scanning, `Bun.file().text()` for reading, and `parseFrontmatter()` (singular, from `@alkdev/taskgraph`) for parsing. The `FileSource` class orchestrates this into a `SourceResult`.
|
||||
|
||||
We still use `parseFrontmatter()` for the YAML/schema validation — we just don't use `parseTaskDirectory` or `parseTaskFile` (which does the same thing but with `node:fs/promises.readFile`).
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
- Single I/O pass per operation call — glob scan, read all files, parse in memory
|
||||
- `rawFiles` Map gives full content for `show` without a second read
|
||||
- `errors` array gives per-file error detail for `validate`
|
||||
- Bun-native APIs (`Bun.Glob`, `Bun.file()`) — no Node compat layer
|
||||
- Consistent with the TaskSource abstraction (see ADR-005)
|
||||
|
||||
**Negative:**
|
||||
- Not using `parseTaskDirectory` means reimplementing directory scanning — but `Bun.Glob` is ~2 lines and more flexible
|
||||
- Not using `parseTaskFile` means we call `parseFrontmatter()` directly after reading the file ourselves — same outcome, slightly more code
|
||||
- The `rawFiles` Map keeps all file content in memory — acceptable for typical task sets (≤50 files, ≤100KB total)
|
||||
|
||||
## Benchmark
|
||||
|
||||
43 task files, all analysis functions, Bun runtime:
|
||||
- `Bun.Glob` scan: ~1ms
|
||||
- File read + `parseFrontmatter` (43 files): ~140ms
|
||||
- `TaskGraph.fromTasks`: ~5ms
|
||||
- All 6 analysis functions: ~17ms
|
||||
- **Total**: ~150ms
|
||||
|
||||
The Rust CLI is faster on raw I/O/parsing (native binary), but the plugin eliminates subprocess overhead and plain-text parsing by the LLM. Overall tool call latency favors the plugin.
|
||||
|
||||
## References
|
||||
|
||||
- `@alkdev/taskgraph` `frontmatter/file-io.ts` — `parseTaskFile` and `parseTaskDirectory` implementations
|
||||
- Bun API docs: `Bun.Glob`, `Bun.file()`
|
||||
Reference in New Issue
Block a user