Conversation
ADR for temper#128. Commits architectural direction for extending the existing ReactionDispatcher with four additive capabilities: params_from (dynamic params from source fields), Create target resolver (fresh sim_uuid), ReactionGuard (sync source-field + async cross-entity + bounded composites), and docs/AGENTS.md amendment. Sub-Decision 5 reaffirms reactions stay separate from the action layer: verification tractability, authorization scope, transactional boundary, and cascade bounds all depend on the separation. Track 4 extends reactions; it does not fuse them into actions. All additions are additive with serde defaults; paw-fs reactions and [[agent_trigger]] synthesis remain byte-identical. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 of temper#128. Adds ReactionTarget.params_from: a BTreeMap of target-param-name → source-field-name. At dispatch time, a new shared helper build_effective_params merges the static params with values read off the source entity's fields. Both ReactionDispatcher (prod, async) and SimReactionSystem (deterministic sim) route through the helper, so prod and sim apply identical merge semantics. Semantics: - Collision between params and params_from is a parse-time error. - Missing source field at runtime logs a warning and skips the key; the reaction still fires with partial params. - Non-object static params bypass the dynamic merge and log a warning. paw-fs reactions.toml and [[agent_trigger]]-synthesized rules remain byte-identical (params_from defaults to empty via #[serde(default)]; registry/relations.rs supplies BTreeMap::new() explicitly). 32 reaction lib tests + 9 reaction_cascade integration tests pass (including two new integration tests: missing-source-field cascade, TOML params_from round-trip through the registry). DST compliance: BTreeMap for deterministic iteration, pure helper with no I/O, no clock, no random, single shared implementation across prod and sim. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2 of temper#128. Adds TargetResolver::Create — a resolver variant that returns a genuinely new entity ID on every reaction dispatch via temper_runtime::scheduler::sim_uuid(). Distinct from CreateIfMissing (keyed on a source field, intended for per-source-entity singletons). Motivating use case: pipeline chaining where each source action spawns a brand-new target entity instance — composed with params_from from Phase 1, this retires the build_session_message WASM pattern from katagami-curation: fresh target ID + fields piped from source, all in TOML with zero code. DST compliance: - sim_uuid() (not uuid::Uuid::new_v4) — seeded DeterministicIdGen under simulation, Uuid::now_v7() in production. - New test create_resolver_is_deterministic_across_seeded_runs asserts two identical install_deterministic_context(42) runs produce the same 3-ID sequence. SimReactionSystem cascades stay reproducible. - New test create_resolver_returns_fresh_id_each_call asserts two consecutive calls within one seeded context produce distinct IDs. paw-fs reactions and [[agent_trigger]] synthesis are unchanged (neither uses Create). 34 reaction lib tests + 9 integration tests pass. rustfmt clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tity
Phase 3 of temper#128. Adds `guard: Option<ReactionGuard>` to
ReactionTrigger. Reactions with a guard only fire when the guard
evaluates true; guard-skipped rules do NOT emit a ReactionResult
(they never fired).
ReactionGuard variants:
- Sync, source-only: FieldEquals, FieldIn, BoolTrue, BoolFalse, StateIn
(StateIn matches source post-action status, complementing to_state).
- Async, cross-entity: CrossEntityStateIn { entity_type,
entity_id_source, required_status } — mirrors the IOA variant from
ADR-0015 so developers transfer knowledge.
- Composite: AllOf, AnyOf, Not. Bounded at MAX_GUARD_DEPTH = 4,
validated at parse time (TigerStyle: budgets not limits).
Evaluation uses the same two-pass pattern as IOA guards
(state/dispatch/cross_entity.rs):
1. collect_cross_entity_queries walks the tree, emits queries with
target IDs already read from source fields.
2. Caller resolves queries into a CrossStatusMap via the appropriate
lookup primitive (prod: async state.resolve_entity_status — the
existing helper, reused; sim: sync entity_to_actor + inner.status).
3. evaluate_with_resolved re-walks using source fields, source post-
status, and the resolved map. Pure fn — no I/O, no clock, no random.
Strictness: cross-entity guards with a missing/empty target ID do NOT
collect a query and evaluate to false (stricter than IOA vacuous-truth
— for reactions, an empty target id is almost always a misconfiguration).
DST compliance:
- BTreeMap for CrossStatusMap; deterministic key ordering.
- No duplicated fetch logic between prod and sim; both converge on
the same CrossEntityQuery::key() format.
- Sim path is fully sync; prod path awaits only the existing
resolve_entity_status helper.
paw-fs reactions and [[agent_trigger]] synthesis unchanged (guard: None).
51 reaction lib tests + 11 integration tests pass (15 new unit tests
on guard evaluation, 5 new registry parse tests, 2 new cascade
integration tests — state_in gate + Not composite).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The existing os-apps/temper-fs/reactions/reactions.toml used PascalCase
resolver types ("CreateIfMissing", "Field"), but parse_reactions only
matches snake_case ("create_if_missing", "field"). The file was
effectively dead data: the platform bootstrap path registers tenants
with `Vec::new()` reactions, so the mismatch was silent.
Renames the three resolver types to snake_case and adds a regression
test (paw_fs_reactions_toml_loads_through_parser) that includes the
file verbatim and asserts all three rules parse. Future edits to
reactions.toml now fail at test time if they drift from the parser.
No behavior change to any running system — the reactions were not being
loaded anywhere before this fix. This makes the ADR-0045 "paw-fs
regression posture intact" commitment load-bearing rather than vacuous.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 4 of temper#128. Adds docs/reactions.md — the full developer reference for reactions: when to use them vs WASM integrations, the TOML schema (when+guard, then+params+params_from, resolve_target for all 5 variants), three example patterns (pipeline chain, session callback, cleanup-on-failed), and the invariants that don't change (fire-and-forget, MAX_REACTION_DEPTH=8, tenant isolation, system principal, determinism). Also amends docs/AGENT_GUIDE.md §9 with a short "two mechanisms" preamble distinguishing reactions from WASM integrations. The plan originally called for an AGENTS.md amendment; the repo has no AGENTS.md — AGENT_GUIDE.md is the equivalent developer-facing guide, so that's where the pointer lives. No code change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the ADR-0045 verification loop. The existing reaction_cascade
tests exercise SimReactionSystem (the deterministic sim path); this
new test file exercises the production ReactionDispatcher (async,
through ServerState.dispatch_tenant_action) so the full live stack is
verified:
parse_reactions → try_register_tenant_with_reactions
→ build_reaction_registry → ReactionDispatcher
→ ServerState.dispatch_tenant_action → target entity transition
Four scenarios:
- prod_dispatcher_fires_basic_reaction: full stack wires up; Payment
transitions to Authorized in response to Order.ConfirmOrder.
- prod_dispatcher_honours_source_field_guard: two rules, state_in
guard selects exactly one; proves ReactionGuard evaluation through
the prod path.
- prod_dispatcher_not_guard_skips_firing: Not(state_in) composite
guard skips the rule; target stays in initial state — no stray
side effect.
- prod_dispatcher_params_from_missing_field_still_fires: ADR
"warn + skip the key" policy verified; reaction fires with partial
params, target commits successfully.
Create resolver is not exercised here; it requires target actor
on-demand-spawn infra separate from this PR. Determinism of the
resolver itself is already proven in resolver.rs unit tests under
SimReactionSystem.
All four pass. cargo fmt clean, cargo clippy -D warnings clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Owner
Author
Live-server E2E verification (added after initial push)Started Setup
Reactions registered (real file loaded through the new parser): [[reaction]]
name = "order_confirmed_authorizes_payment"
[reaction.when]
entity_type = "Order"
action = "ConfirmOrder"
to_state = "Confirmed"
[reaction.then]
entity_type = "Payment"
action = "AuthorizePayment"
params = { origin = "e2e" }
params_from = { piped_field = "nonexistent_source_field" }
[reaction.resolve_target]
type = "same_id"
[[reaction]]
name = "skipped_when_confirmed"
[reaction.when]
entity_type = "Order"
action = "ConfirmOrder"
[reaction.when.guard]
type = "not"
[reaction.when.guard.guard]
type = "state_in"
values = ["Confirmed"]
[reaction.then]
entity_type = "Payment"
action = "FailPayment"
[reaction.resolve_target]
type = "same_id"Spec verification L0–L3 all passed for both entities. Flow Server log evidence (from the live run): Final OData state (
Proved live:
Rita asked for the real-server verification gap to be closed. Closed. |
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.
Summary
Closes #128. Extends the existing
ReactionDispatcheralong four additive axes so apps can replace hand-written "WASM-as-plumbing" modules (e.g. katagami-curation'sbuild_session_message) with declarative TOML.What's new:
ReactionTarget.params_from— pipe source-entity field values into target action params at dispatch time. Staticparams+ dynamicparams_frommerge via a shared helper; collisions are a parse-time error; missing source fields log and skip the key (reaction still fires with partial params).TargetResolver::Create— fresh UUID per dispatch viasim_uuid()(DST-safe). Distinct fromCreateIfMissing; needed for pipeline chaining where each source action spawns a brand-new target instance.ReactionGuard— conditional firing with sync source-field variants (field_equals,field_in,bool_true/false,state_in), async cross-entitycross_entity_state_in(reusesServerState::resolve_entity_status), and compositeall_of/any_of/notbounded atMAX_GUARD_DEPTH = 4. Guard-skipped rules do NOT emit aReactionResult.docs/reactions.md(full TOML reference + three example patterns) and a pointer fromAGENT_GUIDE.md§9.What's not new (architectural reaffirmation, per ADR Sub-Decision 5):
#[serde(default)]; paw-fs reactions and[[agent_trigger]]-synthesized rules remain byte-identical.Also includes:
os-apps/temper-fs/reactions/reactions.tomlresolver types had been PascalCase ("CreateIfMissing","Field") while the parser matched only snake_case — dead data since merge. Snake-case corrected + regression test added.Commits (ADR → feature → docs → e2e)
Test plan
Local verification (all green):
cargo build --workspacecargo fmt --all -- --checkcargo clippy --workspace --all-targets -- -D warningscargo test -p temper-server --lib reaction::— 51/51cargo test -p temper-server --test reaction_cascade— 12/12 (sim dispatcher, plus paw-fs reactions.toml regression load through the parser, guard pass/skip, params_from cascade, Create determinism underSimReactionSystem)cargo test -p temper-server --test reaction_e2e_prod— 4/4 production-path E2E (real asyncReactionDispatcherviaServerState::dispatch_tenant_action; basic reaction, source-field guard selection,Not(state_in)skip,params_frommissing-field tolerance)cargo test -p temper-platform --test platform_e2e_dst— 6/6Not run locally — deferred to CI:
cargo test --workspace— the full workspace suite includes several heavyweight platform DST tests (dst_platform_cedar,dst_platform_random, etc.) that run stateright model-checking and individually take 5–10+ minutes. None of them touch the reaction layer. Attempts to run the suite locally blew past 30 min; pushed with--no-verifyafter Rita explicitly authorized falling back to the narrower verification. CI will run the full suite.Verification posture:
paw-fsreactions.tomlnow load-bearing: parsed in a regression test that fails if the file drifts from the parser again.[[agent_trigger]]synthesized rules unchanged —guard: None,params_fromempty, verified by the existing tests.ReactionDispatcherpath (not justSimReactionSystem) covered end-to-end inreaction_e2e_prod.rs.Create resolver is not yet exercised through the live production path — the
prod_dispatcher_*tests would need target-actor-on-demand spawn infrastructure which is out of scope for this PR. Its determinism is proven underSimReactionSystem(create_resolver_is_deterministic_across_seeded_runs).🤖 Generated with Claude Code