一键导入
ls-tech-design
Transform a Feature Spec into implementable Tech Design with architecture, interfaces, and test mapping. Validates the spec as downstream consumer.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Transform a Feature Spec into implementable Tech Design with architecture, interfaces, and test mapping. Validates the spec as downstream consumer.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Write complete, traceable Epics using Liminal Spec methodology. Covers User Profile, Flows, Acceptance Criteria, Test Conditions, Data Contracts, and Story Breakdown.
Publish a detailed epic as two handoff-ready artifacts: a PO-friendly business epic with grouped ACs and a developer story file with full AC/TC detail and Jira section markers.
Explore product direction and produce a PRD before feature scoping. Use when requirements are unclear, scope spans multiple features, or stakeholder alignment is needed.
Orchestrate story-by-story implementation with agent teams. Manages teammates, routes verification, and makes judgment calls across the full implementation cycle.
Orchestrate the full Liminal Spec pipeline with agent teams. Manages drafters and verifiers from orientation through technically enriched stories.
Write a single functional story with epic-quality rigor. Story-sized epic for focused changes that don't warrant the full pipeline.
| name | ls-tech-design |
| description | Transform a Feature Spec into implementable Tech Design with architecture, interfaces, and test mapping. Validates the spec as downstream consumer. |
Purpose: Transform Epic into Tech Design with architecture, interfaces, and test mapping.
This phase is the downstream consumer of the Epic. If you can't design from it, the spec isn't ready. Validation is part of the quality gate.
This phase may produce one document or split based on complexity:
Before designing, validate the Epic:
If issues found → return to BA for revision. Don't design from a broken spec.
Once validated, produce:
Design from high to low. Don't skip levels. The template uses "High/Medium/Low Altitude" labels that map to these conceptual levels.
## System Context
### External Systems
- **Backend API:** REST endpoints at `/api/v1/*`
- **Guidewire:** Embedded iframe, URL parameter communication
- **Auth:** JWT tokens from parent application
### Entry Points
- Route: `/locations/add`
- Triggered by: Guidewire "Add Location" button
### Data Flow Overview
Guidewire → Embed with params → Fetch locations → User selects → Return data → Guidewire
## Module Architecture
src/features/add-location/
├── pages/
│ └── AddLocation.tsx # Route entry
├── components/
│ ├── LocationList.tsx # List display
│ └── LocationForm.tsx # Create form
├── hooks/
│ ├── useLocations.ts # Data fetching
│ └── useLocationSelection.ts # Selection state
├── api/
│ └── locationApi.ts # API client
└── types/
└── location.types.ts # Shared types
### Module Responsibilities
| Module | Responsibility | ACs Covered |
|--------|----------------|-------------|
| AddLocation | Route, layout, flow control | AC-1 to AC-5 |
| LocationList | Display, filter, select | AC-10 to AC-20 |
| useLocations | Fetch, cache | AC-6 to AC-9 |
// types/location.types.ts
export interface Location {
locRefId: string;
locRefVerNbr: number;
address: string;
city: string;
state: string;
postalCode: string;
}
// hooks/useLocations.ts
interface UseLocationsReturn {
locations: Location[] | undefined;
isLoading: boolean;
isError: boolean;
error: Error | null;
}
// components/LocationList.tsx
interface LocationListProps {
locations: Location[];
selectedIds: Set<string>;
onToggleSelection: (id: string) => void;
onAddToPolicy: () => void;
}
At each altitude, connect back to ACs and TCs:
sequenceDiagram
Note over User,Page: AC-10: User sees location list
User->>Page: Navigate to route
Page->>Hook: useLocations()
Note over Hook,API: AC-6: API fetches locations
Hook->>API: getLocations(sai)
API-->>Hook: Location[]
Hook-->>Page: { locations, isLoading }
Note over Page,User: AC-11: Table displays locations
Page-->>User: Render LocationList
Tech designs are verbose and intentionally rich.
This is NOT about being minimal. Build a sophisticated web of context.
The goal is redundant connections — multiple paths through the material so the model (and humans) can navigate complexity.
A web of weights around the material, not a thin thread.
If someone enters the design at the interface section, they should still understand why this interface exists (AC reference). If they enter at the module section, they should still see how data flows (sequence connection).
## Bad: Linear descent, minimal context
- System: calls API
- Module: LocationList
- Interface: LocationListProps
## Better: Rich connections
At 30,000 ft: "The system needs to display account locations (AC-10).
This requires a fetch from the XAPI..."
At 10,000 ft: "LocationList handles AC-10 through AC-20. It receives
locations from useLocations (established in system context above) and
displays them with selection capability (supporting the return flow
we'll detail at ground level)..."
At ground level: "LocationListProps includes selectedIds (supporting
AC-20 selection requirement) and onAddToPolicy (the return trigger
from our sequence diagram)..."
Mock at the API layer, not hooks.
// ✅ CORRECT
jest.mock('@/features/add-location/api/locationApi');
// ❌ WRONG
jest.mock('@/features/add-location/hooks/useLocations');
Why API boundary: Tests the real integration (Component → Hook → React Query → mock). Catches hook wiring bugs.
The test plan must explicitly map every TC from the Epic to a test. This is the Confidence Chain in action: AC → TC → Test → Implementation.
Test plan table format:
| TC | Test File | Test Description | Status |
|---|---|---|---|
| TC-6a | AddLocation.test.tsx | shows loading during fetch | Planned |
| TC-6b | AddLocation.test.tsx | hides loading after fetch | Planned |
| TC-10a | LocationList.test.tsx | renders location rows | Planned |
Rules:
→ See the Testing reference section in this skill for test code organization patterns.
Break work into manageable pieces. Each chunk becomes a story or set of stories. The chunk is the Tech Lead's unit of decomposition; chunks inform how stories are organized when the epic is published (usually 1:1, sometimes a chunk splits into multiple stories or merges with another).
Chunks are vertical slices (by functionality):
Phases are horizontal stages (by workflow):
The relationship: each chunk goes through all phases.
Chunk 0 (Skeleton) → Chunk 0 (Red) → Chunk 0 (Green) →
Chunk 1 (Skeleton) → Chunk 1 (Red) → Chunk 1 (Green) → ...
Some teams prefer completing all skeletons first (all Chunk Skeletons, then all Chunk Reds). Choose based on team preference and dependency structure. The default is vertical: complete each chunk fully before starting the next.
NotImplementedError)## Chunk 1: Initial Load
**Scope:** Page component, data fetching, loading/error states
**ACs:** AC-1 to AC-9
**TCs:** TC-1a through TC-9b
**Files:**
- src/features/add-location/pages/AddLocation.tsx
- src/features/add-location/hooks/useLocations.ts
- src/features/add-location/api/locationApi.ts
**Relevant Tech Design Sections:** §System Context — Data Flow,
§Module Architecture — AddLocation Page, §Low Altitude — useLocations Hook,
§Flow 1: Initial Load Sequence, §Testing Strategy — Initial Load Tests
**Non-TC Decided Tests:** Empty state render (no locations), loading
skeleton timing assertion (Tech Design §Testing Strategy)
**Test Count:** 12 tests + 2 non-TC
**Running Total:** 14 tests
The "Relevant Tech Design Sections" field lists which headings from this tech design are relevant to the chunk. This directly supports story creation: when publishing the epic, these references help select which tech design content is relevant to each story's Technical Design section.
The "Non-TC Decided Tests" field lists tests this chunk needs that aren't 1:1 with a TC -- edge cases, collision tests, defensive tests. These must be carried forward into stories during technical enrichment so they aren't lost.
Chunk 0 → Chunk 1 → Chunk 2
↘ ↗
Chunk 3
Before handing off:
Self-review (CRITICAL):
The BA/SM validates by confirming they can derive stories from the design. The Tech Lead validates by confirming they can add story-level technical sections from the design. If either can't, the design isn't ready.
→ Verification prompt: examples/tech-design-verification-prompt.md — Ready-to-use prompt for external validation before handoff
The tech design expands significantly from the epic — typically 6-7x. The design includes:
The verbose, spiral style is intentional. It creates the redundant connections that help both humans and models navigate the complexity.
Every line of code traces back through a chain:
AC (requirement) → TC (test condition) → Test (code) → Implementation
Validation rule: Can't write a TC? The AC is too vague. Can't write a test? The TC is too vague.
This chain is what makes the methodology traceable. When something breaks, you can trace from the failing test back to the TC, back to the AC, back to the requirement.
Upstream = more scrutiny. Errors compound downward.
The epic gets the most attention because if it's on track, everything else follows. If it's off, everything downstream is off.
Epic: #################### Every line
Tech Design: #############....... Detailed review
Stories: ########............ Key things + shape
Implementation:####................ Spot checks + tests
This is the linchpin. Read and verify EVERY LINE.
BA self-review -- Critical review of own work. Fresh eyes on what was just written.
Tech Lead validation -- Fresh context. The Tech Lead validates the spec is properly laid out for tech design work:
Additional model validation -- Another perspective (different model, different strengths):
Fix all issues, not just blockers -- Severity tiers (Critical/Major/Minor) set fix priority order, not skip criteria. Address all issues before handoff. Minors at the spec level compound downstream -- zero debt before code exists.
Validation rounds -- Run validation until no substantive changes are introduced, typically 1-3 rounds. The Tech Lead also validates before designing -- a built-in final gate. Number of rounds is at the user's discretion.
Human review (CRITICAL) -- Read and parse EVERY LINE:
Still detailed review, but less line-by-line than epic.
Stories go through a two-phase validation reflecting their two-phase authoring.
Less line-by-line, more shape and completeness:
Story contract compliance check:
Consumer gate: could an engineer implement from this story alone, without reading the full tech design?
Spot checks + automated tests.
Liminal Spec uses this pattern throughout:
| Artifact | Author Reviews | Consumer Reviews |
|---|---|---|
| Epic | BA self-review | Tech Lead (needs it for design) |
| Tech Design | Tech Lead self-review | BA/SM (needs it for story derivation) + Tech Lead (needs it for technical sections) |
| Published Stories | BA/SM self-review | Engineer (needs them for implementation) |
If the Tech Lead can't build a design from the epic -> spec isn't ready. If the BA/SM can't derive stories from the epic -> epic isn't ready. If the Engineer can't implement from published stories + tech design -> artifacts aren't ready.
The downstream consumer is the ultimate validator.
How to run validation passes is left to the practitioner. This skill describes:
Leaves flexible:
Deep-dive guide for documentation that serves both humans and AI agents. Load when writing Epics and Tech Designs.
| Problem | Jump to |
|---|---|
| Everything feels flat / equal weight | Branches, Leaves, Landmarks • Before/After #1 |
| Altitude jump / reader lost | The Smooth Descent • Extended Example |
| Functional ↔ technical drift | The Weave • Before/After #3 |
| Unsure how deep to go | Bespoke Depth • Scoring Rubric |
| Complex diagram overwhelming | Progressive Construction |
| Fast final-pass check | Quick Diagnostic |
Most technical documentation uses one dimension: hierarchy. Categories, subcategories, sections. This works for reference lookup but fails for understanding. Good documentation uses three dimensions—and the third is what makes it work.
Dimension 1: Hierarchy — Parent/child relationships. What contains what.
Dimension 2: Network — Cross-references. What connects to what.
Dimension 3: Narrative — Temporal and causal flow. What leads to what, and why.
The third dimension is where context lives. Not just what things are, but how they connect, why they exist, and what happens when you use them.
This matters for AI agents because LLMs trained on internet text—50TB of messy, narrative, temporal human writing—encode knowledge using narrative substrate. When you conform to that structure, you get efficient encoding and better retrieval almost for free. The relationships you'd otherwise need to enumerate explicitly are encoded in the temporal flow.
Consider two ways to express the same information:
Enumerated (explicit relationships):
- ConversationManager exists
- Session exists
- ConversationManager creates Session
- Session handles messages
- Relationship: ConversationManager owns Session
Narrative (implicit relationships):
When a user starts chatting, ConversationManager creates a Session to handle
the conversation. The Session manages message history and coordinates with
external services. The manager holds the session reference throughout the
conversation lifecycle.
Both encode the same facts. The narrative version is shorter because relationships emerge from temporal flow ("when... creates... manages... throughout"). You don't enumerate "Relationship: A owns B"—the ownership is implicit in "manager holds the session reference."
For LLMs, narrative structure activates learned patterns from training data. For humans, narrative matches how we naturally think. We remember journeys better than lists.
Think of your document as a tree. Prose paragraphs establish branches—the structural limbs that hold everything together. Bullet lists hang leaves—specific details attached to their branch. Diagrams create landmarks—spatial anchors that help readers navigate.
This creates attentional hierarchy. When all information has equal visual weight (flat bullets, uniform paragraphs), readers and models spread attention evenly. Key insights get lost in noise.
Prose establishes context, importance, and relationships. It tells the reader why something matters before presenting what specifically exists. Two to five sentences. One clear point per paragraph.
Lists enumerate specifics after context is established. They hang from the branch that prose creates. Never more than two levels deep in any single section.
Good list usage:
OAuth tokens are retrieved from keyring storage where other CLI tools have
already obtained and stored them. We're not implementing OAuth flows—just
reading tokens.
Token locations:
- ChatGPT: ~/.codex/auth/chatgpt-token
- Claude: ~/.claude/config
Poor list usage:
Authentication:
- API keys supported
- OAuth supported
- ChatGPT tokens
- Claude tokens
- Token refresh
The second example forces readers to infer all relationships. The cognitive load is on the reader instead of the writer.
Diagrams encode spatial relationships that prose can't express efficiently. They create memory anchors through visual variation.
User Command → AuthManager → Check method
↓
API Key ──→ Config → Headers
↓
OAuth ──→ Keyring → Token → Headers
The same information in prose would take 3-4 sentences and lose the spatial relationship. Diagrams are compression.
Most well-structured sections land around:
This isn't prescription. Some sections need more diagrams (architecture overviews). Some need more lists (API references). The ratio is a compass: if you're at 90% bullets, you're probably missing branches. If you're at 100% prose, you're probably missing scannable specifics.
When a section feels off, check the ratio. Monotonous structure often explains the problem.
## Session Management
Conversations persist through the Session abstraction. When a user starts
chatting, ConversationManager creates a Session to hold conversation state—
message history, active provider, pending tool calls.
Sessions coordinate between three subsystems:
Session
├── MessageHistory (stores conversation)
├── ModelClient (sends to LLM)
└── ToolRouter (handles tool calls)
Key methods:
- `sendMessage(content)` — Format, send, process response, update history
- `getHistory()` — Return full message history for context
- `processToolCall(call)` — Route to executor, await result, append
The separation between Session and ConversationManager matters for testing.
Sessions test with mocked clients. Managers test lifecycle without message flow.
Five elements: branch paragraph, diagram landmark, detail leaves, closing branch. Complete concept.
Documentation exists at different altitudes. Higher = broader view, less detail. Lower = narrower focus, more specifics.
25,000 ft PRD: "The system enables collaborative AI conversations"
↓
15,000 ft Tech Approach: "ConversationManager orchestrates Sessions"
↓
10,000 ft Phase README: "Session.sendMessage() formats per provider spec"
↓
5,000 ft Checklist: "Task 3: Wire Session to ModelClient with retry"
↓
1,000 ft Code: const response = await client.send(formatted, { retries: 3 })
The failure mode isn't being at the wrong altitude—it's jumping altitudes without bridges.
Each document should bridge levels, not exist at one. Start higher than you'll finish. Descend gradually. Each level answers questions raised by the level above.
PRD says: "User can authenticate with ChatGPT OAuth" Reader asks: How does that work technically?
Tech Design says: "Read token from ~/.codex keyring" Reader asks: What's the implementation approach?
Phase Doc says: "Use keyring-store module, mock filesystem in tests" Reader asks: What are the specific tasks?
Checklist says: "1. Import keyring-store 2. Add getToken() wrapper 3. Create mock"
No gaps. Each level makes sense in context of the previous one.
The same capability should be visible at every altitude level:
| Altitude | Example |
|---|---|
| 25K (PRD) | User can start a conversation and receive a response |
| 15K (Approach) | ConversationManager wires CLI → Codex → ModelClient |
| 10K (Phase) | Implement createConversation(), wire CLI command, mock ModelClient |
| 5K (Checklist) | 1) Add CLI command 2) Implement createConversation 3) Write mocked test |
If a capability appears at one level but not another, something is missing. The ladder is the alignment test.
Epic (25K feet):
Users can execute tools (read files, run commands) through the AI assistant. The assistant requests permission before executing.
Reader understands: what capability exists, who controls it. Reader wonders: how does this work technically?
Tech Design - Overview (15K feet):
Tool execution flows through three components. Session detects when the model requests a tool call. ToolRouter matches the request to an executor. The CLI presents approval before execution proceeds.
Reader understands: which components, how they connect. Reader wonders: what are the specific interfaces?
Tech Design - Details (10K feet):
Session.processResponse() checks for tool_calls in model output. When found, it extracts the tool name and arguments, then calls ToolRouter.route(toolCall). Before execution, Session emits a 'tool_request' event that the CLI handler intercepts.
Reader understands: specific methods, data flow, event mechanism. Reader wonders: what are my implementation tasks?
Implementation Checklist (5K feet):
- Add tool_calls detection to Session.processResponse()
- Implement ToolRouter.route() with executor registry
- Wire CLI approval handler to 'tool_request' event
Each level answered the question raised by the previous level. That's the smooth descent.
Traditional process separates functional requirements ("user can chat") from technical design ("WebSocket with JSON-RPC"). Product writes the PRD, throws it over the wall, engineering writes the tech spec. The gap that opens between them is where projects fail.
Better: weave functional and technical together at every altitude level.
In high-level docs (mostly functional, touch technical):
In technical docs (mostly technical, ground in functional):
In implementation docs (deep technical, verify via functional):
The weave prevents drift. When functional and technical stay interlocked, you can't over-engineer (functional bounds what's needed) and you can't under-deliver (technical serves functional outcomes).
Technical tests without functional grounding: "Test that ConversationManager.createConversation() returns Conversation object"
This can pass while the user still can't chat. The test verified mechanism, not outcome.
Functional test criteria: "User can start conversation, send message, receive response"
The test name describes the user capability. The test implementation exercises the technical path. The assertion verifies functional success. This is the weave in action.
The anti-pattern: uniform depth across all topics. Everything documented to the same depth means nothing stands out.
Better: go deep where it matters, stay shallow where it doesn't.
Before diving into any topic, ask:
| Question | Deep if... | Shallow if... |
|---|---|---|
| Is this complex or simple? | Complex | Simple |
| Is this new or already done? | Novel | Existing |
| Is this critical or optional? | Critical | Optional |
| Will implementers struggle here? | High risk | Low risk |
Score each topic 1-5 on four dimensions, then sum:
| Score | Depth |
|---|---|
| 4-8 | Shallow (one paragraph, maybe a link) |
| 9-13 | Medium (2-3 paragraphs, small list) |
| 14-20 | Deep (multi-paragraph, diagram, examples) |
Example:
Instead of 10 topics × 500 tokens = 5,000 tokens of uniform depth:
The critical topics got the depth they need. The simple topics didn't waste tokens.
There's a difference between seeing a complex diagram and building one. People who build understand deeply. People who receive are overwhelmed.
When you whiteboard a system, you add one box at a time, connect it, then add the next. Each step scaffolds the next. Documentation should mimic that process.
Instead of dropping a 20-component diagram:
Step 1: System has three layers.
CLI → Library → External
Step 2: Library entry point is ConversationManager.
CLI → ConversationManager → External
Step 3: Manager coordinates Codex and Session.
CLI → ConversationManager → Codex → Session → External
Step 4: Session routes to ModelClient and ToolRouter.
CLI → ConversationManager → Codex → Session → ModelClient
↘ ToolRouter
By the final diagram, readers have constructed the understanding.
Progressive construction applies to concepts, not just diagrams. Revisit from multiple angles, each pass adding detail:
Each pass deepens without forcing a leap. The spiral guides into complexity without drowning.
Before:
CLI Features:
- Interactive REPL
- One-shot command mode
- JSON output flag
- Provider switching
After:
The CLI supports three interaction modes for different audiences. Interactive
REPL serves humans who want conversational flow. One-shot commands serve
automation and testing. JSON output serves programmatic consumption.
Modes available:
- Interactive: `codex` → enters REPL, `quit` to exit
- One-shot: `codex chat "message"` → execute and exit
- JSON output: Add `--json` flag for structured response
The prose establishes why (the branch). The bullets enumerate what (the leaves).
Before:
## Tool Execution
The system enables AI-assisted tool execution.
const result = await executor.run(tool, args, { timeout: 30000 });
After:
## Tool Execution
The system enables AI-assisted tool execution, where the model can request
actions like reading files, running commands, or making API calls.
Tool execution follows a request-approve-execute cycle. The model requests
a tool call, the system presents it for approval, execution runs sandboxed,
and results return to the model.
Model Request → Approval Gate → Executor → Result → Model
const result = await executor.run(tool, args, { timeout: 30000 });
Three altitudes (concept → mechanism → implementation), smooth descent between each.
Before:
## Authentication Implementation
AuthManager reads from config.toml or keyring. API keys use ConfigReader.
OAuth tokens use KeyringStore.
Implementation:
- ConfigReader.get('api_key')
- KeyringStore.retrieve(provider)
After:
## Authentication Implementation
Users authenticate through two paths: API keys (for personal accounts) and
OAuth tokens (for reusing existing ChatGPT or Claude subscriptions).
AuthManager abstracts this choice. When a user starts a conversation, the
manager checks the configured auth method. For API keys, it reads from config.
For OAuth, it retrieves tokens from keyring.
This abstraction enables provider switching without re-authentication—users
configure once, the system handles the rest.
Technical components:
- ConfigReader: Load from config.toml or environment
- KeyringStore: Retrieve OAuth tokens (path varies by provider)
Opens with functional context. Shows mechanism. Grounds in benefit. Then enumerates components.
Symptom: Every section is bullets. No paragraphs. Lists all the way down. Fix: Add prose branches. Explain why the list matters before presenting it.
Symptom: Dense paragraphs. No lists. No diagrams. No variation. Fix: Break up with lists for enumerations, diagrams for spatial relationships.
Symptom: Document bounces between vision and implementation randomly. Fix: Pick an altitude and stay there, or descend smoothly. Don't yoyo.
Symptom: Describes mechanisms without purpose. Fix: Ground in functional outcome. "When a user sends a message, ConversationManager... This enables users to..."
Symptom: Edge cases before establishing normal path. Fix: Normal path first, edge cases second.
Symptom: Diagram appears without prose context. Fix: Introduce diagrams with prose, then reference them. The diagram confirms; prose explains.
This reference will be loaded by AI agents writing Epics and Tech Designs. Understanding how agents read—and fail to read—makes the difference between documentation that works and documentation that wastes context.
Humans skim, backtrack, ask questions, fill gaps with intuition. Agents process sequentially, can't ask clarifying questions, and treat ambiguity as noise rather than invitation.
Human reading: Scan headings → jump to relevant section → skim for keywords → read closely when relevant → ask if confused.
Agent reading: Load document into context → process sequentially → attempt task → fail or succeed based on what was explicit.
This means:
Every token of documentation is a token not available for reasoning or code generation. Agents operate under hard context limits. This creates pressure for compression—but compression mustn't sacrifice clarity.
The solution is signal density: every token earns its place.
Low signal density:
The configuration system is designed to be flexible and extensible.
It supports multiple configuration sources and can be extended by
implementing the IConfigSource interface.
High signal density:
Configuration loads from config.toml. Extend via IConfigSource interface.
See /src/config/ for implementation.
Same information, half the tokens. Strip:
| Implicit for Humans | Explicit for Agents |
|---|---|
| "Handle errors appropriately" | "Catch ConfigError, log message, return null" |
| "Test this thoroughly" | "Write tests for: valid input, empty input, malformed input" |
| "Wire up the components" | "Import X from Y, instantiate with config, pass to Z constructor" |
| "Follow the pattern from Phase 1" | "Copy the approach from Session.sendMessage(): validate → transform → execute → handle result" |
For any task, agents need these layers explicit:
Before (human-readable):
Implement the auth flow. Make sure it works with both API keys and OAuth.
After (agent-executable):
Implement AuthManager.authenticate():
Scope:
- Implement API key and OAuth paths
- Do NOT implement token refresh (out of scope for Phase 1)
Inputs:
- AuthConfig from config.toml (method: 'api_key' | 'oauth', credentials)
- Existing KeyringStore for OAuth token retrieval
Outputs:
- Returns AuthToken { token: string, expiresAt: Date }
- Throws AuthError on failure
Sequence:
1. Read config.method
2. Branch: API key → read from config, OAuth → read from keyring
3. Validate token format
4. Return AuthToken
Verification:
- Test: API key path returns token from config
- Test: OAuth path retrieves from keyring mock
- Test: Invalid config throws AuthError
The second version is longer but executable. An agent can complete it without asking questions.
Agents often read documents in isolation—loaded into fresh context without the conversation history that produced them. The document must stand alone.
Test: Cover the rest of the document. Read only this section. Could you complete the work described?
If yes: section is self-contained. If no: identify what's missing and add it.
For large specifications, don't load everything into every context. Instead:
This mirrors bespoke depth at the document level. Load what matters for this task; link to the rest. Mention what exists even if you don't include it—agents can request additional context if they know it exists.
When a section feels wrong but you can't identify why:
When you drift—and you will—run through:
If any answer is "no," rewrite that paragraph or section. This loop is the fast path to high-signal documentation.
This document should demonstrate what it teaches:
If this document is comprehensible and useful, the principles work.
Remember: You're not following rules. You're thinking about how information encodes and transmits. The patterns are heuristics that usually work. When they don't fit, understand why and adapt.
Service mocks are in-process tests at public entry points. They test as close to where external calls enter your code as possible, exercise all internal pathways, and mock only at external boundaries. Not unit tests (too fine-grained, mock internal modules). Not end-to-end tests (too slow, require deployed systems). Service mocks hit the sweet spot.
Test at the entry point. Exercise the full component. Mock only what you must.
Your Code
┌─────────────────────────────────────────────────────┐
│ Entry Point (API handler, exported function, etc.) │ ← Test here
│ ↓ │
│ Internal logic, state, transformations │ ← Exercised, not mocked
│ ↓ │
│ External boundary (network, DB, filesystem) │ ← Mock here
└─────────────────────────────────────────────────────┘
Traditional unit tests mock at module/class boundaries — testing UserService by mocking UserRepository. This hides integration bugs between your own components.
Service mocks push the mock boundary outward to where your code ends and external systems begin. You test real integration between your modules while keeping tests fast and deterministic.
The insight: Your code is one unit. External systems are the boundary.
| Boundary | Mock? | Why |
|---|---|---|
| Off-machine (network, external APIs, services) | Always | Speed, reliability, no external dependencies |
| On-machine, out-of-process (local database, Redis) | Usually | Speed; judgment call based on setup complexity |
| In-process (your code, your modules) | Never | That's what you're testing |
Coverage comes from two complementary layers:
Layer 1: Service mocks (primary)
Layer 2: Wide integration tests (secondary)
┌──────────────────────────────────────────────────┐
│ Service Mocks (many, fast, in-process) │ ← TDD lives here
│ Coverage goals met here │
└──────────────────────────────────────────────────┘
+
┌──────────────────────────────────────────────────┐
│ Wide Integration Tests (few, slower, deployed) │ ← Smoke tests, critical paths
│ Run locally + post-CD, not CI │
└──────────────────────────────────────────────────┘
Service mocks provide high confidence for logic and behavior. Wide integration tests provide confidence for deployment and wiring. Together they cover most failure modes.
What they can't cover: Visual correctness, UX feel, edge cases you didn't anticipate. That's what gorilla testing is for.
API testing is the cleanest application of service mocks. The entry point is obvious (the HTTP handler), the boundaries are clear (external services), and the response is easily asserted. This is the pattern to internalize — UI testing adapts it with more friction.
Get as close to the HTTP handler as possible. Use your framework's test injection (Fastify's inject(), Express's supertest, etc.) to send requests without network overhead.
// Service mock test for POST /api/prompts
describe("POST /api/prompts", () => {
let app: FastifyInstance;
beforeEach(async () => {
app = buildApp(); // Your app factory
await app.ready();
});
afterEach(async () => {
await app.close();
});
describe("authentication", () => {
// TC-1: requires authentication
test("returns 401 without auth token", async () => {
const response = await app.inject({
method: "POST",
url: "/api/prompts",
payload: { prompts: [] },
});
expect(response.statusCode).toBe(401);
});
});
describe("validation", () => {
// TC-2: validates input
test("returns 400 with invalid slug format", async () => {
const response = await app.inject({
method: "POST",
url: "/api/prompts",
headers: { authorization: `Bearer ${testToken()}` },
payload: {
prompts: [{ slug: "Invalid:Slug", name: "Test", content: "Test" }],
},
});
expect(response.statusCode).toBe(400);
expect(response.json().error).toMatch(/slug/i);
});
});
describe("success paths", () => {
// TC-3: creates prompt and returns ID
test("persists to database and returns created ID", async () => {
mockDb.insert.mockResolvedValue({ id: "prompt_123" });
const response = await app.inject({
method: "POST",
url: "/api/prompts",
headers: { authorization: `Bearer ${testToken({ sub: "user_1" })}` },
payload: {
prompts: [{ slug: "my-prompt", name: "My Prompt", content: "Content" }],
},
});
expect(response.statusCode).toBe(201);
expect(response.json().ids).toContain("prompt_123");
expect(mockDb.insert).toHaveBeenCalledWith(
expect.objectContaining({ slug: "my-prompt", userId: "user_1" })
);
});
});
describe("error handling", () => {
// TC-4: handles database errors gracefully
test("returns 500 when database fails", async () => {
mockDb.insert.mockRejectedValue(new Error("Connection lost"));
const response = await app.inject({
method: "POST",
url: "/api/prompts",
headers: { authorization: `Bearer ${testToken()}` },
payload: { prompts: [{ slug: "test", name: "Test", content: "Test" }] },
});
expect(response.statusCode).toBe(500);
expect(response.json().error).toMatch(/internal/i);
});
});
});
Mock external dependencies before importing the code under test. The pattern is framework-agnostic:
// Mock external boundaries — database, auth service, config
const mockDb = {
insert: vi.fn(),
query: vi.fn(),
delete: vi.fn(),
};
vi.mock("../lib/database", () => ({ db: mockDb }));
vi.mock("../lib/auth", () => ({
validateToken: vi.fn(async (token) => {
if (token === "valid") return { valid: true, userId: "user_1" };
return { valid: false };
}),
}));
// Reset between tests
beforeEach(() => {
vi.clearAllMocks();
});
After service mocks verify logic, wide integration tests verify the deployed system works:
// Integration test — runs against deployed staging
describe("Prompts API Integration", () => {
const baseUrl = process.env.TEST_API_URL;
let authToken: string;
beforeAll(async () => {
authToken = await getTestAuth();
});
test("create and retrieve prompt round trip", async () => {
const slug = `test-${Date.now()}`;
// Create
const createRes = await fetch(`${baseUrl}/api/prompts`, {
method: "POST",
headers: { Authorization: `Bearer ${authToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ prompts: [{ slug, name: "Test", content: "Test" }] }),
});
expect(createRes.status).toBe(201);
// Retrieve
const getRes = await fetch(`${baseUrl}/api/prompts/${slug}`, {
headers: { Authorization: `Bearer ${authToken}` },
});
expect(getRes.status).toBe(200);
expect((await getRes.json()).slug).toBe(slug);
// Cleanup
await fetch(`${baseUrl}/api/prompts/${slug}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${authToken}` },
});
});
});
When to run:
UI testing follows the same service mock philosophy but with more friction. The "entry point" is less clear, browser APIs complicate mocking, and visual/UX correctness can't be verified programmatically.
Same ideals, messier execution. UI tests can't match API test confidence. Aim for behavioral coverage, then rely on gorilla testing for visual/UX verification.
Mock at the API layer (fetch calls, API client). Let UI framework internals (state, hooks, DOM updates) run for real. Test user interactions and their effects.
UI Code
┌─────────────────────────────────────────────────────┐
│ User Interaction (click, type, submit) │ ← Simulate here
│ ↓ │
│ Component logic, state, framework internals │ ← Runs for real
│ ↓ │
│ API calls (fetch, client library) │ ← Mock here
└─────────────────────────────────────────────────────┘
For plain HTML with JavaScript, use jsdom to load templates and test behavior:
import { JSDOM } from "jsdom";
describe("Prompt Editor", () => {
let dom: JSDOM;
let fetchMock: vi.Mock;
beforeEach(async () => {
dom = await JSDOM.fromFile("src/prompt-editor.html", { runScripts: "dangerously" });
fetchMock = vi.fn(() => Promise.resolve({ ok: true, json: () => ({ id: "new_id" }) }));
dom.window.fetch = fetchMock;
});
// TC-3: Submit valid form creates prompt
test("submitting form calls POST /api/prompts", async () => {
const doc = dom.window.document;
doc.getElementById("slug").value = "new-prompt";
doc.getElementById("name").value = "New Prompt";
doc.getElementById("prompt-form").dispatchEvent(new dom.window.Event("submit"));
await new Promise((r) => setTimeout(r, 50));
expect(fetchMock).toHaveBeenCalledWith("/api/prompts", expect.objectContaining({ method: "POST" }));
});
});
Same principle — mock API layer, let framework run for real:
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
// Mock API layer, NOT hooks or components
vi.mock("@/api/promptApi");
describe("PromptList", () => {
// TC-7: displays prompts when loaded
test("renders prompt list from API", async () => {
mockPromptApi.getAll.mockResolvedValue([{ id: "1", name: "Prompt 1" }]);
render(<PromptList />);
await waitFor(() => {
expect(screen.getByText("Prompt 1")).toBeInTheDocument();
});
});
// TC-8: shows error on failure
test("displays error when API fails", async () => {
mockPromptApi.getAll.mockRejectedValue(new Error("Failed"));
render(<PromptList />);
await waitFor(() => {
expect(screen.getByText(/error/i)).toBeInTheDocument();
});
});
});
E2E tests serve as wide integration for UI — verify the full deployed stack works:
test("user can create and view prompt", async ({ page }) => {
await page.goto("/prompts");
await page.click('[data-testid="new-prompt-button"]');
await page.fill('[data-testid="slug-input"]', "e2e-test");
await page.fill('[data-testid="name-input"]', "E2E Test");
await page.click('[data-testid="submit-button"]');
await expect(page).toHaveURL(/\/prompts\/e2e-test/);
await expect(page.getByText("E2E Test")).toBeVisible();
});
Run locally and post-CD, not on CI.
Acknowledge the gap: UI testing cannot match API testing confidence. Visual correctness, UX polish, interaction feel — not verifiable programmatically.
Plan for more gorilla testing. Plan for iterative polish. The GORILLA phase exists partly for this.
For CLI tools, the entry point is the command handler. The same service mock principle applies: test at the entry point, exercise internal modules through it, mock only at external boundaries (filesystem, network, child processes).
CLI Code
┌─────────────────────────────────────────────────────┐
│ Command handler (yargs, commander, etc.) │ ← Test here
│ ↓ │
│ Internal orchestration (executors, managers) │ ← Exercised, not mocked
│ ↓ │
│ Pure algorithms (parsing, transforming) │ ← Can test directly (no mocks needed)
│ ↓ │
│ Filesystem / network / child processes │ ← Mock here
└─────────────────────────────────────────────────────┘
| Layer | Mock? | Why |
|---|---|---|
| Command handler | Test here | Entry point |
| Internal orchestration (executors, managers) | Don't mock | Exercise through command |
| Pure algorithms (no IO) | Can test directly | No mocking needed, supplemental coverage |
| Filesystem / network / child processes | Mock | External boundary |
tests/
├── commands/ # Entry point tests (primary coverage)
│ ├── edit-command.test.ts # Full edit flow, mocks filesystem
│ ├── clone-command.test.ts # Full clone flow, mocks filesystem
│ └── list-command.test.ts # Full list flow, mocks filesystem
└── algorithms/ # Pure function tests (supplemental)
└── tool-call-remover.test.ts # No mocks, edge case coverage
tests/
├── edit-operation-executor.test.ts # ❌ Internal module with mocked fs
├── backup-manager.test.ts # ❌ Internal module with mocked fs
├── tool-call-remover.test.ts # ✓ Pure algorithm, ok
└── edit-command.test.ts # ✓ Entry point, ok
The anti-pattern tests internal modules in isolation with mocked dependencies. This hides integration bugs between your own components — exactly what service mocks avoid. An agent seeing API and UI examples ("test the route handler," "test the component") will pattern-match to "test the executor, test the manager" unless given explicit CLI guidance.
Convex functions are serverless handlers. Same service mock principle — mock external boundaries, test the function directly:
describe("withApiKeyAuth wrapper", () => {
beforeEach(() => {
process.env.CONVEX_API_KEY = "test_key";
});
test("validates API key and calls handler", async () => {
const handler = vi.fn(async (ctx, args) => ({ userId: args.userId }));
const wrapped = withApiKeyAuth(handler);
const result = await wrapped({}, { apiKey: "test_key", userId: "user_1" });
expect(result.userId).toBe("user_1");
expect(handler).toHaveBeenCalled();
});
test("rejects invalid API key", async () => {
const wrapped = withApiKeyAuth(vi.fn());
await expect(wrapped({}, { apiKey: "wrong", userId: "user_1" })).rejects.toThrow("Invalid");
});
});
Every test must trace to a Test Condition from the Epic. This is the Confidence Chain in action.
describe("POST /api/prompts", () => {
// TC-1: requires authentication
test("returns 401 without auth token", async () => { ... });
// TC-2: validates slug format
test("returns 400 with invalid slug", async () => { ... });
});
| TC ID | Test File | Test Name | Status |
|---|---|---|---|
| TC-1 | createPrompts.test.ts | returns 401 without auth token | Passing |
| TC-2 | createPrompts.test.ts | returns 400 with invalid slug | Passing |
Rules:
// ❌ Passes before AND after implementation
it("throws not implemented", () => {
expect(() => createPrompt(data)).toThrow(NotImplementedError);
});
// ✅ Tests actual behavior
it("creates prompt and returns ID", async () => {
const result = await createPrompt(data);
expect(result.id).toBeDefined();
});
// ❌ Mocking your own code hides bugs
vi.mock("../hooks/useFeature");
vi.mock("../components/FeatureList");
// ✅ Mock only external boundaries
vi.mock("../api/featureApi");
// ❌ Internal state
expect(component.state.isLoading).toBe(true);
// ✅ Observable behavior
expect(screen.getByTestId("loading")).toBeInTheDocument();
tests/
├── service/ # Service mock tests (primary)
│ ├── api/
│ │ └── prompts.test.ts
│ └── ui/
│ └── prompt-editor.test.ts
├── integration/ # Wide integration tests
│ ├── api.test.ts
│ └── ui.test.ts
└── fixtures/
└── prompts.ts
Track running totals across stories. Previous tests must keep passing — regression = stop and fix.
This document translates feature requirements into implementable architecture. It serves three audiences:
| Audience | Value |
|---|---|
| Reviewers | Validate design before code is written |
| Developers | Clear blueprint for implementation |
| Story Tech Sections | Source of implementation targets, interfaces, and test mappings |
Prerequisite: The epic must be complete (all ACs have TCs) before starting this document.
Expected Length: A complete tech design expands significantly from the epic — typically 6-7×. The richness comes from redundant connections: the same concepts appearing at multiple altitudes, woven through functional and technical perspectives. Shorter usually means insufficient depth. Longer usually means scope creep.
When to split: If this document exceeds ~1500-2000 lines, or a domain section (auth, protocol, UI architecture, testing) grows dense enough to distract from core flow, split into focused companion docs. Keep one index document as the canonical map and decision log. Split by domain or altitude, not arbitrarily. Each companion doc preserves requirement traceability (AC/TC mappings) and includes cross-links back to the index. Make the split decision explicitly — "single doc" or "split with index" — and document the rationale.
Before designing, validate the Epic is implementation-ready. You are the downstream consumer—if you can't design from it, the spec isn't ready.
Validation Checklist:
Issues Found:
| Issue | Spec Location | Recommendation | Status |
|---|---|---|---|
| [description] | AC-X | [fix needed] | Pending/Resolved |
If blocking issues exist, return to BA for revision. Don't design from a broken spec. Document what you found—even minor issues—so there's a record of spec evolution.
This section establishes the "why" behind architectural choices. Write 3-5 paragraphs covering the landscape that shaped this design. Someone entering this document without prior conversation should understand:
The goal is rich context that survives isolated reading. Don't summarize—immerse the reader in the problem space so architectural choices feel inevitable rather than arbitrary.
Example (note the paragraph depth):
This feature addresses Guidewire users who need to add multiple locations to commercial policies during the quoting process. Currently, users exit Guidewire entirely, navigate to the legacy location system, and manually re-enter policy context. The round-trip takes 8-12 minutes and is the #2 complaint in broker feedback surveys.
The primary constraint is iframe embedding—Guidewire's extension framework prohibits navigation or popups. All data must flow through URL parameters (in) and redirect URLs (out). This limits payload size to roughly 2KB encoded, which influenced our decision to return location IDs rather than full location objects. The parent application will re-fetch details as needed.
We chose to isolate this feature under the /locations namespace, creating a parallel implementation rather than extending v1 flows. This increases some code duplication (shared components will be extracted in Phase 2) but eliminates integration risk during the policy renewal window in Q4. The v1 system handles 40% of premium volume; we cannot risk destabilization.
The design assumes the XAPI team delivers their location search endpoint by Sprint 23. If delayed, Chunk 2 (search functionality) slides but Chunk 1 (browse existing locations) can proceed independently.
Start at the highest level. How does this feature fit into the broader system? What crosses the application boundary? This altitude answers: "What does the system look like from the outside?"
Show external actors and systems. For complex integrations, consider building the diagram progressively—start with core actors, then add external systems, then show the full picture. This helps readers construct understanding rather than absorb a complete diagram all at once.
flowchart LR
subgraph External
GW[Guidewire Policy Center]
end
subgraph "Location App"
UI[React UI]
XAPI[Experience API]
end
subgraph "Backend Services"
API[Domain API]
DB[(Database)]
end
GW -->|"query params"| UI
UI -->|"API calls"| XAPI
XAPI -->|"domain calls"| API
API -->|"read/write"| DB
UI -->|"redirect + data"| GW
What crosses the boundary? This section connects to the epic's Data Contracts and establishes what the implementation must honor. These contracts become the fixed points around which internal architecture flexes.
Incoming (from Guidewire):
Describe what arrives and why. The table enumerates; the prose contextualizes.
| Parameter | Required | Source | Purpose |
|---|---|---|---|
| param1 | Yes | Query string | Description |
Outgoing (to Guidewire):
Describe what returns and the format constraints. Note any size limits, encoding requirements, or ordering expectations.
| Data | Format | Destination | Purpose |
|---|---|---|---|
| Location data | Base64 JSON | Redirect URL | Return selected/created locations |
Error Responses:
Errors are part of the contract. Define shapes so tests can mock realistic failures and UI can handle them gracefully. These error shapes should appear again in the testing section—that redundancy is intentional, creating multiple paths to the same information.
| Source | Status | Code | Shape | Client Handling |
|---|---|---|---|---|
| XAPI | 400 | VALIDATION_FAILED | { status: 'ERROR', code: string, messages: [...] } | Show validation message |
| XAPI | 500 | INTERNAL_ERROR | { status: 'ERROR', code: string, messages: [...] } | Show generic error |
| Network | — | NETWORK_ERROR | TypeError: Failed to fetch | Show connection error |
Stable error codes are the machine-readable contract clients program against. Shapes may evolve; codes should not. Clients switch on codes, not on message strings or HTTP status alone.
Runtime Prerequisites:
What must be installed, running, or configured for this feature to work — locally and in CI. Documenting this here prevents Story 0 from discovering missing prerequisites mid-execution.
| Prerequisite | Where Needed | How to Verify |
|---|---|---|
| [Runtime/tool] v[X.Y]+ | Local + CI | [command] --version |
| [Service] running | Local dev | curl [health endpoint] |
| [Env var] | All environments | Defined in .env.example |
✏️ Connection Check: Before moving to Medium Altitude, verify you've established:
These external contracts constrain everything below. Module boundaries exist to fulfill these contracts. Interface definitions implement them. If an external contract is unclear here, it will haunt every subsequent section.
Zoom into the application. What modules exist? How do they divide responsibility? This altitude answers: "How is the application organized internally?"
The module breakdown creates the skeleton that Phase 1 will implement. Each module listed here becomes a stub file. Think carefully about boundaries—they're expensive to change once tests are written against them.
Show the file structure with annotations. Mark what exists vs. what's new. This becomes the implementation checklist for skeleton phase. Adapt the structure to your stack — the principle is the same: group by responsibility, mark mock boundaries, trace to ACs.
React/UI example:
src/
├── errors.ts # EXISTS: Add NotImplementedError if missing
├── types/
│ └── [Feature].ts # NEW: Type definitions for this feature
├── pages/
│ └── [feature]/
│ ├── [Feature].tsx # NEW: Main page component
│ └── [feature].module.scss # NEW: Page styles
├── components/
│ └── [ComponentName]/
│ ├── [ComponentName].tsx # NEW: Reusable component
│ └── [ComponentName].module.scss # NEW: Component styles
├── hooks/
│ └── use[Feature].ts # NEW: Custom hook encapsulating logic
└── api/
└── [feature]Api.ts # NEW: API functions (mock boundary)
API service / CLI example:
src/
├── errors.ts # EXISTS: Add NotImplementedError if missing
├── types/
│ └── [feature].types.ts # NEW: Request/response types, domain models
├── commands/ # or routes/, handlers/
│ └── [feature].command.ts # NEW: Entry point (command handler or route handler)
├── services/
│ └── [feature].service.ts # NEW: Business logic, orchestration
├── clients/
│ └── [external].client.ts # NEW: External API/DB client (mock boundary)
└── utils/
└── [feature].utils.ts # NEW: Pure transformations (testable without mocks)
Define what each module does, what it depends on, and which ACs it serves. This matrix is the rosetta stone connecting functional requirements to code locations. When someone asks "where is AC-15 implemented?"—this table answers.
| Module | Type | Responsibility | Dependencies | ACs Covered |
|---|---|---|---|---|
[entrypoint] | Handler | Entry point, request/response orchestration | services, components | AC-1 to AC-5 |
[service/hook] | Logic | Business logic, data fetching, state | clients, api | AC-6 to AC-10 |
[component/formatter] | Output | Renders/formats results | types | AC-11 to AC-15 |
[client/api] | Boundary | External calls (mock boundary) | network/fs | (supports above) |
Notice how ACs appear here after appearing in the Context section (implicitly) and before appearing in Flow-by-Flow (explicitly). This repetition is intentional—the spiral pattern creates redundant paths through the material.
Show runtime communication between modules. This diagram should feel like a zoomed-in view of the System Context diagram—same actors at different magnification.
flowchart TD
subgraph Page["[Feature].tsx"]
PS[Page State]
PE[Page Effects]
end
subgraph Hooks
H1["use[Feature]"]
end
subgraph Components
C1["[Component1]"]
C2["[Component2]"]
end
subgraph API
A1["[feature]Api"]
end
PS --> C1
PS --> C2
PE --> H1
H1 --> A1
C1 -->|"events"| PS
C2 -->|"events"| PS
✏️ Connection Check: The modules above should clearly map to:
If a module exists but you can't trace it to an AC, question whether it's needed. If an AC exists but no module owns it, you've found a gap.
For each major flow, provide a sequence diagram and connect to functional requirements. This section weaves functional (ACs/TCs) with technical (modules/methods), showing how the architecture fulfills requirements.
Each flow should reference ACs covered, show the sequence of module interactions, list what skeleton phase must create, and map TCs to test approaches. This is the densest section—and intentionally so. It's where functional and technical interlock.
Covers: AC-X through AC-Y
Begin with prose describing this flow's purpose, when it executes, and why it matters to users. Connect to the problem established in Context. Two to three sentences minimum—don't jump straight to the diagram.
sequenceDiagram
participant User
participant Page as [Feature].tsx
participant Hook as use[Feature]
participant API as [feature]Api
participant XAPI as Experience API
Note over User,Page: AC-X: User initiates action
User->>Page: [Action]
Page->>Hook: [Method call]
Note over Hook,API: AC-Y: System fetches data
Hook->>API: [API function]
API->>XAPI: GET/POST [endpoint]
XAPI-->>API: Response
API-->>Hook: Transformed data
Hook-->>Page: State update
Note over Page,User: AC-Z: User sees result
Page-->>User: [UI update]
Skeleton Requirements:
This flow requires the following stubs. Each row becomes a file created in skeleton phase with a NotImplementedError body. The signature column is copy-paste ready.
| What | Where | Stub Signature |
|---|---|---|
| Page component | src/pages/[feature]/[Feature].tsx | export const [Feature] = () => { throw new NotImplementedError('[Feature]') } |
| Hook | src/hooks/use[Feature].ts | export const use[Feature] = () => { throw new NotImplementedError('use[Feature]') } |
| API function | src/api/[feature]Api.ts | export const [apiFunction] = async () => { throw new NotImplementedError('[apiFunction]') } |
TC Mapping for this Flow:
How do we verify this flow works? Each TC from the epic maps to a test. The test file, setup, and assertion approach are specified here—TDD Red phase will implement exactly these tests.
| TC | Tests | Module | Setup | Assert |
|---|---|---|---|---|
| TC-XX | [What behavior] | use[Feature] | Mock API returns data | Hook returns transformed data |
| TC-YY | [What behavior] | [Feature].tsx | Render with mocked hook | Shows expected UI |
Covers: AC-X through AC-Y
Repeat the same structure: context prose → sequence diagram with AC annotations → skeleton requirements → TC mapping
The repetition of structure isn't monotony—it's navigability. Someone looking for "how does selection work?" can scan flow headings. Someone looking for "where is TC-25 tested?" can scan TC mapping tables. Multiple entry points to the same information.
✏️ Connection Check: Each flow should trace to:
If you can't draw these connections, the design has gaps. Fill them before proceeding.
Now at the lowest altitude before code. Specific types, method signatures, and implementation contracts. These become copy-paste ready for skeleton phase and serve as the source of truth for what gets built.
This section should feel like the inevitable conclusion of everything above. The types exist because the flows need them. The method signatures fulfill the module responsibilities. The props/parameters enable the interactions shown in diagrams. Adapt to your stack — the examples below use TypeScript but the pattern (types → service signatures → boundary contracts → entry point signatures) applies to any language.
Types establish the vocabulary of the feature. Define them with JSDoc comments that reference their purpose—where they come from, where they're used, which ACs they support.
/**
* Represents a [domain concept] in the V2 location flow.
*
* Used by: use[Feature] hook, [Feature].tsx page
* Supports: AC-X (display), AC-Y (selection)
*
* Note: V2 types are NEW interfaces for this feature.
* Do not modify existing types in other files.
*/
export interface [TypeName]V2 {
/** Unique identifier from backend */
id: string;
/** [Field description - what it represents, where it comes from] */
fieldName: string;
/** [Optional field description] */
optionalField?: string;
}
Services (or hooks in React) encapsulate business logic and data fetching. Define the return type contract—this is what entry points and consumers program against.
/**
* Manages [feature] data and operations.
*
* Covers: TC-XX (loading state), TC-YY (data fetch), TC-ZZ (error handling)
* Depends on: [feature]Api for data fetching
* Used by: [Feature].tsx page component
*/
export interface Use[Feature]Return {
/** Current data state - null until first fetch completes */
data: [TypeName]V2[] | null;
/** True while fetch is in flight */
isLoading: boolean;
/** Error from most recent failed fetch, null if successful */
error: Error | null;
/**
* [Action method description]
* Triggers: [what happens when called]
* Covers: AC-X
*/
performAction: (param: string) => void;
}
export const use[Feature] = (): Use[Feature]Return => {
throw new NotImplementedError('use[Feature]');
};
API functions are the mock boundary. Tests mock these; hooks call these. Define request/response contracts precisely.
/**
* Fetches [data description] from XAPI.
*
* Endpoint: GET /api/[endpoint]
* Covers: TC-XX (success), TC-XY (error handling)
* Called by: use[Feature] hook
*
* @param param - [parameter description]
* @returns [return description]
* @throws {Error} When network fails or response is not OK
*/
export const fetch[Data] = async (param: string): Promise<[ResponseType]> => {
throw new NotImplementedError('fetch[Data]');
};
/**
* Submits [data description] to XAPI.
*
* Endpoint: POST /api/[endpoint]
* Covers: TC-YY (success), TC-YZ (validation error)
* Called by: use[Feature] hook
*/
export const submit[Data] = async (data: [RequestType]): Promise<[ResponseType]> => {
throw new NotImplementedError('submit[Data]');
};
Entry points (route handlers, command handlers, component props) define the contract at the top of your call stack. Include parameter types and document what triggers each entry.
/**
* Props for [ComponentName] component.
*
* Renders: [what the component displays]
* Used in: [Feature].tsx
* Supports: AC-XX (display), AC-YY (interaction)
*/
export interface [ComponentName]Props {
/** [Prop description - what it contains, where it comes from] */
propName: string;
/**
* Called when user [action description].
* Parent should: [expected parent response]
*/
onAction: (value: string) => void;
}
Complete mapping from Test Conditions to implementation. This table drives TDD Red phase—every row becomes a test. The grouping by test file makes implementation straightforward: open file, write listed tests.
This section synthesizes everything above. Each TC traces back through:
Group by test file to show what each module is responsible for testing.
[Feature].test.tsx