Skip to content
GitHub
Decisions

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_published
  • source: platform (Operations curation pipeline lives in platform)
  • target: worlds
  • idempotency_key: str(event_id) (no stronger business key at alpha; Operations may strengthen later)
  • Lives in spectral.platform.contracts.events.rule_candidate_published per ADR-065 D2 (the original spectral.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 provenance

Additive-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_reader grant 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-065spectral.core admission discipline
  • ADR-044 — event substrate; envelope shape; additive-only versioning
  • ADR-054 — retry semantics for the Worlds consumer
  • ADR-060RuleCandidateOutcomeReader referenced 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