Skip to content

Adds Driver.new/3's after_step: callback - #91

Merged
johnnyt merged 1 commit into
mainfrom
sp-c48-after-step
Sep 8, 2026
Merged

Adds Driver.new/3's after_step: callback#91
johnnyt merged 1 commit into
mainfrom
sp-c48-after-step

Conversation

@johnnyt

@johnnyt johnnyt commented Sep 8, 2026

Copy link
Copy Markdown
Member

Implements sp-c48 (SF039, Lane P): the code half of ADR-0008's
2026-09-08 after_step: amendment (sp-sli, merged at 95ea54f, still at
proposed - sp-nhl flips it).

The seam

Clause 1 of the amendment left one thing to this bead and ruled out the
easy answer: the whole effect list has to reach the driver without
widening the return of a public StatifierPersistence.Runs function.

What landed is a package-internal step_reporter: option on
Runs.create/4 and Runs.step/5, documented in the t:opt/0 typedoc
beside linkage:, invoke_id: and child_count: as "this package's
own, never a host's". persist_tail/7 calls it with the list it was
handed - before Enum.split_with/2 splits the lifecycle effects off -
once write_run/6 has landed. Driver writes the option only when a
callback is actually in play, and writes it as a fun that sends the list
back to the driving process on this drive's own reference, the same
buffer shape buffer/4 already uses for a dispatched invocation's
answer. The driver drains that message and fires the host's callback
itself, after the entry point has returned.

That is what makes clause 3 true rather than asserted: the callback for a
run never runs inside that run's own exclusion, and nothing a host wrote
runs inside a serialized section this package opened.

Two fire sites cover every door - create/3 for Runs.create/4, the
private step/5 for Runs.step/5. send_event/4, done_invocation/5,
failed_invocation/5, answer_parent/3 and Runs.fail/4's driver:
path all arrive at one of them, so the parent's step on the answer path
reports under the parent's run id with nothing plumbed for it.

Also implemented, as the amendment's closing section allows: a per-call
after_step: outranks the driver's own. It is
Keyword.get(opts, :after_step, driver.after_step) rather than a literal
Keyword.put_new/3 into the Runs opts, because the option is the
driver's and is deleted before the entry point is called; the semantics
are the shape invoke_types: and serialization: already have.

Per the record and not changed here: a discarded delivery reports
nothing, Runs.cascade_cancel/3 reports nothing, the return value is
ignored, a raise propagates, and no public return is widened.

Drift between the bead and the merged record

The bead's paraphrase says the callback fires "outside the run lock".
The merged record's clause 3 is narrower: the guarantee is scoped to the
run being reported. Decision 3's child is created inside the parent's
step, so the child's callback fires while the parent's exclusion is held,
and the record says explicitly not to "fix" that. The code follows the
record; a test pins the ordering.

Provenance

  • test/statifier_persistence/driver_subchart_test.exs is outside the
    bead's file map. The parent-answer case needs that file's linked-child
    fixtures (subchart_dispatch/1, parent_resolver/1,
    Linkage.child_run_id/3), and rebuilding them in driver_test.exs
    would duplicate the harness. One test and one private helper were
    added; nothing existing was changed.
  • lib/statifier_persistence/runs.ex is in the map conditionally ("only
    if the seam needs it"). It did: the option, the t:opt/0 member and
    its typedoc, the reporter threaded through stepped/7 and
    persist_tail/7, and report_step/3.

Verification

  • Full mix quality green on this HEAD: 527 tests, 95.7% coverage,
    credo and dialyzer clean.
  • Seven sabotage runs, one per assertion, each verified red and
    reverted, recorded in a comment above the test it discriminates.

ADR-0008's 2026-09-08 amendment: a host keeping its own record of what a
run did cannot see the steps this package takes for it - decision 3's
answer steps the parent from inside the child's drive, and Runs.fail/4's
driver: option steps the parent from a call about a different run
entirely. Neither reaches a host, so its fold and the stored checkpoint
disagree for exactly the deliveries a subchart's lifecycle is made of.

after_step: is that seam: a 3-arity callback on the driver, nil by
default, called with the id of the run that was stepped, that step's
machine state, and the whole effect list it produced.

The seam the amendment's clause 1 left open, having ruled out widening a
public Runs return: a package-internal step_reporter: option threaded to
the persist tail, which hands it the effect list before the lifecycle
split and once the write has landed. The driver sets it only when a
callback is in play, and sets it to a fun that sends the list back on
this drive's own reference - the same buffer shape a dispatched
invocation's answer already uses. The driver fires the host's callback
itself, after the entry point has returned, so the callback for a run
never runs inside that run's exclusion.

Firing at Runs.create/4 and the private step/5 covers every door: the
answer path reaches step/5 through reenter/5 on a driver over the
parent's chart, so the parent's step reports under the parent's id with
nothing plumbed for it.

A discarded delivery and a cascade_cancel step nothing and report
nothing; the return is ignored and a raise propagates. A per-call
after_step: outranks the driver's own, the widening the amendment's
closing section left to this bead.
@johnnyt
johnnyt merged commit e3209bd into main Sep 8, 2026
1 check passed
@johnnyt
johnnyt deleted the sp-c48-after-step branch September 8, 2026 22:26
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