fabro/docs/superpowers/plans/2026-05-02-event-module-split.md
Bryan Helmkamp ae5ccb5ce2
refactor(workflow): split event module by responsibility
Keep fabro_workflow::event as the public facade while moving event conversion, names, redaction, sink, emitter, stored-field helpers, and StageScope into focused modules. Co-locate the existing event tests with the moved code and update the events strategy docs for the new module layout.
2026-05-02 14:52:10 -04:00

9.6 KiB

Mechanical Event Module Split Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Split fabro-workflow's oversized event module into focused child modules while preserving fabro_workflow::event::{...} as the public API.

Architecture: Keep src/event.rs as a facade that declares child modules and re-exports the existing public symbols. Move code mechanically by responsibility, co-locate tests with the modules they cover, and move StageScope to a crate-root module while preserving fabro_workflow::event::StageScope. event/events.rs intentionally remains the largest file because it keeps the Event enum and its exhaustive tracing behavior together.

Tech Stack: Rust, Tokio, serde/serde_json, chrono, uuid, fabro-types RunEvent / EventBody, fabro-store EventPayload, existing cargo nextest workflow tests.


Files

  • Modify: lib/crates/fabro-workflow/src/event.rs
  • Create: lib/crates/fabro-workflow/src/event/events.rs
  • Create: lib/crates/fabro-workflow/src/event/names.rs
  • Create: lib/crates/fabro-workflow/src/event/stored_fields.rs
  • Create: lib/crates/fabro-workflow/src/event/convert.rs
  • Create: lib/crates/fabro-workflow/src/event/redaction.rs
  • Create: lib/crates/fabro-workflow/src/event/sink.rs
  • Create: lib/crates/fabro-workflow/src/event/emitter.rs
  • Create: lib/crates/fabro-workflow/src/stage_scope.rs
  • Modify: lib/crates/fabro-workflow/src/lib.rs
  • Modify: docs/internal/events-strategy.md

Task 1: Confirm Private Helpers and Build the Facade

  • Confirm these helpers are not used outside event.rs before moving them:
rg -n "StoredEventFields|event_body_from_event|normalized_event_value|redacted_event_value|RunEventCommand|RunEventSinkFuture|RunEventSinkCallback|RunEventTransform|agent_tool_call_id|agent_actor_for_event|default_node_label|node_stored_fields|billed_token_counts_from_llm|stage_status_from_string|epoch_millis" . --glob '!target' --glob '!apps/fabro-web/dist/**'

Expected: production hits are limited to lib/crates/fabro-workflow/src/event.rs; plan and historical docs may mention the names.

  • Replace event.rs with child module declarations and public re-exports:
mod convert;
mod emitter;
mod events;
mod names;
mod redaction;
mod sink;
mod stored_fields;

pub use fabro_types::{EventBody, RunNoticeLevel};

pub use self::convert::{to_run_event, to_run_event_at};
pub use self::emitter::Emitter;
pub use self::events::Event;
pub use self::names::event_name;
pub use self::redaction::{
    build_redacted_event_payload, event_payload_from_redacted_json, redacted_event_json,
};
pub use self::sink::{
    RunEventLogger, RunEventSink, StoreProgressLogger, append_event, append_event_to_sink,
};
pub use crate::stage_scope::StageScope;
  • Add mod stage_scope; to lib.rs. Do not expose a new fabro_workflow::stage_scope public module in this pass; preserve the existing public path through pub use crate::stage_scope::StageScope in event.rs.

Task 2: Move Event, Names, Stage Scope, and Stored Fields

  • Move the Event enum, Event::pull_request_created, and Event::trace into event/events.rs. Keep all derives, serde attributes, clippy allowances, variant fields, tracing levels, tracing fields, and PullRequestRecord behavior unchanged.

  • Move event_name into event/names.rs. Keep the exhaustive match and all returned strings unchanged.

  • Move StageScope into stage_scope.rs. Keep constructors and stage_id() unchanged, including use of visit_from_context. Preserve fabro_workflow::event::StageScope by re-exporting it from event.rs.

  • In stage_scope.rs, import only the dependencies needed by StageScope: fabro_types::{ParallelBranchId, StageId}, crate::context::{Context as WfContext, WorkflowContext}, and crate::run_dir::visit_from_context. Do not import from crate::event.

  • Move stored-field helpers into event/stored_fields.rs:

    • StoredEventFields
    • default_node_label
    • node_stored_fields
    • stored_event_fields
    • stored_event_fields_for_variant
    • agent_tool_call_id
    • agent_actor_for_event
  • Make both StoredEventFields and stored_event_fields pub(super) because convert.rs calls the function and reads fields from its return value. Keep default_node_label, node_stored_fields, stored_event_fields_for_variant, agent_tool_call_id, and agent_actor_for_event private to stored_fields.rs.

