From 91d480595d90cff2a116b836f315151e7ba2b607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Doubravsk=C3=BD?= Date: Thu, 24 Sep 2026 10:15:24 +0200 Subject: [PATCH] Add API and web application design documents --- API/DESIGN_DOCUMENT_API.md | 227 ++++++++++++++++++++++++++++ CHANGELOG.md | 37 +++++ Claude/AGENTS.md | 25 ++-- README.md | 4 + WEB/DESIGN_DOCUMENT_WEBAPP.md | 270 ++++++++++++++++++++++++++++++++++ check_versions.py | 6 +- 6 files changed, 553 insertions(+), 16 deletions(-) create mode 100644 API/DESIGN_DOCUMENT_API.md create mode 100644 WEB/DESIGN_DOCUMENT_WEBAPP.md diff --git a/API/DESIGN_DOCUMENT_API.md b/API/DESIGN_DOCUMENT_API.md new file mode 100644 index 0000000..d13d726 --- /dev/null +++ b/API/DESIGN_DOCUMENT_API.md @@ -0,0 +1,227 @@ +# API Design Guidelines + +**Document Version:** v2 + +> **Note on Versioning:** +> - This document version is independent — reused across projects +> - **Project version** source of truth: the project manifest (`pyproject.toml`, `Cargo.toml`) +> - The **URL version** (`/v0`, `/v1`) is a separate axis and moves independently — see section 1 +> - `CHANGELOG.md` uses the project version, never the URL version + +## Related Documents + +- **README.md** — Project overview, tool descriptions, build instructions +- **AGENTS.md** — Rules for AI assistants +- **PROJECT.md** — Project goals and current state +- **CHANGELOG.md** — Version history +- **DESIGN_DOCUMENT_LANGUAGE.md** — everything language-specific (`DESIGN_DOCUMENT_PYTHON.md`, …) + +This document applies to any project that exposes an HTTP API, in addition to the design document +for the language it is written in. Where the two disagree, the language document wins on language +matters and this one wins on API surface. + +--- + +## 1. Two Version Axes + +An API carries two versions that must never be conflated. + +| Axis | Where it lives | What it means | How often it moves | +|------|----------------|---------------|--------------------| +| **URL version** | the path — `/v0/...` | the contract with the consumer | almost never | +| **Product version** | project manifest, job manifest | the release of the software | every release | + +The product version follows semantic versioning and the rules in `AGENTS.md`. It is what the +OpenAPI/Swagger `version` field reports, and what `CHANGELOG.md` is organised by. + +The URL version is **not** semantic. It is a single integer identifying a frozen contract. A +consumer pinned to `/v0` keeps working across every product release until `/v0` is formally retired. + +### 1.1 When the URL version is bumped + +Bump `/v0` to `/v1` **only** on a breaking change to an existing endpoint: + +- a field is removed or renamed +- a field changes type or meaning +- a default changes in a way that alters the response +- an endpoint is removed or its path changes +- a previously optional parameter becomes required + +The following are **not** breaking and never justify a bump: + +- adding a new endpoint +- adding a new optional query parameter +- adding a new field to a response +- performance, caching or refresh-strategy changes that leave the payload identical + +### 1.2 Coexistence and retirement + +When the version is bumped, the old version keeps serving until every consumer has moved: + +- both versions are routed at the same time; the old one is not silently redirected +- the old version is marked `deprecated` in the OpenAPI schema +- retirement is announced in `CHANGELOG.md` at least one release before removal +- removal is a MAJOR bump of the product version + +A new URL version is a cost paid by every consumer. Prefer an additive change, a new endpoint, or +an opt-in query parameter over a bump. + +--- + +## 2. Path Naming + +``` +/v// +``` + +| Segment | Rule | Example | +|---------|------|---------| +| `v` | lowercase `v` + integer, no dots | `/v0`, `/v1` | +| `` | the consuming system, as its own abbreviation | `WEXT`, `PDH`, `SALESOFFER` | +| `` | lowercase `kebab-case` | `insert-relation`, `attribute-defs` | + +- **Collections are plural**, single lookups append the identifier: + `/v0/WEXT/products` and `/v0/WEXT/products/{productnr}` +- **Sub-resources nest**, they are not hyphenated into the parent: + `/v0/WEXT/assets/general`, not `/v0/WEXT/assets-general` +- **Never PascalCase or SCREAMING_CASE a resource.** The consumer segment is the only place an + uppercase abbreviation is allowed, because it is a proper name rather than a word. +- **Spell resources out.** `number-of-inserts` over `noi` — the URL is read by people who do not + know the internal acronym. + +### 2.1 Endpoints outside the versioned space + +Two different kinds of endpoint carry no version prefix, and the same rule does not cover both. + +**Operational endpoints** — `/health`, `/cache/stats`, `/cache/reset` — are called by operators and +monitoring, never by a consumer. They change with the product version, no ceremony. + +**Unversioned consumer endpoints** — the token endpoints (`/subscribe`) and any feed that predates +the versioning scheme — carry no prefix but are still contracts, and the token endpoints are the +widest of them: every delta consumer holds a token one of them issued. A breaking change to any of +these follows section 1.1 exactly. Having no version to bump is not permission to break. The +endpoint is either extended additively, or it enters the versioned space as a new path while the +old one keeps serving and is marked deprecated. + +Unversioned is not unprotected. What decides is who calls the endpoint, not whether the path has a +number in it. + +--- + +## 3. Parameter Naming + +| Kind | Convention | Example | +|------|------------|---------| +| Path parameter | `snake_case` | `/products/{productnr}` | +| Query parameter | `snake_case` | `?web_only=true`, `?dict_id=42` | +| Header | `X-` prefix, `Kebab-Case` | `X-Token` | + +Reserved query parameter names, to be used for nothing else: + +| Name | Meaning | +|------|---------| +| `limit` | rows per page | +| `offset` | row offset | +| `since` | ISO-8601 low-water mark for a delta pull | +| `format` | response format override, where an endpoint serves more than one | + +--- + +## 4. Code Naming + +The path is the contract; the code that serves it mirrors the path mechanically, so the two cannot +drift apart. + +| Element | Convention | Example | +|---------|------------|---------| +| Router variable | `_router` | `wext_router` | +| Handler function | `_`, `snake_case` | `wext_attribute_defs` | +| Single-item handler | `_`, singular | `wext_product` | +| OpenAPI tag | ` feed` | `"WEXT feed"` | + +One router per consumer, declared with its own prefix and tag and included on the application in a +single place. A handler never hardcodes its own prefix. + +--- + +## 5. Responses + +### 5.1 Envelope + +A list endpoint returns its rows inside an envelope carrying the paging state, never a bare array: + +```json +{"table": "wext-products", "since": null, "count": 3, + "limit": 5000, "offset": 0, "has_more": false, "rows": []} +``` + +For XML, the same metadata goes on the root element as attributes (`limit`, `offset`, `returned`, +`hasMore`). + +Where the response body is a message defined by the **consumer** rather than by us, the paging +metadata stays outside that message — the body must remain exactly what the consumer schema expects. + +### 5.2 Paging + +**Every list endpoint is paged.** An endpoint with no cap is a denial of service waiting for the +first client that omits a filter. + +- `limit` has a documented default and a hard maximum, both configurable by environment variable +- the maximum is chosen by measurement — response size, and the time a request holds any shared + lock — not by guess +- `has_more` is reported so the client knows to continue +- deep offsets over a very large table are not the intended access path; document the alternative + (walking `since` windows) instead + +### 5.3 Delta feeds + +An endpoint whose source has a reliable change column may support incremental pulls: + +- `?since=` returns only rows changed strictly after that instant +- a subscription token in `X-Token` supplies, and then advances, a server-tracked watermark +- an explicit `?since` always wins over the token value +- a malformed `?since` is rejected with **422** before it reaches the query + +**A snapshot message is never a delta.** If a response states a complete set as of a timestamp, +answering with only the changed rows tells the consumer that the set shrank to those rows. Such an +endpoint is a full snapshot and says so in its description. + +### 5.4 Status codes + +| Code | When | +|------|------| +| `200` | success, including a legitimately empty collection | +| `302` | redirect to an external asset | +| `400` | the request is missing something it needs | +| `403` | unknown or revoked token | +| `404` | a named single entity does not exist | +| `422` | a parameter is present but malformed | +| `500` | internal failure | + +An empty collection is `200` with an empty list, not `404`. `404` means *this identifier does not +exist*, which only a single-entity lookup can determine. + +Error bodies carry a short, generic `detail`. **Internal detail — driver strings, connection +parameters, SQL, stack traces — goes to the log only, never to the client.** + +--- + +## 6. Documentation + +- Every endpoint declares a `summary` and a `description`. The description states where the data + comes from, whether the endpoint is a delta or a snapshot, and any non-obvious default. +- The OpenAPI `version` field is read from the project manifest, never hand-maintained. +- The interactive schema (`/docs`, `/openapi.json`) is the reference for consumers. Prose that only + restates it belongs nowhere. +- A new consumer feed is announced in `CHANGELOG.md` under the product version that ships it. + +--- + +## 7. Testing + +In addition to the testing rules in the language design document: + +- every endpoint has at least one test asserting the **shape** of its response +- the query an endpoint builds is asserted, so a changed filter cannot pass unnoticed +- tests stub the data-access layer; they never require a live database +- the empty-result path is tested explicitly — it is the one most often wrong diff --git a/CHANGELOG.md b/CHANGELOG.md index 60f0bc1..d4c8488 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,43 @@ ships documents, so it uses the same `v#` scheme as the documents themselves. Individual guideline documents carry their own `Document Version` header, and the skills carry `metadata.version`; both are independent of the revisions below. +## v7 — 2026-09-24 + +### Added +- **`API/DESIGN_DOCUMENT_API.md` v2** — guidelines for any project that exposes an HTTP API, + carried in addition to the language design document. It settles what was previously left to + precedent: the URL version (`/v0`) and the product version are separate axes, the first naming a + frozen consumer contract that moves only on a breaking change, the second following semantic + versioning per release. Also fixes path naming + (`/v//`, resources spelled out and never PascalCase), + parameter and handler naming, the response envelope and its reserved paging keys, when an + endpoint may be a delta and when it must stay a full snapshot, status-code meanings (an empty + collection is `200`, not `404`), and the endpoint-level testing floor. +- `API/DESIGN_DOCUMENT_API.md` registered in `check_versions.py` and in the project-type table, so + `--sync` propagates it like every other guideline document. +- Section 2.1 was rewritten before release (v1 → v2). The v1 draft filed `/subscribe` under + endpoints that are "not part of any consumer contract" and declared the whole group free to + change with the product version. It is the opposite: the token endpoints are the widest + contract in the API, every delta consumer holds a token they issued, and the unversioned + legacy feeds are contracts too. The section now splits operational endpoints from unversioned + consumer endpoints and subjects the second group to section 1.1 — having no version to bump + is not permission to break. +- **`WEB/DESIGN_DOCUMENT_WEBAPP.md` v1** — guidelines for a server-rendered web interface driven + over a websocket (NiceGUI on FastAPI), carried in addition to `DESIGN_DOCUMENT_PYTHON.md`. Covers + layers, one connection and one lock per service, page and dialog structure, where the language + belongs, tables as the main idiom (including the slot trap), hand-written canvases, theming, + performance, errors and notifications, authentication, testing, and a checklist for a new screen. + +### Changed +- **`AGENTS.md` v8 → v9** — the table mapping project types to design documents is gone. Every + `DESIGN_DOCUMENT*.md` in the project root now applies, with the suffix saying what it covers (a + language, application vs. library, or a part of the system such as `_WEBAPP`), so the list can no + longer go stale. Where two design documents disagree, the more specific one wins for what it covers. +- The `MASTERS` comment in `check_versions.py` no longer claims a design document is always named + after a language — it is named after what it scopes, which may be a cross-cutting concern. + +--- + ## v6 — 2026-08-18 ### Added diff --git a/Claude/AGENTS.md b/Claude/AGENTS.md index 933fa33..d7639f6 100644 --- a/Claude/AGENTS.md +++ b/Claude/AGENTS.md @@ -1,24 +1,21 @@ # AI Agents - Project Rules -**Document Version:** v8 (independent, incremented on structural changes) +**Document Version:** v9 (independent, incremented on structural changes) Language-agnostic rules for AI assistants (Claude Code, Cursor, Copilot, etc.). -Everything language-specific — package manager, formatter, linter, test framework, -logging library, line length, project layout — lives in the matching design document: +Everything specific — package manager, formatter, linter, test framework, logging library, +line length, project layout, interface conventions — lives in the design documents. -| Project type | Design document | -|--------------------|------------------------------------| -| Python application | `DESIGN_DOCUMENT_PYTHON.md` | -| Python library | `DESIGN_DOCUMENT_PYTHON_MODULE.md` | -| Rust application | `DESIGN_DOCUMENT_RUST.md` | -| Rust library | `DESIGN_DOCUMENT_RUST_LIB.md` | -| Godot | `DESIGN_DOCUMENT_GODOT.md` | +**Find them, do not look them up in a list:** every file matching `DESIGN_DOCUMENT*.md` in the +project root applies to this project. The suffix says what it covers — the language +(`DESIGN_DOCUMENT_PYTHON.md`), whether it is an application or a library +(`..._PYTHON_MODULE.md`), or a part of the system that has conventions of its own +(`..._WEBAPP.md`). A project carries as many as it needs and nothing collides, so no list here +needs maintaining and none can go stale. -The language is part of the file name, so a project that mixes languages carries one design document -per language and nothing collides. - -Where this file and a design document disagree, **the design document wins**. +Where this file and a design document disagree, **the design document wins**. Where two design +documents disagree, the more specific one wins for what it covers. ## First-time setup diff --git a/README.md b/README.md index 79bd684..81e5385 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,9 @@ Dokumentace/ │ ├── DESIGN_DOCUMENT_GODOT.md # Guidelines for Godot projects │ └── .gitignore # Godot-specific ignore rules │ +├── API/ # API design guidelines +│ └── DESIGN_DOCUMENT_API.md # Guidelines for projects exposing an HTTP API +│ ├── Project template/ # Reusable files for new projects │ ├── CHANGELOG.md # Changelog template │ ├── PROJECT.md # Project documentation template @@ -67,6 +70,7 @@ Where the two disagree, **the design document wins**. | Rust (application) | `Rust/DESIGN_DOCUMENT_RUST.md` | | Rust (library) | `Rust/DESIGN_DOCUMENT_RUST_LIB.md` | | Godot | `Godot/DESIGN_DOCUMENT_GODOT.md` | +| Any project exposing an HTTP API | `API/DESIGN_DOCUMENT_API.md` (in addition to the language document) | ### Starting a new project diff --git a/WEB/DESIGN_DOCUMENT_WEBAPP.md b/WEB/DESIGN_DOCUMENT_WEBAPP.md new file mode 100644 index 0000000..9ea3d61 --- /dev/null +++ b/WEB/DESIGN_DOCUMENT_WEBAPP.md @@ -0,0 +1,270 @@ +# Web Application Guidelines + +**Document Version:** v1 + +> **Note on Versioning:** +> - This document version is independent — reused across projects +> - It describes a **server-rendered web interface driven over a websocket** (NiceGUI on FastAPI); the language rules stay in `DESIGN_DOCUMENT_PYTHON.md` +> - Where this document and the language document disagree about the same thing, the language document wins for language matters, this one for interface matters + +## Related Documents + +- **README.md** — Project overview, tool descriptions, build instructions +- **AGENTS.md** — Rules for AI assistants +- **DESIGN_DOCUMENT_PYTHON.md** — Language rules: tooling, typing, logging, layout +- **PROJECT.md** — Project goals and current state + +--- + +## 1. What This Stack Is + +One Python process serves the pages, holds the session state and talks to the database. The browser +renders components and sends events back over a websocket; there is no separate front-end build, no +REST layer between the screen and the data, and no client-side router. + +That buys a great deal — a screen is a function, a test can call it, a value is one attribute access +away — and it costs three things that shape every rule below: + +| Property | Consequence | +|---|---| +| The server holds the UI state | A slow call blocks the screen, and often every screen | +| Events cross a websocket | Anything the browser does must reach the server to have happened | +| Components are a third-party library | Its escape hatches (slots, CSS layers) follow its rules, not yours | + +--- + +## 2. Layers + +``` +core/ decisions: models, rules, calculations — no UI, no database +repository/ storage: one class per table or aggregate — no UI +services/ one wired-up object: connection + repositories + shared services +web/ screens: build components, read services, write back +api/ HTTP routes for machines +``` + +Dependencies point one way: `web → services → repository → core`. A core module never imports the +web layer, a repository never renders, a screen never opens a connection. + +**Every screen has a pure counterpart.** What a screen *decides* — which rows, in what order, which +cells may be edited, what a value reads as — belongs in a core module that takes data and returns +plain structures. The screen is then the part that cannot be unit tested but also barely has logic +in it. This is the single most useful rule in this document: it is what makes a complicated screen +testable without a browser. + +--- + +## 3. Services: One Connection, One Lock + +The whole point of a server is that the database connection exists once, not per user: + +- One process-wide `Services` object, built lazily on first use, holding the connection and every + repository built on it +- Database drivers are usually **not** safe for concurrent use, so every call goes through one lock: + `svc.run(fn, *args)` +- A screen never touches the driver, only `svc.run(...)` + +The lock has a consequence worth writing on the wall: **a slow query freezes the application for +everybody**, not just for the person who triggered it. See §9. + +Replace the lock with a connection pool before the user count makes this untrue — the call sites do +not change. + +--- + +## 4. Page and Screen Structure + +- **Entry point** starts the server, installs logging before anything that can fail, and prints every + address it can be reached at +- **One page per URL**: a sign-in page and a main page are usually enough; everything else is a + dialog opened from the main page +- **A screen is a function**: `open_(svc, ...)` builds its components and returns. No global + state, no singletons per screen, nothing left registered when it closes +- **State lives in one dict per page**, not in module globals: which object is selected, which tab, + which ticks. A dict, because it is shared with the nested functions that redraw parts of the page +- **Redraw in units, not wholesale.** Mark the parts that change as refreshable and refresh those; + rebuilding the page loses scroll position, focus and any transient state the browser holds + +### Dialogs + +Dialogs are for decisions and rare edits — a confirmation, a picker, managing relations. They are not +where everyday data is edited: a value a person changes ten times a minute belongs in the table they +are already looking at (§6). + +--- + +## 5. Where the Language Belongs + +Pick one language for the interface and one for the code, and keep them apart: + +- Code, comments, identifiers, log messages, commit messages: **English** +- Labels, notifications, validation messages the user reads: **the user's language** + +Mixing them inside one screen is what happens by accident when a feature is written in a hurry; +mixing them across screens is what the user notices. + +--- + +## 6. Tables Are the Main Idiom + +For a data application, the table is the interface. Two rules: + +**Build rows in core, render in web.** A core function turns records into rows and cells — text, +whether it can be edited, where it came from, whether it is held by hand. The screen turns that into +component properties. Sorting, grouping and "show only what differs" then have unit tests. + +**Edit in the cell, not in a dialog.** An input in the cell, written when the field is left or Enter +is pressed — never per keystroke, which turns one edit into a dozen writes. + +### The slot trap + +Component libraries let you replace a cell with your own markup (a "slot"). Framework event bindings +written inside such a slot **may never fire** — the slot is compiled in a scope that is not the one +the binding expects. Verified in a browser: the handler was never reached, with no error anywhere. + +The reliable pattern is to bypass the framework's event system for that one case: + +- A plain `` with a **native** attribute handler (`onblur`, `onkeydown`) — compiled by the + browser itself +- The handler calls the framework's own "send an event to the server" function with a small payload: + which panel, which record, what value + +### Addressing the panel + +An event that carries an id, and a server-side registry of live panels, is the general shape for +anything the browser drives: + +``` +mount(panel_id, handler) # once per page build, outside any refreshable +emit → {panel: , …} # from the browser +dispatch → registry[id] # server routes it, ignores unknown ids +release on disconnect # a browser that goes away stops answering +``` + +Two rules that are easy to get wrong: + +- **Subscribe once per page, never inside the refreshable** that redraws the table. A subscription per + redraw saves a typed value once per redraw that has ever happened +- **Put the client id in the panel id.** The registry is process-wide; without it, a second person's + typing is handed to the first person's screen, written as them + +--- + +## 7. Custom Canvases and Other Hand-Written Markup + +When a component library has nothing for what you need — a node editor, a diagram — hand-write the +markup and keep the browser side thin: + +- **The browser reports, the server decides.** A gesture (a wire dropped, a box let go) is sent as an + event; the state changes on the server, where a test can reach it. Only what is impossible to do + server-side — the box following the pointer — stays in the browser +- **One id per canvas**, as in §6, so two open canvases cannot answer for each other +- **Attach with a retry**: markup and script arrive in separate messages, so the script's entry point + polls a few times for its own element instead of assuming it is there +- **Sanitisers strip `