96 lines
3.9 KiB
Markdown
96 lines
3.9 KiB
Markdown
# 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 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)
|