Task 3: Move Conversion and Redaction

  • Move conversion helpers into event/convert.rs:

    • billed_token_counts_from_llm
    • stage_status_from_string
    • event_body_from_event
    • to_run_event
    • to_run_event_at
  • Keep event_body_from_event private to convert.rs. Import stored_event_fields from event/stored_fields.rs. Keep to_run_event and to_run_event_at public through the facade re-export.

  • Move redaction helpers into event/redaction.rs:

    • build_redacted_event_payload
    • redacted_event_json
    • normalized_event_value
    • redacted_event_value
    • event_payload_from_redacted_json
  • Keep normalized_event_value and redacted_event_value private. Keep redaction behavior exactly as RunEvent::to_value() -> normalize_json_value -> redact_json_value.

Task 4: Move Sink, Logger, and Emitter Plumbing

  • Move sink and logger code into event/sink.rs:

    • append_event
    • append_event_to_sink
    • RunEventSink
    • RunEventSinkFuture
    • RunEventSinkCallback
    • RunEventTransform
    • RunEventCommand
    • RunEventLogger
    • StoreProgressLogger
  • Keep RunEventCommand and callback type aliases private. Preserve the iterative RunEventSink::write_run_event stack logic and redacted JSONL output behavior.

  • Move emitter code into event/emitter.rs:

    • epoch_millis
    • EventListener
    • Emitter
    • Debug, Default, and inherent impls
  • Keep dispatch_run_event as pub(crate) and keep all public Emitter methods unchanged.

Task 5: Co-locate Tests and Update Docs

  • Move each existing inline test into the module it characterizes. Keep test names, fixtures, assertions, and async test attributes unchanged.

  • Use this test placement:

    • emitter.rs: event_emitter_*
    • sink.rs: run_event_sink_*, run_event_logger_*, append_event_writes_store_event_shape
    • redaction.rs: build_redacted_event_payload_*
    • names.rs: event_name_matches_new_dot_notation, run_archived_event_name_matches_dot_notation
    • stored_fields.rs: only direct helper tests introduced during the move, if needed; it is acceptable for this module to have no tests
    • convert.rs: all run_event_* tests, actor envelope tests, tool call id tests, parallel id tests, stage id tests, metadata snapshot mapping tests, and stage_scope_populates_stage_id_on_non_stage_events
    • stage_scope.rs: direct StageScope constructor tests introduced during the move, if needed; it is acceptable for this module to have no tests
  • Update docs/internal/events-strategy.md references that say the canonical conversion is in fabro-workflow/src/event.rs so they refer to the fabro-workflow::event module. Do not change event-strategy rules.

  • Do not update production call sites outside the event module unless compilation requires an import fix. The intended result is that existing imports such as crate::event::{Emitter, Event} and fabro_workflow::event::{Event, to_run_event} continue to work.

Task 6: Verify the Mechanical Split

  • Run focused event tests:
cargo nextest run -p fabro-workflow event

Expected: all matching tests pass.

  • Run the full workflow crate test suite:
cargo nextest run -p fabro-workflow

Expected: all fabro-workflow tests pass.

  • Run format check:
cargo +nightly-2026-04-14 fmt --check --all

Expected: no formatting diffs.

  • Run clippy:
cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings

Expected: no warnings.

Acceptance Criteria

  • lib/crates/fabro-workflow/src/event.rs is a small facade module.
  • Existing public API paths under fabro_workflow::event::{...} still compile.
  • StageScope lives at crate root and remains available from fabro_workflow::event::StageScope.
  • Tests are co-located with the module they cover; there is no catch-all event/tests.rs.
  • Event wire names, envelope metadata, EventBody conversion, redaction, JSONL sink output, store payload shape, and emitter dispatch behavior are unchanged.
  • No macro registry, generated event table, or domain-split Event enum is introduced in this pass.
  • docs/internal/events-strategy.md remains accurate after the file split.

Assumptions

  • This is a strictly mechanical refactor; deeper cleanup like domain-specific event enums or shared event DTO extraction is out of scope.
  • event/events.rs remains intentionally large because keeping Event::trace with Event avoids splitting a pure inherent Event behavior into a separate file.
  • Keeping convert.rs focused on Event -> EventBody body conversion is acceptable even if it remains one of the larger event files.
  • RunEventSink, RunEventLogger, and StoreProgressLogger remain together in sink.rs for this pass; split them later only if that file remains hard to navigate after tests are co-located.
  • Existing tests are sufficient characterization coverage for this split; new behavior tests are not required unless a moved module exposes an accidental visibility issue.