A task's run entry is either a plain string or exactly one table shape, { split = "...", on = "..." }. RunArg in crates/devkit-config/src/lib.rs hardcodes that table as the table an entry can be, so split is not one action among several, it is the only one the type admits. Adding a second means bolting on another untagged variant and hoping the key sets stay disjoint, and the derived JSON Schema gains another anonymous anyOf branch with no shared name for "an action entry".
Generalize the type so split becomes one member of a named action set.
Shape
#[serde(untagged, deny_unknown_fields)]
pub enum Arg {
Scalar(String),
Split { split: String, on: String },
// room for join, default, optional
}
Constraints that carry over:
- Untagged plus
deny_unknown_fields, for the reason already recorded on the derive: without it a misspelled key beside a well-formed pair deserializes silently and the typo changes nothing the author can see. With more variants in the union this matters more, not less.
template() stays the one accessor for the minijinja source, so port scanning and variable discovery never learn the variant list.
- The schema is derived, so
devkit schema picks the new shape up on its own. schema/devkit-config.json is regenerated with DEVKIT_UPDATE_SCHEMA=1 cargo test, and the drift test keeps it honest.
Fields
tasks.<name>.run is the only field typed on this entry today, so it is the only one that changes. If the type has been accepted anywhere else by the time this lands, every such field moves with it rather than being left on the old shape.
Call sites
crates/devkit-ports/src/task.rs: resolution, argv assembly, and the two shape checks (the program must be a plain string; a split must be last or followed only by splits).
crates/devkit-ports/src/guard/mod.rs: configured_task, which reads a task's leading static words by finding the first non-scalar entry.
crates/devkit-ports/src/guard/tasks.rs: template collection.
docs/configuration.md: the task run section, which documents the split table and its shape rules.
Guard and resolution both match on the variant rather than on the rendered template, so every new action is a compile error at each site. That is the design, keep it: no _ => arm.
Not in this change
Implementing join, default or optional. They are what motivates the shape, but each one is its own semantics question. This issue is the change that makes them cheap to add.
A task's
runentry is either a plain string or exactly one table shape,{ split = "...", on = "..." }.RunArgincrates/devkit-config/src/lib.rshardcodes that table as the table an entry can be, sosplitis not one action among several, it is the only one the type admits. Adding a second means bolting on another untagged variant and hoping the key sets stay disjoint, and the derived JSON Schema gains another anonymousanyOfbranch with no shared name for "an action entry".Generalize the type so
splitbecomes one member of a named action set.Shape
Constraints that carry over:
deny_unknown_fields, for the reason already recorded on the derive: without it a misspelled key beside a well-formed pair deserializes silently and the typo changes nothing the author can see. With more variants in the union this matters more, not less.template()stays the one accessor for the minijinja source, so port scanning and variable discovery never learn the variant list.devkit schemapicks the new shape up on its own.schema/devkit-config.jsonis regenerated withDEVKIT_UPDATE_SCHEMA=1 cargo test, and the drift test keeps it honest.Fields
tasks.<name>.runis the only field typed on this entry today, so it is the only one that changes. If the type has been accepted anywhere else by the time this lands, every such field moves with it rather than being left on the old shape.Call sites
crates/devkit-ports/src/task.rs: resolution, argv assembly, and the two shape checks (the program must be a plain string; a split must be last or followed only by splits).crates/devkit-ports/src/guard/mod.rs:configured_task, which reads a task's leading static words by finding the first non-scalar entry.crates/devkit-ports/src/guard/tasks.rs: template collection.docs/configuration.md: the taskrunsection, which documents the split table and its shape rules.Guard and resolution both match on the variant rather than on the rendered template, so every new action is a compile error at each site. That is the design, keep it: no
_ =>arm.Not in this change
Implementing
join,defaultoroptional. They are what motivates the shape, but each one is its own semantics question. This issue is the change that makes them cheap to add.