As I was thinking about writing actual files for the task runner, I decided I wanted to make some changes to the API. Those changes have knock-on effects that need to be carefully reasoned through.
Basically, the current interface uses fluent methods on an immutable Registry object to incrementally build up a configuration. This is somewhat of a mismatch, because the decision of "what to add" should be independent of "what's in there", so extension packages are burdened by the need to take a Registry as input. They are further burdened by the fact that the fluency constrains the output type: if a helper function wants to output additional target or action information, then its output needs to be explicitly unpacked, which breaks up the fluent chain.
I believe these issues could be addressed by changing from "here is a method on a Registry object" to "here is an object that describes a change to a Registry". This means that, instead of a fluent chain, the means of change is a sequence of change objects, which can be manipulated as data. Instead of chaining methods, just concatenate sequences.
The question this raises for me is, how free should extension packages be to customize the behavior of change objects? The most general interface is Callable[[Registry], Registry], but doing this would discard guarantees like "the output is (loosely speaking) a 'superset' of the input". That guarantee could be reclaimed by creating some kind of "delta" object that must be merged with the Registry. This all seems kind of complicated, though. The easiest sufficient solution is to convert each method to a type, then dispatch from the type to the underlying method.
Thinking about this suggested some invariants that I might want to code explicit invariants for, once I have proper profiling data:
- Targets and Actions (at least, those reachable from named Targets) form a DAG
- Action names are unique (for this, I may switch the Action and ActionName types in the definition of Registry.)
- TargetNames that are skipped by default are all mapped to at least one Target. (I could see downgrading this to a warning.)
Thinking about this, the two things to definitely try are to swap Action and ActionName, and to make a simple external type so I can work on deprecating the fluent interface.
I may try moving validator logic out of the update methods, but I don't want to mess with things that have obvious performance implications before I have performance testing that I somewhat trust, and that'll probably be easier to handle once I nail down the interface, so I don't end up needing to rewrite the benchmark.
None of that is happening tonight, however; this is all planning for the next time I start changing around the code.