Files
poimen/tasks/T3.1-workflowdef-ir-canonicalization.md

126 lines
4.9 KiB
Markdown
Raw Permalink 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.
# T3.1 — `WorkflowDef` IR + canonicalization
| Field | Value |
|---|---|
| Phase | P3 — Workflow as data |
| Size | L — over 3 days |
| Status | Not started |
| Flags | — |
| Spec | inlined below |
| Blocks | T3.2T3.6 |
## Goal
The validated intermediate representation every workflow format parses into, and
the canonicalization that makes `WorkflowVersion` a content hash of *meaning*
rather than of source text.
## Facts (inlined — no spec read needed)
```rust
pub struct WorkflowDef {
pub id: WorkflowId,
pub schema: SchemaVersion,
pub steps: Vec<StepDef>,
pub transitions: Vec<Transition>,
pub rubric: RubricDef,
pub budget: BudgetDef,
}
pub struct StepDef {
/// Author-assigned, stable across versions.
pub id: StepId,
pub kind: StepKind,
pub tools: ToolSelector,
pub verify: Vec<VerifierRef>,
pub retry: RetryPolicy,
pub timeout: Duration,
}
pub enum StepKind {
Model { prompt: PromptTemplate, effort: ReasoningEffort },
Tool { tool: ToolId, args: ArgTemplate },
Parallel { branches: Vec<StepId>, join: JoinPolicy },
Conditional { on: Predicate, then: StepId, otherwise: Option<StepId> },
SubWorkflow { workflow: WorkflowId, version: VersionSelector },
}
```
- **`WorkflowVersion` is the Blake3 hash of the canonicalized IR, not of the
source text.** Two YAML files differing only in key order produce the same
version — which is what makes "did this change affect results" answerable.
- Validation and canonicalization live in the **kernel** and run on the IR, so a
new format (T3.2) inherits every check without reimplementing one.
- Domain states are open: a workflow declares its own step names, ordering and
transitions as data. The kernel validates the declaration, then executes it.
Adding a domain state is a config change, never a recompile.
- Versions form a DAG: content-addressed, parent-pointered, **never edited**.
Editing a version in place destroys every result already attributed to it.
## Steps
1. Define the IR types above. Keep them free of parser concerns — no source
spans, no format-specific fields.
2. Write the canonicalizer: sort every unordered collection by a defined key,
normalize whitespace inside templates only where semantically irrelevant,
drop optional fields that equal their default, and emit a deterministic byte
encoding.
3. Hash the canonical bytes with Blake3 into `WorkflowVersion`.
4. Add the parent pointer: a new version records its parent's hash. Nothing
mutates an existing version record.
5. Decide and document what is *not* canonicalized — prompt template text is
semantic and must hash as written. Put that decision in a comment at the
canonicalizer, since the next person will otherwise "improve" it.
6. Test: build two structurally identical IRs from different field orders and
assert equal hashes; change one prompt character and assert different hashes.
## Acceptance
- Two YAML files differing only in key order and whitespace produce identical
`WorkflowVersion`.
## Verify
**Harness:** pairs of source files that differ only in ways that must not matter,
and pairs that differ in ways that must.
**Integration test**`tests/it_canonical_version.rs`:
1. **Must match:** two YAML files with reordered top-level keys, reordered map
entries, different indentation, and trailing whitespace. Assert identical
`WorkflowVersion`.
2. **Must differ:** change one character inside a prompt template. Assert a
different version. This is the boundary — prompt text is semantic.
3. **Must differ:** change a `timeout`, a `retry` count, a `StepId`. One test per
field, table-driven, so a canonicalizer that drops a field is caught by the
field it drops.
4. Round trip: canonicalize twice; assert the byte output is stable.
5. Parent pointer: build v2 from v1; assert v2 records v1's hash and that v1's
record is unchanged on disk.
6. Property test: generate random IRs, permute the order of every unordered
collection, assert hash invariance.
**Command:** `cargo test -p workflow canonical`
**False pass:**
- Only testing step 1. A canonicalizer that discards fields it does not
understand passes every "these should match" test and silently makes two
different workflows the same version. Step 3's per-field table is the guard.
- Hashing the source bytes, which passes step 2 and fails step 1 — check both
directions.
- A property test that permutes only the collection the implementation already
sorts.
## Traps
- Hashing the source bytes. It is one line simpler and makes every reformat look
like a behaviour change.
- Canonicalizing prompt text. A whitespace change inside a prompt *is* a
behaviour change.
- Allowing a version record to be updated in place.
---
Background (not required to do this task):
[rust-agentic-sys.md](../../../rust-agentic-sys.md) §2, §4.1, §12.2 ·
[rust-agentic-task.md](../../../rust-agentic-task.md)