Skip to content

One run, one answer about itself (#104) - #454

Merged
jeremymanning merged 1 commit into
mainfrom
feat/runtime-context-main
Aug 3, 2026
Merged

One run, one answer about itself (#104)#454
jeremymanning merged 1 commit into
mainfrom
feat/runtime-context-main

Conversation

@jeremymanning

Copy link
Copy Markdown
Member

Reopens #452 against main. #452 was merged into fix/global-call-validation — my own stacked base, which main had already taken — so it reported "merged" while its code never reached main. Same commit, correct base, re-verified on main as it now stands.

The defect, measured

Seven sites built execution in four timestamp formats, plus an eighth field set (hybrid._get_execution_metadata also emitted iso_timestamp, pipeline_id, execution_id).

Worse than cross-engine disagreement: _execute_level rebuilt the dict at every level of the graph, overwriting what the run had registered. One run, two steps:

step one   ->   2026-08-02T20:01:55.182681
step two   ->   2026-08-02T20:01:55.184368

Anything naming an output file after the timestamp wrote several. Now both steps report the same instant.

Two corrections to my earlier diagnosis

My #448 note said execution was never populated. It is. And "reject pipeline/context/env" was already the behaviour — the data-flow validator accepted them, but the template validator rejected the base name, so pipelines were refused anyway. That permissiveness was invisible, not harmless.

The live defect was the opposite: execution.timestamp ran correctly and failed validation, across 59 references in 32 files.

expression validate (before → after) run
execution.timestamp rejected → accepted
execution.id / started_at / date / time rejected → accepted
execution.strated_at, execution.bogus rejected → rejected
pipeline.name, context.foo, env.HOME rejected → rejected

validate and run now agree on every case.

One deviation, taken deliberately

The specified schema was id/started_at/timestamp. I also expose date and time, because both work today and two catalogue files use date — dropping them would remove working behaviour inside a hardening change. Everything else is refused. Two-line change plus two example migrations if you'd rather they go.

Measured on main

main this PR
catalogue validating 20 / 117 30 / 117
blocking suite 670 passed 696 passed, 0 failed

Mutations — five, all killed

mutation tests that failed
rebuild the namespace per level the stability test
restore the wildcard namespace accept 7
accept any execution field 3
template validator forgets the namespace 5
local time instead of UTC 1

The wildcard mutation survived twice before it spoke. First because every end-to-end test rejects pipeline.name via the template validator whether or not the data-flow validator accepts it — so the line I changed had no test at all, which is exactly why that permissiveness survived this long. Then because my first mutation wasn't a faithful inverse: it routed the phantom namespaces through the field check, which still rejected them. Four direct unit tests now hold that line.

Two other self-inflicted problems caught before commit: caching the RuntimeContext object broke every checkpointed run (not JSON serializable), and the generated doc embedded a live clock, so its own --check test would have failed on every regeneration.

🤖 Generated with Claude Code

`{{ execution.timestamp }}` had seven implementations in four formats:

    orchestrator.py:307              %Y-%m-%d-%H:%M:%S
    orchestrator.py:1424, :1994      .isoformat()
    control_system.py:222            %Y-%m-%d %H:%M:%S
    hybrid_control_system.py:594     %Y-%m-%d %H:%M:%S
    hybrid_control_system.py:325     %Y-%m-%dT%H:%M:%S, plus iso_timestamp,
                                     pipeline_id and execution_id
    declarative_engine.py:121        no timestamp at all -- start_time

They did not merely disagree between engines. `_execute_level` rebuilt the
dict at every level of the graph, overwriting what the run had registered,
so one run answered its own question differently each time:

    step one   ->   2026-08-02T20:01:55.182681
    step two   ->   2026-08-02T20:01:55.184368

Anything naming an output file after the timestamp wrote several.

Meanwhile `validate` rejected the expression outright -- 59 references
across 32 catalogue pipelines, every one of which ran correctly and failed
validation. My earlier note called `execution` unpopulated; it is populated,
and that was wrong.

The namespace was both too permissive and too strict, in different
validators. The data-flow validator accepted `execution.anything` plus
`pipeline`, `context` and `env`, which nothing populates; the template
validator rejected the base name, so pipelines were refused anyway and the
permissiveness sat unnoticed. Both now read one schema.

    execution.id           run-4f2a91c07e3b
    execution.started_at   2026-01-15T14:30:45+00:00
    execution.timestamp    (the same instant under its older name)
    execution.date         2026-01-15
    execution.time         14:30:45

`date` and `time` are beyond the three fields the review specified. They
work today and two catalogue files use `date`, so dropping them would have
removed working behaviour from a hardening change. Everything else is
refused: `{{ execution.strated_at }}` is a typo, not a field, and an open
namespace would render it as an empty string and report success.

UTC, so stamps from two machines compare and a run spanning a
daylight-saving change does not go backwards.

The cached value is a plain dict: caching the `RuntimeContext` object made
every checkpointed run fail with "Object of type RuntimeContext is not JSON
serializable".

Measured: catalogue validating 20 -> 30 of 117, the largest jump so far;
blocking suite 670 -> 696 passed, 0 failed.

Five mutations, all killed:
  - rebuild the namespace per level          -> the stability test
  - restore the wildcard namespace accept    -> 7 tests
  - accept any execution field               -> 3 tests
  - template validator forgets the namespace -> 5 tests
  - local time instead of UTC                -> 1 test

The wildcard mutation survived twice before it spoke. First because every
end-to-end test rejects `pipeline.name` through the *template* validator
whether or not the data-flow validator accepts it -- so the line I changed
had no test at all, which is exactly why the permissiveness survived this
long. Then because my first attempt at the mutation was not a faithful
inverse: it routed the phantom namespaces through the field check, which
still rejected them. The direct unit tests are the ones that hold it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant