Skip to content

Traces the durable arc as one linked span graph - #48

Merged
johnnyt merged 1 commit into
mainfrom
se-opg-one-trace-capstone
Sep 2, 2026
Merged

Traces the durable arc as one linked span graph#48
johnnyt merged 1 commit into
mainfrom
se-opg-one-trace-capstone

Conversation

@johnnyt

@johnnyt johnnyt commented Sep 1, 2026

Copy link
Copy Markdown
Member

Provenance

Rebased onto main@fe90e3a after its parent PR #47
(se-6ag-durable-subchart-proof) landed. #47 was rebase-merged, so its
original commits were orphaned under new SHAs on main; this branch was
replayed with git rebase --onto origin/main fa4106b, which carries only
this PR's own commit (2727ba6 -> b52509a).

Conflicts: none. The expected mix.exs / mix.lock overlap with #47
did not materialise — #47's statifier_persistence pin move landed in the
shared ancestor 5a72cd5, so this branch's forward pins applied cleanly on
top. All four family pins verified in both mix.exs and mix.lock after
the rebase: statifier_persistence 0749481, statifier_oban 4bf3a7d,
opentelemetry_statifier 99d7791, statifier_blocks 05f0a4a
(inherited, unchanged).

Gate on the rebased HEAD (b52509a): green. Full mix 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 — parent
session → 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.

  • a macrostep span roots its own trace — ots-ADR-0003 decision 8 starts
    every span from a fresh context and never the ambient one, so no host
    span can gather them;
  • a child run is linked from its parent's step, never parented — sp
    ADR-0008, since parenthood would hold the parent's trace open for the
    child's whole life;
  • a fired timer is linked to the trace that armed it — sob-ADR-0006
    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.Compiler emits 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 one
value no library can supply, in W3C text form.

StatifierExamples.TraceCollector (test support) — 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 and hide the whole structure.

one_trace_test.exs — asserts each of the three edges; the one place two
runs genuinely share a trace (the child's step span nests inside the
parent's, which is where start_child creates it, per ots-ADR-0004's
bridge-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_persistence documents (its docs/telemetry.md, citing
st-ADR-0067) that it emits the interpreter's family with
driver: :persistence by calling Statifier.Telemetry from the stepper
seam. It does not — nothing calls that module anywhere in its lib/.
Only Statifier.Session emits macrostep events, and the durable path
never runs one. A visible consequence: [:statifier_oban, :timer, :scheduled] is contracted to land as a span event on the session's open
macrostep span, and lands as a detached root because there is none.

2. caller_context reaches no timer this app arms. Every job row
stores nil. Half is by design — the :initialize macrostep is stamped
nil per st-ADR-0063 decision 3, having no calling event. The other half
is 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_context onto them, so a host cannot
link 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):

dep ref why
statifier_persistence 0749481 ADR-0009's storage-phase telemetry (the step-span seam); moved forward from 1416a7b
statifier_oban 4bf3a7d ADR-0006's eleven events (the timer seams)
opentelemetry_statifier 99d7791 the two sibling setup calls; new dep for this app

statifier_blocks stays on the inherited 05f0a4a. opentelemetry and
opentelemetry_api are new plain Hex requirements: the bridge brings
only the API on purpose, so the host names the SDK and the exporter — and
this app's default exporter is :none.

mix_deps_test.exs asserts all three pins against mix.exs and
mix.lock, so none can be forgotten quietly.

Verification

Full mix quality green: 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.md untouched):
026-screens/se-opg-span-graph.txt and se-opg-walkthrough.md.
Private ports only; the frozen stack was never touched.

Base automatically changed from se-6ag-durable-subchart-proof to main September 2, 2026 02:55
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
johnnyt force-pushed the se-opg-one-trace-capstone branch from 2727ba6 to b52509a Compare September 2, 2026 02:58
@johnnyt
johnnyt marked this pull request as ready for review September 2, 2026 02:58
@johnnyt
johnnyt merged commit a23a336 into main Sep 2, 2026
1 check passed
@johnnyt
johnnyt deleted the se-opg-one-trace-capstone branch September 2, 2026 03:01
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