Traces the durable arc as one linked span graph - #48
Merged
Conversation
The campaign-026 capstone (se-opg). The whole durable arc - parent session, durable step, `start_child`, the child's own run, a timer firing, and the child's completion re-entering the parent - is now observable, and asserted, end to end. Not as one trace id. The bead asked for that first, and asking was the useful part: every seam in this arc is a deliberate root boundary in the accepted design, because each one can outlive the request that started it. A macrostep roots its own trace (ots-ADR-0003 decision 8 starts every span from a fresh context, so no host span can gather them), a child run is linked from its parent's step rather than parented (sp ADR-0008), and a fired timer is linked to the trace that armed it (sob-ADR-0006 decision 7). Ruling RQ-026-4 re-worded the criterion to the graph the design does produce: N roots joined by shared correlation ids and by link edges. Compile is outside it entirely - the block compiler emits no telemetry - so the arc starts at session start. `StatifierExamples.Charts.Tracing` attaches all three bridge halves and stamps `caller_context`, the one value no library can supply. `StatifierExamples.TraceCollector` collects real spans from the real SDK and renders the graph, walking link and correlation edges as well as parent edges - a renderer that only walked parents would draw a dozen unrelated stumps. `one_trace_test.exs` asserts each of the three edges, the one place two runs genuinely share a trace (the child's step nests inside the parent's, which is where `start_child` creates it), and the claim that matters: every stage is reachable from the parent's first step span. Nothing presses a button in the arc - fired timers carry it from the child's first rest to both terminal statuses. Two findings, neither worked around here: `statifier_persistence` documents that it emits the interpreter's family with `driver: :persistence`, and does not - nothing calls `Statifier.Telemetry` in its `lib/`. So a durable run produces no macrostep spans, and `timer.scheduled`, contracted to land on the session's open macrostep span, lands as a detached root instead. `caller_context` reaches no timer this app arms. Half is by design - the `:initialize` macrostep is stamped `nil` per st-ADR-0063 decision 3 - and half is an open question: the driver builds its own answer events for later turns of one drive and does not carry the driving event's stamp onto them. Three interim git pins, re-pin to Hex owed (se-a5y): statifier_persistence at 0749481 for ADR-0009's storage-phase telemetry, statifier_oban at 4bf3a7d for ADR-0006's events, opentelemetry_statifier at 99d7791 for the two sibling setup calls. The bridge brings only the API, so the SDK is named here. se-opg
johnnyt
force-pushed
the
se-opg-one-trace-capstone
branch
from
September 2, 2026 02:58
2727ba6 to
b52509a
Compare
johnnyt
marked this pull request as ready for review
September 2, 2026 02:58
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Provenance
Rebased onto
main@fe90e3aafter its parent PR #47(
se-6ag-durable-subchart-proof) landed. #47 was rebase-merged, so itsoriginal commits were orphaned under new SHAs on
main; this branch wasreplayed with
git rebase --onto origin/main fa4106b, which carries onlythis PR's own commit (
2727ba6->b52509a).Conflicts: none. The expected
mix.exs/mix.lockoverlap with #47did not materialise — #47's
statifier_persistencepin move landed in theshared ancestor
5a72cd5, so this branch's forward pins applied cleanly ontop. All four family pins verified in both
mix.exsandmix.lockafterthe rebase:
statifier_persistence0749481,statifier_oban4bf3a7d,opentelemetry_statifier99d7791,statifier_blocks05f0a4a(inherited, unchanged).
Gate on the rebased HEAD (
b52509a): green. Fullmix quality—format, compile (warnings as errors), deps, credo, dialyzer, 263/263 tests,
77.9% coverage.
The campaign-026 capstone (
se-opg). The whole durable arc — parentsession → durable step →
start_child→ child run → timer fire →completion re-entry — observable end to end, and asserted.
Not one trace id, and that is the finding
The bead asked for a single trace id spanning the arc. It cannot exist,
and asking for it was the useful part: every seam here is a deliberate
root boundary in the accepted design, because each one can outlive the
request that started it.
every span from a fresh context and never the ambient one, so no host
span can gather them;
ADR-0008, since parenthood would hold the parent's trace open for the
child's whole life;
decision 7, same reason across a longer gap.
Ruling RQ-026-4 adopted the linked-graph shape: N roots joined by
shared correlation ids and by link edges, with the child-start,
timer-fire and completion re-entry edges each visible. Stage 1 (compile)
is dropped from the arc —
StatifierBlocks.Compileremits no telemetry,so a compile has no span under any configuration.
Capture: 103 spans, 51 distinct trace ids. That is the design
working.
What is here
StatifierExamples.Charts.Tracing— attaches all three bridge halves(interpreter, persistence, Oban) and stamps
caller_context, the onevalue no library can supply, in W3C text form.
StatifierExamples.TraceCollector(test support) — collects real spansfrom the real SDK and renders the graph, walking link and correlation
edges as well as parent edges. A renderer that only walked parents would
draw a dozen unrelated stumps and hide the whole structure.
one_trace_test.exs— asserts each of the three edges; the one place tworuns genuinely share a trace (the child's step span nests inside the
parent's, which is where
start_childcreates it, per ots-ADR-0004'sbridge-owned nesting); and the claim that matters — every stage is
reachable from the parent's first step span. Nothing presses a button in
the arc: fired timers carry it from the child's first rest to both
terminal statuses.
Two findings, neither worked around here
1. A durable run produces no macrostep spans.
statifier_persistencedocuments (itsdocs/telemetry.md, citingst-ADR-0067) that it emits the interpreter's family with
driver: :persistenceby callingStatifier.Telemetryfrom the stepperseam. It does not — nothing calls that module anywhere in its
lib/.Only
Statifier.Sessionemits macrostep events, and the durable pathnever runs one. A visible consequence:
[:statifier_oban, :timer, :scheduled]is contracted to land as a span event on the session's openmacrostep span, and lands as a detached root because there is none.
2.
caller_contextreaches no timer this app arms. Every job rowstores
nil. Half is by design — the:initializemacrostep is stampednilper st-ADR-0063 decision 3, having no calling event. The other halfis an open question for statifier-ex/statifier_persistence: the driver
builds its own answer events for later turns of one drive and does not
carry the driving event's
caller_contextonto them, so a host cannotlink a timer armed anywhere but the first macrostep of an
externally-driven step, whatever it stamps.
Both are on the bead. Per this repo's CLAUDE.md, an upstream contract
problem is raised upstream rather than patched here.
Pins
All interim, re-pin to Hex owed (se-a5y):
statifier_persistence07494811416a7bstatifier_oban4bf3a7dopentelemetry_statifier99d7791statifier_blocksstays on the inherited05f0a4a.opentelemetryandopentelemetry_apiare new plain Hex requirements: the bridge bringsonly the API on purpose, so the host names the SDK and the exporter — and
this app's default exporter is
:none.mix_deps_test.exsasserts all three pins againstmix.exsandmix.lock, so none can be forgotten quietly.Verification
Full
mix qualitygreen: 263/263 tests, coverage 77.9%, credo clean,dialyzer clean, format clean.
Every new test carries a sabotage note, and every one was actually run:
eleven mutations, all confirmed red and reverted. Two of my first drafts
came back green under sabotage — the reachability and arc-completion
tests were not pinning what their comments claimed — and both the tests
and the comments were corrected rather than the notes kept.
Captures (capture-only per R25-4;
docs/demo-script.mduntouched):026-screens/se-opg-span-graph.txtandse-opg-walkthrough.md.Private ports only; the frozen stack was never touched.