- name
- robs-design
- description
- Use when designing software architecture or new features: producing technical design documents with model definitions, service code, API contracts, migration strategies, and testing plans. Works alongside OpenSpec proposals or stands alone for feature design. This is NOT the `design.md` UI design system format (colors, typography, components) — this is purely for engineering architecture documentation.
- license
- MIT
- metadata
- {"author":"Robert Hafner","source":"https://github.com/tedivm/opencode-config"}
## Quick start
Every `design.md` is a comprehensive technical document that an implementer can follow without guessing. It includes full model definitions, complete service code, API contracts, migration order, testing strategy, documentation plan, and risk analysis. The implementer should never need to ask "what does this look like?"
## Document structure
Write the document in this exact order. Omit sections that don't apply.
1. **`# Design: <name>`** — Title line with change name and brief scope
2. **`## Context`** — Current state, existing conventions, what this builds on
3. **`## References`** — Documentation reviewed during design: links with one-sentence descriptions
4. **`## Goals / Non-Goals`** — Two subsections: `**Goals:**` (bulleted) and `**Non-Goals:**` (bulleted)
5. **`## Decisions`** — Numbered decisions (D1, D2, ...) with full code
6. **`## Data Storage`** — Full model/entity definitions for every new or changed persistence structure
7. **`## Data Structures`** — Input and output schema definitions
8. **`## Interfaces`** — All user-facing interfaces: REST APIs, CLI commands, MCP tools, TUI screens, GUI components
9. **`## Implementation Detail`** — Full service code, background tasks, CLI, settings
10. **`## Migrations`** — Creation order, dependencies, compatibility notes
11. **`## Testing Philosophy`** — Prose sections (not bullets) per test area
12. **`## Documentation Plan`** — Prose sections (not tables) per doc file
13. **`## Risks / Trade-offs`** — Prose sections with **Risk:** and **Mitigation:** paragraphs
### Context example
```markdown
## Context
The project has infrastructure scaffolding (FastAPI, async SQLAlchemy 2.0, Celery, Docker Compose) but zero domain functionality. No models, no routes, no business logic. This design establishes the first complete data flow.
The architecture follows the existing template's conventions: async SQLAlchemy with `mapped_column`, Pydantic v2 settings, Typer CLI with `@syncify`, and the router pattern for FastAPI.
```
### References example
```markdown
## References
- [feedparser docs](https://feedparser.readthedocs.io/) — Defines how feed parsing and normalization behave, informing the URL normalization strategy.
- [SQLAlchemy async session docs](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html) — Validates the `_syncify` bridge pattern for running async sessions inside Celery tasks.
- [FastAPI dependency injection](https://fastapi.tiangolo.com/tutorial/dependencies/) — Informs how service instances are injected into route handlers.
```
### Goals / Non-Goals example
```markdown
## Goals / Non-Goals
**Goals:**
- 7 domain models with migration
- URL normalization for source uniqueness
- Global article deduplication by URL
- Feed fetching service using `feedparser` + `httpx`
- REST API: CRUD for sources, list/detail for articles
- CLI commands: `fetch` and `seed`
- Settings: `default_fetch_interval`, `feed_fetch_timeout`
**Non-Goals:**
- User authentication, reactions, or preferences (Phase 2)
- AI summaries, embeddings, breaking news (Phase 3)
- Per-source fetch intervals — global default only
- Translation logic (structural placeholder only)
- Frontend, MCP server (future phases)
```
## Decision format
Each decision is a numbered subsection: `### D1: <name>`, `### D2: <name>`, etc.
Every decision must include:
1. **Decision statement** — What was chosen and why, in prose
2. **Code** — Full, working code. Not pseudocode. Not stubs. Include file path comments (`# package/module/file.py`)
3. **Alternatives considered** — `**Alternative considered:**` followed by what was rejected and why
4. **Rationale** — Inline prose explaining the choice
5. **Trade-offs** — Note any downsides, deferred work, or future migration needs
Decisions are separated by `---` (horizontal rule).
### Decision example — with code
````markdown
### D1: URL normalization with `yarl`
**Decision:** Source URLs are normalized before storage and uniqueness checks using `yarl.URL`. Normalization: lowercase scheme and host, strip trailing path slashes, remove default ports (`:80` for HTTP, `:443` for HTTPS), drop query and fragment.
```python
# package/services/feed.py
from yarl import URL
def normalize_url(raw: str) -> str:
"""Normalize a feed URL for consistent storage and deduplication."""
url = URL(raw)
if (url.scheme == "http" and url.port == 80) or (url.scheme == "https" and url.port == 443):
url = url.with_port(None)
path = url.path.rstrip("/") or "/"
return str(url.with_path(path).with_query(None).with_fragment(None))
```
````
`yarl` is chosen over `urllib.parse` because it provides an immutable URL object with clean manipulation methods and handles edge cases (Unicode domains, IPv6 literals) that manual parsing would miss.
**Alternative considered:** `urllib.parse.urlparse` (stdlib). Rejected — requires manual reassembly, more error-prone for edge cases.
### Decision example — architecture (no code)
```markdown
### D2: In-memory state only for Phase 1
**Choice:** Player state is a Python dataclass held in memory for the duration of a session. It is not persisted between sessions.
**Alternatives considered:**
- Database persistence from day one: locks us into a schema before we understand the full shape of state.
- JSON file storage: commits to a serialization format prematurely.
**Rationale:** We cannot design the DB schema without knowing the full shape of state first. Building the in-memory model first validates the data model before committing it to migrations.
```
## Code-level detail requirements
### Data Storage models
Write the complete class/structure with every field:
- Identity: `id` as `Mapped[UUID]` with `primary_key=True, default=uuid4` (or equivalent)
- References: all foreign key / reference columns with appropriate cascade behavior
- Timestamps: `created_at` / `updated_at` with `DateTime(timezone=True)`
- Constraints: `UniqueConstraint` in `__table_args__` when composite uniqueness is needed
- Docstrings: explaining purpose and any future-expansion notes
Do not defer FK constraints to "later" unless the referenced table genuinely doesn't exist yet. If a FK target doesn't exist, create the column as a plain nullable `UUID` with a `comment` noting the future FK.
**See [references/sqlalchemy-models.md](references/sqlalchemy-models.md) for relational model examples.**
**See [references/in-memory-models.md](references/in-memory-models.md) for dataclass and in-memory examples.**
### Data Structures
Write complete input and output schemas. Separate input from output — never reuse the same model for both. Include `Field(...)` with `description` and `default`/`optional` annotations.
```python
# package/schemas/source.py
from datetime import datetime
from pydantic import BaseModel, Field
from uuid import UUID
class SourceCreate(BaseModel):
url: str = Field(
description="Feed URL. Will be normalized before storage.",
examples=["https://feeds.example.com/rss"],
)
name: str | None = Field(
default=None,
description="Optional display name. Derived from feed on first fetch if omitted.",
)
class SourceRead(BaseModel):
id: UUID = Field(description="Unique identifier.")
url: str = Field(description="Normalized URL.")
name: str | None = Field(description="Display name.")
is_default: bool = Field(description="Whether this is a built-in default source.")
added_at: datetime = Field(description="When the source was added.")
last_fetched_at: datetime | None = Field(
default=None,
description="When the source was last successfully fetched.",
)
```
For complex discriminated unions (condition trees, step types, event types), use Pydantic's `Annotated[Union[...], Field(discriminator="type")]` pattern.
**See [references/pydantic-schemas.md](references/pydantic-schemas.md) for schema patterns and discriminated unions.**
### Service classes
Write the complete class with all methods:
- Constructor with configuration from settings
- Core business logic methods (upsert, fetch, parse, etc.)
- Helper methods (`_extract_*`, `_parse_*`, `_detect_*`)
- Custom exception classes
- Error handling and logging
Include the full method body — not just signatures. Every public method gets a docstring. Use `logger.getLogger(__name__)` for logging.
### Background tasks
Use a `_syncify` pattern to bridge async service code from sync task runners. Never scatter `asyncio.run()` through task bodies — define a `_syncify` wrapper and thin sync helpers for cache/DB operations.
Structure: `_syncify` function + sync cache/DB wrappers + `async def _impl(...)` + `@celery.task` decorated sync entry point that calls `_syncify(_impl)`.
**See [references/celery-tasks.md](references/celery-tasks.md) for full task patterns.**
### CLI commands
Show the full command function with the sync bridge decorator (`@syncify`).
**See [references/typer-cli.md](references/typer-cli.md) for CLI command patterns.**
### Settings
Show the exact `Field(...)` definitions with `default` and `description`:
```python
# package/conf/settings.py
default_fetch_interval: int = Field(
default=30,
description="Interval in minutes between automatic fetches.",
)
feed_fetch_timeout: int = Field(
default=30,
description="Timeout in seconds for individual HTTP requests.",
)
```
### Interfaces
Document every user-facing interface. Use tables or structured lists for each interface area. Include error responses.
The Interfaces section covers all applicable types — use the table/list format appropriate to each:
- **REST APIs** — Method, path, request, response, description + error response table
- **CLI commands** — Command name, arguments, description, exit codes
- **MCP tools** — Tool name, parameters, return type, description
- **TUI screens** — Screen name, options, navigation
- **GUI components** — Component name, properties, events
```markdown
### REST API — Sources
| Method | Path | Request | Response | Description |
| ------ | --------------- | -------------- | -------------- | ------------------- |
| `GET` | `/sources` | — | `SourceRead[]` | List all sources |
| `POST` | `/sources` | `SourceCreate` | `SourceRead` | Create a new source |
| `GET` | `/sources/{id}` | — | `SourceRead` | Get source by ID |
### CLI Commands
| Command | Arguments | Description |
| ------- | --------- | ------------------------------ |
| `fetch` | — | Trigger a fetch of all sources |
| `seed` | — | Seed default sources |
### MCP Tools
| Tool | Parameters | Returns | Description |
| -------------- | ---------- | -------------- | ---------------- |
| `list_sources` | — | `SourceRead[]` | List all sources |
```
**See [references/fastapi-routes.md](references/fastapi-routes.md) for REST endpoint patterns.**
**See [references/typer-cli.md](references/typer-cli.md) for CLI command patterns.**
### Accessibility
Every user-facing interface requires an accessibility section. Use `## Accessibility` as a top-level section after `## Interfaces`. Cover each interface type with actionable requirements, not aspirational language.
```markdown
## Accessibility
### Visual Design
- **Color contrast:** All text meets WCAG 2.1 AA minimum contrast ratios (4.5:1 for normal text, 3:1 for large text and UI components). Verify with automated tools during development.
- **Color as the only indicator:** No information conveyed by color alone. Status indicators include icons, text labels, or patterns in addition to color.
- **Focus indicators:** All interactive elements have visible focus indicators with sufficient contrast against their background. Do not remove or style `outline: none` without providing an alternative.
- **Motion and animation:** Respect `prefers-reduced-motion`. Disable non-essential animations when the user's OS preference is set. Provide static fallbacks.
### Screen Reader Support
- **Semantic HTML:** Use native elements (`<button>`, `<nav>`, `<main>`, `<header>`) over generic `<div>` wrappers. Apply ARIA roles only when native semantics are insufficient.
- **Labels and descriptions:** Every interactive element has an accessible name. Use `aria-label`, `aria-labelledby`, or visible labels — never rely on placeholder text alone.
- **Live regions:** Dynamic content updates (notifications, data loading, errors) use `aria-live` regions with appropriate politeness (`polite` for updates, `assertive` for errors).
- **Status messages:** Form validation, success confirmations, and error states are announced to screen readers via `role="alert"` or `aria-live` regions.
### Keyboard Navigation
- **Complete keyboard access:** Every interactive function is operable using keyboard alone. No keyboard traps — users can tab forward and backward through all elements.
- **Logical tab order:** Tab order follows visual reading order. Use DOM order rather than `tabindex` positive values. Skip links provide navigation past repeated content.
- **Custom keyboard shortcuts:** If shortcuts exist, they are documented, configurable, and do not conflict with browser or screen reader shortcuts. Provide visual indicators of active shortcuts.
### TUI-Specific Requirements
- **Terminal color support:** Use ANSI color codes that work on both light and dark terminal backgrounds. Do not assume a specific terminal theme.
- **Braille display compatibility:** Ensure text output does not rely on Unicode graphics characters (block elements, drawings) for conveying information. Provide fallback text.
- **Resize behavior:** The interface degrades gracefully when the terminal is resized. Content reflows or truncates without breaking layout.
### CLI-Specific Requirements
- **Help text:** Every command and flag has descriptive help text accessible via `--help`. Output is parseable and readable without color.
- **Error messages:** Errors include actionable guidance, not just codes. Messages are descriptive enough for users who cannot see color-coded output.
Voir sur GitHub