generated from coulomb/repo-seed
Implemented foundation of task-flow-engine
This commit is contained in:
200
docs/task-flow-engine-spec.md
Normal file
200
docs/task-flow-engine-spec.md
Normal file
@@ -0,0 +1,200 @@
|
||||
---
|
||||
id: task-flow-engine-spec
|
||||
type: design-spec
|
||||
title: "Task Flow Engine Specification"
|
||||
status: draft
|
||||
created: "2026-05-01"
|
||||
updated: "2026-05-01"
|
||||
---
|
||||
|
||||
# Task Flow Engine Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
The task flow engine is a lightweight, declarative workflow substrate for
|
||||
information objects that move through named workstations. It replaces local
|
||||
status enums and hardcoded transition tables with pure assertions over an
|
||||
object's observable properties.
|
||||
|
||||
The engine is intentionally small: it receives a plain dictionary plus a flow
|
||||
definition, evaluates assertions, and returns a machine-readable result. It
|
||||
does not know about SQLAlchemy, FastAPI, State Hub routers, or Custodian domain
|
||||
rules.
|
||||
|
||||
## Core Terms
|
||||
|
||||
### Information Object
|
||||
|
||||
An information object is any entity with:
|
||||
|
||||
- a current workstation label, usually exposed as `workstation` or `status`
|
||||
- a bag of observable properties
|
||||
- optional nested collections of related entities
|
||||
|
||||
Examples include workstreams, tasks, contributions, capability requests, and
|
||||
future interface changes. The engine treats all of them as plain dictionaries.
|
||||
|
||||
### WorkstationDef
|
||||
|
||||
A workstation is a named position an information object can occupy.
|
||||
|
||||
```yaml
|
||||
name: active
|
||||
description: Work is underway.
|
||||
entry_assertions: []
|
||||
exit_assertions:
|
||||
- id: tasks.all_done
|
||||
target: tasks.*.status
|
||||
op: all_eq
|
||||
value: [done, cancelled]
|
||||
description: All child tasks are done or cancelled.
|
||||
```
|
||||
|
||||
Schema:
|
||||
|
||||
- `name: str`
|
||||
- `entry_assertions: list[AssertionDef]`
|
||||
- `exit_assertions: list[AssertionDef]`
|
||||
- `description: str`
|
||||
|
||||
A workstation with no entry assertions is always reachable. A workstation with
|
||||
no exit assertions is always exitable.
|
||||
|
||||
### AssertionDef
|
||||
|
||||
An assertion is a pure predicate over object data.
|
||||
|
||||
Schema:
|
||||
|
||||
- `id: str`
|
||||
- `target: str`
|
||||
- `op: str`
|
||||
- `value: Any`
|
||||
- `description: str`
|
||||
|
||||
The `target` is a dot path into the information object. It supports normal dict
|
||||
and attribute traversal plus `*` for collection expansion:
|
||||
|
||||
- `tasks.*.status`
|
||||
- `dependencies.all.workstation`
|
||||
- `metadata.approved_by`
|
||||
|
||||
The built-in operations are:
|
||||
|
||||
- `all_eq`: every resolved value equals the expected value, or is included in
|
||||
the expected list
|
||||
- `any_eq`: at least one resolved value equals the expected value, or is
|
||||
included in the expected list
|
||||
- `none_eq`: no resolved values equal the expected value, or are included in
|
||||
the expected list
|
||||
- `exists`: at least one non-empty value resolves
|
||||
- `count_gte`: the number of resolved values is greater than or equal to the
|
||||
expected integer
|
||||
- `custom`: delegates evaluation to a host-injected callable
|
||||
|
||||
Assertions never mutate state.
|
||||
|
||||
### FlowDef
|
||||
|
||||
A flow definition is a named workstation graph for one entity type.
|
||||
|
||||
Schema:
|
||||
|
||||
- `id: str`
|
||||
- `entity_type: str`
|
||||
- `workstations: list[WorkstationDef]`
|
||||
|
||||
Multiple flows may exist for the same entity type, for example a lightweight
|
||||
workstream flow and a governance-heavy workstream flow.
|
||||
|
||||
### Transition
|
||||
|
||||
Transition is not a first-class model. The engine derives reachable
|
||||
workstations by evaluating every workstation's entry assertions against the
|
||||
current object state. If the assertions for a target workstation are satisfied,
|
||||
that workstation is reachable from the current workstation.
|
||||
|
||||
The current workstation's exit assertions determine whether the object is
|
||||
blocked where it is. Unsatisfied exit assertions become blocking reasons.
|
||||
|
||||
### FlowResult
|
||||
|
||||
Evaluation returns:
|
||||
|
||||
```yaml
|
||||
current_workstation: active
|
||||
exit_blocked: true
|
||||
blocking_assertions:
|
||||
- id: tasks.all_done
|
||||
passed: false
|
||||
reason: "Expected all values at tasks.*.status to be in ['done', 'cancelled']; got ['done', 'todo']."
|
||||
reachable:
|
||||
- todo
|
||||
- active
|
||||
unreachable:
|
||||
- workstation: completed
|
||||
blocking:
|
||||
id: tasks.all_done
|
||||
passed: false
|
||||
reason: "Expected all values at tasks.*.status to be in ['done', 'cancelled']; got ['done', 'todo']."
|
||||
```
|
||||
|
||||
Schema:
|
||||
|
||||
- `current_workstation: str`
|
||||
- `exit_blocked: bool`
|
||||
- `blocking_assertions: list[AssertionResult]`
|
||||
- `reachable: list[str]`
|
||||
- `unreachable: list[UnreachableWorkstation]`
|
||||
|
||||
## Expressiveness Across Existing Entities
|
||||
|
||||
### Workstreams
|
||||
|
||||
Workstreams can express readiness for completion by asserting that child tasks
|
||||
are `done` or `cancelled`. They can express dependency blocking by checking that
|
||||
all dependency workstreams have reached `completed`.
|
||||
|
||||
### Tasks
|
||||
|
||||
Tasks can express human intervention with the existing `needs_human` flag.
|
||||
Returning from `blocked` to `in_progress` is an entry assertion over that same
|
||||
flag. Lightweight completion remains unconstrained because curator intent is
|
||||
the deciding signal.
|
||||
|
||||
### Contributions
|
||||
|
||||
Contributions can reproduce the current draft, submitted, acknowledged,
|
||||
accepted, merged, rejected, and withdrawn lifecycle by giving each workstation
|
||||
entry assertions that describe which previous statuses may enter it. This keeps
|
||||
the current lifecycle readable without baking domain transitions into engine
|
||||
code.
|
||||
|
||||
### Capability Requests
|
||||
|
||||
Capability requests can reproduce the existing requested, routing disputed,
|
||||
accepted, in progress, ready for review, completed, rejected, and withdrawn
|
||||
lifecycle the same way. Host-specific effects such as notifications remain in
|
||||
the State Hub router; the flow engine only answers whether the target
|
||||
workstation is reachable.
|
||||
|
||||
## Host Boundary
|
||||
|
||||
The engine owns:
|
||||
|
||||
- dataclasses for flow definitions and results
|
||||
- target path resolution
|
||||
- built-in predicate evaluation
|
||||
- host-injected custom predicate dispatch
|
||||
- reachable and blocked derivation
|
||||
|
||||
State Hub owns:
|
||||
|
||||
- loading domain-specific YAML flow definitions
|
||||
- converting ORM entities into plain dictionaries
|
||||
- migrations from enum-backed status fields to strings
|
||||
- router side effects such as timestamps and notifications
|
||||
- MCP tools and user-facing explanations
|
||||
|
||||
This boundary keeps the first implementation extractable into a standalone
|
||||
`task-flow-engine` package once the API stabilizes.
|
||||
Reference in New Issue
Block a user