# 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; } ``` - 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 1–4 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)