Critical fixes: - Rename qualityDegradation → qualityRetention across all docs (semantically inverted: 0.9 meant 90% quality RETAINED, not 90% degradation). Updated schemas, graph-model, cost-benefit, ADRs. - Add TaskInput → TaskGraphNodeAttributes transformation section to graph-model.md, documenting how Nullable(Optional) input fields map to Optional graph attributes - Fix DuplicateEdgeError fields: source/target → prerequisite/dependent to match the established edge direction convention - Fix resolveDefaults signature: Partial<TaskGraphNodeAttributes> → Partial<...> & Pick<TaskGraphNodeAttributes, 'name'> to require the name field - Move Nullable helper definition before its first use in schemas.md - Fix 'construction never throws' contradiction: rephrase to 'construction enforces uniqueness, not data quality' - Define all 6 enum value sets in schemas.md (previously only TaskScope and TaskRisk were explicit) - Add EvConfig parameter table with defaults and semantics - Document WorkflowCostOptions.limit parameter - Add construction error handling table to graph-model.md - Add graph.raw mutation safety warning to api-surface.md - Update build-distribution.md error class list to include DuplicateNodeError and DuplicateEdgeError
9.0 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 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
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
Warning on graph.raw: Mutating the underlying graphology instance directly bypasses TaskGraph's validation and invariants. Operations that violate the "no parallel edges" constraint (adding a second edge between the same node pair without using addEdgeWithKey), or that create self-loops in a graph configured to disallow them, will not be caught by TaskGraph methods. Consumers using raw should treat the graph as read-only for structural changes and use TaskGraph methods for all mutations.
Notes:
topologicalOrder()throwsCircularDependencyError(withcyclespopulated) when cyclic — see ADR-003subgraph()returns a newTaskGraphwith matching nodes and only edges where both endpoints are in the filtered set — see ADR-007addDependencyusesaddEdgeWithKeywith deterministic keys (${source}->${target}) — see ADR-006addTaskthrowsDuplicateNodeErrorif the ID already exists,addDependencythrowsDuplicateEdgeErrorif the edge already exists, andTaskNotFoundErrorif either endpoint doesn't exist in the graph — see 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
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
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: TakesTaskGraphNodeAttributes(nullable categorical fields) and internally callsresolveDefaultsforriskandscope. 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.
Note on
workflowCostvscalculateTaskEv:calculateTaskEvis a pure math function (takes numeric inputs, returnsEvResult).workflowCostorchestrates the per-task calls, handles DAG propagation, and enriches results withtaskIdandnamefrom the graph's node attributes. The per-taskEvResultis a subset ofWorkflowCostResult.tasks[i].
Categorical enum numeric methods
function scopeCostEstimate(scope: TaskScope): number // 1.0–5.0
function scopeTokenEstimate(scope: TaskScope): number // 500–10000
function riskSuccessProbability(risk: TaskRisk): number // 0.50–0.98
function riskWeight(risk: TaskRisk): number // 0.02–0.50
function impactWeight(impact: TaskImpact): number // 1.0–3.0
function resolveDefaults(attrs: Partial<TaskGraphNodeAttributes>): ResolvedTaskAttributes
Cost-benefit core
function calculateTaskEv(p: number, scopeCost: number, impactWeight: number, config?: EvConfig): EvResult
See 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 for the full naming convention.
RiskPathResult
const RiskPathResult = Type.Object({
path: Type.Array(Type.String()),
totalRisk: Type.Number(),
})
DecomposeResult
const DecomposeResult = Type.Object({
shouldDecompose: Type.Boolean(),
reasons: Type.Array(Type.String()),
})
WorkflowCostOptions
const WorkflowCostOptions = Type.Object({
includeCompleted: Type.Optional(Type.Boolean()), // default: false. When false, completed tasks are excluded from output but remain in propagation with p=1.0
limit: Type.Optional(Type.Number()), // default: no limit. If set, limits the number of tasks in the result. Useful for large graphs when only top-N results are needed.
propagationMode: Type.Optional(
Type.Union([Type.Literal("independent"), Type.Literal("dag-propagate")])
),
defaultQualityRetention: Type.Optional(Type.Number()), // default: 0.9. Edge quality when not explicitly set.
})
WorkflowCostResult
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
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
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.
Validation API
// 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 for error types and validation details.
Constraints
- No write actions in analysis functions — all analysis functions are pure reads.
shouldDecomposeTaskonly inspects attributes, it doesn't modify the graph. - throw-on-cycle for topo sort —
topologicalOrderthrows rather than returning a partial result. See ADR-003. - Analysis functions are independent — they can be called in any order, without prerequisites beyond a valid graph.