本文へ移動
cccskills
無料GitHub で公開

design-an-interface

Designs the public interface of a module before any implementation exists: writes the caller scenarios first, produces three candidates with genuinely different shapes, type-checks each one against the scenarios, scores them on measurable criteria and records the decision. Use when someone says "design the API for this module", "what should this signature look like", "compare interface options", "this function is awkward to call", "show me alternatives before I build it", or is about to add a module, library entry point, endpoint or command that other code will depend on.

インストール方法を見る

含まれるファイル(2)

  • SKILL.md15.2 KB
  • _scores.json1.6 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Design an Interface

Overview

An interface is everything a caller must know to use a module: the names, the parameter and result types, the order calls have to happen in, how failures show up, and what is promised to stay stable. It is the part that costs most to change later, because every caller is written against it. This skill explores the alternatives while they are still free: caller scenarios come first, then three candidates of different shape, then a proof that each candidate can express the scenarios, a scored comparison and a short decision record. No implementation is written at any point.

Instructions

1. Collect the facts

Read the project before asking anything:

  • Callers. Search for existing or future call sites (grep -rn "sendNotification(" src/), count them, and note which arguments vary and which are always the same.
  • House conventions. How do neighbouring modules report errors (exceptions or result values), are they async, how are dependencies passed in, how are things named?
  • Stability promise. If every caller lives in this repository the interface can change later with one refactoring commit. If it is published (a package, a public HTTP route, a CLI) every incompatible change costs a major version under Semantic Versioning and a migration for people you cannot reach.

Then ask the user only what the code cannot answer: the three to five things callers need to get done, hard constraints (latency, ordering, idempotency, permissions), two changes likely in the next year, and what is out of scope.

2. Write the caller scenarios first

Write three to five scenarios, each one a concrete task from a caller's point of view with real inputs and the outcome it expects. Include at least one failure path and one test scenario (how a caller's own test controls or replaces the module). The scenarios are the yardstick for every later step, so get the user to confirm them before drafting signatures.

3. Draft three candidates of different shape

Give each candidate one shape from this list, choosing three that could plausibly fit:

ShapeWhat calling it feels likeUsually suits
Plain functionsdata in, data out, nothing to hold on tostateless transformations, one-shot operations
Handle objectcreate once with configuration, then call methodsconnections, shared settings, anything with a lifecycle
Declarative tabledescribe the wanted result as data, the module works out the stepsrules, schemas, routing, pipelines
Stream or eventsiterate or subscribe as results arrivelong-running, large or incremental work
Ecosystem mirrorlooks like a library the callers already usecases where familiarity beats invention

Where sub-agents exist (Claude Code), start all three in one message so they run concurrently and cannot see each other's drafts. Elsewhere, write them one after another, each time starting again from the scenarios. Every drafter receives the facts, the scenarios, its assigned shape and this required output:

  1. Declarations only: types and signatures, no bodies.
  2. Every scenario written as real calling code against those declarations.
  3. What a caller has to know, and what the module keeps to itself.
  4. The single channel through which each failure is reported.
  5. For each of the two expected changes: does the interface absorb it, grow, or break?

Then run the difference test: put the scenario 1 code of the candidates side by side. If two differ only in names they are the same design; drop one and assign an unused shape.

4. Prove each candidate against the scenarios

Put the declarations and the scenario code in a scratch folder and run the type checker. Nothing is implemented; the question is only whether the scenarios can be written and whether wrong calls are rejected.

mkdir -p design/rate-limit && cd design/rate-limit
cat > tsconfig.json <<'JSON'
{ "compilerOptions": { "strict": true, "noEmit": true, "target": "ES2022", "module": "ESNext",
    "moduleResolution": "Bundler", "skipLibCheck": true, "types": ["node"] }, "include": ["*.ts"] }
JSON
# candidate-a.ts + scenarios-a.ts, candidate-b.ts + scenarios-b.ts, ...
npx -p typescript tsc -p .        # exit 0 = every scenario can be written
  • Write the declarations in ordinary .ts files (export declare function …), not .d.ts: skipLibCheck skips every .d.ts, so a misspelled type in a candidate would pass unnoticed.
  • Mark each wrong call that must be rejected with // @ts-expect-error. The compiler then fails with error TS2578 if the interface accepts the call after all.
  • Python: declare the interface as a typing.Protocol plus dataclasses, write the scenarios as functions, run mypy --strict notifier_draft.py scenarios.py. A deliberate misuse carries # type: ignore[arg-type]; strict mode reports an unused ignore, so an interface that accepts the misuse fails the check.
  • Go and Rust: an interface or trait in a scratch package, scenario functions next to it, go vet ./... or cargo check.
  • HTTP and CLI surfaces have no type checker. The declarations are the route or command table with request and response bodies, and each scenario is the exact request or command line a client would send.

5. Score on evidence

MeasureHow to get it
Scenario fitscenarios that needed no workaround, out of all scenarios
Surfacenames a caller must learn for the main scenario, and in total
Call-site costlines of caller code in the most frequent scenario
Misusewrong calls that still type-check; list each one
Leaksimplementation facts visible in the signatures: storage, vendor, internal ids, call order
Change toleranceeach expected change: no change, additive or breaking
Test seamcan a caller's test substitute a hand-written fake that satisfies the type?
Failure channeldoes every failure have exactly one way to surface?

Scenario fit is a gate: a candidate that cannot express a scenario is repaired or dropped. After that, weigh fewer misuse paths and fewer leaks above shorter call sites, and when two candidates are level take the smaller surface, since a name can be exported later but never taken back without breaking someone. A hybrid is allowed only if it goes through step 4 again.

6. Record the decision and stop

Write the result where the project keeps design notes (an existing docs/adr/ or docs/design/ folder; otherwise propose docs/design/):

# Rate limiter interface
Status: accepted 2026-10-01. Callers: API gateway, login route, webhook worker.
## Scenarios
## Chosen interface      (the declarations, copied from the file that passed step 4)
## Comparison            (the score table)
## Why not the others    (one paragraph each, naming the measure that decided it)
## Compatibility         (stability promise; how the two expected changes will land)
## Open questions

Hand the scenario file over as the first tests of the future implementation. Building the module is a separate task.

7. When the interface already has callers

  • Group the existing call sites by argument pattern. The combinations really in use are the scenarios; the unused ones are the first things to remove.
  • Plan the change in three phases: add the new interface beside the old one, move the callers in batches while the old entry point forwards to the new one and emits a deprecation warning, then delete the old entry point once a search finds no callers.
  • For a published package the deprecation ships in a minor release and the removal waits for the next major.

Examples

Example 1: a new rate limiter (TypeScript)

Harbor Lane runs an invoicing API on Express with Redis. Facts from the code: errors are thrown, everything is async, three places need limiting. Expected changes: a token bucket instead of a fixed window, and limits that depend on the customer's plan.

Scenarios: S1 the gateway allows 120 requests a minute per API key and answers 429 with Retry-After; S2 login allows 5 attempts per 15 minutes per email and a successful login clears the count; S3 the webhook worker sends at most 10 deliveries a second per endpoint and waits instead of failing; S4 a test moves time forward without sleeping.

Candidates: A plain functions hit(store, key, rule, now?) and reset(store, key); C a declarative table defineLimits({...}) returning Express middleware; B a handle with named policies:

// candidate-b.ts
export type Span = `${number}${'s' | 'm' | 'h'}`
export type Verdict = { allowed: true; remaining: number } | { allowed: false; retryAfterMs: number }
export interface Policy {
  take(key: string): Promise<Verdict>
  forgive(key: string): Promise<void>
  waitFor(key: string, opts?: { signal?: AbortSignal }): Promise<void>
}
export interface LimiterOptions { store?: 'memory' | { redisUrl: string }; clock?: () => number }
export declare function createLimiter(options?: LimiterOptions): {
  policy(name: string, rule: { limit: number; per: Span }): Policy
}
// scenarios-b.ts (excerpt)
import { createLimiter } from './candidate-b'
const limiter = createLimiter({ store: { redisUrl: process.env.REDIS_URL ?? 'redis://127.0.0.1:6379' } })
const api = limiter.policy('api', { limit: 120, per: '1m' })
export async function guard(apiKey: string) {                      // S1
  const verdict = await api.take(apiKey)
  return verdict.allowed ? { status: 200 } : { status: 429, retryAfter: Math.ceil(verdict.retryAfterMs / 1000) }
}
// @ts-expect-error per must be a span such as '15m', not a bare number
limiter.policy('bad', { limit: 5, per: 900 })

