@gridlock/channel-contract
v0.3.1
Published
Engine-generic channel-adapter contract. Type-only definitions for narrative channels: NPCChannel, PlayerAction, NarrativeEvent, ChannelAdapter, BroadcastAdapter, ChannelRepository. Game systems implement these interfaces over their chosen transport (What
Readme
@gridlock/channel-contract
Engine-generic, type-only contract for narrative channels. Game systems
on the gridlock engine implement these interfaces over their chosen
transport (WhatsApp, Discord, Telegram, iMessage, Slack, …); the dispatcher
routes events through any conforming ChannelAdapter without knowing the
transport implementation.
Lifted from @tic/channel-manager/src/types.ts under MS-33 #1608. The
source package is now a re-export shim — TIC consumers see no API change.
What's in here
| Category | Exports |
|---|---|
| Tier + transport | ChannelTier, ChannelTransport, ChannelOriginTransport |
| Provisioned-channel side | NPCChannel, NPCChannelStatus |
| Player side | PlayerChannelProfile |
| Outbound | NarrativeEvent, NarrativeContent |
| Inbound | PlayerAction, PlayerActionContent, ChannelOrigin |
| Adapters | ChannelAdapter, BroadcastAdapter, DeliveryResult, DeliveryMode |
| Persistence | ChannelRepository, CaseChannel, CaseChannelRepository |
Zero runtime logic. Every export is a type, an interface, or a string-literal union.
What's not in here (and why)
- Transport adapters (WhatsAppAdapter, DiscordAdapter, …) — these are product/transport-specific implementations. Gridlock owns the shape; the implementation lives where the deps live (the WhatsApp adapter pulls in Twilio, the Discord adapter pulls in discord.js, etc.).
- Dispatcher —
ChannelDispatcherstays in@tic/channel-managerfor now. The dispatcher's routing rules contain TIC-specific assumptions (channel-manager observability projection, retry policy defaults) that haven't been audited yet. - Narrative event kinds — the typed payload shapes for
dialogue.response,sage.analysis,case.broadcast, etc. live in@tic/channel-manager/src/narrativeEventKinds.tsbecause they encode product-specific concepts (SAGE, accusation, case files). The genericNarrativeEventenvelope lives here; the specific payloads are product-side.
Residual TIC-flavour (back-compat)
Three identifiers kept for back-compat:
NarrativeEvent.caseId— engine-generic concept ("scenario id"), name is a TIC holdover.CaseChannelinterface — engine-generic broadcast target; name is a TIC holdover.CaseChannelRepositoryinterface — same.
A future PR can introduce scenarioId / ScenarioChannel aliases and
deprecate the TIC names. Renaming today would force a runtime translation
layer that buys nothing for the engine extraction effort.
Usage from a third-party adapter
import {
ChannelAdapter,
ChannelTransport,
DeliveryResult,
NarrativeEvent,
NPCChannel,
PlayerChannelProfile,
} from '@gridlock/channel-contract';
export class TelegramAdapter implements ChannelAdapter {
transport: ChannelTransport = 'discord' as ChannelTransport;
// ...
async send(
event: NarrativeEvent,
recipient: PlayerChannelProfile,
source: NPCChannel,
): Promise<DeliveryResult> {
// Talk to the Telegram Bot API, return delivery result.
return { status: 'delivered', providerMessageId: 'tg-1', attempts: 1 };
}
}(Note: ChannelTransport is a fixed 'whatsapp' | 'discord' union today.
A new entry for 'telegram' lands as part of the reference Telegram
adapter PR, #1609.)
Acceptance suite (@gridlock/channel-contract/acceptance, #1610)
Adapter authors can run a ready-made acceptance suite against their implementation to prove it conforms to the contract:
import { runAdapterAcceptanceSuite } from '@gridlock/channel-contract/acceptance';
import { TelegramAdapter } from './TelegramAdapter';
runAdapterAcceptanceSuite({
name: 'TelegramAdapter',
createAdapter: () => new TelegramAdapter({ token: 'fake' }),
successFixture: { event, recipient, source }, // happy path
failureFixtures: [ // optional negatives
{ scenario: 'rate-limit', event, recipient, source, expectedErrorCode: 'RATE_LIMIT' },
{ scenario: 'unknown-recipient', event, recipient, source },
],
inbound: { // optional inbound parser
rawPayload: { ... },
parse: (raw) => parseInbound(raw),
expected: { playerId: 'p1', targetNpcId: 'alice', contentKind: 'text' },
},
});Two entry points:
runAcceptanceChecks(config)returnsArray<{ name, ok, error? }>for programmatic inspection.runAdapterAcceptanceSuite(config)registers onenode:testper check for nice CI output.
The suite covers: outbound delivery shape (status, providerMessageId, error, attempts), the no-throw-on-success invariant, failure-fixture scenarios with optional error-code pinning, and an optional inbound parser check. It calls only contract methods — the same suite works for any transport.
@tic/channel-manager wires the suite into
tests/AdapterAcceptance.test.ts against WhatsAppAdapterStub +
DiscordAdapterStub.
Tests
- 19 type-shape conformance tests in
tests/contract.test.ts— each constructs a literal of the contract type so a rename/remove fails to compile. - 12 acceptance-meta tests in
tests/acceptance.test.ts— drive the suite against working + deliberately broken stubs to verify clear diagnostics ("broken adapter fails clearly" — #1610 acceptance).
pnpm --filter @gridlock/channel-contract test # 31 / 31 pass
pnpm --filter @gridlock/channel-contract typecheck # tsc --noEmit