Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions changelog.d/sui-67d.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
### Added

- Every `effect.*` wire message now carries the engine's `round` stamp in
its envelope, alongside `macrostep` and `microstep` -
`effect.budget_exhausted` already carried it, and the other nine types
gain it. An additive field under the wire format's must-ignore rule, so
the format version stays 1; consumers reading older recorded streams
must still tolerate `effect.*` messages without the key.
59 changes: 32 additions & 27 deletions docs/wire-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ Every message is a JSON object. Every message carries:
| type | string | always |
| session | string | always |
| seq | integer | always |
| macrostep | integer | `trace.*` only |
| microstep | integer | `trace.*` only |
| round | integer | `trace.*` only |
| macrostep | integer | `trace.*` and `effect.*` |
| microstep | integer | `trace.*` and `effect.*` |
| round | integer | `trace.*` and `effect.*` |

`type` is a dotted-namespace string identifying the message shape; the
remaining sections of this document define every value it may take.
Expand All @@ -54,19 +54,22 @@ consumer can share one channel across a session and, later, an invoke tree
of related sessions. `seq` is a per-session monotonic integer, described
fully under "Ordering" below.

**`effect.*` messages carry `macrostep` and `microstep` but not `round`,
even though the engine now stamps `round` on every core effect.** As of
ADR-0046 (`st-xb2b`), every core (non-trace) statifier effect payload
carries `macrostep`, `microstep`, and `round` - effects emitted before that
fold carry `round: 0`. This producer does not yet propagate the engine's
`round` onto `effect.*` messages; that propagation is tracked as
`sui-t36.5`. This is not an oversight this document papers over: the gap is
in this producer, not upstream, and inventing a value on the wire ahead of
the propagation work would be a guess baked into the format. When this
producer's `effect.*` payloads gain `round`, this document's `effect.*`
schemas gain the key. That is an additive change under the versioning rule
above, not a version bump. The envelope table above reflects what this
producer emits today: `round` on `trace.*` only.
**`effect.*` messages carry all three counters, `round` included**
(`sui-67d`). The value is the engine's own stamp, carried verbatim: as of
statifier ADR-0046 (`st-xb2b`), every core (non-trace) statifier effect
payload carries `macrostep`, `microstep`, and `round` - effects emitted
before that fold carry `round: 0` - and this producer propagates the
stamp rather than inventing one. Between ADR-0046 landing upstream and
`sui-67d` landing here, this producer emitted `round` on `trace.*`
messages and `effect.budget_exhausted` only; the other `effect.*` types
gained the key later. **Versioning decision, recorded rather than left
implicit:** adding `round` to the remaining `effect.*` envelopes is an
additive field change, exactly what the conformance section's MUST-ignore
rule makes safe, so **the format version stays 1** - the same reasoning
`effect.datamodel_change` records below for adding a whole type. A
consumer reading an older recorded stream must still tolerate `effect.*`
messages without `round` (the must-ignore rule's mirror image: absence of
a field a newer producer would have written is not an error).

Beyond the fields in the table, every message carries a **payload**: the
type-specific fields, documented below one type at a time. A payload key
Expand Down Expand Up @@ -516,13 +519,12 @@ ADR-0005 leaves non-trace effect naming to this document
because a bare top-level `done` type would sit confusingly next to
`trace.done` - a different message about the same moment in the run.
`effect.*` and `trace.*` are two visibly distinct halves of the vocabulary:
`trace.*` are the nine Appendix D phase boundaries, stamped with all three
counters; `effect.*` are the core effect vocabulary, stamped with
`macrostep`/`microstep` only (see "The envelope" above for why `round` is
absent).
`trace.*` are the nine Appendix D phase boundaries and `effect.*` are the
core effect vocabulary, both stamped with all three counters (see "The
envelope" above; `round` joined the `effect.*` envelopes in `sui-67d`,
after `effect.budget_exhausted` had carried it from the start).

Every `effect.*` message carries `macrostep` and `microstep`, never
`round`.
Every `effect.*` message carries `macrostep`, `microstep`, and `round`.

### `effect.log`