npx -p typescript tsc -p . exits 0 for all three candidates. Scores:

MeasureA functionsB policiesC table
Scenario fit3 of 4: S3 needs a sleep loop in the caller4 of 42 of 4: no reset, nothing outside HTTP
Surface, main / total4 / 54 / 92 / 2
Call-site lines, S1321
Misuse that compiles3: rules can differ per call site, now usable in production, hand-built keys collide1: same policy name declared twice0
Leaksstore type, window algorithm (windowMs)Redis URLExpress request type
Token bucket laterbreakingno changeno change
Per-plan limits laterno changeadditive: limit may become a functionadditive
Test seamyes, via nowyes, via clockonly through HTTP

Decision: B. It is the only candidate that passes the gate. C's one-line middleware is kept as a possible helper built on top of B, and the Redis URL in the options is logged as an open question.

Example 2: reshaping a function with 23 callers (Python)

A dental clinic's booking backend has send_notification(patient_id, kind, title, body, urgent=False, email=True, sms=False, push=False, retries=3). Grouping the 23 call sites shows three patterns only: 14 reminders (email and SMS), 6 cancellations (all channels, urgent), 3 recalls (email). No caller ever picks channels freely, and nine files assemble the same wording by hand. The chosen candidate lets callers name the event and moves channels, wording and retries inside:

# notifier_draft.py: declarations only
from dataclasses import dataclass
from datetime import date, datetime
from enum import Enum
from typing import Protocol

class Channel(Enum):
    EMAIL = "email"
    SMS = "sms"
    PUSH = "push"

@dataclass(frozen=True)
class Reminder:
    appointment_id: int
    starts_at: datetime

@dataclass(frozen=True)
class Cancellation:
    appointment_id: int
    reason: str

@dataclass(frozen=True)
class Recall:
    due_on: date

@dataclass(frozen=True)
class Receipt:
    delivered: frozenset[Channel]
    failed: dict[Channel, str]

class Notifier(Protocol):
    def notify(self, patient_id: int, event: Reminder | Cancellation | Recall) -> Receipt: ...

The scenario file holds a reminder, a cancellation, a recall, a RecordingNotifier fake used by a caller's test, and one misuse: fake.notify(4012, "reminder") # type: ignore[arg-type].

$ mypy --strict notifier_draft.py scenarios.py
Success: no issues found in 2 source files

Rejected: keyword-only arguments on the old function (still nine files of duplicated wording) and a message object with explicit channels (keeps a choice no caller needs). Migration: the first pull request adds Notifier and turns send_notification into a forwarding wrapper that emits a DeprecationWarning; the next two move the 14 reminder call sites and then the other 9; the last deletes the wrapper when grep -rn "send_notification(" app/ returns nothing.

