Let process steps run once per scenario (#252) - #327
Conversation
Processing work often needs to be run across a set of arguments rather
than once, which the process namespace had no way to express. A step can
now name an entry in the top-level `scenarios:` section, exactly as a
`simulate` entry already does, and runs once per scenario tuple.
Scenario values reach the step by substituting `{name}` placeholders in
its configuration, recursively through lists and mappings. Two details
are deliberate. Substitution replaces only the names the scenario
defines, rather than going through str.format, so a command like
`awk '{print $1}'` survives being templated; process steps are usually
shell commands, where braces are ordinary text. And a placeholder always
resolves to text, because a number substituted into a `list[str]` field
is rejected by the module's own validation, whereas text is coerced back
to whatever the field declares.
The fan-out happens within a plan step rather than by multiplying plan
entries, so #323's dependency contract is untouched: `depends` names
steps, and a dependent runs after every scenario of what it depends on
without needing per-scenario edges.
Scenario references are validated across the whole section before any
step runs, collecting every bad name at once, matching how `depends` is
handled.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 539186cd70
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| for name, replacement in values.items(): | ||
| value = value.replace("{" + name + "}", str(replacement)) |
There was a problem hiding this comment.
Substitute placeholders in one pass
When a string-valued scenario parameter contains another valid placeholder token, this loop substitutes that newly introduced token during a later iteration. For example, values {"first": "{second}", "second": "VALUE"} turn "{first}" into "VALUE" rather than preserving the actual first value, and reversing parameter declaration order changes the result. Match placeholders from the original string and replace them simultaneously so scenario values are not recursively reinterpreted.
Useful? React with 👍 / 👎.
|
one big picture note to try to address: the process and scenario link should be specified by a tag on scenario, rather than on process, so that the same process can be used for multiple scenarios. |
Closes #252.
What changed
A process step can now name an entry in the top-level
scenarios:section and runs once per scenario tuple, with that tuple's values substituted into its configuration:flepimop2 process -t analyzerunsfetchonce, thenanalyzetwice. Verified end to end through the CLI, not just at unit level.This follows the existing precedent rather than inventing one:
SimulateSpecificationModelalready has ascenariofield naming a key inscenarios:, andSimulateCommandalready loopsscenario_config.scenarios(). Process steps now do the same thing.Three decisions worth review
1. Fan-out happens inside a plan step, not by multiplying plan nodes.
dependsnames steps, so if a parameterized step became N nodes, a downstream step would need to say which scenario it depends on, which means per-scenario dependency edges and a much larger change to the #323 contract. Keeping it one node means a dependent runs after all of its dependency's scenarios, which is also the intuitive reading. #323's DAG is untouched, and there's a test pinning that.2. Substitution is a plain replacement of the scenario's own names, not
str.format. Process steps are usually shell commands, andawk '{print $1}'is ordinary text there. Going throughstr.formatwould raiseKeyErroron it. Only names the scenario defines are replaced; every other brace survives. Test included.3. A placeholder always resolves to text. I initially preserved the raw type when a value was exactly one placeholder, which reads nicer (
count: "{n}"staying anint). It's wrong: pydantic rejects anintinside alist[str], which is preciselyShellProcess.args, the most common field this will touch. It coerces the other direction happily ("3"into anintfield is anint), so text substitution is strictly safer and loses nothing. There's a test that builds the expanded configs into realShellProcessinstances so this cannot regress into a runtime failure.module,depends, andscenariodescribe the step rather than its work, so they are never rewritten.Validation
Scenario references are checked across the whole section before any step runs, collecting every bad name at once, matching
depends. Confirmed that a typo aborts before the dependency executes rather than partway through the pipeline.Verification
tests/process+tests/cliat 126 passing.ruff format,ruff check,mypy(139 files), and doctests all clean.main(85 failed / 498 passed) and are a local environment issue, not from this change.docs/guides/scenarios.md, which already ended with a process step consuming scenario output, so the per-scenario shape belongs alongside it.