Skip to content

Runs the wizard child as its own persisted run - #47

Merged
johnnyt merged 1 commit into
mainfrom
se-6ag-durable-subchart-proof
Sep 2, 2026
Merged

Runs the wizard child as its own persisted run#47
johnnyt merged 1 commit into
mainfrom
se-6ag-durable-subchart-proof

Conversation

@johnnyt

@johnnyt johnnyt commented Sep 1, 2026

Copy link
Copy Markdown
Member

Closes se-6ag.

The durable-subchart proof: the wizard child runs as its own persisted
run
, exercised through a cold restart and through cancel, with captures.

signup_onboarding used to reach its core.subchart and be refused -
{:start_child, _, _} was Statifier.Session's instruction and nobody
else's, so the durable driver routed the run down on_error. It is not any
more. StatifierPersistence.Driver executes the same instruction by
creating the child as an ordinary run with linkage metadata, and this app is
the host that wires it.

What a reader sees

Press Run on /editor?doc=signup_onboarding and the feed says:

2 | Invoke dispatched  | statifier_blocks:subchart on blk_so_wizard
3 | Child chart started| bdoc_signup_demo as run <parent>/blk_so_wizard/0

That id opens as a run of its own at
/editor?doc=signup_wizard&run=<parent>/blk_so_wizard/0, on the position
the child actually reached. Drive it to the end there and the parent
finishes too, without anyone pressing anything on the parent's page. Press
Stop on the parent instead and the child is cancelled with it, keeping
its stored position byte-identical.

The host-side pieces

  • StatifierExamples.Persistence.list_runs_by_metadata/2 is new, and
    exporting it is what opts this adapter into durable subcharts at all: the
    driver refuses to start a child over a store that cannot enumerate one.
    The Ecto adapter's version is a jsonb @> query and SQLite has no such
    operator, so this is the containment test written out in Elixir, with the
    table scan it costs stated in the moduledoc rather than hidden. The
    package's conformance suite now generates its child-listing case here and
    passes it.
  • Durable's dispatch fun routes statifier_blocks:subchart to
    StatifierBlocks.Runtime.DurableSubchart before Charts.dispatch/3 is
    reached. Charts.dispatch/3 still refuses the type by name, now spelled
    {:subchart_not_a_sync_call, type} - the old :durable_subchart_unsupported
    is no longer true of this app.
  • chart_resolver: on the driver is how the child's driver reaches the
    parent's chart, which it does not hold. The package cannot supply it, so
    this app walks the documents it publishes and matches on content hash.
  • Durable.abandon/1 cascades into the run's children, guarded on the
    store's own child_listing_supported?/1.
  • Durable.resume/1 picks the compile recipe off the stored record.

Three defects the browser captures found, all fixed here

The proof was captured before it was believed, and the captures earned their
place. None of these had a test asking for them.

  1. The child's effects were scoped to the parent. A child is driven by
    the parent's driver with only the machine swapped, so the same effects:
    executor ran the child's effects - and it keyed on the run id it closed
    over. The wizard's 24-hour wait and its abandonment reminder were stored
    as jobs against the parent, where they would fire into a chart with no
    such event while the child waited forever. Both consumers now key on
    context.run_id.
  2. The child could not be opened by URL. The editor page compiles the
    document with the root recipe and a child's stored identity is keyed on
    the child recipe, so the page's resume was refused on identity - two
    compiles of one document, and the guard doing its job for a reader who
    could not act on it. Durable.resume/1 reads the recipe off the record.
  3. A cancelled run took the page down. :cancelled is a fourth stored
    status as of statifier_persistence 0.4.0, and finish/2 had clauses
    for the three that existed before, so opening a cancelled child raised a
    FunctionClauseError. Cancellation retains, so a cancelled run is
    precisely a run somebody opens.

Interim pins, re-pin to Hex owed

Both are recorded for the ledger and both retire on the operator's publish.

dep ref why
statifier_persistence 1416a7b98ce70efe0511050d23dd9cc8f12d0d3e sp-2yx widened Driver.dispatch_context/0 to carry :invoke; without the whole effect a dispatch fun cannot reach src and the handler raises. Everything else ADR-0008 needs is in 0.4.0.
statifier_blocks 05f0a4ab0c9a1adb1c8857d0b5642a8b19cc7e98 StatifierBlocks.Runtime.DurableSubchart landed after 0.13.0.

test/statifier_examples/mix_deps_test.exs asserts both pins in mix.exs
and mix.lock, so the re-pin has to be a deliberate edit rather than
something that can be forgotten quietly.

Verification

Full mix quality green: 254 tests, 78.0% coverage, dialyzer and credo
clean. Every new test asserting lib/ behaviour was sabotaged and confirmed
red first, with the mutation noted above it.

/wurk:verify --unattended has an empty backlog here - no plan stage ran,
the bead was fully specified - so the substitute pass applies, and each
acceptance criterion was machine-checked against the live artefact:

criterion result evidence
child run visible as its own persisted run with linkage metadata checked test asserts run id <parent>/blk_so_wizard/0 and the exact linkage map; capture 3 shows it open in the editor
cancel cascade demonstrated checked test asserts parent :failed/host:stopped, child :cancelled, position byte-identical; capture 4
restart-and-resume demonstrated checked test resumes the child cold, drives it to done, and the parent is answered to :completed
captures saved to the campaign journal checked five PNGs plus three text dumps under 026-screens/, se-6ag- prefixed
full gate green checked above

Captures were taken against a private instance on port 8649, driven over
CDP (the Chrome extension was not connected this session). The frozen stack
on 8645 / 8643 / 8642 / 4002 was not touched, restarted or repointed.

Deferred to the operator

docs/demo-script.md section 11 still describes the refusal as the shipped
behaviour, and names this change as its own deliberate follow-up. Campaign
ruling R25-4 is capture-only - the demo script is not edited - so it is left
stale and flagged rather than corrected here. It wants a bead.

A durable run reaching a `core.subchart` used to be refused: the
`{:start_child, _, _}` instruction was `Statifier.Session`'s and nobody
else's, so `signup_onboarding` routed down `on_error`. The durable
driver executes it now, and this app is the host that wires it.

`StatifierExamples.Persistence` gains `list_runs_by_metadata/2`, which
is what opts an adapter into durable subcharts at all - the driver
refuses to start a child over a store that cannot enumerate one. The
Ecto adapter's version is a `jsonb @>` query and SQLite has no such
operator, so this is the containment test in Elixir, with the table
scan it costs stated rather than hidden.

`Durable` routes the subchart type to
`StatifierBlocks.Runtime.DurableSubchart`, carries a `chart_resolver:`
so a finished child can answer its parent, cascades `abandon/1` into
live children, and resumes a run by id alone.

Three defects the browser captures found, none of which had a test
asking for them: the child's timers and asynchronous jobs were scoped
to the parent's run id, the child could not be opened by URL because
the page compiled the wrong recipe, and a cancelled run raised in
`finish/2`, which knew only three of the four stored statuses.

Both family deps move to interim git pins, re-pin to Hex owed:
statifier_persistence at 1416a7b for sp-2yx's widened dispatch
context, statifier_blocks at 05f0a4a for the durable handler.
`mix_deps_test.exs` asserts both, so the re-pin cannot be forgotten
quietly.

se-6ag
@johnnyt
johnnyt merged commit fe90e3a into main Sep 2, 2026
1 check passed
@johnnyt
johnnyt deleted the se-6ag-durable-subchart-proof branch September 2, 2026 02:55
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