dotkernel-feature-docs
Use when writing down what a feature does and why in a Dotkernel project — after building or changing a route, module, command or entity, when asked to document a feature, write release notes, record
SKILL.md.claude/skills/dotkernel-feature-docs/SKILL.md
GitHubFeature documentation
Applies to
Projects: - all
Packages: - none
Code records what a project does. A feature doc records why, plus the four things that are
spread across four files — the routes, the roles allowed to reach them, the data added, and how to
exercise it. That is the part a fresh session cannot reconstruct by reading src/, and the part
lost when a session is cleared.
One doc per feature, never per commit and never per route. Two routes that only make sense together belong in one doc.
Where it goes
Detect before writing. Some Dotkernel repositories already own a documentation/ directory,
holding some mix of command/*.md, a generated openapi.json and Postman collections. Among the
stock projects only the API ships one, but derived projects add or drop it freely — check, do not
infer it from the project type:
ls -d documentation docs 2>/dev/null
| Repo already has | Write to |
|---|---|
documentation/ |
documentation/features/ |
docs/ |
docs/features/ |
| neither | create docs/features/ |
Never create a second documentation root next to an existing one. Filename is
YYYY-MM-DD-<kebab-slug>.md, the date being the day the doc is written — sortable, and it makes
the reading order obvious. The directory also carries a README.md index, one row per doc.
The template
---
feature: Book reviews
date: 2026-08-05
variant: dotkernel/api
status: shipped
modules: [Book]
routes:
- POST /book/{id}/review
handlers:
- Api\Book\Handler\Book\Review\PostBookReviewResourceHandler
---
# Book reviews
## Why
Two or three sentences: the need, and the constraint that shaped the approach. The one section
that cannot be recovered by reading the code.
## Surface
| Route | Name | Handler | Roles |
| --- | --- | --- | --- |
| `POST /book/{id}/review` | `book::create-review` | `PostBookReviewResourceHandler` | `user` |
## Data
Entities and columns added or changed — in `Core` for API and Admin, in the module's own `Entity/`
for Frontend. Migration required: yes / no — and the command, not run.
## Config
Authorization entries, ConfigProvider registrations, new config keys, and where their secrets
live (`*.local.php`, never committed).
## Testing
What is covered, and how to run just this feature's tests.
## Follow-ups
Known gaps, deferred decisions, anything a reviewer waved through.
The frontmatter is the machine-readable part: /dk-review greps routes: and handlers: to
decide whether a change is documented. Keep both accurate or the staleness check silently passes.
status is one of planned, shipped or deprecated.
Writing it from the code, not from memory
Every claim in the doc is read from somewhere. Where it came from:
| Section | Source of truth |
|---|---|
| Routes and route names | the module's RoutesDelegator |
| Roles | authorization.global.php (API); Admin / Frontend: the permission in authorization-guards.global.php, then the roles holding it in authorization.global.php |
| Handler class and path | the filesystem under src/<Module>/src/Handler/ |
| Registrations | the module's ConfigProvider |
| Data and migration need | changed entities under src/Core/ (API, Admin); Frontend: the module's own Entity/; Light and Queue: none |
| Testing | the test files that actually exist |
An undocumented gap is better than an invented fact. If something could not be verified — the role
mapping is unclear, the migration status is unknown — write unverified and say which file to
check, rather than asserting the plausible answer. A wrong feature doc is consulted with the same
confidence as a right one.
What a feature doc is not
- Not an API reference. In a project that publishes OpenAPI,
OpenAPI.phpis the contract (seedotkernel-openapi). Name the operation, do not restate its schemas — two descriptions of one payload diverge, and the generated one wins. - Not a changelog. No per-release history, no version bumps. The doc describes the feature as
it is now;
git logholds how it got there. - Not a code dump. Reference
src/Book/src/Handler/…by path. Pasted bodies go stale silently and the real thing is one grep away. - Not a design proposal. A doc with
status: plannedstates what is intended and says so; it does not describe intent in the past tense.
Keeping it current
Update in place when the same feature changes — new route on an existing resource, a role added, a
field renamed. Add a new doc only when the feature is genuinely new. Refresh the README.md index
row in the same edit.
A breaking change handled through dotkernel-evolution-pattern touches the doc twice: status
becomes deprecated with the sunset date and the replacement named in Follow-ups when the
deprecation lands, and the doc is removed only once the route is gone.
Security constraints
- Never put a real credential, token, internal hostname, customer email or production identifier in an example. Use obvious placeholders. Documentation is the artefact most likely to be copied into a ticket, a chat or a public repository.
- Name where a secret lives (
config/autoload/*.local.php), never its value. - Do not document an internal-only route in a way that implies it is a public surface.
Definition of done
- Written to the features directory this repo actually uses, not a new one.
- Filename
YYYY-MM-DD-<kebab-slug>.md; frontmatter parses;statusaccurate. -
routes:andhandlers:list every route and handler the feature added. - Every route name in the Surface table matches
RoutesDelegator, and every role matches this variant's authorization config. - Every file path and class name in the doc resolves.
-
Whyexplains a constraint, not the mechanics — the mechanics are in the code. - Migration need stated, and the command given rather than run.
- No credential, token, internal hostname or real customer data anywhere in it.
-
README.mdindex row added or updated.