Add API and web application design documents
This commit is contained in:
@@ -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<n>/<CONSUMER>/<resource>
|
||||
```
|
||||
|
||||
| Segment | Rule | Example |
|
||||
|---------|------|---------|
|
||||
| `v<n>` | lowercase `v` + integer, no dots | `/v0`, `/v1` |
|
||||
| `<CONSUMER>` | the consuming system, as its own abbreviation | `WEXT`, `PDH`, `SALESOFFER` |
|
||||
| `<resource>` | 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 | `<consumer>_router` | `wext_router` |
|
||||
| Handler function | `<consumer>_<resource>`, `snake_case` | `wext_attribute_defs` |
|
||||
| Single-item handler | `<consumer>_<resource>`, singular | `wext_product` |
|
||||
| OpenAPI tag | `<Consumer> 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=<ISO-8601>` 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
|
||||
@@ -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<n>/<CONSUMER>/<resource-in-kebab-case>`, 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
|
||||
|
||||
+11
-14
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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_<thing>(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 `<input>` 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: <id>, …} # 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 `<style>` and `<script>`.** If the framework sanitises raw HTML by default, an
|
||||
opt-out is required for hand-written markup — and then **every value interpolated into that markup
|
||||
must be escaped where it is built**. Opting out without escaping is how a catalogue name becomes an
|
||||
injection
|
||||
- **Pan and zoom belong to the browser** and must survive a redraw; recomputing a fit on every edit
|
||||
throws away what the person set up
|
||||
|
||||
---
|
||||
|
||||
## 8. Theming
|
||||
|
||||
- **One module holds the palette** and applies it once per page: the framework's colour variables plus
|
||||
a stylesheet. Screens use the names, never the hex codes
|
||||
- **One selection style** for every pick-one list in the application. Five copies of "the selected row
|
||||
is light blue" drift apart within a month
|
||||
- **A brand colour is a fill, not small text.** A mid-tone brand colour on white fails contrast at
|
||||
11 px. Filled buttons get it with white on top; flat and outlined buttons keep near-black labels
|
||||
- **Component libraries fight back with `!important`.** When an override does not take, the cause is
|
||||
usually CSS layers: for important declarations, a layered rule beats an unlayered one, and the
|
||||
earlier layer wins. Put overrides in a layer the framework declares *before* its own important
|
||||
layer, rather than escalating specificity
|
||||
- **Status colours are not brand colours.** Error, warning and success carry meaning and stay
|
||||
recognisable even when the palette changes
|
||||
- **Static assets** (logo, images, icon) are served from one directory at a fixed URL and bundled by
|
||||
the packaging step. Check that the packaging spec includes them; a page that works from source and
|
||||
is bare in the executable is the default failure
|
||||
|
||||
---
|
||||
|
||||
## 9. Performance
|
||||
|
||||
The lock in §3 turns any slow call into a global freeze, so the interface layer owns performance:
|
||||
|
||||
- **Never walk a tree node by node.** One recursive query instead of one query per node. A real case
|
||||
from this project: a depth lookup cost 8 491 round trips and 22.5 s, ran on every click, and made
|
||||
the whole application unusable while it did
|
||||
- **One round trip per screen refresh** is the target; a handful is acceptable; per-row queries are a
|
||||
defect
|
||||
- **Measure before and after with a query counter**, not with impressions. Wrap the database's execute
|
||||
method, count calls, print the time. It takes ten minutes and it is the only way to know
|
||||
- **Page long lists** and cap what a screen renders (rows, columns, hits) with an explicit, visible
|
||||
limit — "showing the first 200 of 7 890" — rather than a silent truncation
|
||||
- **Cache what is recomputed per redraw**, such as compiled patterns, at module level. Anything
|
||||
derived from code, not from data, can be computed once per process
|
||||
|
||||
---
|
||||
|
||||
## 10. Errors, Refusals and Notifications
|
||||
|
||||
- **Refuse rather than guess.** A value that cannot be parsed, a move that would create a cycle, a
|
||||
rename that would orphan something: say what is wrong, change nothing
|
||||
- **Every write says what it did**, as a short notification: what was saved, how many objects it
|
||||
reached, what it left alone
|
||||
- **One failure must not cost the whole screen.** A record that will not load is dropped with a
|
||||
logged warning; the other sixty still render
|
||||
- **Log for the operator, notify for the user.** They are different messages: the log has ids and
|
||||
counts, the notification has the sentence
|
||||
|
||||
---
|
||||
|
||||
## 11. Authentication and the Machine Interface
|
||||
|
||||
- Every page checks the session and redirects to sign-in; every role check happens **on the server**,
|
||||
never by hiding a menu item
|
||||
- HTTP routes for machines live beside the pages in the same process and authenticate per request
|
||||
(HTTP Basic is enough behind a firewall); they never rely on the browser session
|
||||
- **A health endpoint** is worth more than it costs: unauthenticated it answers "is the server up",
|
||||
authenticated it answers "is the data in the state it should be" — schema version, drift, coverage.
|
||||
It is where a broken deployment announces itself
|
||||
- Turn on the framework's API documentation and make the addresses visible in the interface, showing
|
||||
**the address the browser used**, not `localhost`, which on anybody else's machine is their own
|
||||
computer
|
||||
- Secrets come from the environment; nothing secret is ever logged
|
||||
|
||||
---
|
||||
|
||||
## 12. Testing
|
||||
|
||||
Three levels, each for what only it can see:
|
||||
|
||||
| Level | Covers | Cost |
|
||||
|---|---|---|
|
||||
| Core functions | Rows, ordering, what is editable, every rule | Milliseconds |
|
||||
| Simulated client | The screen assembles, a click reaches its handler, a dialog opens | Fast, no browser |
|
||||
| Real browser | Anything that only exists in a browser: gestures, styles, native events | Slow, opt-in |
|
||||
|
||||
- **The simulated client cannot see into component properties.** A table's contents live in its props,
|
||||
not in the page text, so assert on the element's `columns` and `rows`, not on visible text
|
||||
- **A real-browser test is worth writing for the parts that burned you.** Drive it over the browser's
|
||||
debugging protocol: sign in, click, type real keystrokes, read the DOM back. Two bugs in this
|
||||
project — an unstyled canvas and a dead event binding — were invisible to every other level
|
||||
- **Skip, do not fail, when no browser is present** (`CDE_BROWSER=none`), so the suite still runs on a
|
||||
machine that has none
|
||||
- **Never verify against production data without restoring it.** If a check must write, write one
|
||||
field, read it back through the database, restore it, and say so in the report
|
||||
|
||||
---
|
||||
|
||||
## 13. Checklist for a New Screen
|
||||
|
||||
1. What does it decide? Put that in a core module with tests
|
||||
2. What does it show? Build it from the core structures; one refreshable per part that changes
|
||||
3. What does it write? One function per write, each reporting what it did
|
||||
4. Does the browser drive anything? Give it an id, subscribe once, release on disconnect
|
||||
5. How many queries does one refresh cost? Count them
|
||||
6. Does it use the palette, or does it invent a colour?
|
||||
7. Which of the three test levels does it need? At least the first
|
||||
+4
-2
@@ -34,8 +34,9 @@ import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
# Guideline documents owned by this repository, mapped to their master copy. The language is part of
|
||||
# every design document name, so a project may carry several of them without any collision.
|
||||
# Guideline documents owned by this repository, mapped to their master copy. Design documents are named
|
||||
# after what they scope - a language, or a cross-cutting concern such as the API surface - so a project
|
||||
# may carry several of them without any collision.
|
||||
MASTERS: dict[str, str] = {
|
||||
"CLAUDE.md": "Claude/CLAUDE.md",
|
||||
"AGENTS.md": "Claude/AGENTS.md",
|
||||
@@ -44,6 +45,7 @@ MASTERS: dict[str, str] = {
|
||||
"DESIGN_DOCUMENT_RUST.md": "Rust/DESIGN_DOCUMENT_RUST.md",
|
||||
"DESIGN_DOCUMENT_RUST_LIB.md": "Rust/DESIGN_DOCUMENT_RUST_LIB.md",
|
||||
"DESIGN_DOCUMENT_GODOT.md": "Godot/DESIGN_DOCUMENT_GODOT.md",
|
||||
"DESIGN_DOCUMENT_API.md": "API/DESIGN_DOCUMENT_API.md",
|
||||
}
|
||||
|
||||
# Names used before the language became part of the file name. A project still carrying one of these
|
||||
|
||||
Reference in New Issue
Block a user