Files
taskgraph_ts/docs/architecture/api-surface.md
glm-5.1 ac9dee9c10 Fix TypeBox schema consistency and add TypeBox patterns research
- schemas.md: Replace interface ResolvedTaskAttributes with TypeBox schema
  + Static<typeof> derivation (was the only raw interface in the doc set)
- schemas.md: Add explicit TypeBox naming convention table and pattern guide
- schemas.md: Use Nullable() helper for TaskInput optional categorical fields
  that can be explicitly set to null in YAML
- schemas.md: Reference typebox-patterns.md research for full analysis
- api-surface.md: Add note about Static<typeof> pattern consistency
- errors-validation.md: Use Value.Errors() for structured validation instead
  of bare Value.Check()
- New: docs/research/typebox-patterns.md — comprehensive TypeBox pattern
  evaluation covering Static, Values, Convert, Pointer, TemplateLiteral,
  generics, defaults, and concrete schema recommendations
2026-04-26 07:57:23 +00:00

215 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
status: draft
last_updated: 2026-04-26
---
# API Surface
The library's public API: a thin `TaskGraph` data class for graph construction/mutation/basic queries, plus standalone composable analysis functions.
## Design Principle: Decomposition over Monolith
The `TaskGraph` class handles **graph construction, mutation, and basic queries only**. All analysis functions (parallel groups, critical path, cost-benefit, etc.) are standalone functions that take a `TaskGraph` as their first argument.
**Why**: Both consumers (alkhub, OpenCode plugin) need the same analysis functions but through different dispatch mechanisms. The library exports pure functions; each consumer wraps them in its own dispatch. This avoids duplicate work and prevents the class from becoming a 25+ method monolith.
> The operations/dispatch pattern belongs at the consumer layer, not the library layer. The library is a toolkit, not a service.
## TaskGraph Class
```typescript
class TaskGraph {
// Construction
static fromTasks(tasks: TaskInput[]): TaskGraph
static fromRecords(tasks: TaskInput[], edges: DependencyEdge[]): TaskGraph
static fromJSON(data: TaskGraphSerialized): TaskGraph
addTask(id: string, attributes: TaskGraphNodeAttributes): void
addDependency(prerequisite: string, dependent: string): void
// Mutation
removeTask(id: string): void
removeDependency(prerequisite: string, dependent: string): void
updateTask(id: string, attributes: Partial<TaskGraphNodeAttributes>): void
updateEdgeAttributes(prerequisite: string, dependent: string, attrs: Partial<TaskGraphEdgeAttributes>): void
// Queries
hasCycles(): boolean
findCycles(): string[][]
topologicalOrder(): string[] // throws CircularDependencyError if cyclic
dependencies(taskId: string): string[]
dependents(taskId: string): string[]
taskCount(): number
getTask(taskId: string): TaskGraphNodeAttributes | undefined
// Subgraph
subgraph(filter: (taskId: string, attrs: TaskGraphNodeAttributes) => boolean): TaskGraph
// Export
export(): TaskGraphSerialized
toJSON(): TaskGraphSerialized
// Reactivity
get raw(): Graph // underlying graphology instance for direct event listener attachment
}
```
**Notes**:
- `topologicalOrder()` throws `CircularDependencyError` (with `cycles` populated) when cyclic — see [ADR-003](decisions/003-topo-order-throws-on-cycle.md)
- `subgraph()` returns a new `TaskGraph` with matching nodes and only edges where both endpoints are in the filtered set — see [ADR-007](decisions/007-subgraph-internal-only.md)
- `addDependency` uses `addEdgeWithKey` with deterministic keys (`${source}->${target}`) — see [ADR-006](decisions/006-deterministic-edge-keys.md)
- `addTask` throws `DuplicateNodeError` if the ID already exists, `addDependency` throws `DuplicateEdgeError` if the edge already exists, and `TaskNotFoundError` if either endpoint doesn't exist in the graph — see [errors-validation.md](errors-validation.md)
## Standalone Analysis Functions
All analysis functions take a `TaskGraph` (or its raw graphology `Graph`) as their first argument. They are composable and stateless.
### Graph analysis
```typescript
function parallelGroups(graph: TaskGraph): string[][]
function criticalPath(graph: TaskGraph): string[]
function weightedCriticalPath(graph: TaskGraph, weightFn: (taskId: string, attrs: TaskGraphNodeAttributes) => number): string[]
function bottlenecks(graph: TaskGraph): Array<{ taskId: string; score: number }>
```
### Cost-benefit analysis
```typescript
function riskPath(graph: TaskGraph): RiskPathResult
function shouldDecomposeTask(attrs: TaskGraphNodeAttributes): DecomposeResult
function workflowCost(graph: TaskGraph, options?: WorkflowCostOptions): WorkflowCostResult
function riskDistribution(graph: TaskGraph): RiskDistributionResult
```
> **Note on `shouldDecomposeTask`**: Takes `TaskGraphNodeAttributes` (nullable categorical fields) and internally calls `resolveDefaults` for `risk` and `scope`. Unassessed fields (null) use defaults that are below the decomposition threshold, so only explicitly-assessed high-risk or broad-scope tasks are flagged. See [cost-benefit.md](cost-benefit.md).
> **Note on `workflowCost` vs `calculateTaskEv`**: `calculateTaskEv` is a pure math function (takes numeric inputs, returns `EvResult`). `workflowCost` orchestrates the per-task calls, handles DAG propagation, and enriches results with `taskId` and `name` from the graph's node attributes. The per-task `EvResult` is a subset of `WorkflowCostResult.tasks[i]`.
### Categorical enum numeric methods
```typescript
function scopeCostEstimate(scope: TaskScope): number // 1.05.0
function scopeTokenEstimate(scope: TaskScope): number // 50010000
function riskSuccessProbability(risk: TaskRisk): number // 0.500.98
function riskWeight(risk: TaskRisk): number // 0.020.50
function impactWeight(impact: TaskImpact): number // 1.03.0
function resolveDefaults(attrs: Partial<TaskGraphNodeAttributes>): ResolvedTaskAttributes
```
### Cost-benefit core
```typescript
function calculateTaskEv(p: number, scopeCost: number, impactWeight: number, config?: EvConfig): EvResult
```
> See [schemas.md](schemas.md) for the enum definitions and numeric mapping tables.
## Return Types
All return types are defined as TypeBox schemas (for runtime validation + JSON Schema export). The corresponding TypeScript types are derived via `Static<typeof Schema>` — no separate `interface` or `type` definitions. See [schemas.md](schemas.md) for the full naming convention.
### RiskPathResult
```typescript
const RiskPathResult = Type.Object({
path: Type.Array(Type.String()),
totalRisk: Type.Number(),
})
```
### DecomposeResult
```typescript
const DecomposeResult = Type.Object({
shouldDecompose: Type.Boolean(),
reasons: Type.Array(Type.String()),
})
```
### WorkflowCostOptions
```typescript
const WorkflowCostOptions = Type.Object({
includeCompleted: Type.Optional(Type.Boolean()),
limit: Type.Optional(Type.Number()),
propagationMode: Type.Optional(
Type.Union([Type.Literal("independent"), Type.Literal("dag-propagate")])
),
defaultQualityDegradation: Type.Optional(Type.Number()),
})
```
### WorkflowCostResult
```typescript
const WorkflowCostResult = Type.Object({
tasks: Type.Array(
Type.Object({
taskId: Type.String(),
name: Type.String(),
ev: Type.Number(),
pIntrinsic: Type.Number(),
pEffective: Type.Number(),
probability: Type.Number(),
scopeCost: Type.Number(),
impactWeight: Type.Number(),
})
),
totalEv: Type.Number(),
averageEv: Type.Number(),
propagationMode: Type.Union([
Type.Literal("independent"),
Type.Literal("dag-propagate"),
]),
})
```
### EvConfig / EvResult
```typescript
const EvConfig = Type.Object({
retries: Type.Optional(Type.Number()),
fallbackCost: Type.Optional(Type.Number()),
timeLost: Type.Optional(Type.Number()),
valueRate: Type.Optional(Type.Number()),
})
const EvResult = Type.Object({
ev: Type.Number(),
pSuccess: Type.Number(),
expectedRetries: Type.Number(),
})
```
### RiskDistributionResult
```typescript
const RiskDistributionResult = Type.Object({
trivial: Type.Array(Type.String()),
low: Type.Array(Type.String()),
medium: Type.Array(Type.String()),
high: Type.Array(Type.String()),
critical: Type.Array(Type.String()),
unspecified: Type.Array(Type.String()),
})
```
> Full schema definitions with Static type exports are in [schemas.md](schemas.md).
## Validation API
```typescript
// On TaskGraph instances:
validateSchema(): ValidationError[] // TypeBox validation on input data
validateGraph(): GraphValidationError[] // Graph-level invariants (cycles, dangling refs)
validate(): ValidationError[] // Both, for convenience
```
> See [errors-validation.md](errors-validation.md) for error types and validation details.
## Constraints
- **No write actions in analysis functions** — all analysis functions are pure reads. `shouldDecomposeTask` only inspects attributes, it doesn't modify the graph.
- **throw-on-cycle for topo sort** — `topologicalOrder` throws rather than returning a partial result. See [ADR-003](decisions/003-topo-order-throws-on-cycle.md).
- **Analysis functions are independent** — they can be called in any order, without prerequisites beyond a valid graph.