Expand All @@ -547,8 +549,9 @@ The terminal effect, emitted once after top-level final entry.
### `effect.budget_exhausted`

Emitted when a macrostep's fold spends its round budget without reaching
quiescence (ADR-0019). This is the one core effect that does carry `round`
- see "The envelope" above.
quiescence (ADR-0019). This was the one core effect that carried `round`
before `sui-67d` propagated the key onto the rest of the `effect.*`
family - see "The envelope" above.

| Field | Type | Presence |
|---|---|---|
Expand Down Expand Up @@ -912,9 +915,11 @@ that test rather than drifting silently.
- ADR-0003 (`docs/adr/0003-fixtures-as-the-example-data-contract.md`) - the
fixtures sidecar object `session.start`'s `fixtures` field carries
verbatim.
- `st-nbmj` - the upstream gap behind `effect.*` messages carrying no
`round`, closed by ADR-0046; this producer's own propagation of `round`
onto `effect.*` messages is tracked separately as `sui-t36.5`.
- `st-nbmj` - the upstream gap behind `effect.*` messages once carrying no
`round`, superseded by `st-xb2b`, whose ADR-0046 settled it (the two ids
name one thread of work: `st-nbmj` filed the gap, `st-xb2b` decided it);
this producer's own propagation of `round` onto `effect.*` messages was
`sui-67d`, above.
- `st-r6l9` - the upstream reordering seam behind the old ordering warning
above, closed by ADR-0044.
- `st-1xwh` - the upstream effect (`Statifier.Effect.DatamodelInit`) behind
Expand Down
10 changes: 6 additions & 4 deletions lib/statifier_ui/datamodel_explorer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,12 @@ defmodule StatifierUI.DatamodelExplorer do
least one write stamped at that macrostep carried a decoded
`prior_value` different from its decoded `new_value` - so a first
binding, which has no `prior_value` at all, counts as a change from
`:undefined`. This is macrostep granularity, not round: every
`effect.*` type but `effect.budget_exhausted` carries a `nil` `round`
(`normalizer.ex:271`), so finer marking would need an upstream wire
change.
`:undefined`. This is macrostep granularity, not round - chosen when
`effect.datamodel_change` still carried a `nil` `round` envelope.
sui-67d has since stamped `round` on every `effect.*` message, so
round-granular marking is now a possible refinement here rather than
an upstream wire change; this pane deliberately stays at macrostep
granularity, and never reads the `round` field at all.
7. `shape` and `label` are computed once, from each entry's final decoded
value, so an entry written more than once in one macrostep is
labelled from its end state.
Expand Down
18 changes: 9 additions & 9 deletions lib/statifier_ui/event_log.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ defmodule StatifierUI.EventLog do

## Routing `effect.*` messages

Every `trace.*` message carries a `round`; eight of the nine `effect.*`
types do not and are routed to their macrostep's `effects` instead. The
ninth, `effect.budget_exhausted`, is "the one core effect that does carry
`round`" (`docs/wire-format.md:546-547`), so it is routed to its round
bucket instead of `effects`. The routing rule is therefore "does this
message carry a `round`", never a check against the `"effect."` prefix -
which is also what makes a future propagation of `round` onto the
remaining `effect.*` types (tracked separately, not part of this plan) a
pure data change that needs no change here.
The routing rule is "does this message carry a `round`", never a check
against the `"effect."` prefix. When this module was written only
`trace.*` messages and `effect.budget_exhausted` carried `round`, so the
other `effect.*` types landed in their macrostep's `effects`; sui-67d
then propagated `round` onto every `effect.*` type, and - exactly as
planned - that was a pure data change needing no change here: an
`effect.*` message carrying a `round` is routed to its round bucket, and
`effects` still catches any message without one (an older recorded
stream, for example - the must-ignore rule cuts both ways).

## Errors

Expand Down
8 changes: 5 additions & 3 deletions lib/statifier_ui/trace/message.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ defmodule StatifierUI.Trace.Message do
One message of the trace wire format (`docs/wire-format.md`), held as a
struct in process and rendered to the documented JSON object by `to_map/1`.

