Problem
Channel configuration models a channel as a primary key smeared across parallel
collections, not as an object. One Slack channel C123 is expressed three ways:
- an entry in
AllowedChannelIds: [ ... ] (the deny-by-default allow-list),
- an optional entry in
ChannelAudiences: { "C123": "team" },
- an optional entry in
MentionRequiredInThreadByChannel: { "C123": true }.
Each new per-channel knob adds another parallel map keyed by channel ID. The
collections can drift, and there is no single place that holds a channel and its
metadata. ChannelAudiences also overloads its key space with a special "dm"
entry for direct-message audience, which is not a channel at all.
Proposed model
Model a channel as a first-class object and stop keying parallel maps by channel ID.
- The
Channels array membership replaces AllowedChannelIds (it becomes the allow-list).
Audience per entry replaces ChannelAudiences[id].
MentionRequiredInThread per entry replaces the map.
- The
"dm" audience moves to a dedicated DirectMessages.Audience.
- The object becomes the extension point, so the next per-channel setting is a
property, not a fourth parallel map.
Apply the same shape to all three connectors (Slack, Discord, Mattermost); they
are structurally identical.
Blast radius (about 25 non-test files)
- 3 options types (
{Slack,Discord,Mattermost}ChannelOptions)
- 3 ACL policies (read the allow-list + audiences)
AudienceResult.Resolve (the shared resolver, plus the "dm" case)
- 3 thread-history fetchers
- 3 target / destination resolvers
- Doctor (
SlackAclDoctorCheck, DoctorFixService)
- About 6 TUI files (channels editor, wizard steps, config builder, dashboard)
- The JSON schema (3 sections)
- Tests for all of the above
Backwards-compatibility hazard (the hard part)
There is no config-file migration framework today:
SchemaMigrator is SQLite-only. It runs versioned SQL against the sessions
database and does nothing for the JSON config.
SchemaFixResolver (what doctor --fix runs) does only leaf-level fixes:
integer-to-enum coercion, insert-missing-default, and remove properties
disallowed by additionalProperties: false. It cannot move data between
shapes.
A naive schema swap is therefore destructive, not merely non-loading:
- A live config still has
AllowedChannelIds + ChannelAudiences (deployed,
security-critical data).
- The new schema no longer lists those keys, so
additionalProperties: false
flags them.
doctor --fix runs TryRemoveDisallowedProperties and deletes them,
silently wiping the channel allow-list and audiences, while the new
Channels array stays empty.
On a default-deny allow-list, auto-fix would quietly erase the operator's access
config.
Migration strategy (additive + lazy)
- Add
Channels (and DirectMessages.Audience). Keep AllowedChannelIds /
ChannelAudiences in the schema as deprecated but still valid, so
doctor --fix never deletes them.
- Runtime prefers
Channels; when it is absent, it composes the allow-list and
audiences from the old keys. This is a bounded, documented compatibility read,
not a silent degradation.
- The TUI writes the new shape and drops the old keys on the next save, so
configs migrate as operators touch them.
- A later major version ships a one-shot mechanical migrator for stragglers and
then removes the old keys from the schema.
Required proof (Automation Floor)
- Old-shape to new-shape round-trip tests.
- An ACL-equivalence test proving the allow-list and audience decisions are
identical before and after migration.
Sequencing
Do this as its own OpenSpec change. The hard part is the backwards-compatible
migration above, not the C# refactor. The MentionRequiredInThread per-channel
setting should fold into this object model rather than ship as a separate map.
Related
Problem
Channel configuration models a channel as a primary key smeared across parallel
collections, not as an object. One Slack channel
C123is expressed three ways:AllowedChannelIds: [ ... ](the deny-by-default allow-list),ChannelAudiences: { "C123": "team" },MentionRequiredInThreadByChannel: { "C123": true }.Each new per-channel knob adds another parallel map keyed by channel ID. The
collections can drift, and there is no single place that holds a channel and its
metadata.
ChannelAudiencesalso overloads its key space with a special"dm"entry for direct-message audience, which is not a channel at all.
Proposed model
Model a channel as a first-class object and stop keying parallel maps by channel ID.
Channelsarray membership replacesAllowedChannelIds(it becomes the allow-list).Audienceper entry replacesChannelAudiences[id].MentionRequiredInThreadper entry replaces the map."dm"audience moves to a dedicatedDirectMessages.Audience.property, not a fourth parallel map.
Apply the same shape to all three connectors (Slack, Discord, Mattermost); they
are structurally identical.
Blast radius (about 25 non-test files)
{Slack,Discord,Mattermost}ChannelOptions)AudienceResult.Resolve(the shared resolver, plus the"dm"case)SlackAclDoctorCheck,DoctorFixService)Backwards-compatibility hazard (the hard part)
There is no config-file migration framework today:
SchemaMigratoris SQLite-only. It runs versioned SQL against the sessionsdatabase and does nothing for the JSON config.
SchemaFixResolver(whatdoctor --fixruns) does only leaf-level fixes:integer-to-enum coercion, insert-missing-default, and remove properties
disallowed by
additionalProperties: false. It cannot move data betweenshapes.
A naive schema swap is therefore destructive, not merely non-loading:
AllowedChannelIds+ChannelAudiences(deployed,security-critical data).
additionalProperties: falseflags them.
doctor --fixrunsTryRemoveDisallowedPropertiesand deletes them,silently wiping the channel allow-list and audiences, while the new
Channelsarray stays empty.On a default-deny allow-list, auto-fix would quietly erase the operator's access
config.
Migration strategy (additive + lazy)
Channels(andDirectMessages.Audience). KeepAllowedChannelIds/ChannelAudiencesin the schema as deprecated but still valid, sodoctor --fixnever deletes them.Channels; when it is absent, it composes the allow-list andaudiences from the old keys. This is a bounded, documented compatibility read,
not a silent degradation.
configs migrate as operators touch them.
then removes the old keys from the schema.
Required proof (Automation Floor)
identical before and after migration.
Sequencing
Do this as its own OpenSpec change. The hard part is the backwards-compatible
migration above, not the C# refactor. The
MentionRequiredInThreadper-channelsetting should fold into this object model rather than ship as a separate map.
Related
MentionRequiredInThreadbool, merged todev; to be removed)MentionRequiredInThread)