Pipeline-Event Convergence — One Taxonomy from Kernel to UI

Status: SHIPPED. Commit bf8cf695 on feat/cxp-direct-transport, deployed to the test-fleet app container (10.53.2.13). Post-deploy Playwright acceptance against the redeployed kernel+gateway pair: 160 passed / 0 failed / 4 skipped (the 4 were filed conditional-infra guards, not dead specs; commit ac9b763f later made those specs self-provisioning — the suite now executes 164/164 with 0 skipped). Certifying gate on the commit: 30,782 / 0 / 0 / 0.

Design digest of plans/pipeline-event-convergence-design.md (+ contract plans/contracts/pipeline-event-convergence.mmd) in the orchestrata-ultimate repo.

The defect — a two-producer fork

Two producers published onto the same UI topic pipeline.<thread_id> with incompatible payload shapes:

The fork was spec-vs-spec: SPEC-003's REQ-003-US-004-102 certified the fallback shape in direct contradiction of SPEC-001's "enum is the source of truth" requirement.

The one-taxonomy design

Compat window

_EMIT_LEGACY_COMPAT = True (module flag in orchestrator/pipeline_kernel.py): only the fallback client dual-emits the legacy flat fields, merged additively into the canonical payload, so stale browser tabs keep rendering for exactly one deploy cycle. The kernel bus never emits legacy fields (would break the golden-bytes guard and poison the durable replay streams). Removal = Packet 5, the NEXT deploy cycle — delete _legacy_compat_fields + the flag, gated on pipeline-e2e.spec.js green while asserting canonical-only. Legacy-shape test checks are guarded behind the same flag so they degrade to no-op at Packet 5.

Gadget dispatch contract

All three gadgets now dispatch on payload.event_type (never payload.event) with fields under payload.data.*. Completion failure is pipeline.complete + data.success === false (the bespoke pipeline.error branches are gone); paused/cancelled render as their own hold/terminal states instead of "Running..."; the orchestrator gadget renders data.error when present.

Requirement renumbering

The pipeline-event-type addendum's requirement ids were renumbered REQ-006-010/011/012 → REQ-006-031/032/033: the old ids collided with SPEC-001's pre-existing US-006 workflow-engine requirements, conflating RTM rows for unrelated requirements. All live @traces and citations updated; a renumbering note lives in specs/001-kernel/pipeline-event-type-addendum.md (historical documents retain the old ids and must be read through that note). REQ-003-US-004-102 was rewritten to reference the SPEC-001 taxonomy with a dated supersession note — the cross-spec contradiction is closed.

Driver provenance — resolve on output, never mutate the caller's event

Status: SHIPPED (session 262, 2026-08-12), on top of the convergence above. PipelineEvent gained a driver field (str = "", default unattributed — empty is legal, never an error) so a UI consumer can show which execution produced an event ("inprocess" today; "headless:<harness>" at VolkDriver Phase 2).

The design that shipped supersedes an earlier one that was refuted before any code landed: making driver default to "inprocess" on the dataclass would have made every unattributed producer look attributed, destroying the one observable signal (driver == "" on the wire) that found this gap in the first place — a third, un-audited producer would silently read as "inprocess" forever.

The contract instead: both production producers of the canonical payload carry their own producer identity as a constructor default (driver: str = "inprocess"), and resolve it onto the serialized output, never onto the PipelineEvent object itself:

The structural guard. A third producer is not caught by a behavioural test — it has to be named to be constructed. Instead, tests/orchestrator/test_pipeline_kernel.py::test_every_production_payload_producer_resolves_driver walks every to_payload() call site under src/ via ast and asserts the set equals exactly {pipeline_events.py::emit, pipeline_kernel.py::_emit_event} — a new production call site changes that set and turns the test red until it either resolves driver too or the list is amended deliberately. Paired with test_cross_producer_driver_attribution_matches_for_same_event and test_no_producer_emits_unattributed_driver_on_the_wire in the same module.

The XSS lesson

Event-derived strings reach innerHTML in gadget renderers. The reviewer wave on this epic caught un-escaped event data; the rule is now: every event-derived string must go through escapeHtml before rendering (e.g. the orchestrator gadget escapes thread.error, stage names, statuses — vizint-ui/gadgets/orchestrator/gadget.js). The security-xss e2e specs (TaskBoard/ProjectExplorer escape proofs) are green against the real gateway.

Repo pointers