The envelope fields (`type`, `session`, `seq`, and the `trace.*`-only
`macrostep`/`microstep`/`round`) live on the struct directly; everything
The envelope fields (`type`, `session`, `seq`, and the counters
`macrostep`/`microstep`/`round`, carried by `trace.*` and `effect.*`
messages and by no `session.*` message) live on the struct directly; everything
type-specific lives in `payload`, already in wire shape - a string-keyed
map ready to merge over the envelope. This is decision 1 of the plan: one
envelope struct rather than one struct per message type, so the spec stays
Expand Down Expand Up @@ -73,7 +74,8 @@ defmodule StatifierUI.Trace.Message do

`"type"`, `"session"`, and `"seq"` are always present; `"macrostep"`,
`"microstep"`, and `"round"` are present only when non-`nil` (decisions 3
and 5 - `effect.*` messages carry no `round`, `session.*` messages carry
and 5, amended by sui-67d - `trace.*` and `effect.*` messages carry all
three, `session.*` messages carry
none of the three). Specced for a valid message: call `validate/1` first,
or construct through `StatifierUI.Trace.Normalizer`, which always does.
"""
Expand Down
18 changes: 9 additions & 9 deletions lib/statifier_ui/trace/normalizer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ defmodule StatifierUI.Trace.Normalizer do
with {:ok, base} <- put_owner(base, p.owner),
{:ok, base} <- put_defined(base, "new_value", p.new_value),
{:ok, base} <- put_defined(base, "prior_value", p.prior_value) do
{:ok, {"effect.datamodel_change", p.macrostep, p.microstep, nil, base}}
{:ok, {"effect.datamodel_change", p.macrostep, p.microstep, p.round, base}}
end
end

Expand All @@ -278,15 +278,15 @@ defmodule StatifierUI.Trace.Normalizer do
with {:ok, base} <- put_owner(base, p.owner),
{:ok, base} <- put_value(base, "value", p.value) do
base = put_present(base, "c_index", p.c_index)
{:ok, {"effect.log", p.macrostep, p.microstep, nil, base}}
{:ok, {"effect.log", p.macrostep, p.microstep, p.round, base}}
end
end

defp core_message(%Done{} = p) do
base = %{"configuration" => configuration(p.configuration)}

with {:ok, payload} <- put_value(base, "donedata", p.donedata) do
{:ok, {"effect.done", p.macrostep, p.microstep, nil, payload}}
{:ok, {"effect.done", p.macrostep, p.microstep, p.round, payload}}
end
end

Expand Down Expand Up @@ -315,13 +315,13 @@ defmodule StatifierUI.Trace.Normalizer do

with {:ok, base} <- put_value(base, "params", p.params),
{:ok, base} <- put_value(base, "content", p.content) do
{:ok, {"effect.invoke", p.macrostep, p.microstep, nil, base}}
{:ok, {"effect.invoke", p.macrostep, p.microstep, p.round, base}}
end
end

defp core_message(%CancelInvoke{} = p) do
payload = %{"invoke_id" => p.invoke_id, "state_index" => p.state_index}
{:ok, {"effect.cancel_invoke", p.macrostep, p.microstep, nil, payload}}
{:ok, {"effect.cancel_invoke", p.macrostep, p.microstep, p.round, payload}}
end

defp core_message(%Autoforward{} = p) do
Expand All @@ -332,7 +332,7 @@ defmodule StatifierUI.Trace.Normalizer do
"event" => event_obj
}

{:ok, {"effect.autoforward", p.macrostep, p.microstep, nil, payload}}
{:ok, {"effect.autoforward", p.macrostep, p.microstep, p.round, payload}}
end
end

Expand All @@ -345,7 +345,7 @@ defmodule StatifierUI.Trace.Normalizer do
with {:ok, base} <- put_owner(base, p.owner),
{:ok, base} <- put_value(base, "data", p.data) do
base = put_present(base, "c_index", p.c_index)
{:ok, {"effect.send", p.macrostep, p.microstep, nil, base}}
{:ok, {"effect.send", p.macrostep, p.microstep, p.round, base}}
end
end

Expand All @@ -363,15 +363,15 @@ defmodule StatifierUI.Trace.Normalizer do
with {:ok, base} <- put_owner(base, p.owner),
{:ok, base} <- put_value(base, "data", p.data) do
base = put_present(base, "c_index", p.c_index)
{:ok, {"effect.send_delayed", p.macrostep, p.microstep, nil, base}}
{:ok, {"effect.send_delayed", p.macrostep, p.microstep, p.round, base}}
end
end

defp core_message(%Cancel{} = p) do
base = put_present(%{"send_id" => p.send_id}, "c_index", p.c_index)

with {:ok, base} <- put_owner(base, p.owner) do
{:ok, {"effect.cancel", p.macrostep, p.microstep, nil, base}}
{:ok, {"effect.cancel", p.macrostep, p.microstep, p.round, base}}
end
end

Expand Down
25 changes: 25 additions & 0 deletions test/statifier_ui/event_log_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,31 @@ defmodule StatifierUI.EventLogTest do
assert macrostep.rounds == []
end

# Since sui-67d the live producer stamps `round` on every `effect.*`
# message, so this is the live-stream shape; the round-less case above
# remains what an older recorded stream looks like.
test "a round-carrying effect lands in its round, not in Macrostep.effects" do
messages = [
%Message{
type: "effect.log",
session: "s",
seq: 0,
macrostep: 2,
microstep: 1,
round: 3,
payload: %{"label" => "hi"}
}
]

{:ok, log} = EventLog.build(messages)
[macrostep] = log.macrosteps
[round] = macrostep.rounds

assert macrostep.effects == []
assert round.round == 3
assert [%Message{type: "effect.log"}] = round.messages
end

test "effect.budget_exhausted lands in its round, not in Macrostep.effects" do
payload = %{
"budget" => 10,
Expand Down
14 changes: 7 additions & 7 deletions test/statifier_ui/trace/normalizer_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,10 @@ defmodule StatifierUI.Trace.NormalizerTest do
owner: {:transition, 0},
macrostep: 1,
microstep: 0,
round: 0
round: 2
}

assert {:ok, %Message{type: "effect.log", macrostep: 1, microstep: 0, round: nil} = message} =
assert {:ok, %Message{type: "effect.log", macrostep: 1, microstep: 0, round: 2} = message} =
Normalizer.normalize({:log, payload}, @ctx)

assert message.payload == %{
Expand All @@ -234,14 +234,14 @@ defmodule StatifierUI.Trace.NormalizerTest do
round: 0
}

assert {:ok, %Message{type: "effect.done", round: nil} = message} =
assert {:ok, %Message{type: "effect.done", round: 0} = message} =
Normalizer.normalize({:done, payload}, @ctx)

assert message.payload == %{"configuration" => [0]}
refute Map.has_key?(message.payload, "donedata")
end

test "effect.budget_exhausted carries round, unlike every other core effect" do
test "effect.budget_exhausted carries round, like every core effect since sui-67d" do
pending = [%Event{name: "go", type: :internal}]

payload = %BudgetExhausted{
Expand Down Expand Up @@ -292,7 +292,7 @@ defmodule StatifierUI.Trace.NormalizerTest do
round: 0
}

assert {:ok, %Message{type: "effect.invoke", round: nil} = message} =
assert {:ok, %Message{type: "effect.invoke", round: 0} = message} =
Normalizer.normalize({:invoke, payload}, @ctx)

assert message.payload["invoke_id"] == "inv1"
Expand Down Expand Up @@ -349,7 +349,7 @@ defmodule StatifierUI.Trace.NormalizerTest do
round: 0
}

assert {:ok, %Message{type: "effect.send", round: nil} = message} =
assert {:ok, %Message{type: "effect.send", round: 0} = message} =
Normalizer.normalize({:send, payload}, @ctx)

assert message.payload["id_from_author"] == true
Expand Down Expand Up @@ -714,7 +714,7 @@ defmodule StatifierUI.Trace.NormalizerTest do
type: "effect.datamodel_change",
macrostep: 2,
microstep: 1,
round: nil
round: 0
} = message} = Normalizer.normalize({:datamodel_change, payload}, @ctx)

assert message.payload == %{
Expand Down
Loading