Date: 2026-04-04 Status: Implemented
ControlPayload.config was a single flat Record<string, unknown> with control_type: 'connector_config'. It was ambiguous: unclear whether the config update targeted the source or destination. The orchestrator hardcoded merging into source.
Apply Stripe's standard {type, [type]: payload} polymorphism pattern to ControlPayload:
ControlPayload = {
control_type: 'source_config' | 'destination_config',
source_config?: Record<string, unknown>,
destination_config?: Record<string, unknown>,
}The control_type discriminator names the sub-hash key (per Stripe API design: Trailhead api-design/polymorphism-in-the-stripe-api).
- StateMessage → SourceStateMessage: Renamed to clarify that state is always the source's cursor position, even when the destination echoes it back.
- ConnectorSpecification: Renamed
stream_state→source_state_stream, addedsource_state_global, renamedinput→source_input. - SourceInputMessage: New protocol-level message type for source input data.
- Principle 10: Added "Stripe polymorphism pattern" to
docs/architecture/principles.md.
- Flat config +
_emitted_byrouting: Keep config flat, orchestrator parses_emitted_by: 'source/stripe'to determine role. Simpler protocol but not self-describing. - Engine wraps config in
{type, [type]: payload}envelope: Connector emits flat, engine transforms. Rejected because engine should be a transparent pipe — adding metadata (tags) is fine, but structural transformation is not. - Connector self-identifies via
spec.name: Each connector declares its type name and wraps control configs itself. Too many abstraction layers for an internal service. - Full Stripe
{type, [type]: payload}on config field: The nested envelope insideconfigcreated 4 levels of nesting. Thecontrol_type-as-discriminator approach is flatter.
- Connector
spec.namefor richer self-description - SourceInput may simplify from discriminated union to plain union
- Consider whether SourceInputMessage should be used in the
Source.read()interface