Files
suaveplan/openspec/archive/2026-05-18-add-slack-client/proposal.md
2026-05-27 23:29:53 +01:00

6.5 KiB

Add @suaveplan/slack-client

Package: @suaveplan/slack-client Status: PROPOSED Type: new-package

Why

Genesis needs a typed, deterministic, OpenTelemetry-instrumented Slack client for transactional messaging (alerting, notifications, ops bots). The upstream @suaveplan/slack-client@1.0.1 at ../suaveplan/packages/universal/slack-client/ works but does not meet genesis enterprise-quality bar — it monkey-patches globalThis.fetch in tests (vi.stubGlobal), has no Zod boundary validation, no OTel meters, no injectable Clock/Rng/Logger, leaks as unknown casts, and registers codes through the legacy ErrorCodes.registerMany runtime registry instead of defineCodes. This change ships a fresh build at enterprise quality with full functional parity.

What changes

  • ADDED: @suaveplan/slack-client@0.1.0 under packages/messaging/slack-client/
  • ADDED: SlackApiClient — Slack Web API: sendMessage, updateMessage, deleteMessage, listChannels, getChannelInfo, listUsers, getStatus
  • ADDED: SlackWebhook — Slack incoming-webhook client: send, sendText
  • ADDED: SLACK_CODES registry (defineCodes("SLACK", [...])) with 8 reasons: API_ERROR, AUTH_ERROR, RATE_LIMITED, CHANNEL_NOT_FOUND, WORKSPACE_NOT_FOUND, CONFIG_MISSING, WEBHOOK_FAILED, TIMEOUT
  • ADDED: 8 typed error subclasses extending SuaveplanError<SlackErrorCode> (one per reason)
  • ADDED: Zod schemas at every public boundary: SlackApiClientOptionsSchema, SlackWebhookOptionsSchema, SlackMessageSchema, SlackBlockSchema, SlackBlockElementSchema, SlackTextObjectSchema
  • ADDED: OTel meter instruments — slack.requests, slack.errors, slack.latency_ms, slack.webhook_sends — emitted from every production hot path
  • ADDED: Injectable transport bag — Clock, Rng, FetchLike, LoggerLike — all from @suaveplan/types
  • ADDED: Deterministic FakeFetch test double under src/fakes/ for parity tests (no vi.mock anywhere)
  • ADDED: Retry-After parser supporting delay-seconds, HTTP-date, and x-ratelimit-reset epoch/ISO
  • ADDED: Slack-error string classifier (invalid_auth / channel_not_found / team_not_found families)

Out of scope

None. The package ships at full functional parity with the upstream package plus the enterprise upgrades demanded by enterprise/slack-client.md §3.

Source mapping

This change derives from ../suaveplan/enterprise/slack-client.md and the working implementation at ../suaveplan/packages/universal/slack-client/src/.

Requirement Source lines Notes
Generic Slack API client + webhook surface enterprise/slack-client.md L10-L13 verbatim
Zero browser/Node/React deps (universal tier) enterprise/slack-client.md L37 verbatim
Zod schemas at every boundary enterprise/slack-client.md L38 verbatim
Deterministic — no module-scope Math.random / Date.now, injectable clock/RNG enterprise/slack-client.md L40 verbatim
OpenTelemetry instrumentation via @opentelemetry/api enterprise/slack-client.md L41 verbatim
Structured errors extending @suaveplan/error with stable codes enterprise/slack-client.md L42, L61 verbatim
AbortSignal cancellation everywhere async enterprise/slack-client.md L43 verbatim
Idempotency + retry primitives wired enterprise/slack-client.md L44 applied to Retry-After / rate-limit; cooperative retry left to caller's @suaveplan/retry (no double-wrap)
i18n-ready (no hard-coded user-visible strings) enterprise/slack-client.md L46 every thrown error carries a stable code + structured context; messages are templated
Test coverage 100% statement/branch/function/line (istanbul) enterprise/slack-client.md L50 + genesis CLAUDE.md "Quality" genesis policy is 100%, stricter than the 95% in the source
File-size budget 250/300 soft/hard, 500 test enterprise/slack-client.md L52 + CLAUDE.md enforced by manual split
No mocks — injectable fakes only enterprise/slack-client.md L53 + CLAUDE.md rule 2 verbatim
sideEffects: false enterprise/slack-client.md L55 verbatim
Biome clean, zero suppressions enterprise/slack-client.md L58 + CLAUDE.md rule 9 verbatim
TypeScript strict, no any / ts-ignore enterprise/slack-client.md L59 verbatim
Error hierarchy with typed discriminants enterprise/slack-client.md L61 verbatim
chat.postMessage, chat.update, chat.delete, conversations.list, conversations.info, users.list upstream packages/universal/slack-client/src/core/slack-api-client/slack-api-client.ts L52-L138 parity
Webhook POST with text + blocks upstream packages/universal/slack-client/src/core/slack-webhook/slack-webhook.ts L41-L105 parity
HTTP 429 Retry-After / x-ratelimit-reset parsing upstream slack-api-client.ts L259-L284 parity
Slack-error → code classifier (invalid_auth, channel_not_found, team_not_found) upstream slack-api-client.ts L286-L313 parity

Requirements with no source line

Requirement Justification
100% coverage threshold (vs. 95% in source) Genesis-wide policy — CLAUDE.md Quality section
Test runner imports MUST go through @suaveplan/testing/runner Genesis rule 11 — not applicable to upstream which predates the rule
defineCodes("SLACK", […]) registry style Genesis-wide error-codes convention — upstream used the legacy ErrorCodes.registerMany API
FakeFetch test double + setup polyfills wiring real MeterProvider Genesis pre-archive checklist row 5 (OTel asserted with real MeterProvider, not vi.mock)

Verification approach

  • src/client/slack-api-client.test.ts — pins every SlackApiClient method scenario with FakeFetch
  • src/webhook/slack-webhook.test.ts — pins every SlackWebhook scenario with FakeFetch
  • src/errors/errors.test.ts — pins error-code identity and message keys for all 8 codes
  • src/schemas/schemas.test.ts — Zod boundary parse + safeParse assertions for every public schema
  • src/client/retry-after.test.ts — pins every Retry-After header form (delay-seconds, HTTP-date, x-ratelimit-reset epoch + ISO, missing)
  • src/client/classify.test.ts — pins HTTP-status and slack-error-string classifiers
  • src/otel/metrics.test.ts — asserts the four OTel instruments emit on every production path, via real in-memory MeterProvider
  • src/fakes/fake-fetch.test.ts — pins the deterministic test double itself
  • src/index.test.ts — pins the public-surface barrel

Migration impact

None — additive. No existing consumer of @suaveplan/slack-client exists in genesis.