feat(graph/queries): implement query methods — hasCycles, findCycles, topologicalOrder, dependencies, dependents, taskCount, getTask

- hasCycles(): uses graphology-dag.hasCycle() as fast boolean check
- findCycles(): SCC pre-check + custom 3-color DFS for cycle path extraction
- topologicalOrder(): graphology-dag.topologicalSort + CircularDependencyError on cycle
- dependencies/dependents: inNeighbors/outNeighbors with TaskNotFoundError
- taskCount(): graph.order, getTask(): node attributes or undefined
- 45 unit tests covering all acceptance criteria
This commit is contained in:
2026-04-27 12:00:17 +00:00
parent 9ad0ec902c
commit 98cc05d266
4 changed files with 814 additions and 12 deletions

View File

@@ -1,7 +1,7 @@
---
id: graph/queries
name: Implement TaskGraph query methods (hasCycles, findCycles, topologicalOrder, dependencies, dependents, taskCount, getTask)
status: pending
status: completed
depends_on:
- graph/taskgraph-class
scope: moderate
@@ -21,21 +21,21 @@ Per [errors-validation.md](../../../docs/architecture/errors-validation.md):
## Acceptance Criteria
- [ ] `hasCycles(): boolean` — uses `graphology-dag.hasCycle()` or `graphology-components` SCC check as fast pre-check
- [ ] `findCycles(): string[][]`:
- [x] `hasCycles(): boolean` — uses `graphology-dag.hasCycle()` or `graphology-components` SCC check as fast pre-check
- [x] `findCycles(): string[][]`:
- Uses `stronglyConnectedComponents()` as pre-check: if zero multi-node SCCs and no self-loops, skip DFS
- Custom 3-color DFS (WHITE/GREY/BLACK) to extract cycle paths
- Returns one representative cycle per back edge, not exhaustive enumeration
- Each inner array is an ordered node sequence where last node has edge back to first
- [ ] `topologicalOrder(): string[]`:
- [x] `topologicalOrder(): string[]`:
- Uses `graphology-dag.topologicalSort()` for the actual sort
- **Throws `CircularDependencyError`** (with `cycles` from `findCycles()`) when graph is cyclic
- Returns `string[]` of task IDs in prerequisite→dependent order
- [ ] `dependencies(taskId: string): string[]` — returns prerequisite task IDs (inNeighbors). Throws `TaskNotFoundError` if ID doesn't exist.
- [ ] `dependents(taskId: string): string[]` — returns dependent task IDs (outNeighbors). Throws `TaskNotFoundError` if ID doesn't exist.
- [ ] `taskCount(): number` — returns number of nodes
- [ ] `getTask(taskId: string): TaskGraphNodeAttributes | undefined` — returns node attributes or undefined
- [ ] Unit tests: cycle detection on known cyclic/acyclic graphs, topologicalOrder on DAG, topologicalOrder throws on cyclic graph, dependency/dependent queries
- [x] `dependencies(taskId: string): string[]` — returns prerequisite task IDs (inNeighbors). Throws `TaskNotFoundError` if ID doesn't exist.
- [x] `dependents(taskId: string): string[]` — returns dependent task IDs (outNeighbors). Throws `TaskNotFoundError` if ID doesn't exist.
- [x] `taskCount(): number` — returns number of nodes
- [x] `getTask(taskId: string): TaskGraphNodeAttributes | undefined` — returns node attributes or undefined
- [x] Unit tests: cycle detection on known cyclic/acyclic graphs, topologicalOrder on DAG, topologicalOrder throws on cyclic graph, dependency/dependent queries
## References
@@ -46,8 +46,11 @@ Per [errors-validation.md](../../../docs/architecture/errors-validation.md):
## Notes
> To be filled by implementation agent
findCycles uses a custom 3-color DFS (WHITE/GREY/BLACK) as specified — graphology-components only gives SCCs, not cycle paths. The DFS traces the recursion stack on back edges to extract ordered cycle paths.
## Summary
> To be filled on completion
Implemented all 7 query methods in `src/graph/queries.ts` as free functions operating on the inner graphology graph, and integrated them as instance methods on `TaskGraph`.
- Created: `src/graph/queries.ts`, `test/queries.test.ts`
- Modified: `src/graph/construction.ts` (added query method imports and 7 instance methods)
- Tests: 45, all passing (full suite: 302 passing)