@cool-ai/eslint-plugin-beach
v0.4.0
Published
ESLint rules that enforce Beach's architectural commitments — channel-blind interior, no geometry tokens in prompts, escalated-bypass justification.
Downloads
60
Readme
@cool-ai/eslint-plugin-beach
ESLint rules that enforce Beach's architectural commitments. The rules are mechanical guardrails for the design rules consumers absorb when they integrate Beach — channel-blind interior, surface-agnostic prompts, escalated-bypass justification.
Home: cool-ai.org · Documentation: cool-ai.org/docs
Install
npm install --save-dev @cool-ai/eslint-plugin-beach eslintESLint v9 (flat config) is required.
Use the recommended preset
// eslint.config.js
import beach from '@cool-ai/eslint-plugin-beach';
export default [
beach.configs.recommended,
];The preset enables all four rules at error. Each rule can also be configured individually if you want to opt in selectively, override defaults, or downgrade severity:
import beach from '@cool-ai/eslint-plugin-beach';
export default [
{
plugins: { beach },
rules: {
'beach/no-channel-mode-branching': 'error',
'beach/no-channel-types-in-non-adapter-packages': ['error', {
channelPackages: ['@cool-ai/beach-channel-whatsapp', /* ... */],
adapterPackagePatterns: ['/packages/channel-', '/packages/beach-channel-'],
}],
'beach/no-geometry-tokens-in-prompts': ['error', {
geometryTokens: ['above the fold', /* ... */],
promptIdentifiers: ['prompt', 'systemPrompt', /* ... */],
}],
'beach/escalated-bypass-required': ['error', {
bypassPropertyNames: ['bypass', 'rendererBypass'],
}],
},
},
];The rules
beach/no-channel-mode-branching
Forbids branching on session.channelMode inside handler interior code. The interior is channel-blind by Beach's design.
The interior is channel-blind by contract. Variation across channels lives in destinations, manifest contributors, completion-trigger configuration, or filter rules — never in if (channelMode === ...) branches.
// ✗ violates the rule
if (session.channelMode === 'batched') { /* ... */ }
switch (session.channelMode) { case 'streaming': /* ... */ }
// ✓ instead — express the variation in the wiring
const collector = config.channelMode === 'batched' ? new BatchedChannelCollector(...) : null;beach/no-channel-types-in-non-adapter-packages
Forbids importing channel-specific transport types (e.g. WhatsAppMessage, EmailMessage) in non-adapter packages. Adapter packages — those matching packages/channel-*, packages/beach-channel-*, packages/beach-a2ui-renderer-* — are the only places where channel identity is allowed to leak.
// in packages/session/... — ✗ violates the rule
import { WhatsAppMessage } from '@cool-ai/beach-channel-whatsapp';
// in packages/beach-channel-whatsapp/... — ✓ permitted
import { WhatsAppMessage } from '@cool-ai/beach-channel-whatsapp';The default channelPackages list covers the @cool-ai/beach-channel-* adaptors (email, whatsapp, sse, a2a, mcp, webhook, cron). Override via the rule's option to add custom channel packages.
beach/no-geometry-tokens-in-prompts
Forbids geometry / layout tokens (e.g. "above the fold", "tap the button", "scroll down") in string properties whose key looks like a prompt (prompt, systemPrompt, instructions, persona, etc.). The handler produces channel-blind, surface-agnostic content; geometry is expressed in composers, not prompts.
// ✗ violates — the handler must not direct surface geometry
const config = { prompt: 'Tell the user to tap the button to confirm.' };
// ✓ — surface-neutral instruction; the composer renders the call-to-action
const config = { prompt: 'Confirm the booking with the user.' };The default token list is conservative; consumers extend via the geometryTokens option.
beach/escalated-bypass-required
Requires that any per-renderer surface-parity bypass declaration set escalated: true with a non-empty reason. A bypass without escalation indicates the contract was waived without an explicit decision — the rule fails CI to surface it for review.
// ✗ — missing escalation
const cfg = { bypass: { reason: 'WhatsApp template constraint' } };
// ✗ — escalated: false is not a valid bypass
const cfg = { bypass: { escalated: false, reason: 'temp workaround' } };
// ✓ — explicit escalation + non-empty reason
const cfg = { bypass: { escalated: true, reason: 'WhatsApp template constraint forces flat layout' } };The default bypassPropertyNames covers bypass, rendererBypass, surfaceBypass, parityBypass. Override via the rule's option for custom property names.
Why this exists
Beach's standing rules are absorbed by consumers when they integrate. Without mechanical enforcement, the rules drift. This plugin is the mechanical guardrail — every rule corresponds to a Beach architectural commitment, and CI catches the violation before it lands.
The plugin is published independently of @cool-ai/beach-core so consumers can adopt the rules at their own cadence; Beach's own code is checked by the same plugin via the precommit and prepush gates.
Licence
Apache 2.0 — see LICENSE.
