Files
poimen/tasks/T3.2-workflowformat-trait-yaml-and-json.md
T

96 lines
3.9 KiB
Markdown
Raw Normal View History

2026-08-17 23:05:20 -07:00
# T3.2 — `WorkflowFormat` trait + YAML and JSON
| Field | Value |
|---|---|
| Phase | P3 — Workflow as data |
| Size | M — 1 to 3 days |
| Status | Not started |
| Flags | — |
| Spec | inlined below |
| Blocks | — |
## Goal
Formats are plugins. Ship YAML and JSON; prove a third format needs no kernel
change.
## Facts (inlined — no spec read needed)
```rust
pub trait WorkflowFormat: Send + Sync {
fn extensions(&self) -> &[&str];
fn parse(&self, src: &[u8]) -> Result<WorkflowDef, ParseError>;
}
```
- A user wanting a DSL, Starlark, or a database row implements the trait.
- **Validation and canonicalization live in the kernel and run on the IR** (T3.1,
T3.3), so a new format inherits every check without reimplementing one. A
format that does its own validation has forked the rules.
- Because the version hash is over the canonical IR, the same workflow in two
formats must produce the same `WorkflowVersion` — that equality is the test
that the boundary is in the right place.
- Defaults ship working; every default is a port. A user who wants built-in
behaviour writes no code; a user who wants their own writes an impl, not a fork.
## Steps
1. Define the trait exactly as above. `parse` returns the IR and nothing else —
no side effects, no registration, no validation.
2. Implement `YamlFormat` and `JsonFormat` over `serde`. Both target the same IR
types from T3.1.
3. Build the format registry keyed by extension. Resolution is by extension, with
a clear error when two formats claim the same one.
4. Make `ParseError` carry position information where the format supplies it —
this is the only error type that legitimately knows about source text.
5. Write the same workflow in YAML and in JSON; assert equal `WorkflowVersion`.
6. In a **separate test crate**, implement a third trivial format (for example
TOML or a line-based DSL) against the public API only. If it needs anything
`pub(crate)`, the trait is short — fix the trait, not the test.
## Acceptance
- The same workflow expressed in both formats produces the same
`WorkflowVersion`.
- A third format added in a test crate needs **no kernel change**.
## Verify
**Harness:** a **separate test crate outside the workspace** depending only on
the published API — that crate's existence is the deliverable, not a convenience.
**Integration test**`tests/it_format_equivalence.rs` plus
`third-format-crate/`:
1. Express one non-trivial workflow (all five `StepKind`s) in YAML and in JSON.
2. Parse both; assert identical `WorkflowVersion`.
3. Assert the parsed IRs are byte-identical after canonicalization.
4. In the external crate, implement a third format (TOML or a line DSL) against
the public API only. Build it. If it needs any `pub(crate)` item, the trait is
short — that is a finding, not a test workaround.
5. Register the third format and run the **same** validation suite (T3.3) against
it; assert it inherits every check with no new code.
6. Extension collision: register two formats claiming `.yaml`; assert a clear
error rather than last-one-wins.
**Command:** `cargo test -p workflow format_equivalence && cargo test --manifest-path third-format-crate/Cargo.toml`
**False pass:**
- The third-format crate living inside the workspace, where it can reach
internals. It then passes while the public API is unusable — which is the
entire thing being tested.
- Step 5 omitted: a format that does its own validation passes steps 14 and has
quietly forked the rules.
## Traps
- Format-specific validation creeping into `parse`. Two formats then disagree
about what is legal.
- Leaking `serde` types into the IR, which makes the third-format test need a
serde dependency it should not have.
---
Background (not required to do this task):
[rust-agentic-sys.md](../../../rust-agentic-sys.md) §1, §4.2 ·
[rust-agentic-task.md](../../../rust-agentic-task.md)