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.
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.rsbefore 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.rswith 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;tolib.rs. Do not expose a newfabro_workflow::stage_scopepublic module in this pass; preserve the existing public path throughpub use crate::stage_scope::StageScopeinevent.rs.
Task 2: Move Event, Names, Stage Scope, and Stored Fields
-
Move the
Eventenum,Event::pull_request_created, andEvent::traceintoevent/events.rs. Keep all derives, serde attributes, clippy allowances, variant fields, tracing levels, tracing fields, andPullRequestRecordbehavior unchanged. -
Move
event_nameintoevent/names.rs. Keep the exhaustive match and all returned strings unchanged. -
Move
StageScopeintostage_scope.rs. Keep constructors andstage_id()unchanged, including use ofvisit_from_context. Preservefabro_workflow::event::StageScopeby re-exporting it fromevent.rs. -
In
stage_scope.rs, import only the dependencies needed byStageScope:fabro_types::{ParallelBranchId, StageId},crate::context::{Context as WfContext, WorkflowContext}, andcrate::run_dir::visit_from_context. Do not import fromcrate::event. -
Move stored-field helpers into
event/stored_fields.rs:StoredEventFieldsdefault_node_labelnode_stored_fieldsstored_event_fieldsstored_event_fields_for_variantagent_tool_call_idagent_actor_for_event
-
Make both
StoredEventFieldsandstored_event_fieldspub(super)becauseconvert.rscalls the function and reads fields from its return value. Keepdefault_node_label,node_stored_fields,stored_event_fields_for_variant,agent_tool_call_id, andagent_actor_for_eventprivate tostored_fields.rs.
Task 3: Move Conversion and Redaction
-
Move conversion helpers into
event/convert.rs:billed_token_counts_from_llmstage_status_from_stringevent_body_from_eventto_run_eventto_run_event_at
-
Keep
event_body_from_eventprivate toconvert.rs. Importstored_event_fieldsfromevent/stored_fields.rs. Keepto_run_eventandto_run_event_atpublic through the facade re-export. -
Move redaction helpers into
event/redaction.rs:build_redacted_event_payloadredacted_event_jsonnormalized_event_valueredacted_event_valueevent_payload_from_redacted_json
-
Keep
normalized_event_valueandredacted_event_valueprivate. Keep redaction behavior exactly asRunEvent::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_eventappend_event_to_sinkRunEventSinkRunEventSinkFutureRunEventSinkCallbackRunEventTransformRunEventCommandRunEventLoggerStoreProgressLogger
-
Keep
RunEventCommandand callback type aliases private. Preserve the iterativeRunEventSink::write_run_eventstack logic and redacted JSONL output behavior. -
Move emitter code into
event/emitter.rs:epoch_millisEventListenerEmitterDebug,Default, and inherent impls
-
Keep
dispatch_run_eventaspub(crate)and keep all publicEmittermethods 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_shaperedaction.rs:build_redacted_event_payload_*names.rs:event_name_matches_new_dot_notation,run_archived_event_name_matches_dot_notationstored_fields.rs: only direct helper tests introduced during the move, if needed; it is acceptable for this module to have no testsconvert.rs: allrun_event_*tests, actor envelope tests, tool call id tests, parallel id tests, stage id tests, metadata snapshot mapping tests, andstage_scope_populates_stage_id_on_non_stage_eventsstage_scope.rs: directStageScopeconstructor tests introduced during the move, if needed; it is acceptable for this module to have no tests
-
Update
docs/internal/events-strategy.mdreferences that say the canonical conversion is infabro-workflow/src/event.rsso they refer to thefabro-workflow::eventmodule. 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}andfabro_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.rsis a small facade module.- Existing public API paths under
fabro_workflow::event::{...}still compile. StageScopelives at crate root and remains available fromfabro_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,
EventBodyconversion, redaction, JSONL sink output, store payload shape, and emitter dispatch behavior are unchanged. - No macro registry, generated event table, or domain-split
Eventenum is introduced in this pass. docs/internal/events-strategy.mdremains 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.rsremains intentionally large because keepingEvent::tracewithEventavoids splitting a pure inherentEventbehavior into a separate file.- Keeping
convert.rsfocused onEvent -> EventBodybody conversion is acceptable even if it remains one of the larger event files. RunEventSink,RunEventLogger, andStoreProgressLoggerremain together insink.rsfor 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.