ADR-055: Curation → Worlds interface contract — `rule_candidate_published` event + Worlds outcome materialization
Status: Accepted (2026-04-25) — D3’s SQL grant worlds_outcomes_reader removed by ADR-063; D3’s Option A vs B sub-decision resolved by ADR-064 in favor of Option A (event-driven local replica); D1, D2, D4, D5 remain authoritative.
Context
ADR-044 locked the event substrate between contexts; SPEC-269 (authority version metadata) landed authority-lineage fields. This ADR specifies the rule_candidate_published event payload + the rejection semantics on the Worlds consumer side.
The event represents an already-curated, already-approved candidate crossing the context boundary — algorithmic curation + HITL approval (where applicable) happen upstream of the event in the Operations curation pipeline. TA-7’s surface starts at the moment Operations decides to publish.
Decision
D1 — Event: rule_candidate_published
event_type:rule_candidate_publishedsource:platform(Operations curation pipeline lives in platform)target:worldsidempotency_key:str(event_id)(no stronger business key at alpha; Operations may strengthen later)- Lives in
spectral.platform.contracts.events.rule_candidate_publishedper ADR-065 D2 (the originalspectral.core.events.signals.platform.*placement is superseded; producer-owned typed payloads relocate to<producer>.contracts.events.*)
D2 — Payload schema
class RuleCandidatePublishedPayload(BaseModel): rule_candidate_id: UUID workspace_id: UUID rule_text: str authority_version_id: UUID # SPEC-269 authority metadata authority_lineage: list[UUID] # full lineage chain per SPEC-269 published_at: datetime published_by_user_id: UUID | None # nullable for system-initiated publication curation_metadata: dict[str, Any] # extensible curation provenanceAdditive-only versioning per ADR-044 D11. curation_metadata provides extensibility without a schema bump for evolving curation provenance.
D3 — Worlds rejection semantics: worlds.rule_candidate_outcomes outcome table
Worlds consumes the event and classifies the outcome:
- Transient failure (DB unavailable, network hiccup, OperationalError): handler raises exception → ADR-054 retry → eventual DLQ if exhausted. No outcome row written.
- Terminal failure (schema invalid, authority mismatch, business conflict): handler writes outcome row with
outcome='rejected'+reason_code+reason; ACKs the event (no retry). - Success: handler integrates rule into worlds-side state; writes outcome row with
outcome='accepted'.
Schema:
create table worlds.rule_candidate_outcomes ( event_id uuid primary key, rule_candidate_id uuid not null, outcome text not null check (outcome in ('accepted', 'rejected')), reason_code text null, reason text null, recorded_at timestamptz not null default now());Reason codes (alpha): schema_invalid | authority_mismatch | business_conflict | other. Additive-only enum.
Outcome read mechanism between contexts. Worlds emits a rule_candidate_outcome_recorded event onto the substrate per ADR-064; Operations materializes a local replica in platform.rule_candidate_outcomes_replica and reads from same-context state. The strict-unidirectional posture is preserved: worlds writes the outcome; operations consumes the event and reads its own replica — no rejection event flows back to worlds beyond the outcome event itself.
(History: D3 originally specified an inter-context SQL grant worlds_outcomes_reader, removed by ADR-063; the Option A vs B framing that briefly sat between the two ADRs was resolved in favor of Option A by ADR-064.)
D4 — Backpressure via existing TA-16 observability surface
No net-new TA-7 mechanism. core.outbox pending-row count by event_type is exposed via TA-16 observability; Sentry alerts fire on lag exceeding threshold per handler. The Operations curation pipeline can throttle publication via the same EventPublisher interface — no special backpressure protocol on this event.
D5 — Additive-only versioning ratified from ADR-044 D11
Payload field additions are non-breaking; field removals or semantic changes require an event_version bump to a new payload model (RuleCandidatePublishedPayloadV2), with both consumers staying live during the migration window. Standard ADR-044 versioning posture.
Alternatives considered
Rejection event back to Operations (rule_candidate_rejected). Rejected; introduces bidirectional event flow Worlds↔Operations; rejection is a state observation, not a “thing happened” signal; Operations actively queries for outcome when the operator UX needs it.
Terminal-as-DLQ (handler raises on terminal failures, lets retry exhaust). Rejected; wastes retries on known-terminal failures; conflates substrate failure with domain rejection; operator’s mental model is “did my candidate land?” not “look at DLQ rows.”
Outcome table at producer side (Operations writes “I published X”). Rejected; producer doesn’t know outcome until Worlds writes it; producer-side outcome would just shadow worlds-side state.
Single outcomes table for all candidate types. Accepted at alpha; if rule-candidate types diverge later, schema split is straightforward.
Consequences
- Symmetric outcome surface — acceptance and rejection both live in same query path (with the read mechanism reframed by ADR-063).
- No bidirectional event topology for the substrate.
- Read pattern between contexts reframed — the original
worlds_outcomes_readergrant is removed by ADR-063; ADR-064 resolves Option A (richer event payload + local replica) as the definitive choice. - Operator surface (whatever consumes outcomes) is decoupled from event flow.
- Pull-based outcome visibility — operator UX queries on demand vs reactive notification (acceptable: outcome render happens when operator opens candidate detail; no real-time-update requirement).
- Rejection reasons are an enum at alpha — extensions require schema bump (acceptable: enum is small + stable).
- High-volume + high-rejection scenario — outcome-query path may become hot read; standard query optimization + index on
(rule_candidate_id, outcome)mitigates.
References
- ADR-017 — strict-unidirectional signal path between contexts
- ADR-065 —
spectral.coreadmission discipline - ADR-044 — event substrate; envelope shape; additive-only versioning
- ADR-054 — retry semantics for the Worlds consumer
- ADR-060 —
RuleCandidateOutcomeReaderreferenced as Option B reader pattern - ADR-063 — D3 grant removal; Option A / Option B framing
- TA-7 disposition — SPEC-310 comment
48240483 - TA-7 verification — SPEC-310 comment
3df0f3ee - TA-15 D3 grant removal — SPEC-310 comment
e167ca40 src/spectral/platform/contracts/events/rule_candidate_published.py- Codex
system-design/foundations/contract-surfaces/event-substrate.mdx— close-pass updates