Guidelines

  • Three candidates that share one shape are one candidate with three spellings. The assigned shapes and the difference test exist to prevent that.
  • Do not score what was not proven. "Easier to use" needs the scenario code beside it; "more flexible" needs a scenario or an expected change that requires the flexibility.
  • A short call site bought by hiding a required decision in a default is a misuse path. Count it.
  • Flag these in any candidate: positional booleans, a parameter that is only passed through to another layer, methods that must be called in a fixed order, a result that mixes data with an error code, storage or vendor names in signatures.
  • Keep the implementation's convenience out of the comparison. How hard a candidate is to build matters only when it changes what callers experience, such as latency.
  • npx tsc without TypeScript installed runs an unrelated package named tsc; use npx -p typescript tsc. From TypeScript 6 on, passing file names next to a tsconfig.json is an error (hence -p), and @types packages load only when listed in types. The "types": ["node"] entry needs @types/node installed in the project the scratch folder sits in; where it is missing tsc stops with TS2688, so remove the entry and keep Node globals such as process out of the scenarios.
  • A type check proves the scenarios can be written, not that the design is right. Behaviour that types cannot express (ordering, idempotency, time limits) belongs in the scenario text and the decision record.
  • Skip the full procedure for a private helper with one caller: write it, and reshape it when a second caller arrives. Use it when at least two callers, a published surface or a costly migration are in play.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Scripts and configures production rendering in Autodesk 3ds Max with the V-Ray and Corona renderers: output size and files, render elements, denoising, light mix, batch and command-line rendering, and network rendering. Use when a user asks to set up a production render, render several cameras in one batch, render from the command line or on a render farm, add render passes for compositing, or cut render time for archviz and product shots.

日本語の概要は準備中です。原文の説明を表示しています。

TerminalSkills/skills1632026年10月4日 更新

Covers scripting Autodesk 3ds Max, the 3D modeling and rendering application, with MAXScript and Python (pymxs): scene manipulation, object creation, material assignment, camera and light setup, batch operations, and file I/O. Use when tasks involve automating repetitive 3ds Max workflows, batch processing scenes, running scripts headless with 3dsmaxbatch, creating custom tools, or scripting scene setup for archviz, product visualization, or VFX.

日本語の概要は準備中です。原文の説明を表示しています。

TerminalSkills/skills1632026年10月4日 更新

3proxy

無料

3proxy is a small open-source proxy server that runs HTTP/HTTPS, SOCKS4/5, SNI and TCP/UDP port-mapping proxies from one config file. Use when a user asks to set up an HTTP or SOCKS5 proxy, add proxy users and passwords, write 3proxy access rules, chain or rotate upstream (parent) proxies, limit bandwidth, connections or monthly traffic per user, run 3proxy in Docker, or fix a 3proxy.cfg that will not start.

日本語の概要は準備中です。原文の説明を表示しています。

TerminalSkills/skills1632026年10月4日 更新

Builds Agent2Agent (A2A) servers and clients, the open protocol (originally from Google, now under the Linux Foundation) that lets AI agents from different frameworks call each other. Use when the user wants to create an A2A-compliant agent, build an Agent Card, implement task management, connect agents across frameworks, set up agent discovery, handle streaming responses, implement push notifications, or orchestrate multi-agent workflows. Trigger words: a2a, agent to agent, agent2agent, a2a protocol, a2a server, a2a client, agent card, agent interoperability, agent collaboration, multi-agent, agent discovery, a2a sdk, a2a task.

日本語の概要は準備中です。原文の説明を表示しています。

TerminalSkills/skills1632026年10月4日 更新

Plans a controlled experiment (A/B test) so its result can be trusted: writes the hypothesis, picks one primary metric and the guardrails, computes sample size and run time, specifies how visitors are assigned and when exposure is logged, and reads out the result with a confidence interval. Use when someone says "set up an A/B test", "split test this page", "how many visitors do I need", "how long should the experiment run", "is this result significant", "can I stop the test early", or wants to test a headline, price, layout or onboarding change against the current version.

日本語の概要は準備中です。原文の説明を表示しています。

TerminalSkills/skills1632026年10月4日 更新

ably

無料

Ably is a hosted realtime messaging service: clients publish and subscribe to named channels over WebSockets, see who is present, replay message history, and resume after a dropped connection. Use when a user asks to "add realtime updates", "push live notifications to the browser", "show who is online", "add a chat room with typing indicators", "publish from a serverless function", or "authenticate Ably clients without exposing the API key". Covers the ably 2.x JavaScript SDK (Realtime and REST), JWT token authentication, presence, history and rewind, batch publishing, and the @ably/chat 1.x SDK.

日本語の概要は準備中です。原文の説明を表示しています。

TerminalSkills/skills1632026年10月4日 更新

TerminalSkills のスキルをすべて見る

このスキルの問題を報告する