feat: slice 1 - the neckbeard framework chain runs end to end
Tracer bullet of the migration design (Gate 4, slice 1): pinned v0.1.1 baseline under docs/sources/upstream/ with provenance note, the Karpathy block moved verbatim to docs/sources/regelwerk/ (standing rule mapped onto the sources read-only mechanism), AGENTS.md assembled from the byte-true upstream sections plus the project section 6 (group rules condensed from the old CLAUDE.md), CLAUDE.md reduced to the upstream pointer, WORKFLOW.md and all four templates copied, schema.yaml extended (issue milestone/priority/status columns, component type, wiki area vision - all flagged in the header), validate.py and gen_status.py forked with marked extensions, pruefe_upstream_drift.py added, STATUS.md generated, CI gains the offline validate job, README directory link defused. Verified: validate 0 errors 0 warnings (the three pre-existing directory-link errors are gone), gen_status --check current, drift check 0 findings, baseline byte-identical to the reference checkout (10/10 files), four negative tests fire (WIP limit 3x in-progress, waiting without wartegrund, component slug mismatch, single-byte drift in WORKFLOW.md). gen_status needs Python >= 3.10 locally (write_text newline) - noted for the design AAR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
5e46372ea8
commit
e36ed337a7
@@ -33,3 +33,19 @@ stillstandspruefung:
|
|||||||
# Befunde sind kein Betriebsausfall, aber sie sollen sichtbar bleiben. Die rote
|
# Befunde sind kein Betriebsausfall, aber sie sollen sichtbar bleiben. Die rote
|
||||||
# Pipeline ist bei uns die Alarmanlage (gitops/CLAUDE.md, TURN-Rotation).
|
# Pipeline ist bei uns die Alarmanlage (gitops/CLAUDE.md, TURN-Rotation).
|
||||||
allow_failure: false
|
allow_failure: false
|
||||||
|
|
||||||
|
# Offline-Gate bei jedem Push: Artefakte gegen schema.yaml, STATUS.md
|
||||||
|
# aktuell, Framework-Dateien unveraendert (Design 2026-08-11, Slice 1).
|
||||||
|
# Braucht nur den Baum - bewusst ohne Token und ohne Netz.
|
||||||
|
validate:
|
||||||
|
stage: pruefen
|
||||||
|
image: python:3.12-alpine
|
||||||
|
rules:
|
||||||
|
- if: $CI_PIPELINE_SOURCE == "push"
|
||||||
|
before_script:
|
||||||
|
- pip install --quiet pyyaml
|
||||||
|
script:
|
||||||
|
- python3 scripts/validate.py
|
||||||
|
- python3 scripts/gen_status.py --check
|
||||||
|
- python3 scripts/pruefe_upstream_drift.py
|
||||||
|
allow_failure: false
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
# AGENTS.md — Canonical Agent Instructions
|
||||||
|
|
||||||
|
Canonical instruction set for any coding agent working in this repository
|
||||||
|
(Claude Code, GPT-OSS harnesses, others). `CLAUDE.md` points here.
|
||||||
|
This file is loaded into every session — keep it short. Process details
|
||||||
|
live in `WORKFLOW.md`; read that when a task begins, not preemptively.
|
||||||
|
|
||||||
|
Tradeoff: these rules bias toward caution over speed. For trivial tasks,
|
||||||
|
use judgment — but say so.
|
||||||
|
|
||||||
|
## 1. Operating Rules
|
||||||
|
|
||||||
|
### Think before coding
|
||||||
|
- State your assumptions explicitly. If uncertain, ask.
|
||||||
|
- If multiple interpretations exist, present them — don't pick silently.
|
||||||
|
- If a simpler approach exists, say so. Push back when warranted.
|
||||||
|
- If something is unclear, stop. Name what's confusing. Ask.
|
||||||
|
|
||||||
|
### Simplicity first
|
||||||
|
- Minimum code that solves the problem. Nothing speculative.
|
||||||
|
- No features beyond what was asked. No abstractions for single-use code.
|
||||||
|
- No "flexibility" or "configurability" that wasn't requested.
|
||||||
|
- No error handling for impossible scenarios.
|
||||||
|
- If you write 200 lines and it could be 50, rewrite it.
|
||||||
|
- Test: "Would a senior engineer call this overcomplicated?" If yes, simplify.
|
||||||
|
- Before writing new code, stop at the first rung that holds:
|
||||||
|
needed at all? → codebase already has it? → stdlib? → platform-native?
|
||||||
|
→ installed dependency? → one line? → only then: the minimum that works.
|
||||||
|
(Ladder after ponytail, MIT.)
|
||||||
|
- Never cut, at any rung: trust-boundary validation, data-loss handling,
|
||||||
|
security, accessibility.
|
||||||
|
- Lazy about the solution, never about reading the code first.
|
||||||
|
|
||||||
|
### Surgical changes
|
||||||
|
- Touch only what you must. Match existing style, even if you'd differ.
|
||||||
|
- Don't "improve" adjacent code, comments, or formatting.
|
||||||
|
- Don't refactor things that aren't broken.
|
||||||
|
- If you notice unrelated dead code, mention it — don't delete it.
|
||||||
|
- Remove imports/variables/functions that YOUR changes made unused;
|
||||||
|
leave pre-existing dead code alone unless asked.
|
||||||
|
- Every changed line must trace directly to the request.
|
||||||
|
|
||||||
|
### Goal-driven execution
|
||||||
|
- Transform tasks into verifiable goals:
|
||||||
|
"fix the bug" → "write a test that reproduces it, then make it pass".
|
||||||
|
- For multi-step work, state a brief plan: step → verify, step → verify.
|
||||||
|
- A task is well-defined only if it names all four:
|
||||||
|
**files, action, verify, done.** Missing one? The task is too vague — say so.
|
||||||
|
|
||||||
|
### Verification before completion
|
||||||
|
- Never claim something works without evidence: a test run, command
|
||||||
|
output, a rendered result. "Should work" is not a status.
|
||||||
|
- Report every task/slice with exactly one status:
|
||||||
|
`DONE` | `DONE_WITH_CONCERNS` | `NEEDS_CONTEXT` | `BLOCKED`.
|
||||||
|
- Uncertainty is reported, never swallowed. Flag your shakiest calls.
|
||||||
|
|
||||||
|
## 2. Project Initialization (Gate 0)
|
||||||
|
|
||||||
|
At session start, read `PROJECT.md`. If it does not exist, initialization
|
||||||
|
is your first task: before anything else, ask the Gate 0 questions defined
|
||||||
|
in `WORKFLOW.md` — response language, size-S gate exception (yes/no),
|
||||||
|
one-line project purpose, audience — write the answers to `PROJECT.md`,
|
||||||
|
and have `validate.py` accept it. Never guess these answers; ask.
|
||||||
|
|
||||||
|
## 3. Workflow
|
||||||
|
|
||||||
|
For anything beyond a trivial change, read `WORKFLOW.md` and follow its
|
||||||
|
gates. At task start, propose a size class (S/M/L); the human confirms
|
||||||
|
(possibly batched later). **Never advance past a gate without explicit
|
||||||
|
human approval** — sole exception: size-S tasks, and only if `PROJECT.md`
|
||||||
|
explicitly grants that exception.
|
||||||
|
|
||||||
|
## 4. Repository Map
|
||||||
|
|
||||||
|
| Path | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `WORKFLOW.md` | Gate 0 (init) + Gates 1–5, size classes, debugging path, session handoff, refinement ritual |
|
||||||
|
| `PROJECT.md` | Per-project answers from Gate 0: language, size-S exception, purpose, audience |
|
||||||
|
| `STATUS.md` | Generated overview: open issues, active designs, recent ADRs — do not edit by hand |
|
||||||
|
| `schema.yaml` | Frontmatter schema — single source of truth for artifact structure |
|
||||||
|
| `docs/adr/` | Architecture Decision Records — binding; never edited, only superseded |
|
||||||
|
| `docs/design/` | One design doc per undertaking; completed ones move to `done/` |
|
||||||
|
| `docs/aar/` | Standalone After Action Reviews (incidents, major deviations only) |
|
||||||
|
| `docs/issues/` | In-repo issues, one file each; status lives in frontmatter |
|
||||||
|
| `docs/wiki/` | Wiki areas as folders, created on demand — rules in `docs/wiki/index.md` |
|
||||||
|
| `docs/sources/` | Immutable original sources; wiki pages cite them — read-only for agents |
|
||||||
|
| `scripts/` | Deterministic tooling: `validate.py`, `gen_status.py` |
|
||||||
|
|
||||||
|
Before proposing options (Gate 2), read the relevant ADRs and AARs first —
|
||||||
|
past decisions and learnings are input, not trivia.
|
||||||
|
|
||||||
|
## 5. Artifact Rules
|
||||||
|
|
||||||
|
- All artifacts are standard Markdown with YAML frontmatter conforming to
|
||||||
|
`schema.yaml`. Standard links only (`[text](path.md)`), no wikilinks.
|
||||||
|
Diagrams as Mermaid. This keeps every artifact portable across LLMs,
|
||||||
|
GitLab, and Obsidian.
|
||||||
|
- Never invent frontmatter fields or status values. `validate.py` is
|
||||||
|
authoritative; if it rejects your artifact, fix the artifact, not the
|
||||||
|
validator.
|
||||||
|
- Deterministic jobs (status generation, validation, link checks) are done
|
||||||
|
by scripts, not by you. If a deterministic job lacks a script, propose
|
||||||
|
one instead of doing it by inference.
|
||||||
|
|
||||||
|
<!-- projektabschnitt -->
|
||||||
|
|
||||||
|
## 6. Gruppenregeln (Projekt axion1337.chat)
|
||||||
|
|
||||||
|
Dieses Repo steuert die Gruppe `axion1337.chat`. Die Abschnitte 1–5
|
||||||
|
oben sind neckbeard v0.1.1 und bleiben byte-treu (Baseline:
|
||||||
|
`docs/sources/upstream/neckbeard-v0.1.1/`, Prüfung:
|
||||||
|
`scripts/pruefe_upstream_drift.py`); §1 ist destilliert aus den
|
||||||
|
wortgleich archivierten
|
||||||
|
[Karpathy-Guidelines](docs/sources/regelwerk/karpathy-guidelines.md).
|
||||||
|
**Änderungen an dieser Datei nur mit sorb abgestimmt.** Dieser
|
||||||
|
Abschnitt gilt für jede Session in allen Repos der Gruppe;
|
||||||
|
Komponenten-Repos tragen nur Projektspezifika plus einen Pointer
|
||||||
|
hierher ([ADR-0013](docs/adr/0013-gruppenregeln-kanonisch-mit-pruefung.md)).
|
||||||
|
Ohne Lab-Zugang: dieses Repo ist als Push-Mirror unter
|
||||||
|
`https://rohana.axion1337.de/sorb/management` lesbar — pushen dorthin ist tabu.
|
||||||
|
|
||||||
|
### Quelle der Wahrheit & Mirror-Topologie
|
||||||
|
|
||||||
|
- `git.lab/axion1337.chat/*` ist kanonisch; Gitea/rohana wird per
|
||||||
|
Push-Mirror beliefert und bleibt Flux-Quelle, Registry und
|
||||||
|
Release-Download ([ADR-0001](decisions/0001-gitlab-kanonisch-push-mirror.md),
|
||||||
|
[ADR-0004](decisions/0004-site-to-site-vpn-hetzner-lab.md)). Die
|
||||||
|
Flux-Quelle **nicht auf git.lab „geradeziehen"** — die Produktion darf
|
||||||
|
nicht an der Lab-Verfügbarkeit hängen.
|
||||||
|
- **Nie direkt zu Gitea pushen** (der Mirror überschreibt per Force).
|
||||||
|
Landet doch ein Commit dort: Kanonisierungs-Verfahren in
|
||||||
|
[verfahren/deploy-uebergabe.md](verfahren/deploy-uebergabe.md).
|
||||||
|
- Einzige bewusste Ausnahme: der TURN-Rotations-CronJob schreibt nach
|
||||||
|
Gitea; der tägliche CI-Job `canonize_rotation` holt es zurück. Seine
|
||||||
|
rote Pipeline **ist** der Alarm — es gibt bewusst keinen zweiten Meldeweg.
|
||||||
|
|
||||||
|
### Issues & Board
|
||||||
|
|
||||||
|
- `docs/issues/` ist kanonisch für den Management-Scope
|
||||||
|
([ADR-0012](docs/adr/0012-issues-im-repo-gitlab-als-spiegel.md));
|
||||||
|
GitLab ist bespiegelte Ansicht. Jedes Issue trägt genau einen
|
||||||
|
Meilenstein (M1–M5) und genau eine Priorität — das Schema erzwingt
|
||||||
|
beides. Zeitkritisches trägt ein `due`-Datum, nicht „bald".
|
||||||
|
- **WIP-Limit 2** (Validator-Regel). Die Zusage-Status `next` und
|
||||||
|
`in-progress` vergibt **nur sorb**; Sessions bilden ab (`waiting` mit
|
||||||
|
`wartegrund`, Erledigtes `done` mit Begründung im Issue-Commit),
|
||||||
|
sagen aber nichts zu.
|
||||||
|
- **ADR-Pflicht** bei Architektur-/Prozessentscheidungen und **jeder
|
||||||
|
dauerhaften Ausnahme von einer Regel** — eine Ausnahme nur zu
|
||||||
|
dokumentieren statt sie zu entscheiden, ist ein Fehler.
|
||||||
|
|
||||||
|
### Secrets & Credentials
|
||||||
|
|
||||||
|
- Token-/Secret-Werte **niemals anzeigen, loggen oder in Dateien
|
||||||
|
echoen** — anzeigen = Exposure = Rotation. Referenz nur über
|
||||||
|
Dateipfade (z. B. `~/.config/gitlab-lab/token`) oder maskierte
|
||||||
|
CI-Variablen. Die Trennung ist „Credential vs. Config":
|
||||||
|
nicht-geheime Konfiguration wird normal committet.
|
||||||
|
|
||||||
|
### Commit-Konventionen
|
||||||
|
|
||||||
|
- Nachrichten auf Englisch, Conventional-Stil; der Betreff sagt *was*,
|
||||||
|
der Rumpf *warum*.
|
||||||
|
- Autor- **und** Committer-Datum auf 12:00:00 UTC des laufenden Tages;
|
||||||
|
kanonische Autor-Identität. Historien-Rewrites nur mit
|
||||||
|
alt→neu-Zuordnung
|
||||||
|
([ADR-0009](decisions/0009-commit-konventionen-und-historien-anonymisierung.md),
|
||||||
|
Tabelle: [shared/commit-zuordnung-2026-08-07.md](shared/commit-zuordnung-2026-08-07.md)).
|
||||||
|
- ⚠️ Das schützt nur die Git-Historie; Plattform-Zeitstempel (Push,
|
||||||
|
Issues, Pipelines, Pakete) tragen die echte Uhrzeit (ADR-0009).
|
||||||
|
|
||||||
|
### Redlichkeit
|
||||||
|
|
||||||
|
- Verifiziert (Messung/Konsole) klar von Vermutung trennen;
|
||||||
|
Korrelation ≠ Kausalität — ein plausibler Verdacht ist kein Befund.
|
||||||
|
- Config-Dateien chirurgisch editieren, **nie re-dumpen**; vor dem Push
|
||||||
|
validieren (`docker compose config`, YAML-Parse).
|
||||||
|
- Fehlschläge und übersprungene Schritte benennen, nicht glätten —
|
||||||
|
„fertig" heißt verifiziert (deckungsgleich mit §1).
|
||||||
@@ -1,233 +1 @@
|
|||||||
# CLAUDE.md — übergreifende Arbeitskonventionen (kanonisch)
|
Read AGENTS.md — the canonical instruction file for this repository. All rules live there.
|
||||||
|
|
||||||
Diese Datei gilt für **jede Claude-/Agenten-Session in allen Projekten** der
|
|
||||||
Gruppe (axion1337.chat-Stack, ThreadNet-Repos, CFGMON/threadnet-operating,
|
|
||||||
Homelab). Projekt-Repos haben eigene CLAUDE.mds für ihre Spezifika (z. B. die
|
|
||||||
ESS-/Flux-Details in „ThreadNet Server Suite" = `axion1337.chat-gitops`) — bei
|
|
||||||
Widerspruch gilt für Arbeitsweise und Prozess **diese** Datei.
|
|
||||||
|
|
||||||
> **Für Sessions ohne Lab-Zugang** (CFGMON, MATRIX, …): dieses Repo ist als
|
|
||||||
> Push-Mirror unter `https://rohana.axion1337.de/sorb/management` von überall
|
|
||||||
> **lesbar** — dort diese Datei und die ADRs nachschlagen. Nur pushen ist tabu.
|
|
||||||
>
|
|
||||||
> 📋 **Kopierbare Kurzfassungen zum Voranstellen:**
|
|
||||||
> [verfahren/textbloecke.md](verfahren/textbloecke.md) — Session-Start, Host-Session,
|
|
||||||
> Deploy-Übergabe, Abschluss, Entscheidungsvorlage. Diese Datei hier bleibt die
|
|
||||||
> Quelle; die Bausteine verweisen nur darauf.
|
|
||||||
|
|
||||||
## Projektrealitäten (Stand 2026-08-01)
|
|
||||||
|
|
||||||
**Das Lab ist die Quelle der Wahrheit** ([ADR-0002](decisions/0002-issues-und-management-ins-lab.md)):
|
|
||||||
|
|
||||||
- Kanonische Repos liegen auf `git.lab/axion1337.chat/*` (nur im Lab/VPN
|
|
||||||
auflösbar). Gitea/rohana wird per **Push-Mirror** beliefert und bleibt
|
|
||||||
Flux-Source, Container-/npm-Registry und Release-Download
|
|
||||||
([ADR-0001](decisions/0001-gitlab-kanonisch-push-mirror.md)).
|
|
||||||
- **Warum überhaupt zwei Orte — und warum das kein Altbestand ist:** Auf git.lab
|
|
||||||
liegen die *Baupläne*, auf Gitea eine Kopie, die der Cluster **ohne verfügbares
|
|
||||||
Lab** erreicht. Der Hetzner-Cluster muss sich bauen und neu ausrollen lassen,
|
|
||||||
wenn das Homelab aus ist, im Umbau steckt oder niemand zu Hause ist — er darf
|
|
||||||
deshalb nicht von einem Host abhängen, der nur im Lab antwortet.
|
|
||||||
⚠️ **Die Flux-Quelle nicht „geradeziehen"** auf git.lab: Das sähe aufgeräumter
|
|
||||||
aus und würde die Verfügbarkeit der Produktion an das Lab koppeln — genau das,
|
|
||||||
was die Trennung verhindert.
|
|
||||||
- **Nie direkt zu Gitea pushen** (gespiegelte Repos) — der Mirror überschreibt
|
|
||||||
per Force.
|
|
||||||
- **Gespiegelt wird nur die Gruppe `axion1337.chat`** (die fünf Produkt-Repos und
|
|
||||||
`management`). Die Gruppe **`homelab`** (`docs`, `wiki`, `wiki-bookstack`) hat
|
|
||||||
bewusst **keine Mirrors**: Sie beschreibt und konfiguriert ausschließlich
|
|
||||||
Lab-Infrastruktur, und seit dem Site-to-Site-VPN
|
|
||||||
([ADR-0004](decisions/0004-site-to-site-vpn-hetzner-lab.md)) erreichen auch
|
|
||||||
Host-Sessions git.lab direkt — Tunnel einschalten genügt. Betriebslehren, die
|
|
||||||
von außen lesbar sein müssen, gehören deshalb in die **AARs** unter
|
|
||||||
`verfahren/aar/` (dieses Repo ist gespiegelt), nicht nur in die READMEs der
|
|
||||||
Lab-Repos.
|
|
||||||
- Landet doch ein Commit auf Gitea (z. B. aus einer Host-Session ohne Lab-Route):
|
|
||||||
**Kanonisierungs-Verfahren** in
|
|
||||||
[verfahren/deploy-uebergabe.md](verfahren/deploy-uebergabe.md) — `.patch`
|
|
||||||
von Gitea ziehen, `git am` (erhält Autorschaft), Push über git.lab.
|
|
||||||
- **Issues leben auf git.lab.** Die alten Gitea-Issues sind geschlossen und
|
|
||||||
verweisen dorthin. ⚠️ gitops-Nummern haben sich beim Umzug verschoben
|
|
||||||
(Gitea zählte PRs mit; z. B. Gitea#48 → GitLab#46) — alte „gitops#N"-Verweise
|
|
||||||
meinen die Gitea-Nummer; verbindlich ist der Migrations-Fußtext im Issue.
|
|
||||||
- **Ausnahme** (bewusst entschieden, nur noch eine): der
|
|
||||||
TURN-Rotations-CronJob schreibt weiter nach Gitea, weil er im Cluster läuft und
|
|
||||||
git.lab nicht erreicht.
|
|
||||||
**Die Rotation nicht von Hand nachziehen und den PR nie auf Gitea mergen** —
|
|
||||||
das erledigt seit 2026-08-02 der geplante CI-Job `canonize_rotation` im
|
|
||||||
gitops-Repo täglich von git.lab aus. Scheitert er, bleibt die Pipeline rot;
|
|
||||||
diese rote Pipeline **ist** der Alarm, einen zusätzlichen Termin gibt es
|
|
||||||
bewusst nicht.
|
|
||||||
- **Dokumentation** ([ADR-0006](decisions/0006-wikis-konsolidieren-docusaurus.md)):
|
|
||||||
Das gitops-Wiki liegt seit 2026-08-02 auf git.lab (*Wiki*-Reiter im Projekt);
|
|
||||||
⚠️ der `wiki`-**Branch** im gitops-Repo ist ein überholter Mai-Abzug von `docs/`
|
|
||||||
und nicht die gepflegte Fassung. Alle Quellen zusammen erscheinen unter
|
|
||||||
**axionwiki.lab** ([`homelab/wiki`](https://git.lab/homelab/wiki), Docusaurus) —
|
|
||||||
Inhalte werden beim Bau geholt, **Änderungen gehören ins Quell-Repo**.
|
|
||||||
|
|
||||||
## Arbeitsframework ([ADR-0005](decisions/0005-pm-framework-kanban.md))
|
|
||||||
|
|
||||||
Kanban-Rückgrat mit leichten Scrum-Elementen:
|
|
||||||
|
|
||||||
- **Alles Offene ist ein Issue** — host-/infra-Scope hier im management-Projekt
|
|
||||||
(`host:`-Labels, alte IDs wie `CFGMON-01` bleiben im Titel), Projekt-Scope im
|
|
||||||
jeweiligen Projekt. Kein neues Backlog-Markdown anlegen; `hosts/`/`shared/`
|
|
||||||
sind nur Bestand + Historie.
|
|
||||||
- **Status über Labels**, genau eins pro Issue: `status:next` (die einzige
|
|
||||||
Zusage), `status:doing` (**WIP-Limit 2** — auch sessionübergreifend zu
|
|
||||||
verteidigen), `status:wartet` (nur mit benanntem Grund). Ohne Label = Backlog.
|
|
||||||
- **ADR-Pflicht** ([decisions/](decisions/)) bei Architektur-/Prozess-
|
|
||||||
entscheidungen und **jeder dauerhaften Ausnahme von einer Regel**. Eine
|
|
||||||
Ausnahme nur zu dokumentieren statt sie als Entscheidung vorzulegen, ist ein
|
|
||||||
Fehler.
|
|
||||||
- **Deploy-Übergaben** („einer baut, ein anderer rollt aus") laufen über das
|
|
||||||
Issue-Template und die Pflichtfelder in
|
|
||||||
[verfahren/deploy-uebergabe.md](verfahren/deploy-uebergabe.md) — das ist
|
|
||||||
unsere Definition of Done für Deployments. Nach Deploys mit Übergabe und nach
|
|
||||||
Incidents: **AAR** ([verfahren/aar/](verfahren/aar/), Vorlage liegt daneben).
|
|
||||||
- Prioritäten über `priority:*`; Zeitkritisches bekommt ein **Datum** im Issue,
|
|
||||||
nicht „bald".
|
|
||||||
- **Der Titel trägt keine Priorität.** Präfixe wie `[HIGH]`/`[MEDIUM]`/`[LOW]`
|
|
||||||
gehören nicht in den Titel — die Priorität steht im Label, und zwar nur dort.
|
|
||||||
Alte Kennungen wie `CFGMON-01` bleiben, die benennen den Gegenstand, nicht die
|
|
||||||
Dringlichkeit.
|
|
||||||
⚠️ Der Grund ist keine Ästhetik: Aus der Gitea-Migration trugen 34 Issues ein
|
|
||||||
Präfix, davon **zwei mit einer anderen Aussage als ihr Label** — wer nach Titel
|
|
||||||
sortierte, bekam ein anderes Bild als wer nach Label sortierte. Zwei Wahrheiten
|
|
||||||
über dieselbe Sache sind schlimmer als eine unvollständige. Bereinigt 2026-08-06.
|
|
||||||
- **Jedes Issue gehört zu genau einem Meilenstein** (Gruppen-Milestones M1–M4,
|
|
||||||
siehe [roadmap.md](roadmap.md)). Label und Meilenstein beantworten verschiedene
|
|
||||||
Fragen: `priority:*` sagt **wie dringend**, der Meilenstein sagt **worauf es
|
|
||||||
einzahlt**. Ein Issue ohne Meilenstein taucht in keiner Roadmap-Ansicht auf und
|
|
||||||
ist damit praktisch unsichtbar — es existiert nur noch für den, der es angelegt
|
|
||||||
hat.
|
|
||||||
M1–M4 haben **bewusst kein Enddatum**: Sie bündeln, sie simulieren keinen
|
|
||||||
Termindruck. Termindruck steht als Datum am einzelnen Issue.
|
|
||||||
|
|
||||||
## Secrets & Credentials
|
|
||||||
|
|
||||||
- **Token-/Secret-Werte niemals anzeigen, loggen oder in Dateien echoen** —
|
|
||||||
anzeigen = Exposure = Rotation. Echte Credentials tippt/legt sorb selbst an;
|
|
||||||
Sessions referenzieren sie nur über Dateipfade (z. B.
|
|
||||||
`~/.config/gitlab-lab/token`) oder maskierte CI-Variablen.
|
|
||||||
- Nicht-geheime Konfiguration wird direkt geschrieben und committet — die
|
|
||||||
Trennung ist „Credential vs. Config", nicht „alles über den Menschen".
|
|
||||||
|
|
||||||
## Commit-Konventionen (seit 2026-08-07)
|
|
||||||
|
|
||||||
Gilt für **alle** Repos der Gruppe `axion1337.chat` und die ThreadNet-Dienste.
|
|
||||||
|
|
||||||
- **Nachrichten auf Englisch**, Conventional-Commit-Stil: `feat:`, `fix:`,
|
|
||||||
`docs:`, `chore:`, `ci:`, `refactor:`. Der Betreff sagt *was*, der Rumpf *warum*.
|
|
||||||
- **Zeitstempel anonymisieren.** Autor- **und** Committer-Datum werden auf
|
|
||||||
**12:00:00 UTC des laufenden Tages** gesetzt, damit sich aus der Historie keine
|
|
||||||
persönlichen Arbeitszeiten ablesen lassen:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export GIT_AUTHOR_DATE="$(date -u +%Y-%m-%d)T12:00:00Z" \
|
|
||||||
GIT_COMMITTER_DATE="$(date -u +%Y-%m-%d)T12:00:00Z"
|
|
||||||
git commit -m "…"
|
|
||||||
```
|
|
||||||
|
|
||||||
⚠️ **Beide Variablen setzen.** Nur `GIT_AUTHOR_DATE` zu setzen bringt nichts —
|
|
||||||
`git log` zeigt zwar das Autordatum, das Committer-Datum bleibt aber im Objekt
|
|
||||||
und ist über `git log --format=%cd` und in jeder Weboberfläche sichtbar.
|
|
||||||
|
|
||||||
📎 Die Umstellung der Alt-Historie am 2026-08-07 hat 251 Commits neue SHAs
|
|
||||||
gegeben. Ältere Verweise bleiben über
|
|
||||||
[`shared/commit-zuordnung-2026-08-07.md`](shared/commit-zuordnung-2026-08-07.md)
|
|
||||||
auflösbar — **statt** geschriebene Issue-Kommentare nachträglich zu ändern. Wer
|
|
||||||
eine SHA nicht findet, hat einen Commit von vor der Grenze vor sich; der gilt
|
|
||||||
unverändert.
|
|
||||||
|
|
||||||
⚠️ **Das schützt nur die Git-Historie.** Push-Zeiten, Issue- und
|
|
||||||
Kommentar-Zeitstempel, Pipeline-Läufe und Paket-Veröffentlichungen tragen
|
|
||||||
weiterhin die echte Uhrzeit und liegen im selben GitLab bzw. auf dem
|
|
||||||
öffentlichen Gitea-Spiegel. Wer daraus wirklich keine Muster ableitbar haben
|
|
||||||
will, muss dort ansetzen — die Commit-Datumsregel allein reicht dafür nicht.
|
|
||||||
|
|
||||||
## Redlichkeit & gelebte Lehren
|
|
||||||
|
|
||||||
- **Aussagen mit Quelle:** Verifiziert (Messung/Konsole) klar von Vermutung
|
|
||||||
trennen; nicht selbst Geprüftes als solches kennzeichnen. Korrelation ≠
|
|
||||||
Kausalität — ein plausibler Verdacht ist kein Befund.
|
|
||||||
- **Config-Dateien textuell/chirurgisch editieren, nie re-dumpen** (YAML/JSON
|
|
||||||
neu serialisieren hat zweimal real Schaden angerichtet). Compose-/YAML-
|
|
||||||
Änderungen vor dem Push validieren (`docker compose config`, YAML-Parse).
|
|
||||||
- Fehlschläge und übersprungene Schritte werden benannt, nicht geglättet;
|
|
||||||
„fertig" heißt verifiziert.
|
|
||||||
|
|
||||||
---
|
|
||||||
name: karpathy-guidelines
|
|
||||||
description: Behavioral guidelines to reduce common LLM coding mistakes. Use when writing, reviewing, or refactoring code to avoid overcomplication, make surgical changes, surface assumptions, and define verifiable success criteria.
|
|
||||||
license: MIT
|
|
||||||
---
|
|
||||||
|
|
||||||
# Karpathy Guidelines
|
|
||||||
|
|
||||||
Behavioral guidelines to reduce common LLM coding mistakes, derived from [Andrej Karpathy's observations](https://x.com/karpathy/status/2015883857489522876) on LLM coding pitfalls.
|
|
||||||
|
|
||||||
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
|
|
||||||
|
|
||||||
## 1. Think Before Coding
|
|
||||||
|
|
||||||
**Don't assume. Don't hide confusion. Surface tradeoffs.**
|
|
||||||
|
|
||||||
Before implementing:
|
|
||||||
- State your assumptions explicitly. If uncertain, ask.
|
|
||||||
- If multiple interpretations exist, present them - don't pick silently.
|
|
||||||
- If a simpler approach exists, say so. Push back when warranted.
|
|
||||||
- If something is unclear, stop. Name what's confusing. Ask.
|
|
||||||
|
|
||||||
## 2. Simplicity First
|
|
||||||
|
|
||||||
**Minimum code that solves the problem. Nothing speculative.**
|
|
||||||
|
|
||||||
- No features beyond what was asked.
|
|
||||||
- No abstractions for single-use code.
|
|
||||||
- No "flexibility" or "configurability" that wasn't requested.
|
|
||||||
- No error handling for impossible scenarios.
|
|
||||||
- If you write 200 lines and it could be 50, rewrite it.
|
|
||||||
|
|
||||||
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
|
|
||||||
|
|
||||||
## 3. Surgical Changes
|
|
||||||
|
|
||||||
**Touch only what you must. Clean up only your own mess.**
|
|
||||||
|
|
||||||
When editing existing code:
|
|
||||||
- Don't "improve" adjacent code, comments, or formatting.
|
|
||||||
- Don't refactor things that aren't broken.
|
|
||||||
- Match existing style, even if you'd do it differently.
|
|
||||||
- If you notice unrelated dead code, mention it - don't delete it.
|
|
||||||
|
|
||||||
When your changes create orphans:
|
|
||||||
- Remove imports/variables/functions that YOUR changes made unused.
|
|
||||||
- Don't remove pre-existing dead code unless asked.
|
|
||||||
|
|
||||||
The test: Every changed line should trace directly to the user's request.
|
|
||||||
|
|
||||||
## 4. Goal-Driven Execution
|
|
||||||
|
|
||||||
**Define success criteria. Loop until verified.**
|
|
||||||
|
|
||||||
Transform tasks into verifiable goals:
|
|
||||||
- "Add validation" → "Write tests for invalid inputs, then make them pass"
|
|
||||||
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
|
|
||||||
- "Refactor X" → "Ensure tests pass before and after"
|
|
||||||
|
|
||||||
For multi-step tasks, state a brief plan:
|
|
||||||
```
|
|
||||||
1. [Step] → verify: [check]
|
|
||||||
2. [Step] → verify: [check]
|
|
||||||
3. [Step] → verify: [check]
|
|
||||||
```
|
|
||||||
|
|
||||||
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Der Karpathy-Block oben ist wortgleich aus der gitops-CLAUDE.md übernommen und
|
|
||||||
darf nicht bearbeitet werden (stehende Regel von sorb). Änderungen an dieser
|
|
||||||
Datei insgesamt: nur mit sorb abgestimmt — sie ist die gemeinsame
|
|
||||||
Arbeitsgrundlage aller Sessions.*
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ GitLab-Issue-Template. **Alle Issues leben auf git.lab.**
|
|||||||
| `vision/` | Eine Vision je Linie: Community (axion1337.chat), Tool (ThreadNet), Plattform (Homelab) |
|
| `vision/` | Eine Vision je Linie: Community (axion1337.chat), Tool (ThreadNet), Plattform (Homelab) |
|
||||||
| `roadmap.md` | Linien, Meilenstein-Kandidaten, Kadenz — GitLab-Milestones halten den Stand |
|
| `roadmap.md` | Linien, Meilenstein-Kandidaten, Kadenz — GitLab-Milestones halten den Stand |
|
||||||
| `decisions/` | ADRs — Pflicht bei Architekturentscheidungen **und dauerhaften Ausnahmen** |
|
| `decisions/` | ADRs — Pflicht bei Architekturentscheidungen **und dauerhaften Ausnahmen** |
|
||||||
| `verfahren/` | Wie wir arbeiten: [Deploy-Übergabe/DoD](verfahren/deploy-uebergabe.md), [Refinement & Retro](verfahren/refinement.md), [AARs](verfahren/aar/), Werkzeuge |
|
| `verfahren/` | Wie wir arbeiten: [Deploy-Übergabe/DoD](verfahren/deploy-uebergabe.md), [Refinement & Retro](verfahren/refinement.md), AARs (`verfahren/aar/`), Werkzeuge |
|
||||||
| `hosts/`, `shared/` | **Bestand + Historie** je Host/Thema — u. a. [Branding](shared/branding.md) (Marke, Paletten, wo welches Theme eingestellt ist); offene Punkte sind Issues |
|
| `hosts/`, `shared/` | **Bestand + Historie** je Host/Thema — u. a. [Branding](shared/branding.md) (Marke, Paletten, wo welches Theme eingestellt ist); offene Punkte sind Issues |
|
||||||
|
|
||||||
Gelesen wird das alles auch gebündelt unter **[axionwiki.lab](https://axionwiki.lab)** —
|
Gelesen wird das alles auch gebündelt unter **[axionwiki.lab](https://axionwiki.lab)** —
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# STATUS
|
||||||
|
|
||||||
|
<!-- Generated by scripts/gen_status.py — do not edit. -->
|
||||||
|
|
||||||
|
## Issues (0 open, 0 closed)
|
||||||
|
|
||||||
|
_none open_
|
||||||
|
|
||||||
|
## Active design docs (1)
|
||||||
|
|
||||||
|
| Design | Gate | Title |
|
||||||
|
|---|---|---|
|
||||||
|
| [2026-08-11-neckbeard-migration](docs/design/2026-08-11-neckbeard-migration.md) | gate-4 | Design: Migration des Management-Systems auf neckbeard |
|
||||||
|
|
||||||
|
## ADRs (2)
|
||||||
|
|
||||||
|
| ADR | Status | Title |
|
||||||
|
|---|---|---|
|
||||||
|
| [0012](docs/adr/0012-issues-im-repo-gitlab-als-spiegel.md) | accepted | ADR-0012: Issues leben im Repo; GitLab wird deterministisch bespiegelt |
|
||||||
|
| [0013](docs/adr/0013-gruppenregeln-kanonisch-mit-pruefung.md) | accepted | ADR-0013: Gruppenregeln kanonisch im management-Repo, Komponenten zeigen und werden geprüft |
|
||||||
|
|
||||||
|
## Open AARs (0)
|
||||||
|
|
||||||
|
_none — nothing awaiting harvest_
|
||||||
+140
@@ -0,0 +1,140 @@
|
|||||||
|
# WORKFLOW.md — Gates, Sizing, and Rituals
|
||||||
|
|
||||||
|
Read this when a task begins, not preemptively. `AGENTS.md` holds the
|
||||||
|
always-on rules; this file holds the process.
|
||||||
|
|
||||||
|
## Size Classes
|
||||||
|
|
||||||
|
Propose one at task start; the human confirms — individually, or batched
|
||||||
|
at the next refinement session.
|
||||||
|
|
||||||
|
| Class | Scope | Process |
|
||||||
|
|---|---|---|
|
||||||
|
| S | One file / one small change, no design decisions | Direct. AGENTS.md rules only. The one-line go-ahead **before starting is the stop** — waived only if `PROJECT.md` grants the size-S exception. |
|
||||||
|
| M | Few files, minor decisions, fits one session | Slice plan in chat, no file. **STOP: plan approval before any code.** Then implement; each slice reports evidence and status inline. Gate 5 is a short AAR note in chat, filed to the wiki only if it produced a real learning. |
|
||||||
|
| L | New feature, multiple files or sessions, real decisions | Full design doc in `docs/design/` following Gates 1–5 below. |
|
||||||
|
|
||||||
|
When in doubt between two classes, pick the larger.
|
||||||
|
|
||||||
|
## Gate 0 — Project Initialization
|
||||||
|
|
||||||
|
Runs once per project, triggered by a missing `PROJECT.md`. Ask, never guess:
|
||||||
|
|
||||||
|
1. Response language? (e.g. de / en)
|
||||||
|
2. Size-S gate exception granted? (yes / no)
|
||||||
|
3. One-line project purpose?
|
||||||
|
4. Audience — who uses this besides the owner? (Drives which wiki areas
|
||||||
|
become mandatory later; see `docs/wiki/index.md`.)
|
||||||
|
|
||||||
|
Write the answers to `PROJECT.md` (frontmatter per `schema.yaml`), run
|
||||||
|
`validate.py`, and confirm the result with the human.
|
||||||
|
|
||||||
|
## Gates 1–5 (size L)
|
||||||
|
|
||||||
|
Each gate is a section of the design doc. A gate ends with **STOP**:
|
||||||
|
present the section, wait for explicit approval. Do not pre-fill later
|
||||||
|
sections.
|
||||||
|
|
||||||
|
### Gate 1 — Product
|
||||||
|
- Problem statement: what user problem, for whom.
|
||||||
|
- Verifiable acceptance criterion. A real number where one exists;
|
||||||
|
otherwise a concretely checkable outcome. "Works" is not a criterion.
|
||||||
|
- Non-goals: what this deliberately does not do.
|
||||||
|
- Announcement paragraph (3–5 sentences): what it is, who it's for, why
|
||||||
|
it's good. If you can't write it, the product isn't understood yet.
|
||||||
|
- UI involved? Plain-HTML mockups of the affected screens.
|
||||||
|
|
||||||
|
**STOP.**
|
||||||
|
|
||||||
|
### Gate 2 — Architecture
|
||||||
|
- Read first: the actual codebase, relevant ADRs, relevant AARs.
|
||||||
|
Past decisions and learnings are input, not trivia.
|
||||||
|
- How it fits the real system: endpoints, tables/schemas, query
|
||||||
|
outlines, the end-to-end flow (Mermaid).
|
||||||
|
- Constraints: non-functional requirements, proportional to the project.
|
||||||
|
- Options & trade-offs where more than one viable way exists: pro/contra
|
||||||
|
each, chosen option, and why. Feature-local decisions stay here.
|
||||||
|
- Lasting directional decisions discovered here become ADRs (one each),
|
||||||
|
linked from the design doc.
|
||||||
|
|
||||||
|
**STOP.**
|
||||||
|
|
||||||
|
### Gate 3 — Program Design
|
||||||
|
- File locations: exact paths, new and touched.
|
||||||
|
- Types and method signatures — no bodies.
|
||||||
|
- Call stack for the main flow(s).
|
||||||
|
- What the tests will assert.
|
||||||
|
- Boundaries: an explicit DO NOT CHANGE list.
|
||||||
|
- Shakiest calls: name the decisions you are least confident about.
|
||||||
|
|
||||||
|
**STOP.**
|
||||||
|
|
||||||
|
### Gate 4 — Vertical Slices
|
||||||
|
- Slice 1 is the tracer bullet: a thin end-to-end path that runs
|
||||||
|
(mocks and stubs allowed). Only then real logic, one testable slice
|
||||||
|
at a time. Never build layer-by-layer horizontally.
|
||||||
|
- Every slice lists its tasks; every task names **files, action,
|
||||||
|
verify, done**.
|
||||||
|
- Each slice ends with verification evidence, a status
|
||||||
|
(`DONE` | `DONE_WITH_CONCERNS` | `NEEDS_CONTEXT` | `BLOCKED`),
|
||||||
|
and a **STOP** for human review before the next slice.
|
||||||
|
|
||||||
|
### Gate 5 — Closeout
|
||||||
|
- AAR section in the design doc: planned / actual / why the
|
||||||
|
difference / learnings.
|
||||||
|
- Harvest: learnings useful to future readers go to the wiki
|
||||||
|
(FAQ, Stolpersteine) with source links. A missing or wrong framework
|
||||||
|
rule becomes a framework issue or update.
|
||||||
|
- Good analyses produced along the way may be filed as wiki pages
|
||||||
|
(with citations) instead of dying in chat history.
|
||||||
|
- Move the design doc to `docs/design/done/`. Run `gen_status.py`.
|
||||||
|
|
||||||
|
## Debugging Path
|
||||||
|
|
||||||
|
For bugs and incidents, any size:
|
||||||
|
|
||||||
|
1. Reproduce first. No reproduction, no fix.
|
||||||
|
2. Hypothesize the root cause; verify the hypothesis with evidence
|
||||||
|
before changing anything.
|
||||||
|
3. Route the failure before fixing (diagnostic failure routing):
|
||||||
|
- **Intent issue** — we built toward the wrong goal → back to Gate 1.
|
||||||
|
- **Spec issue** — the design/plan was wrong → fix the spec
|
||||||
|
(Gate 2/3), then the code.
|
||||||
|
- **Code issue** — plan right, code wrong → fix in place.
|
||||||
|
4. Fix, plus a test that would have caught it.
|
||||||
|
5. Incidents and major misdiagnoses get a standalone AAR in `docs/aar/`.
|
||||||
|
|
||||||
|
## Session Handoff
|
||||||
|
|
||||||
|
- When a slice completes, or context quality degrades, write the current
|
||||||
|
state into the design doc's **Handoff block** — done slices, open
|
||||||
|
decisions, next step — then start a fresh session that resumes from
|
||||||
|
the doc. The doc is the memory; the session is disposable.
|
||||||
|
- End every working session by answering: "Which choices did I make that
|
||||||
|
I'm least confident about?" File the answer in the design doc.
|
||||||
|
|
||||||
|
## Refinement Session
|
||||||
|
|
||||||
|
A recurring, human-triggered ritual. Agenda:
|
||||||
|
|
||||||
|
1. Batched confirmations: size classes and small approvals queued since
|
||||||
|
last time.
|
||||||
|
2. Backlog triage over `docs/issues/`: close, reprioritize, split.
|
||||||
|
3. AAR harvest: walk recent AARs; update the wiki (FAQ, Stolpersteine);
|
||||||
|
propose framework changes.
|
||||||
|
4. Wiki lint (content-level, beyond `validate.py`): contradictions
|
||||||
|
between pages, claims superseded by newer sources, orphan pages,
|
||||||
|
missing cross-references, gaps worth a new page or a web search.
|
||||||
|
5. STATUS review: anything stale or surprising in `STATUS.md`.
|
||||||
|
|
||||||
|
## Knowledge Handling (summary)
|
||||||
|
|
||||||
|
Full rules live in `docs/wiki/index.md`. The short version:
|
||||||
|
|
||||||
|
- Original sources live in `docs/sources/`, immutable — agents read
|
||||||
|
them, never modify them. Wiki pages cite the sources they draw on.
|
||||||
|
- Contradictions are resolved or explicitly flagged — never left
|
||||||
|
silently coexisting.
|
||||||
|
- If the wiki has no confident answer, say so. Never file a
|
||||||
|
low-confidence synthesis back as knowledge.
|
||||||
|
- Git is the changelog. No separate log file.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
type: aar
|
||||||
|
status: open # open | harvested
|
||||||
|
date: YYYY-MM-DD
|
||||||
|
related: [] # design docs, issues, ADRs involved
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Copy to docs/aar/YYYY-MM-DD-slug.md. Delete comments when filling in.
|
||||||
|
Standalone AARs are for incidents and major deviations only —
|
||||||
|
normal undertakings get their AAR as Gate 5 inside the design doc. -->
|
||||||
|
|
||||||
|
# AAR: Title
|
||||||
|
|
||||||
|
## What was planned / expected
|
||||||
|
|
||||||
|
## What happened
|
||||||
|
|
||||||
|
<!-- Facts and timeline, not blame. -->
|
||||||
|
|
||||||
|
## Why the difference
|
||||||
|
|
||||||
|
<!-- Root cause. For failures, name the routing class:
|
||||||
|
intent issue / spec issue / code issue. -->
|
||||||
|
|
||||||
|
## Learnings
|
||||||
|
|
||||||
|
<!-- What future-you should know. Blunt beats polite. -->
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
<!-- Concrete: wiki pages updated (FAQ, Stolpersteine) with links,
|
||||||
|
framework issues opened, tests added. When all actions are done,
|
||||||
|
set status: harvested. The refinement session walks all AARs
|
||||||
|
still marked open. -->
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
type: adr
|
||||||
|
id: "0000"
|
||||||
|
status: proposed # proposed | accepted | superseded
|
||||||
|
date: YYYY-MM-DD
|
||||||
|
supersedes: null # path to older ADR, e.g. docs/adr/0002-old.md
|
||||||
|
superseded_by: null # filled in on the OLD adr when a new one replaces it
|
||||||
|
related: [] # optional: paths to design docs / issues
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Copy to docs/adr/NNNN-slug.md. Delete all comments when filling in. -->
|
||||||
|
|
||||||
|
# ADR-0000: Title
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
<!-- The situation and the forces at play. Constraints upfront:
|
||||||
|
deadlines, scale, team knowledge, existing decisions. -->
|
||||||
|
|
||||||
|
## Options Considered
|
||||||
|
|
||||||
|
<!-- Name each option, even the one you lean toward. Pros/cons per
|
||||||
|
option; a small dimension table (complexity, cost, maintenance,
|
||||||
|
familiarity) where it helps. Keep proportional to the decision. -->
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
<!-- The choice, in one or two sentences. -->
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
<!-- What becomes easier, what becomes harder, what we will need to
|
||||||
|
revisit. Honest cons included. -->
|
||||||
|
|
||||||
|
<!-- Rules: an accepted ADR is never edited — write a new ADR that
|
||||||
|
supersedes it and set superseded_by here. Lasting directional
|
||||||
|
decisions only; feature-local choices belong in the design doc. -->
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
type: design
|
type: design
|
||||||
status: gate-3
|
status: gate-4
|
||||||
date: 2026-08-11
|
date: 2026-08-11
|
||||||
size: L
|
size: L
|
||||||
related:
|
related:
|
||||||
@@ -505,3 +505,40 @@ Hinweis von sorb bei der Gate-3-Freigabe, per Fetch und Live-API
|
|||||||
Akzeptanzkriterium 2 = 11/11. Die Enums bleiben, wie in Annahme 2
|
Akzeptanzkriterium 2 = 11/11. Die Enums bleiben, wie in Annahme 2
|
||||||
benannt, aus dem management-Scope abgeleitet; `area:authentik` liegt
|
benannt, aus dem management-Scope abgeleitet; `area:authentik` liegt
|
||||||
außerhalb (gitops) und wird erst bei dessen Adoption Schema-Thema.
|
außerhalb (gitops) und wird erst bei dessen Adoption Schema-Thema.
|
||||||
|
|
||||||
|
## Gate 4 — Vertikale Slices
|
||||||
|
|
||||||
|
Jeder Slice endet mit Nachweis, Status und **STOP**.
|
||||||
|
|
||||||
|
**Slice 1 — Tracer Bullet: die Framework-Kette läuft Ende-zu-Ende.**
|
||||||
|
Baseline (`docs/sources/upstream/neckbeard-v0.1.1/` + `HERKUNFT.md`),
|
||||||
|
Karpathy-Block wortgleich nach `docs/sources/regelwerk/`, `AGENTS.md`
|
||||||
|
(§1–5 byte-treu + §6 Gruppenregeln), `CLAUDE.md`-Pointer, `WORKFLOW.md`,
|
||||||
|
Templates, erweitertes `schema.yaml`, `validate.py` (+3 Regeln),
|
||||||
|
`gen_status.py` (Fork), `pruefe_upstream_drift.py`, generiertes
|
||||||
|
`STATUS.md`, CI-Job `validate`, README-Verzeichnis-Link entschärft.
|
||||||
|
*Verify:* validate 0 Fehler · gen_status --check aktuell · Drift-Check
|
||||||
|
grün · vier Negativtests feuern · Baseline byte-identisch zur Referenz.
|
||||||
|
|
||||||
|
**Slice 2 — ADR-Port.** `decisions/0001…0011` → `docs/adr/` mit
|
||||||
|
Frontmatter, Verweise nachgezogen, `decisions/` entfällt.
|
||||||
|
*Verify:* 11/11 validieren, Duplikat-ID-Prüfung greift, validate grün.
|
||||||
|
|
||||||
|
**Slice 3 — Wiki, Sources, AARs.** `verfahren/`/`hosts/`/`vision/`/
|
||||||
|
`shared/` an ihre Zielorte, Wiki-Index, `pruefe_prosa.py`; Demos
|
||||||
|
Muster C (F-004-Punkte auf Vor-Stand) und D (6 verwaiste SHAs).
|
||||||
|
*Verify:* validate + pruefe_prosa grün auf Endstand, Demos feuern auf
|
||||||
|
Vor-Stand, alte Wurzelordner leer.
|
||||||
|
|
||||||
|
**Slice 4 — Issue-Import.** `import_issues.py` liest die offenen
|
||||||
|
management-Issues live (read-only), 26+ Dateien + 5 F-004-Issues,
|
||||||
|
`roadmap.md` verliert Zahlen an STATUS.md.
|
||||||
|
*Verify:* alle Issue-Dateien validieren (Pflicht-Meilenstein/-Priorität),
|
||||||
|
Import-Protokoll unter sources/migration, Muster-C-Endstand = 0.
|
||||||
|
|
||||||
|
**Slice 5 — Komponenten, Gruppenprüfung, Spiegel.** 8
|
||||||
|
Komponenten-Deklarationen, `gruppenpruefung.py` (+ CI-Job),
|
||||||
|
`spiegel_issues.py` (Dry-Run-Demo); Demos Muster A (eingefrorener
|
||||||
|
Export ↔ Alt-CLAUDE) und B (Hygiene über lokale Klone), Live-Befund
|
||||||
|
gitops#61.
|
||||||
|
*Verify:* Dry-Run-Ausgabe plausibel, Demos feuern, kein API-Write.
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
---
|
||||||
|
type: design
|
||||||
|
status: gate-1 # gate-1 | gate-2 | gate-3 | gate-4 | gate-5 | done
|
||||||
|
date: YYYY-MM-DD
|
||||||
|
size: L # this template is for size L
|
||||||
|
related: [] # issues, ADRs spawned or read
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Copy to docs/design/YYYY-MM-DD-slug.md. Delete comments when filling in.
|
||||||
|
Fill ONE gate at a time; each gate ends with STOP — do not pre-fill
|
||||||
|
later gates. Advance `status` only after human approval. -->
|
||||||
|
|
||||||
|
# Design: Title
|
||||||
|
|
||||||
|
## Gate 1 — Product
|
||||||
|
|
||||||
|
**Problem.** <!-- What user problem, for whom. -->
|
||||||
|
|
||||||
|
**Acceptance criterion.** <!-- Verifiable. A real number where one
|
||||||
|
exists; otherwise a concretely checkable outcome. "Works" is not one. -->
|
||||||
|
|
||||||
|
**Non-goals.** <!-- What this deliberately does NOT do. The cheapest
|
||||||
|
scope-creep brake there is. -->
|
||||||
|
|
||||||
|
**Announcement.** <!-- 3–5 sentences: what it is, who it's for, why
|
||||||
|
it's good. Can't write it? The product isn't understood yet. -->
|
||||||
|
|
||||||
|
**Mockups.** <!-- Only if UI is involved: plain-HTML mockups, linked. -->
|
||||||
|
|
||||||
|
> **STOP — awaiting Gate 1 approval.**
|
||||||
|
|
||||||
|
## Gate 2 — Architecture
|
||||||
|
|
||||||
|
**Inputs read.** <!-- Which ADRs and AARs were read; one line each on
|
||||||
|
why they matter here. -->
|
||||||
|
|
||||||
|
**System fit.** <!-- Endpoints, tables/schemas, query outlines,
|
||||||
|
end-to-end flow as Mermaid. Against the actual codebase. -->
|
||||||
|
|
||||||
|
**Constraints.** <!-- Non-functional, proportional to the project:
|
||||||
|
performance, security, operations, compatibility. "None relevant"
|
||||||
|
is a valid answer — but say it. -->
|
||||||
|
|
||||||
|
**Options & trade-offs.** <!-- Where more than one viable way exists:
|
||||||
|
name the options, pro/contra each, state the chosen one and WHY.
|
||||||
|
This is the feature-local decision record. Only lasting, binding
|
||||||
|
decisions graduate to an ADR below. -->
|
||||||
|
|
||||||
|
**New ADRs.** <!-- Lasting decisions discovered here → one ADR each,
|
||||||
|
linked. None is a valid answer. -->
|
||||||
|
|
||||||
|
> **STOP — awaiting Gate 2 approval.**
|
||||||
|
|
||||||
|
## Gate 3 — Program Design
|
||||||
|
|
||||||
|
**Files.** <!-- Exact paths, new and touched. -->
|
||||||
|
|
||||||
|
**Signatures.** <!-- Types and method signatures, no bodies. -->
|
||||||
|
|
||||||
|
**Call stack.** <!-- For the main flow(s). -->
|
||||||
|
|
||||||
|
**Test assertions.** <!-- What the tests will assert. -->
|
||||||
|
|
||||||
|
**Boundaries — DO NOT CHANGE.** <!-- Explicit list. -->
|
||||||
|
|
||||||
|
**Shakiest calls.** <!-- The decisions you are least confident about. -->
|
||||||
|
|
||||||
|
> **STOP — awaiting Gate 3 approval.**
|
||||||
|
|
||||||
|
## Gate 4 — Vertical Slices
|
||||||
|
|
||||||
|
<!-- Slice 1 is the tracer bullet: thin end-to-end, runs with mocks.
|
||||||
|
Then real logic, one testable slice at a time. Per task:
|
||||||
|
files / action / verify / done. After each slice: evidence,
|
||||||
|
status, STOP. -->
|
||||||
|
|
||||||
|
### Slice 1 — Tracer bullet
|
||||||
|
- [ ] Task: … — files: … — action: … — verify: … — done: …
|
||||||
|
|
||||||
|
**Evidence:** <!-- command output, test run, screenshot ref -->
|
||||||
|
**Status:** <!-- DONE | DONE_WITH_CONCERNS | NEEDS_CONTEXT | BLOCKED -->
|
||||||
|
|
||||||
|
> **STOP — slice review.**
|
||||||
|
|
||||||
|
### Slice 2 — …
|
||||||
|
|
||||||
|
### Handoff
|
||||||
|
|
||||||
|
<!-- The single place session state lives. Overwrite on every handoff;
|
||||||
|
git keeps the history.
|
||||||
|
Done slices: …
|
||||||
|
Open decisions: …
|
||||||
|
Next step: … -->
|
||||||
|
|
||||||
|
## Gate 5 — Closeout (AAR)
|
||||||
|
|
||||||
|
**Planned vs. actual.** <!-- What was planned, what happened. -->
|
||||||
|
|
||||||
|
**Why the difference.** <!-- Root causes, honestly. -->
|
||||||
|
|
||||||
|
**Learnings.** <!-- What future-you should know. -->
|
||||||
|
|
||||||
|
**Harvested.** <!-- Wiki pages updated (FAQ, Stolpersteine, …) with
|
||||||
|
links; framework issues opened, if a rule was missing or wrong. -->
|
||||||
|
|
||||||
|
**Open uncertainties.** <!-- Session-handoff answers to: "Which choices
|
||||||
|
did I make that I'm least confident about?" -->
|
||||||
|
|
||||||
|
<!-- After approval: set status: done, move this file to
|
||||||
|
docs/design/done/, run gen_status.py. -->
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
type: issue
|
||||||
|
id: "0000"
|
||||||
|
status: open # open | in-progress | done | rejected
|
||||||
|
created: YYYY-MM-DD
|
||||||
|
related: [] # design docs, ADRs, other issues
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Copy to docs/issues/NNNN-slug.md. Delete comments when filling in. -->
|
||||||
|
|
||||||
|
# Issue-0000: Title
|
||||||
|
|
||||||
|
## Problem / Motivation
|
||||||
|
|
||||||
|
<!-- What's wrong or missing, and why it matters. One paragraph. -->
|
||||||
|
|
||||||
|
## Acceptance
|
||||||
|
|
||||||
|
<!-- When is this issue done? Verifiable, like every other criterion
|
||||||
|
in this framework. -->
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
<!-- Optional: context, links, findings gathered along the way.
|
||||||
|
Rules: status is the single source of truth and lives here in the
|
||||||
|
frontmatter — STATUS.md is generated, never edited. An issue that
|
||||||
|
starts real work links its design doc in `related`. Closed means
|
||||||
|
status: done (or rejected, with a one-line reason in Notes) —
|
||||||
|
the file stays; git is the history. -->
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
name: karpathy-guidelines
|
||||||
|
description: Behavioral guidelines to reduce common LLM coding mistakes. Use when writing, reviewing, or refactoring code to avoid overcomplication, make surgical changes, surface assumptions, and define verifiable success criteria.
|
||||||
|
license: MIT
|
||||||
|
---
|
||||||
|
|
||||||
|
# Karpathy Guidelines
|
||||||
|
|
||||||
|
Behavioral guidelines to reduce common LLM coding mistakes, derived from [Andrej Karpathy's observations](https://x.com/karpathy/status/2015883857489522876) on LLM coding pitfalls.
|
||||||
|
|
||||||
|
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
|
||||||
|
|
||||||
|
## 1. Think Before Coding
|
||||||
|
|
||||||
|
**Don't assume. Don't hide confusion. Surface tradeoffs.**
|
||||||
|
|
||||||
|
Before implementing:
|
||||||
|
- State your assumptions explicitly. If uncertain, ask.
|
||||||
|
- If multiple interpretations exist, present them - don't pick silently.
|
||||||
|
- If a simpler approach exists, say so. Push back when warranted.
|
||||||
|
- If something is unclear, stop. Name what's confusing. Ask.
|
||||||
|
|
||||||
|
## 2. Simplicity First
|
||||||
|
|
||||||
|
**Minimum code that solves the problem. Nothing speculative.**
|
||||||
|
|
||||||
|
- No features beyond what was asked.
|
||||||
|
- No abstractions for single-use code.
|
||||||
|
- No "flexibility" or "configurability" that wasn't requested.
|
||||||
|
- No error handling for impossible scenarios.
|
||||||
|
- If you write 200 lines and it could be 50, rewrite it.
|
||||||
|
|
||||||
|
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
|
||||||
|
|
||||||
|
## 3. Surgical Changes
|
||||||
|
|
||||||
|
**Touch only what you must. Clean up only your own mess.**
|
||||||
|
|
||||||
|
When editing existing code:
|
||||||
|
- Don't "improve" adjacent code, comments, or formatting.
|
||||||
|
- Don't refactor things that aren't broken.
|
||||||
|
- Match existing style, even if you'd do it differently.
|
||||||
|
- If you notice unrelated dead code, mention it - don't delete it.
|
||||||
|
|
||||||
|
When your changes create orphans:
|
||||||
|
- Remove imports/variables/functions that YOUR changes made unused.
|
||||||
|
- Don't remove pre-existing dead code unless asked.
|
||||||
|
|
||||||
|
The test: Every changed line should trace directly to the user's request.
|
||||||
|
|
||||||
|
## 4. Goal-Driven Execution
|
||||||
|
|
||||||
|
**Define success criteria. Loop until verified.**
|
||||||
|
|
||||||
|
Transform tasks into verifiable goals:
|
||||||
|
- "Add validation" → "Write tests for invalid inputs, then make them pass"
|
||||||
|
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
|
||||||
|
- "Refactor X" → "Ensure tests pass before and after"
|
||||||
|
|
||||||
|
For multi-step tasks, state a brief plan:
|
||||||
|
```
|
||||||
|
1. [Step] → verify: [check]
|
||||||
|
2. [Step] → verify: [check]
|
||||||
|
3. [Step] → verify: [check]
|
||||||
|
```
|
||||||
|
|
||||||
|
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
|
||||||
|
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# AGENTS.md — Canonical Agent Instructions
|
||||||
|
|
||||||
|
Canonical instruction set for any coding agent working in this repository
|
||||||
|
(Claude Code, GPT-OSS harnesses, others). `CLAUDE.md` points here.
|
||||||
|
This file is loaded into every session — keep it short. Process details
|
||||||
|
live in `WORKFLOW.md`; read that when a task begins, not preemptively.
|
||||||
|
|
||||||
|
Tradeoff: these rules bias toward caution over speed. For trivial tasks,
|
||||||
|
use judgment — but say so.
|
||||||
|
|
||||||
|
## 1. Operating Rules
|
||||||
|
|
||||||
|
### Think before coding
|
||||||
|
- State your assumptions explicitly. If uncertain, ask.
|
||||||
|
- If multiple interpretations exist, present them — don't pick silently.
|
||||||
|
- If a simpler approach exists, say so. Push back when warranted.
|
||||||
|
- If something is unclear, stop. Name what's confusing. Ask.
|
||||||
|
|
||||||
|
### Simplicity first
|
||||||
|
- Minimum code that solves the problem. Nothing speculative.
|
||||||
|
- No features beyond what was asked. No abstractions for single-use code.
|
||||||
|
- No "flexibility" or "configurability" that wasn't requested.
|
||||||
|
- No error handling for impossible scenarios.
|
||||||
|
- If you write 200 lines and it could be 50, rewrite it.
|
||||||
|
- Test: "Would a senior engineer call this overcomplicated?" If yes, simplify.
|
||||||
|
- Before writing new code, stop at the first rung that holds:
|
||||||
|
needed at all? → codebase already has it? → stdlib? → platform-native?
|
||||||
|
→ installed dependency? → one line? → only then: the minimum that works.
|
||||||
|
(Ladder after ponytail, MIT.)
|
||||||
|
- Never cut, at any rung: trust-boundary validation, data-loss handling,
|
||||||
|
security, accessibility.
|
||||||
|
- Lazy about the solution, never about reading the code first.
|
||||||
|
|
||||||
|
### Surgical changes
|
||||||
|
- Touch only what you must. Match existing style, even if you'd differ.
|
||||||
|
- Don't "improve" adjacent code, comments, or formatting.
|
||||||
|
- Don't refactor things that aren't broken.
|
||||||
|
- If you notice unrelated dead code, mention it — don't delete it.
|
||||||
|
- Remove imports/variables/functions that YOUR changes made unused;
|
||||||
|
leave pre-existing dead code alone unless asked.
|
||||||
|
- Every changed line must trace directly to the request.
|
||||||
|
|
||||||
|
### Goal-driven execution
|
||||||
|
- Transform tasks into verifiable goals:
|
||||||
|
"fix the bug" → "write a test that reproduces it, then make it pass".
|
||||||
|
- For multi-step work, state a brief plan: step → verify, step → verify.
|
||||||
|
- A task is well-defined only if it names all four:
|
||||||
|
**files, action, verify, done.** Missing one? The task is too vague — say so.
|
||||||
|
|
||||||
|
### Verification before completion
|
||||||
|
- Never claim something works without evidence: a test run, command
|
||||||
|
output, a rendered result. "Should work" is not a status.
|
||||||
|
- Report every task/slice with exactly one status:
|
||||||
|
`DONE` | `DONE_WITH_CONCERNS` | `NEEDS_CONTEXT` | `BLOCKED`.
|
||||||
|
- Uncertainty is reported, never swallowed. Flag your shakiest calls.
|
||||||
|
|
||||||
|
## 2. Project Initialization (Gate 0)
|
||||||
|
|
||||||
|
At session start, read `PROJECT.md`. If it does not exist, initialization
|
||||||
|
is your first task: before anything else, ask the Gate 0 questions defined
|
||||||
|
in `WORKFLOW.md` — response language, size-S gate exception (yes/no),
|
||||||
|
one-line project purpose, audience — write the answers to `PROJECT.md`,
|
||||||
|
and have `validate.py` accept it. Never guess these answers; ask.
|
||||||
|
|
||||||
|
## 3. Workflow
|
||||||
|
|
||||||
|
For anything beyond a trivial change, read `WORKFLOW.md` and follow its
|
||||||
|
gates. At task start, propose a size class (S/M/L); the human confirms
|
||||||
|
(possibly batched later). **Never advance past a gate without explicit
|
||||||
|
human approval** — sole exception: size-S tasks, and only if `PROJECT.md`
|
||||||
|
explicitly grants that exception.
|
||||||
|
|
||||||
|
## 4. Repository Map
|
||||||
|
|
||||||
|
| Path | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `WORKFLOW.md` | Gate 0 (init) + Gates 1–5, size classes, debugging path, session handoff, refinement ritual |
|
||||||
|
| `PROJECT.md` | Per-project answers from Gate 0: language, size-S exception, purpose, audience |
|
||||||
|
| `STATUS.md` | Generated overview: open issues, active designs, recent ADRs — do not edit by hand |
|
||||||
|
| `schema.yaml` | Frontmatter schema — single source of truth for artifact structure |
|
||||||
|
| `docs/adr/` | Architecture Decision Records — binding; never edited, only superseded |
|
||||||
|
| `docs/design/` | One design doc per undertaking; completed ones move to `done/` |
|
||||||
|
| `docs/aar/` | Standalone After Action Reviews (incidents, major deviations only) |
|
||||||
|
| `docs/issues/` | In-repo issues, one file each; status lives in frontmatter |
|
||||||
|
| `docs/wiki/` | Wiki areas as folders, created on demand — rules in `docs/wiki/index.md` |
|
||||||
|
| `docs/sources/` | Immutable original sources; wiki pages cite them — read-only for agents |
|
||||||
|
| `scripts/` | Deterministic tooling: `validate.py`, `gen_status.py` |
|
||||||
|
|
||||||
|
Before proposing options (Gate 2), read the relevant ADRs and AARs first —
|
||||||
|
past decisions and learnings are input, not trivia.
|
||||||
|
|
||||||
|
## 5. Artifact Rules
|
||||||
|
|
||||||
|
- All artifacts are standard Markdown with YAML frontmatter conforming to
|
||||||
|
`schema.yaml`. Standard links only (`[text](path.md)`), no wikilinks.
|
||||||
|
Diagrams as Mermaid. This keeps every artifact portable across LLMs,
|
||||||
|
GitLab, and Obsidian.
|
||||||
|
- Never invent frontmatter fields or status values. `validate.py` is
|
||||||
|
authoritative; if it rejects your artifact, fix the artifact, not the
|
||||||
|
validator.
|
||||||
|
- Deterministic jobs (status generation, validation, link checks) are done
|
||||||
|
by scripts, not by you. If a deterministic job lacks a script, propose
|
||||||
|
one instead of doing it by inference.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Read AGENTS.md — the canonical instruction file for this repository. All rules live there.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Herkunft dieser Baseline
|
||||||
|
|
||||||
|
Unveränderte Originale aus **neckbeard v0.1.1**, Commit
|
||||||
|
`823a08cac6b03a47d7e2f661200a49ac6e09d38d` (`main`, sauber), Origin
|
||||||
|
`https://git.lab/oss-projekte/ai/neckbeard.git` — der Stand, gegen den
|
||||||
|
der Feldtest (Branch `Neckbeard-v0.1.1-analyse-1`) gemessen hat und aus
|
||||||
|
dem die Migration übernommen wurde (Design:
|
||||||
|
`docs/design/2026-08-11-neckbeard-migration.md`).
|
||||||
|
|
||||||
|
Zweck: Byte-Baseline für `scripts/pruefe_upstream_drift.py`. Ein
|
||||||
|
Framework-Upgrade ersetzt diese Dateien bewusst und in einem eigenen
|
||||||
|
Commit — nie beiläufig.
|
||||||
|
|
||||||
|
| Datei hier | Arbeitskopie | Prüfung |
|
||||||
|
|---|---|---|
|
||||||
|
| `AGENTS.md` | `/AGENTS.md` | Präfix bis zur Marke `<!-- projektabschnitt -->` |
|
||||||
|
| `CLAUDE.md` | `/CLAUDE.md` | byte-identisch |
|
||||||
|
| `WORKFLOW.md` | `/WORKFLOW.md` | byte-identisch |
|
||||||
|
| `templates/adr-template.md` | `docs/adr/template.md` | byte-identisch |
|
||||||
|
| `templates/design-template.md` | `docs/design/template.md` | byte-identisch |
|
||||||
|
| `templates/aar-template.md` | `docs/aar/template.md` | byte-identisch |
|
||||||
|
| `templates/issue-template.md` | `docs/issues/template.md` | byte-identisch |
|
||||||
|
| `schema.yaml` | `/schema.yaml` | **erklärt projekterweitert** — nur Diff-Referenz |
|
||||||
|
| `scripts/validate.py` | `scripts/validate.py` | **erklärt projekterweitert** — nur Diff-Referenz |
|
||||||
|
| `scripts/gen_status.py` | `scripts/gen_status.py` | **erklärt projekterweitert** — nur Diff-Referenz |
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
# WORKFLOW.md — Gates, Sizing, and Rituals
|
||||||
|
|
||||||
|
Read this when a task begins, not preemptively. `AGENTS.md` holds the
|
||||||
|
always-on rules; this file holds the process.
|
||||||
|
|
||||||
|
## Size Classes
|
||||||
|
|
||||||
|
Propose one at task start; the human confirms — individually, or batched
|
||||||
|
at the next refinement session.
|
||||||
|
|
||||||
|
| Class | Scope | Process |
|
||||||
|
|---|---|---|
|
||||||
|
| S | One file / one small change, no design decisions | Direct. AGENTS.md rules only. The one-line go-ahead **before starting is the stop** — waived only if `PROJECT.md` grants the size-S exception. |
|
||||||
|
| M | Few files, minor decisions, fits one session | Slice plan in chat, no file. **STOP: plan approval before any code.** Then implement; each slice reports evidence and status inline. Gate 5 is a short AAR note in chat, filed to the wiki only if it produced a real learning. |
|
||||||
|
| L | New feature, multiple files or sessions, real decisions | Full design doc in `docs/design/` following Gates 1–5 below. |
|
||||||
|
|
||||||
|
When in doubt between two classes, pick the larger.
|
||||||
|
|
||||||
|
## Gate 0 — Project Initialization
|
||||||
|
|
||||||
|
Runs once per project, triggered by a missing `PROJECT.md`. Ask, never guess:
|
||||||
|
|
||||||
|
1. Response language? (e.g. de / en)
|
||||||
|
2. Size-S gate exception granted? (yes / no)
|
||||||
|
3. One-line project purpose?
|
||||||
|
4. Audience — who uses this besides the owner? (Drives which wiki areas
|
||||||
|
become mandatory later; see `docs/wiki/index.md`.)
|
||||||
|
|
||||||
|
Write the answers to `PROJECT.md` (frontmatter per `schema.yaml`), run
|
||||||
|
`validate.py`, and confirm the result with the human.
|
||||||
|
|
||||||
|
## Gates 1–5 (size L)
|
||||||
|
|
||||||
|
Each gate is a section of the design doc. A gate ends with **STOP**:
|
||||||
|
present the section, wait for explicit approval. Do not pre-fill later
|
||||||
|
sections.
|
||||||
|
|
||||||
|
### Gate 1 — Product
|
||||||
|
- Problem statement: what user problem, for whom.
|
||||||
|
- Verifiable acceptance criterion. A real number where one exists;
|
||||||
|
otherwise a concretely checkable outcome. "Works" is not a criterion.
|
||||||
|
- Non-goals: what this deliberately does not do.
|
||||||
|
- Announcement paragraph (3–5 sentences): what it is, who it's for, why
|
||||||
|
it's good. If you can't write it, the product isn't understood yet.
|
||||||
|
- UI involved? Plain-HTML mockups of the affected screens.
|
||||||
|
|
||||||
|
**STOP.**
|
||||||
|
|
||||||
|
### Gate 2 — Architecture
|
||||||
|
- Read first: the actual codebase, relevant ADRs, relevant AARs.
|
||||||
|
Past decisions and learnings are input, not trivia.
|
||||||
|
- How it fits the real system: endpoints, tables/schemas, query
|
||||||
|
outlines, the end-to-end flow (Mermaid).
|
||||||
|
- Constraints: non-functional requirements, proportional to the project.
|
||||||
|
- Options & trade-offs where more than one viable way exists: pro/contra
|
||||||
|
each, chosen option, and why. Feature-local decisions stay here.
|
||||||
|
- Lasting directional decisions discovered here become ADRs (one each),
|
||||||
|
linked from the design doc.
|
||||||
|
|
||||||
|
**STOP.**
|
||||||
|
|
||||||
|
### Gate 3 — Program Design
|
||||||
|
- File locations: exact paths, new and touched.
|
||||||
|
- Types and method signatures — no bodies.
|
||||||
|
- Call stack for the main flow(s).
|
||||||
|
- What the tests will assert.
|
||||||
|
- Boundaries: an explicit DO NOT CHANGE list.
|
||||||
|
- Shakiest calls: name the decisions you are least confident about.
|
||||||
|
|
||||||
|
**STOP.**
|
||||||
|
|
||||||
|
### Gate 4 — Vertical Slices
|
||||||
|
- Slice 1 is the tracer bullet: a thin end-to-end path that runs
|
||||||
|
(mocks and stubs allowed). Only then real logic, one testable slice
|
||||||
|
at a time. Never build layer-by-layer horizontally.
|
||||||
|
- Every slice lists its tasks; every task names **files, action,
|
||||||
|
verify, done**.
|
||||||
|
- Each slice ends with verification evidence, a status
|
||||||
|
(`DONE` | `DONE_WITH_CONCERNS` | `NEEDS_CONTEXT` | `BLOCKED`),
|
||||||
|
and a **STOP** for human review before the next slice.
|
||||||
|
|
||||||
|
### Gate 5 — Closeout
|
||||||
|
- AAR section in the design doc: planned / actual / why the
|
||||||
|
difference / learnings.
|
||||||
|
- Harvest: learnings useful to future readers go to the wiki
|
||||||
|
(FAQ, Stolpersteine) with source links. A missing or wrong framework
|
||||||
|
rule becomes a framework issue or update.
|
||||||
|
- Good analyses produced along the way may be filed as wiki pages
|
||||||
|
(with citations) instead of dying in chat history.
|
||||||
|
- Move the design doc to `docs/design/done/`. Run `gen_status.py`.
|
||||||
|
|
||||||
|
## Debugging Path
|
||||||
|
|
||||||
|
For bugs and incidents, any size:
|
||||||
|
|
||||||
|
1. Reproduce first. No reproduction, no fix.
|
||||||
|
2. Hypothesize the root cause; verify the hypothesis with evidence
|
||||||
|
before changing anything.
|
||||||
|
3. Route the failure before fixing (diagnostic failure routing):
|
||||||
|
- **Intent issue** — we built toward the wrong goal → back to Gate 1.
|
||||||
|
- **Spec issue** — the design/plan was wrong → fix the spec
|
||||||
|
(Gate 2/3), then the code.
|
||||||
|
- **Code issue** — plan right, code wrong → fix in place.
|
||||||
|
4. Fix, plus a test that would have caught it.
|
||||||
|
5. Incidents and major misdiagnoses get a standalone AAR in `docs/aar/`.
|
||||||
|
|
||||||
|
## Session Handoff
|
||||||
|
|
||||||
|
- When a slice completes, or context quality degrades, write the current
|
||||||
|
state into the design doc's **Handoff block** — done slices, open
|
||||||
|
decisions, next step — then start a fresh session that resumes from
|
||||||
|
the doc. The doc is the memory; the session is disposable.
|
||||||
|
- End every working session by answering: "Which choices did I make that
|
||||||
|
I'm least confident about?" File the answer in the design doc.
|
||||||
|
|
||||||
|
## Refinement Session
|
||||||
|
|
||||||
|
A recurring, human-triggered ritual. Agenda:
|
||||||
|
|
||||||
|
1. Batched confirmations: size classes and small approvals queued since
|
||||||
|
last time.
|
||||||
|
2. Backlog triage over `docs/issues/`: close, reprioritize, split.
|
||||||
|
3. AAR harvest: walk recent AARs; update the wiki (FAQ, Stolpersteine);
|
||||||
|
propose framework changes.
|
||||||
|
4. Wiki lint (content-level, beyond `validate.py`): contradictions
|
||||||
|
between pages, claims superseded by newer sources, orphan pages,
|
||||||
|
missing cross-references, gaps worth a new page or a web search.
|
||||||
|
5. STATUS review: anything stale or surprising in `STATUS.md`.
|
||||||
|
|
||||||
|
## Knowledge Handling (summary)
|
||||||
|
|
||||||
|
Full rules live in `docs/wiki/index.md`. The short version:
|
||||||
|
|
||||||
|
- Original sources live in `docs/sources/`, immutable — agents read
|
||||||
|
them, never modify them. Wiki pages cite the sources they draw on.
|
||||||
|
- Contradictions are resolved or explicitly flagged — never left
|
||||||
|
silently coexisting.
|
||||||
|
- If the wiki has no confident answer, say so. Never file a
|
||||||
|
low-confidence synthesis back as knowledge.
|
||||||
|
- Git is the changelog. No separate log file.
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# schema.yaml — single source of truth for artifact frontmatter.
|
||||||
|
# Stage 1 of ADR-0004: scripts/validate.py checks generically against this
|
||||||
|
# file. Extending the framework's metadata means editing THIS file, not code.
|
||||||
|
# Agents: never invent fields or status values; propose a schema change.
|
||||||
|
|
||||||
|
version: 1
|
||||||
|
|
||||||
|
scope:
|
||||||
|
# Files considered artifacts. Templates and raw sources are exempt.
|
||||||
|
include:
|
||||||
|
- "PROJECT.md"
|
||||||
|
- "docs/**/*.md"
|
||||||
|
exclude:
|
||||||
|
- "**/template.md"
|
||||||
|
- "docs/sources/**"
|
||||||
|
- "vendor/**"
|
||||||
|
# Files whose inline links are checked, but which need no frontmatter
|
||||||
|
# (root-level prose: README, AGENTS, WORKFLOW, generated STATUS, ...).
|
||||||
|
link_only:
|
||||||
|
- "*.md"
|
||||||
|
|
||||||
|
# Frontmatter fields whose values are links. Values starting with
|
||||||
|
# http://, https:// or mailto: are treated as external and only
|
||||||
|
# format-checked; everything else must be a repo-root-relative path
|
||||||
|
# to an existing file.
|
||||||
|
link_fields: [related, sources, supersedes, superseded_by]
|
||||||
|
|
||||||
|
types:
|
||||||
|
project:
|
||||||
|
dir: "."
|
||||||
|
filename: "^PROJECT\\.md$"
|
||||||
|
required: [type, language, size_s_exception, purpose, audience]
|
||||||
|
fields:
|
||||||
|
language: { enum: [de, en] }
|
||||||
|
size_s_exception: { kind: bool }
|
||||||
|
purpose: { kind: str }
|
||||||
|
audience: { kind: str }
|
||||||
|
|
||||||
|
adr:
|
||||||
|
dir: "docs/adr"
|
||||||
|
filename: "^\\d{4}-[a-z0-9-]+\\.md$"
|
||||||
|
required: [type, id, status, date]
|
||||||
|
fields:
|
||||||
|
id: { pattern: "^\\d{4}$" }
|
||||||
|
status: { enum: [proposed, accepted, superseded] }
|
||||||
|
date: { kind: date }
|
||||||
|
supersedes: { kind: link, nullable: true }
|
||||||
|
superseded_by: { kind: link, nullable: true }
|
||||||
|
related: { kind: links }
|
||||||
|
rules:
|
||||||
|
# status: superseded requires superseded_by to point at the successor.
|
||||||
|
- superseded_requires_pointer
|
||||||
|
|
||||||
|
design:
|
||||||
|
dir: "docs/design"
|
||||||
|
filename: "^\\d{4}-\\d{2}-\\d{2}-[a-z0-9-]+\\.md$"
|
||||||
|
required: [type, status, date, size]
|
||||||
|
fields:
|
||||||
|
status: { enum: [gate-1, gate-2, gate-3, gate-4, gate-5, done] }
|
||||||
|
size: { enum: [L] }
|
||||||
|
date: { kind: date }
|
||||||
|
related: { kind: links }
|
||||||
|
rules:
|
||||||
|
# status: done if and only if the file lives under docs/design/done/.
|
||||||
|
- done_iff_in_done_dir
|
||||||
|
|
||||||
|
aar:
|
||||||
|
dir: "docs/aar"
|
||||||
|
filename: "^\\d{4}-\\d{2}-\\d{2}-[a-z0-9-]+\\.md$"
|
||||||
|
required: [type, status, date]
|
||||||
|
fields:
|
||||||
|
status: { enum: [open, harvested] }
|
||||||
|
date: { kind: date }
|
||||||
|
related: { kind: links }
|
||||||
|
|
||||||
|
issue:
|
||||||
|
dir: "docs/issues"
|
||||||
|
filename: "^\\d{4}-[a-z0-9-]+\\.md$"
|
||||||
|
required: [type, id, status, created]
|
||||||
|
fields:
|
||||||
|
id: { pattern: "^\\d{4}$" }
|
||||||
|
status: { enum: [open, in-progress, done, rejected] }
|
||||||
|
created: { kind: date }
|
||||||
|
related: { kind: links }
|
||||||
|
|
||||||
|
wiki-page:
|
||||||
|
dir: "docs/wiki"
|
||||||
|
filename: "^[a-z0-9-]+\\.md$"
|
||||||
|
required: [type, area]
|
||||||
|
fields:
|
||||||
|
area:
|
||||||
|
enum:
|
||||||
|
- index
|
||||||
|
- architecture
|
||||||
|
- admin
|
||||||
|
- deployment
|
||||||
|
- user-guide
|
||||||
|
- requirements
|
||||||
|
- faq
|
||||||
|
- stolpersteine
|
||||||
|
sources: { kind: links }
|
||||||
|
related: { kind: links }
|
||||||
|
rules:
|
||||||
|
# Pages other than the index should be linked from somewhere
|
||||||
|
# (reported as WARNING, not error — see validate.py).
|
||||||
|
- warn_if_orphan
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""gen_status.py — generate STATUS.md deterministically from frontmatter.
|
||||||
|
|
||||||
|
Writes STATUS.md (no timestamps — output depends only on repo content, so
|
||||||
|
reruns are diff-clean). With --check, regenerates in memory and fails if
|
||||||
|
the committed STATUS.md is stale; CI uses this mode.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/gen_status.py [repo-root] # write STATUS.md
|
||||||
|
python scripts/gen_status.py --check [repo-root] # verify, exit 1 if stale
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
except ImportError: # pragma: no cover
|
||||||
|
sys.exit("gen_status.py needs PyYAML: pip install pyyaml")
|
||||||
|
|
||||||
|
H1_RE = re.compile(r"^#\s+(.*)$", re.M)
|
||||||
|
|
||||||
|
|
||||||
|
def parse(path: Path):
|
||||||
|
lines = path.read_text(encoding="utf-8").splitlines()
|
||||||
|
if not lines or lines[0].strip() != "---":
|
||||||
|
return None, ""
|
||||||
|
for j in range(1, len(lines)):
|
||||||
|
if lines[j].strip() == "---":
|
||||||
|
meta = yaml.safe_load("\n".join(lines[1:j])) or {}
|
||||||
|
body = "\n".join(lines[j + 1:])
|
||||||
|
return meta, body
|
||||||
|
return None, ""
|
||||||
|
|
||||||
|
|
||||||
|
def title(body: str, fallback: str) -> str:
|
||||||
|
match = H1_RE.search(body)
|
||||||
|
return match.group(1).strip() if match else fallback
|
||||||
|
|
||||||
|
|
||||||
|
def collect(root: Path, subdir: str, wanted_type: str):
|
||||||
|
items = []
|
||||||
|
base = root / subdir
|
||||||
|
if not base.is_dir():
|
||||||
|
return items
|
||||||
|
for path in sorted(base.rglob("*.md")):
|
||||||
|
if path.name == "template.md":
|
||||||
|
continue
|
||||||
|
meta, body = parse(path)
|
||||||
|
if not isinstance(meta, dict) or meta.get("type") != wanted_type:
|
||||||
|
continue
|
||||||
|
rel = path.relative_to(root).as_posix()
|
||||||
|
items.append((rel, meta, title(body, path.stem)))
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
def render(root: Path) -> str:
|
||||||
|
issues = collect(root, "docs/issues", "issue")
|
||||||
|
designs = collect(root, "docs/design", "design")
|
||||||
|
adrs = collect(root, "docs/adr", "adr")
|
||||||
|
aars = collect(root, "docs/aar", "aar")
|
||||||
|
|
||||||
|
out: list[str] = []
|
||||||
|
out.append("# STATUS")
|
||||||
|
out.append("")
|
||||||
|
out.append("<!-- Generated by scripts/gen_status.py — do not edit. -->")
|
||||||
|
out.append("")
|
||||||
|
|
||||||
|
open_issues = [i for i in issues
|
||||||
|
if i[1].get("status") in ("open", "in-progress")]
|
||||||
|
closed = len(issues) - len(open_issues)
|
||||||
|
out.append(f"## Issues ({len(open_issues)} open, {closed} closed)")
|
||||||
|
out.append("")
|
||||||
|
if open_issues:
|
||||||
|
out.append("| Issue | Status | Title |")
|
||||||
|
out.append("|---|---|---|")
|
||||||
|
for rel, meta, name in open_issues:
|
||||||
|
out.append(f"| [{meta.get('id', '?')}]({rel}) "
|
||||||
|
f"| {meta.get('status')} | {name} |")
|
||||||
|
else:
|
||||||
|
out.append("_none open_")
|
||||||
|
out.append("")
|
||||||
|
|
||||||
|
active = [d for d in designs if d[1].get("status") != "done"]
|
||||||
|
out.append(f"## Active design docs ({len(active)})")
|
||||||
|
out.append("")
|
||||||
|
if active:
|
||||||
|
out.append("| Design | Gate | Title |")
|
||||||
|
out.append("|---|---|---|")
|
||||||
|
for rel, meta, name in active:
|
||||||
|
out.append(f"| [{Path(rel).stem}]({rel}) "
|
||||||
|
f"| {meta.get('status')} | {name} |")
|
||||||
|
else:
|
||||||
|
out.append("_none active_")
|
||||||
|
out.append("")
|
||||||
|
|
||||||
|
out.append(f"## ADRs ({len(adrs)})")
|
||||||
|
out.append("")
|
||||||
|
if adrs:
|
||||||
|
out.append("| ADR | Status | Title |")
|
||||||
|
out.append("|---|---|---|")
|
||||||
|
for rel, meta, name in adrs:
|
||||||
|
out.append(f"| [{meta.get('id', '?')}]({rel}) "
|
||||||
|
f"| {meta.get('status')} | {name} |")
|
||||||
|
else:
|
||||||
|
out.append("_none_")
|
||||||
|
out.append("")
|
||||||
|
|
||||||
|
open_aars = [a for a in aars if a[1].get("status") == "open"]
|
||||||
|
out.append(f"## Open AARs ({len(open_aars)})")
|
||||||
|
out.append("")
|
||||||
|
if open_aars:
|
||||||
|
for rel, _meta, name in open_aars:
|
||||||
|
out.append(f"- [{name}]({rel})")
|
||||||
|
else:
|
||||||
|
out.append("_none — nothing awaiting harvest_")
|
||||||
|
out.append("")
|
||||||
|
return "\n".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = [a for a in sys.argv[1:] if a != "--check"]
|
||||||
|
check = "--check" in sys.argv[1:]
|
||||||
|
root = Path(args[0]) if args else Path.cwd()
|
||||||
|
content = render(root)
|
||||||
|
status = root / "STATUS.md"
|
||||||
|
if check:
|
||||||
|
current = status.read_text(encoding="utf-8") if status.is_file() else ""
|
||||||
|
if current != content:
|
||||||
|
print("gen_status --check: STATUS.md is stale — "
|
||||||
|
"run scripts/gen_status.py and commit the result")
|
||||||
|
return 1
|
||||||
|
print("gen_status --check: STATUS.md is current")
|
||||||
|
return 0
|
||||||
|
status.write_text(content, encoding="utf-8", newline="\n")
|
||||||
|
print(f"wrote {status}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""validate.py — deterministic artifact validation against schema.yaml.
|
||||||
|
|
||||||
|
Checks (errors, exit 1):
|
||||||
|
* frontmatter present, parseable, `type` known
|
||||||
|
* file location and filename match the type's rules
|
||||||
|
* required fields, enums, patterns, dates
|
||||||
|
* link fields: repo-root-relative targets exist (http/https/mailto skipped)
|
||||||
|
* inline markdown links in bodies resolve (relative to the file)
|
||||||
|
* per-type rules: superseded_requires_pointer, done_iff_in_done_dir
|
||||||
|
|
||||||
|
Warnings (exit 0):
|
||||||
|
* wiki pages (except index) with no inbound link anywhere
|
||||||
|
|
||||||
|
Usage: python scripts/validate.py [repo-root]
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import fnmatch
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
except ImportError: # pragma: no cover
|
||||||
|
sys.exit("validate.py needs PyYAML: pip install pyyaml")
|
||||||
|
|
||||||
|
DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
||||||
|
INLINE_LINK_RE = re.compile(r"\]\(([^)\s]+)\)")
|
||||||
|
HTML_SRC_RE = re.compile(r"(?:src|srcset)=\"([^\"]+)\"")
|
||||||
|
EXTERNAL_PREFIXES = ("http://", "https://", "mailto:")
|
||||||
|
|
||||||
|
errors: list[str] = []
|
||||||
|
warnings: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
|
def err(path: Path, msg: str) -> None:
|
||||||
|
errors.append(f"ERROR {path}: {msg}")
|
||||||
|
|
||||||
|
|
||||||
|
def warn(path: Path, msg: str) -> None:
|
||||||
|
warnings.append(f"WARN {path}: {msg}")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_frontmatter(text: str):
|
||||||
|
lines = text.splitlines()
|
||||||
|
if not lines or lines[0].strip() != "---":
|
||||||
|
return None, text
|
||||||
|
for j in range(1, len(lines)):
|
||||||
|
if lines[j].strip() == "---":
|
||||||
|
fm = "\n".join(lines[1:j])
|
||||||
|
body = "\n".join(lines[j + 1:])
|
||||||
|
return yaml.safe_load(fm) or {}, body
|
||||||
|
return None, text # unterminated
|
||||||
|
|
||||||
|
|
||||||
|
def is_date(value) -> bool:
|
||||||
|
if isinstance(value, datetime.date):
|
||||||
|
return True
|
||||||
|
return isinstance(value, str) and bool(DATE_RE.match(value))
|
||||||
|
|
||||||
|
|
||||||
|
def as_links(value):
|
||||||
|
"""Normalize a link field's value to a list of strings."""
|
||||||
|
if value is None:
|
||||||
|
return []
|
||||||
|
if isinstance(value, str):
|
||||||
|
return [value]
|
||||||
|
if isinstance(value, list):
|
||||||
|
return [v for v in value if isinstance(v, str)]
|
||||||
|
return None # wrong shape
|
||||||
|
|
||||||
|
|
||||||
|
def discover(root: Path, scope: dict) -> list[Path]:
|
||||||
|
files: set[Path] = set()
|
||||||
|
for pattern in scope.get("include", []):
|
||||||
|
files.update(root.glob(pattern))
|
||||||
|
result = []
|
||||||
|
for f in sorted(files):
|
||||||
|
rel = f.relative_to(root).as_posix()
|
||||||
|
if any(fnmatch.fnmatch(rel, pat) for pat in scope.get("exclude", [])):
|
||||||
|
continue
|
||||||
|
if f.is_file():
|
||||||
|
result.append(f)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def check_fields(path: Path, meta: dict, spec: dict, root: Path) -> None:
|
||||||
|
for field in spec.get("required", []):
|
||||||
|
if field not in meta or meta[field] is None:
|
||||||
|
err(path, f"missing required field '{field}'")
|
||||||
|
for field, rule in (spec.get("fields") or {}).items():
|
||||||
|
if field not in meta:
|
||||||
|
continue
|
||||||
|
value = meta[field]
|
||||||
|
if value is None:
|
||||||
|
if not rule.get("nullable"):
|
||||||
|
# required-check already covers required fields;
|
||||||
|
# a present-but-null optional field is fine unless typed link
|
||||||
|
pass
|
||||||
|
continue
|
||||||
|
if "enum" in rule and value not in rule["enum"]:
|
||||||
|
err(path, f"'{field}: {value}' not in enum {rule['enum']}")
|
||||||
|
if "pattern" in rule and not re.match(rule["pattern"], str(value)):
|
||||||
|
err(path, f"'{field}: {value}' does not match {rule['pattern']}")
|
||||||
|
kind = rule.get("kind")
|
||||||
|
if kind == "date" and not is_date(value):
|
||||||
|
err(path, f"'{field}: {value}' is not a YYYY-MM-DD date")
|
||||||
|
if kind == "bool" and not isinstance(value, bool):
|
||||||
|
err(path, f"'{field}: {value}' is not a boolean")
|
||||||
|
if kind == "str" and not isinstance(value, str):
|
||||||
|
err(path, f"'{field}' must be a string")
|
||||||
|
|
||||||
|
|
||||||
|
def check_links(path: Path, meta: dict, link_fields: list, root: Path,
|
||||||
|
inbound: set) -> None:
|
||||||
|
for field in link_fields:
|
||||||
|
if field not in meta:
|
||||||
|
continue
|
||||||
|
links = as_links(meta[field])
|
||||||
|
if links is None:
|
||||||
|
err(path, f"'{field}' must be a string or list of strings")
|
||||||
|
continue
|
||||||
|
for link in links:
|
||||||
|
if link.startswith(EXTERNAL_PREFIXES):
|
||||||
|
continue
|
||||||
|
target = (root / link)
|
||||||
|
if not target.is_file():
|
||||||
|
err(path, f"'{field}' link target missing: {link}")
|
||||||
|
else:
|
||||||
|
inbound.add(target.resolve())
|
||||||
|
|
||||||
|
|
||||||
|
def check_body_links(path: Path, body: str, root: Path, inbound: set) -> None:
|
||||||
|
# strip fenced code blocks and inline code spans so mermaid, code
|
||||||
|
# samples, and literal link examples in backticks aren't scanned
|
||||||
|
body = re.sub(r"```.*?```", "", body, flags=re.S)
|
||||||
|
body = re.sub(r"`[^`\n]*`", "", body)
|
||||||
|
candidates = [m.group(1) for m in INLINE_LINK_RE.finditer(body)]
|
||||||
|
for raw in (m.group(1) for m in HTML_SRC_RE.finditer(body)):
|
||||||
|
# srcset may list "path 2x, path2 1x" pairs — take each path token
|
||||||
|
for part in raw.split(","):
|
||||||
|
candidates.append(part.strip().split()[0])
|
||||||
|
for link in candidates:
|
||||||
|
if link.startswith(EXTERNAL_PREFIXES) or link.startswith("#"):
|
||||||
|
continue
|
||||||
|
link = link.split("#", 1)[0]
|
||||||
|
if not link:
|
||||||
|
continue
|
||||||
|
target = (path.parent / link).resolve()
|
||||||
|
if not target.is_file():
|
||||||
|
err(path, f"inline link target missing: {link}")
|
||||||
|
else:
|
||||||
|
inbound.add(target)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_rules(path: Path, rel: str, meta: dict, spec: dict) -> None:
|
||||||
|
for rule in spec.get("rules", []):
|
||||||
|
if rule == "superseded_requires_pointer":
|
||||||
|
if meta.get("status") == "superseded" and not meta.get("superseded_by"):
|
||||||
|
err(path, "status 'superseded' requires 'superseded_by'")
|
||||||
|
elif rule == "done_iff_in_done_dir":
|
||||||
|
in_done = "/done/" in f"/{rel}"
|
||||||
|
if (meta.get("status") == "done") != in_done:
|
||||||
|
err(path, "status 'done' <-> file in docs/design/done/ mismatch")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
|
||||||
|
schema = yaml.safe_load((root / "schema.yaml").read_text(encoding="utf-8"))
|
||||||
|
link_fields = schema.get("link_fields", [])
|
||||||
|
types = schema.get("types", {})
|
||||||
|
inbound: set = set()
|
||||||
|
wiki_pages: list[tuple[Path, dict]] = []
|
||||||
|
|
||||||
|
# Root documents: inline links must resolve; no frontmatter required.
|
||||||
|
for rel in schema.get("scope", {}).get("link_only", []):
|
||||||
|
path = root / rel
|
||||||
|
if not path.is_file():
|
||||||
|
continue # e.g. STATUS.md before first generation
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
_meta, body = parse_frontmatter(text)
|
||||||
|
check_body_links(path, body if _meta is not None else text,
|
||||||
|
root, inbound)
|
||||||
|
seen_ids: dict[tuple[str, str], Path] = {}
|
||||||
|
artifacts = discover(root, schema.get("scope", {}))
|
||||||
|
|
||||||
|
for path in artifacts:
|
||||||
|
rel = path.relative_to(root).as_posix()
|
||||||
|
meta, body = parse_frontmatter(path.read_text(encoding="utf-8"))
|
||||||
|
if meta is None:
|
||||||
|
err(path, "missing or unterminated YAML frontmatter")
|
||||||
|
continue
|
||||||
|
if not isinstance(meta, dict) or "type" not in meta:
|
||||||
|
err(path, "frontmatter has no 'type'")
|
||||||
|
continue
|
||||||
|
t = meta["type"]
|
||||||
|
if t not in types:
|
||||||
|
err(path, f"unknown type '{t}'")
|
||||||
|
continue
|
||||||
|
spec = types[t]
|
||||||
|
expected_dir = spec.get("dir", ".")
|
||||||
|
actual_dir = str(Path(rel).parent.as_posix())
|
||||||
|
if expected_dir == ".":
|
||||||
|
if actual_dir != ".":
|
||||||
|
err(path, f"type '{t}' must live in repo root")
|
||||||
|
elif not (actual_dir == expected_dir
|
||||||
|
or actual_dir.startswith(expected_dir + "/")):
|
||||||
|
err(path, f"type '{t}' must live under {expected_dir}/")
|
||||||
|
fn_pattern = spec.get("filename")
|
||||||
|
if fn_pattern and not re.match(fn_pattern, path.name):
|
||||||
|
err(path, f"filename does not match {fn_pattern}")
|
||||||
|
check_fields(path, meta, spec, root)
|
||||||
|
if "id" in (spec.get("fields") or {}) and meta.get("id") is not None:
|
||||||
|
artifact_id = str(meta["id"])
|
||||||
|
if not path.name.startswith(f"{artifact_id}-"):
|
||||||
|
err(path, f"id '{artifact_id}' does not match filename prefix")
|
||||||
|
key = (t, artifact_id)
|
||||||
|
if key in seen_ids:
|
||||||
|
err(path, f"duplicate {t} id '{artifact_id}' "
|
||||||
|
f"(also in {seen_ids[key].name})")
|
||||||
|
else:
|
||||||
|
seen_ids[key] = path
|
||||||
|
check_links(path, meta, link_fields, root, inbound)
|
||||||
|
check_body_links(path, body, root, inbound)
|
||||||
|
apply_rules(path, rel, meta, spec)
|
||||||
|
if t == "wiki-page" and meta.get("area") != "index":
|
||||||
|
wiki_pages.append((path, meta))
|
||||||
|
|
||||||
|
# link-only files: inline links are checked, frontmatter not required
|
||||||
|
already = {p.resolve() for p in artifacts}
|
||||||
|
for pattern in schema.get("scope", {}).get("link_only", []):
|
||||||
|
for path in sorted(root.glob(pattern)):
|
||||||
|
if not path.is_file() or path.resolve() in already:
|
||||||
|
continue
|
||||||
|
meta, body = parse_frontmatter(path.read_text(encoding="utf-8"))
|
||||||
|
if meta is None:
|
||||||
|
body = path.read_text(encoding="utf-8")
|
||||||
|
check_body_links(path, body, root, inbound)
|
||||||
|
|
||||||
|
for path, _meta in wiki_pages:
|
||||||
|
if path.resolve() not in inbound:
|
||||||
|
warn(path, "orphan wiki page — nothing links to it")
|
||||||
|
|
||||||
|
for line in errors + warnings:
|
||||||
|
print(line)
|
||||||
|
print(f"validate: {len(errors)} error(s), {len(warnings)} warning(s)")
|
||||||
|
return 1 if errors else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
type: aar
|
||||||
|
status: open # open | harvested
|
||||||
|
date: YYYY-MM-DD
|
||||||
|
related: [] # design docs, issues, ADRs involved
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Copy to docs/aar/YYYY-MM-DD-slug.md. Delete comments when filling in.
|
||||||
|
Standalone AARs are for incidents and major deviations only —
|
||||||
|
normal undertakings get their AAR as Gate 5 inside the design doc. -->
|
||||||
|
|
||||||
|
# AAR: Title
|
||||||
|
|
||||||
|
## What was planned / expected
|
||||||
|
|
||||||
|
## What happened
|
||||||
|
|
||||||
|
<!-- Facts and timeline, not blame. -->
|
||||||
|
|
||||||
|
## Why the difference
|
||||||
|
|
||||||
|
<!-- Root cause. For failures, name the routing class:
|
||||||
|
intent issue / spec issue / code issue. -->
|
||||||
|
|
||||||
|
## Learnings
|
||||||
|
|
||||||
|
<!-- What future-you should know. Blunt beats polite. -->
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
<!-- Concrete: wiki pages updated (FAQ, Stolpersteine) with links,
|
||||||
|
framework issues opened, tests added. When all actions are done,
|
||||||
|
set status: harvested. The refinement session walks all AARs
|
||||||
|
still marked open. -->
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
type: adr
|
||||||
|
id: "0000"
|
||||||
|
status: proposed # proposed | accepted | superseded
|
||||||
|
date: YYYY-MM-DD
|
||||||
|
supersedes: null # path to older ADR, e.g. docs/adr/0002-old.md
|
||||||
|
superseded_by: null # filled in on the OLD adr when a new one replaces it
|
||||||
|
related: [] # optional: paths to design docs / issues
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Copy to docs/adr/NNNN-slug.md. Delete all comments when filling in. -->
|
||||||
|
|
||||||
|
# ADR-0000: Title
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
<!-- The situation and the forces at play. Constraints upfront:
|
||||||
|
deadlines, scale, team knowledge, existing decisions. -->
|
||||||
|
|
||||||
|
## Options Considered
|
||||||
|
|
||||||
|
<!-- Name each option, even the one you lean toward. Pros/cons per
|
||||||
|
option; a small dimension table (complexity, cost, maintenance,
|
||||||
|
familiarity) where it helps. Keep proportional to the decision. -->
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
<!-- The choice, in one or two sentences. -->
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
<!-- What becomes easier, what becomes harder, what we will need to
|
||||||
|
revisit. Honest cons included. -->
|
||||||
|
|
||||||
|
<!-- Rules: an accepted ADR is never edited — write a new ADR that
|
||||||
|
supersedes it and set superseded_by here. Lasting directional
|
||||||
|
decisions only; feature-local choices belong in the design doc. -->
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
---
|
||||||
|
type: design
|
||||||
|
status: gate-1 # gate-1 | gate-2 | gate-3 | gate-4 | gate-5 | done
|
||||||
|
date: YYYY-MM-DD
|
||||||
|
size: L # this template is for size L
|
||||||
|
related: [] # issues, ADRs spawned or read
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Copy to docs/design/YYYY-MM-DD-slug.md. Delete comments when filling in.
|
||||||
|
Fill ONE gate at a time; each gate ends with STOP — do not pre-fill
|
||||||
|
later gates. Advance `status` only after human approval. -->
|
||||||
|
|
||||||
|
# Design: Title
|
||||||
|
|
||||||
|
## Gate 1 — Product
|
||||||
|
|
||||||
|
**Problem.** <!-- What user problem, for whom. -->
|
||||||
|
|
||||||
|
**Acceptance criterion.** <!-- Verifiable. A real number where one
|
||||||
|
exists; otherwise a concretely checkable outcome. "Works" is not one. -->
|
||||||
|
|
||||||
|
**Non-goals.** <!-- What this deliberately does NOT do. The cheapest
|
||||||
|
scope-creep brake there is. -->
|
||||||
|
|
||||||
|
**Announcement.** <!-- 3–5 sentences: what it is, who it's for, why
|
||||||
|
it's good. Can't write it? The product isn't understood yet. -->
|
||||||
|
|
||||||
|
**Mockups.** <!-- Only if UI is involved: plain-HTML mockups, linked. -->
|
||||||
|
|
||||||
|
> **STOP — awaiting Gate 1 approval.**
|
||||||
|
|
||||||
|
## Gate 2 — Architecture
|
||||||
|
|
||||||
|
**Inputs read.** <!-- Which ADRs and AARs were read; one line each on
|
||||||
|
why they matter here. -->
|
||||||
|
|
||||||
|
**System fit.** <!-- Endpoints, tables/schemas, query outlines,
|
||||||
|
end-to-end flow as Mermaid. Against the actual codebase. -->
|
||||||
|
|
||||||
|
**Constraints.** <!-- Non-functional, proportional to the project:
|
||||||
|
performance, security, operations, compatibility. "None relevant"
|
||||||
|
is a valid answer — but say it. -->
|
||||||
|
|
||||||
|
**Options & trade-offs.** <!-- Where more than one viable way exists:
|
||||||
|
name the options, pro/contra each, state the chosen one and WHY.
|
||||||
|
This is the feature-local decision record. Only lasting, binding
|
||||||
|
decisions graduate to an ADR below. -->
|
||||||
|
|
||||||
|
**New ADRs.** <!-- Lasting decisions discovered here → one ADR each,
|
||||||
|
linked. None is a valid answer. -->
|
||||||
|
|
||||||
|
> **STOP — awaiting Gate 2 approval.**
|
||||||
|
|
||||||
|
## Gate 3 — Program Design
|
||||||
|
|
||||||
|
**Files.** <!-- Exact paths, new and touched. -->
|
||||||
|
|
||||||
|
**Signatures.** <!-- Types and method signatures, no bodies. -->
|
||||||
|
|
||||||
|
**Call stack.** <!-- For the main flow(s). -->
|
||||||
|
|
||||||
|
**Test assertions.** <!-- What the tests will assert. -->
|
||||||
|
|
||||||
|
**Boundaries — DO NOT CHANGE.** <!-- Explicit list. -->
|
||||||
|
|
||||||
|
**Shakiest calls.** <!-- The decisions you are least confident about. -->
|
||||||
|
|
||||||
|
> **STOP — awaiting Gate 3 approval.**
|
||||||
|
|
||||||
|
## Gate 4 — Vertical Slices
|
||||||
|
|
||||||
|
<!-- Slice 1 is the tracer bullet: thin end-to-end, runs with mocks.
|
||||||
|
Then real logic, one testable slice at a time. Per task:
|
||||||
|
files / action / verify / done. After each slice: evidence,
|
||||||
|
status, STOP. -->
|
||||||
|
|
||||||
|
### Slice 1 — Tracer bullet
|
||||||
|
- [ ] Task: … — files: … — action: … — verify: … — done: …
|
||||||
|
|
||||||
|
**Evidence:** <!-- command output, test run, screenshot ref -->
|
||||||
|
**Status:** <!-- DONE | DONE_WITH_CONCERNS | NEEDS_CONTEXT | BLOCKED -->
|
||||||
|
|
||||||
|
> **STOP — slice review.**
|
||||||
|
|
||||||
|
### Slice 2 — …
|
||||||
|
|
||||||
|
### Handoff
|
||||||
|
|
||||||
|
<!-- The single place session state lives. Overwrite on every handoff;
|
||||||
|
git keeps the history.
|
||||||
|
Done slices: …
|
||||||
|
Open decisions: …
|
||||||
|
Next step: … -->
|
||||||
|
|
||||||
|
## Gate 5 — Closeout (AAR)
|
||||||
|
|
||||||
|
**Planned vs. actual.** <!-- What was planned, what happened. -->
|
||||||
|
|
||||||
|
**Why the difference.** <!-- Root causes, honestly. -->
|
||||||
|
|
||||||
|
**Learnings.** <!-- What future-you should know. -->
|
||||||
|
|
||||||
|
**Harvested.** <!-- Wiki pages updated (FAQ, Stolpersteine, …) with
|
||||||
|
links; framework issues opened, if a rule was missing or wrong. -->
|
||||||
|
|
||||||
|
**Open uncertainties.** <!-- Session-handoff answers to: "Which choices
|
||||||
|
did I make that I'm least confident about?" -->
|
||||||
|
|
||||||
|
<!-- After approval: set status: done, move this file to
|
||||||
|
docs/design/done/, run gen_status.py. -->
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
type: issue
|
||||||
|
id: "0000"
|
||||||
|
status: open # open | in-progress | done | rejected
|
||||||
|
created: YYYY-MM-DD
|
||||||
|
related: [] # design docs, ADRs, other issues
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Copy to docs/issues/NNNN-slug.md. Delete comments when filling in. -->
|
||||||
|
|
||||||
|
# Issue-0000: Title
|
||||||
|
|
||||||
|
## Problem / Motivation
|
||||||
|
|
||||||
|
<!-- What's wrong or missing, and why it matters. One paragraph. -->
|
||||||
|
|
||||||
|
## Acceptance
|
||||||
|
|
||||||
|
<!-- When is this issue done? Verifiable, like every other criterion
|
||||||
|
in this framework. -->
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
<!-- Optional: context, links, findings gathered along the way.
|
||||||
|
Rules: status is the single source of truth and lives here in the
|
||||||
|
frontmatter — STATUS.md is generated, never edited. An issue that
|
||||||
|
starts real work links its design doc in `related`. Closed means
|
||||||
|
status: done (or rejected, with a one-line reason in Notes) —
|
||||||
|
the file stays; git is the history. -->
|
||||||
+143
@@ -0,0 +1,143 @@
|
|||||||
|
# schema.yaml — single source of truth for artifact frontmatter.
|
||||||
|
# Stage 1 of ADR-0004: scripts/validate.py checks generically against this
|
||||||
|
# file. Extending the framework's metadata means editing THIS file, not code.
|
||||||
|
# Agents: never invent fields or status values; propose a schema change.
|
||||||
|
#
|
||||||
|
# PROJEKTERWEITERUNGEN gegenüber neckbeard v0.1.1 (Original:
|
||||||
|
# docs/sources/upstream/neckbeard-v0.1.1/schema.yaml; Design:
|
||||||
|
# docs/design/2026-08-11-neckbeard-migration.md, ADR-0012/0013):
|
||||||
|
# * issue: Pflichtfelder milestone (M1–M5) + priority; Status-Enum um
|
||||||
|
# next/waiting erweitert; due/host/area/wartegrund/gitlab_iid;
|
||||||
|
# Regel waiting_requires_reason; globale Regel wip_limit (max. 2
|
||||||
|
# in-progress) in validate.py.
|
||||||
|
# * component: neuer Typ unter docs/components/ (Dateiname = Slug).
|
||||||
|
# * wiki-page: Area-Enum um "vision" erweitert.
|
||||||
|
|
||||||
|
version: 1
|
||||||
|
|
||||||
|
scope:
|
||||||
|
# Files considered artifacts. Templates and raw sources are exempt.
|
||||||
|
include:
|
||||||
|
- "PROJECT.md"
|
||||||
|
- "docs/**/*.md"
|
||||||
|
exclude:
|
||||||
|
- "**/template.md"
|
||||||
|
- "docs/sources/**"
|
||||||
|
- "vendor/**"
|
||||||
|
# Files whose inline links are checked, but which need no frontmatter
|
||||||
|
# (root-level prose: README, AGENTS, WORKFLOW, generated STATUS, ...).
|
||||||
|
link_only:
|
||||||
|
- "*.md"
|
||||||
|
|
||||||
|
# Frontmatter fields whose values are links. Values starting with
|
||||||
|
# http://, https:// or mailto: are treated as external and only
|
||||||
|
# format-checked; everything else must be a repo-root-relative path
|
||||||
|
# to an existing file.
|
||||||
|
link_fields: [related, sources, supersedes, superseded_by]
|
||||||
|
|
||||||
|
types:
|
||||||
|
project:
|
||||||
|
dir: "."
|
||||||
|
filename: "^PROJECT\\.md$"
|
||||||
|
required: [type, language, size_s_exception, purpose, audience]
|
||||||
|
fields:
|
||||||
|
language: { enum: [de, en] }
|
||||||
|
size_s_exception: { kind: bool }
|
||||||
|
purpose: { kind: str }
|
||||||
|
audience: { kind: str }
|
||||||
|
|
||||||
|
adr:
|
||||||
|
dir: "docs/adr"
|
||||||
|
filename: "^\\d{4}-[a-z0-9-]+\\.md$"
|
||||||
|
required: [type, id, status, date]
|
||||||
|
fields:
|
||||||
|
id: { pattern: "^\\d{4}$" }
|
||||||
|
status: { enum: [proposed, accepted, superseded] }
|
||||||
|
date: { kind: date }
|
||||||
|
supersedes: { kind: link, nullable: true }
|
||||||
|
superseded_by: { kind: link, nullable: true }
|
||||||
|
related: { kind: links }
|
||||||
|
rules:
|
||||||
|
# status: superseded requires superseded_by to point at the successor.
|
||||||
|
- superseded_requires_pointer
|
||||||
|
|
||||||
|
design:
|
||||||
|
dir: "docs/design"
|
||||||
|
filename: "^\\d{4}-\\d{2}-\\d{2}-[a-z0-9-]+\\.md$"
|
||||||
|
required: [type, status, date, size]
|
||||||
|
fields:
|
||||||
|
status: { enum: [gate-1, gate-2, gate-3, gate-4, gate-5, done] }
|
||||||
|
size: { enum: [L] }
|
||||||
|
date: { kind: date }
|
||||||
|
related: { kind: links }
|
||||||
|
rules:
|
||||||
|
# status: done if and only if the file lives under docs/design/done/.
|
||||||
|
- done_iff_in_done_dir
|
||||||
|
|
||||||
|
aar:
|
||||||
|
dir: "docs/aar"
|
||||||
|
filename: "^\\d{4}-\\d{2}-\\d{2}-[a-z0-9-]+\\.md$"
|
||||||
|
required: [type, status, date]
|
||||||
|
fields:
|
||||||
|
status: { enum: [open, harvested] }
|
||||||
|
date: { kind: date }
|
||||||
|
related: { kind: links }
|
||||||
|
|
||||||
|
issue:
|
||||||
|
dir: "docs/issues"
|
||||||
|
filename: "^\\d{4}-[a-z0-9-]+\\.md$"
|
||||||
|
required: [type, id, status, created, milestone, priority]
|
||||||
|
fields:
|
||||||
|
id: { pattern: "^\\d{4}$" }
|
||||||
|
status: { enum: [open, next, in-progress, waiting, done, rejected] }
|
||||||
|
created: { kind: date }
|
||||||
|
milestone: { enum: [M1, M2, M3, M4, M5] }
|
||||||
|
priority: { enum: [high, medium, low] }
|
||||||
|
due: { kind: date, nullable: true }
|
||||||
|
host: { enum: [cfgmon, overmind, matrix, game], nullable: true }
|
||||||
|
area: { enum: [security, infrastructure, database, element], nullable: true }
|
||||||
|
wartegrund: { kind: str, nullable: true }
|
||||||
|
gitlab_iid: { pattern: "^\\d+$", nullable: true }
|
||||||
|
related: { kind: links }
|
||||||
|
rules:
|
||||||
|
# status: waiting requires a named reason (old rule: "nur mit
|
||||||
|
# benanntem Grund").
|
||||||
|
- waiting_requires_reason
|
||||||
|
|
||||||
|
component:
|
||||||
|
dir: "docs/components"
|
||||||
|
filename: "^[A-Za-z0-9.-]+\\.md$"
|
||||||
|
required: [type, slug, anzeigename, phase]
|
||||||
|
fields:
|
||||||
|
slug: { kind: str }
|
||||||
|
anzeigename: { kind: str }
|
||||||
|
phase: { enum: [active, staged, external] }
|
||||||
|
gitlab: { kind: str }
|
||||||
|
mirror: { kind: str, nullable: true }
|
||||||
|
related: { kind: links }
|
||||||
|
rules:
|
||||||
|
# The canonical slug is the filename — no second naming scheme.
|
||||||
|
- slug_matches_filename
|
||||||
|
|
||||||
|
wiki-page:
|
||||||
|
dir: "docs/wiki"
|
||||||
|
filename: "^[a-z0-9-]+\\.md$"
|
||||||
|
required: [type, area]
|
||||||
|
fields:
|
||||||
|
area:
|
||||||
|
enum:
|
||||||
|
- index
|
||||||
|
- architecture
|
||||||
|
- admin
|
||||||
|
- deployment
|
||||||
|
- user-guide
|
||||||
|
- requirements
|
||||||
|
- faq
|
||||||
|
- stolpersteine
|
||||||
|
- vision
|
||||||
|
sources: { kind: links }
|
||||||
|
related: { kind: links }
|
||||||
|
rules:
|
||||||
|
# Pages other than the index should be linked from somewhere
|
||||||
|
# (reported as WARNING, not error — see validate.py).
|
||||||
|
- warn_if_orphan
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""gen_status.py — generate STATUS.md deterministically from frontmatter.
|
||||||
|
|
||||||
|
PROJEKTERWEITERUNG gegenüber neckbeard v0.1.1 (Original unter
|
||||||
|
docs/sources/upstream/neckbeard-v0.1.1/scripts/): offene Issues sind
|
||||||
|
alles außer done/rejected (Status-Enum ist projektweit erweitert);
|
||||||
|
Tabelle zeigt Meilenstein/Priorität; Verteilungszeile je Meilenstein
|
||||||
|
ersetzt die früheren Hand-Zählungen der roadmap.md (F-001/F-010).
|
||||||
|
|
||||||
|
Writes STATUS.md (no timestamps — output depends only on repo content, so
|
||||||
|
reruns are diff-clean). With --check, regenerates in memory and fails if
|
||||||
|
the committed STATUS.md is stale; CI uses this mode.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/gen_status.py [repo-root] # write STATUS.md
|
||||||
|
python scripts/gen_status.py --check [repo-root] # verify, exit 1 if stale
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
except ImportError: # pragma: no cover
|
||||||
|
sys.exit("gen_status.py needs PyYAML: pip install pyyaml")
|
||||||
|
|
||||||
|
H1_RE = re.compile(r"^#\s+(.*)$", re.M)
|
||||||
|
|
||||||
|
|
||||||
|
def parse(path: Path):
|
||||||
|
lines = path.read_text(encoding="utf-8").splitlines()
|
||||||
|
if not lines or lines[0].strip() != "---":
|
||||||
|
return None, ""
|
||||||
|
for j in range(1, len(lines)):
|
||||||
|
if lines[j].strip() == "---":
|
||||||
|
meta = yaml.safe_load("\n".join(lines[1:j])) or {}
|
||||||
|
body = "\n".join(lines[j + 1:])
|
||||||
|
return meta, body
|
||||||
|
return None, ""
|
||||||
|
|
||||||
|
|
||||||
|
def title(body: str, fallback: str) -> str:
|
||||||
|
match = H1_RE.search(body)
|
||||||
|
return match.group(1).strip() if match else fallback
|
||||||
|
|
||||||
|
|
||||||
|
def collect(root: Path, subdir: str, wanted_type: str):
|
||||||
|
items = []
|
||||||
|
base = root / subdir
|
||||||
|
if not base.is_dir():
|
||||||
|
return items
|
||||||
|
for path in sorted(base.rglob("*.md")):
|
||||||
|
if path.name == "template.md":
|
||||||
|
continue
|
||||||
|
meta, body = parse(path)
|
||||||
|
if not isinstance(meta, dict) or meta.get("type") != wanted_type:
|
||||||
|
continue
|
||||||
|
rel = path.relative_to(root).as_posix()
|
||||||
|
items.append((rel, meta, title(body, path.stem)))
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
def render(root: Path) -> str:
|
||||||
|
issues = collect(root, "docs/issues", "issue")
|
||||||
|
designs = collect(root, "docs/design", "design")
|
||||||
|
adrs = collect(root, "docs/adr", "adr")
|
||||||
|
aars = collect(root, "docs/aar", "aar")
|
||||||
|
|
||||||
|
out: list[str] = []
|
||||||
|
out.append("# STATUS")
|
||||||
|
out.append("")
|
||||||
|
out.append("<!-- Generated by scripts/gen_status.py — do not edit. -->")
|
||||||
|
out.append("")
|
||||||
|
|
||||||
|
open_issues = [i for i in issues
|
||||||
|
if i[1].get("status") not in ("done", "rejected")]
|
||||||
|
closed = len(issues) - len(open_issues)
|
||||||
|
out.append(f"## Issues ({len(open_issues)} open, {closed} closed)")
|
||||||
|
out.append("")
|
||||||
|
if open_issues:
|
||||||
|
dist: dict[str, int] = {}
|
||||||
|
for _rel, meta, _name in open_issues:
|
||||||
|
m = str(meta.get("milestone", "?"))
|
||||||
|
dist[m] = dist.get(m, 0) + 1
|
||||||
|
out.append("Verteilung: " + " · ".join(
|
||||||
|
f"{m} {n}" for m, n in sorted(dist.items())))
|
||||||
|
out.append("")
|
||||||
|
out.append("| Issue | Status | Meilenstein | Priorität | Title |")
|
||||||
|
out.append("|---|---|---|---|---|")
|
||||||
|
for rel, meta, name in open_issues:
|
||||||
|
out.append(f"| [{meta.get('id', '?')}]({rel}) "
|
||||||
|
f"| {meta.get('status')} | {meta.get('milestone')} "
|
||||||
|
f"| {meta.get('priority')} | {name} |")
|
||||||
|
else:
|
||||||
|
out.append("_none open_")
|
||||||
|
out.append("")
|
||||||
|
|
||||||
|
active = [d for d in designs if d[1].get("status") != "done"]
|
||||||
|
out.append(f"## Active design docs ({len(active)})")
|
||||||
|
out.append("")
|
||||||
|
if active:
|
||||||
|
out.append("| Design | Gate | Title |")
|
||||||
|
out.append("|---|---|---|")
|
||||||
|
for rel, meta, name in active:
|
||||||
|
out.append(f"| [{Path(rel).stem}]({rel}) "
|
||||||
|
f"| {meta.get('status')} | {name} |")
|
||||||
|
else:
|
||||||
|
out.append("_none active_")
|
||||||
|
out.append("")
|
||||||
|
|
||||||
|
out.append(f"## ADRs ({len(adrs)})")
|
||||||
|
out.append("")
|
||||||
|
if adrs:
|
||||||
|
out.append("| ADR | Status | Title |")
|
||||||
|
out.append("|---|---|---|")
|
||||||
|
for rel, meta, name in adrs:
|
||||||
|
out.append(f"| [{meta.get('id', '?')}]({rel}) "
|
||||||
|
f"| {meta.get('status')} | {name} |")
|
||||||
|
else:
|
||||||
|
out.append("_none_")
|
||||||
|
out.append("")
|
||||||
|
|
||||||
|
open_aars = [a for a in aars if a[1].get("status") == "open"]
|
||||||
|
out.append(f"## Open AARs ({len(open_aars)})")
|
||||||
|
out.append("")
|
||||||
|
if open_aars:
|
||||||
|
for rel, _meta, name in open_aars:
|
||||||
|
out.append(f"- [{name}]({rel})")
|
||||||
|
else:
|
||||||
|
out.append("_none — nothing awaiting harvest_")
|
||||||
|
out.append("")
|
||||||
|
return "\n".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = [a for a in sys.argv[1:] if a != "--check"]
|
||||||
|
check = "--check" in sys.argv[1:]
|
||||||
|
root = Path(args[0]) if args else Path.cwd()
|
||||||
|
content = render(root)
|
||||||
|
status = root / "STATUS.md"
|
||||||
|
if check:
|
||||||
|
current = status.read_text(encoding="utf-8") if status.is_file() else ""
|
||||||
|
if current != content:
|
||||||
|
print("gen_status --check: STATUS.md is stale — "
|
||||||
|
"run scripts/gen_status.py and commit the result")
|
||||||
|
return 1
|
||||||
|
print("gen_status --check: STATUS.md is current")
|
||||||
|
return 0
|
||||||
|
status.write_text(content, encoding="utf-8", newline="\n")
|
||||||
|
print(f"wrote {status}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""pruefe_upstream_drift.py — Byte-Vergleich gegen die gepinnte Baseline.
|
||||||
|
|
||||||
|
Schützt die übernommenen Framework-Dateien vor stillem Umschreiben
|
||||||
|
(Entscheidung 8 im Design 2026-08-11, Frage von sorb: „wird die
|
||||||
|
AGENTS.md ggf. durch Agenten umgeschrieben?"). Die Baseline liegt unter
|
||||||
|
docs/sources/upstream/neckbeard-v0.1.1/ (siehe HERKUNFT.md dort); ein
|
||||||
|
Framework-Upgrade aktualisiert Baseline und Arbeitskopie im selben,
|
||||||
|
bewussten Commit.
|
||||||
|
|
||||||
|
Prüfungen (Fehler, Exit 1):
|
||||||
|
* byte-identische Paare laut PAARE
|
||||||
|
* AGENTS.md beginnt byte-identisch mit der Baseline-AGENTS.md und
|
||||||
|
trägt direkt danach die Marke des Projektabschnitts
|
||||||
|
Fehlt eine Baseline-Datei, ist das ein Fehler, kein Skip
|
||||||
|
(Stillstandsprüfungs-Regel: eine Prüfung ohne Gegenseite ist ungeprüft).
|
||||||
|
|
||||||
|
Usage: python scripts/pruefe_upstream_drift.py [repo-root]
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
BASELINE = "docs/sources/upstream/neckbeard-v0.1.1"
|
||||||
|
MARKE = "<!-- projektabschnitt -->"
|
||||||
|
|
||||||
|
# (Arbeitskopie, Baseline-Datei) — byte-identisch
|
||||||
|
PAARE = [
|
||||||
|
("CLAUDE.md", "CLAUDE.md"),
|
||||||
|
("WORKFLOW.md", "WORKFLOW.md"),
|
||||||
|
("docs/adr/template.md", "templates/adr-template.md"),
|
||||||
|
("docs/design/template.md", "templates/design-template.md"),
|
||||||
|
("docs/aar/template.md", "templates/aar-template.md"),
|
||||||
|
("docs/issues/template.md", "templates/issue-template.md"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
|
||||||
|
base = root / BASELINE
|
||||||
|
fehler: list[str] = []
|
||||||
|
|
||||||
|
for arbeit, original in PAARE:
|
||||||
|
a, b = root / arbeit, base / original
|
||||||
|
if not b.is_file():
|
||||||
|
fehler.append(f"Baseline fehlt: {BASELINE}/{original}")
|
||||||
|
continue
|
||||||
|
if not a.is_file():
|
||||||
|
fehler.append(f"Arbeitskopie fehlt: {arbeit}")
|
||||||
|
continue
|
||||||
|
if a.read_bytes() != b.read_bytes():
|
||||||
|
fehler.append(f"DRIFT: {arbeit} weicht von {BASELINE}/{original} ab")
|
||||||
|
|
||||||
|
agents, agents_base = root / "AGENTS.md", base / "AGENTS.md"
|
||||||
|
if not agents_base.is_file():
|
||||||
|
fehler.append(f"Baseline fehlt: {BASELINE}/AGENTS.md")
|
||||||
|
elif not agents.is_file():
|
||||||
|
fehler.append("Arbeitskopie fehlt: AGENTS.md")
|
||||||
|
else:
|
||||||
|
upstream = agents_base.read_bytes()
|
||||||
|
arbeit = agents.read_bytes()
|
||||||
|
if not arbeit.startswith(upstream):
|
||||||
|
fehler.append("DRIFT: AGENTS.md — Upstream-Teil (§1–5) ist "
|
||||||
|
"nicht mehr byte-identisch mit der Baseline")
|
||||||
|
else:
|
||||||
|
rest = arbeit[len(upstream):].decode("utf-8", "replace")
|
||||||
|
if MARKE not in rest.splitlines()[0:3]:
|
||||||
|
fehler.append(f"AGENTS.md: Marke '{MARKE}' fehlt direkt "
|
||||||
|
"nach dem Upstream-Teil")
|
||||||
|
|
||||||
|
for f in fehler:
|
||||||
|
print(f"FEHLER {f}")
|
||||||
|
print(f"pruefe_upstream_drift: {len(fehler)} Fehler")
|
||||||
|
return 1 if fehler else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""validate.py — deterministic artifact validation against schema.yaml.
|
||||||
|
|
||||||
|
PROJEKTERWEITERUNG gegenüber neckbeard v0.1.1 (Original unter
|
||||||
|
docs/sources/upstream/neckbeard-v0.1.1/scripts/): drei Regeln —
|
||||||
|
waiting_requires_reason, slug_matches_filename (je Typ) und das globale
|
||||||
|
WIP-Limit (max. 2 Issues in-progress, altes ADR-0005/F-014).
|
||||||
|
|
||||||
|
Checks (errors, exit 1):
|
||||||
|
* frontmatter present, parseable, `type` known
|
||||||
|
* file location and filename match the type's rules
|
||||||
|
* required fields, enums, patterns, dates
|
||||||
|
* link fields: repo-root-relative targets exist (http/https/mailto skipped)
|
||||||
|
* inline markdown links in bodies resolve (relative to the file)
|
||||||
|
* per-type rules: superseded_requires_pointer, done_iff_in_done_dir
|
||||||
|
|
||||||
|
Warnings (exit 0):
|
||||||
|
* wiki pages (except index) with no inbound link anywhere
|
||||||
|
|
||||||
|
Usage: python scripts/validate.py [repo-root]
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import fnmatch
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
except ImportError: # pragma: no cover
|
||||||
|
sys.exit("validate.py needs PyYAML: pip install pyyaml")
|
||||||
|
|
||||||
|
DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
||||||
|
INLINE_LINK_RE = re.compile(r"\]\(([^)\s]+)\)")
|
||||||
|
HTML_SRC_RE = re.compile(r"(?:src|srcset)=\"([^\"]+)\"")
|
||||||
|
EXTERNAL_PREFIXES = ("http://", "https://", "mailto:")
|
||||||
|
|
||||||
|
errors: list[str] = []
|
||||||
|
warnings: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
|
def err(path: Path, msg: str) -> None:
|
||||||
|
errors.append(f"ERROR {path}: {msg}")
|
||||||
|
|
||||||
|
|
||||||
|
def warn(path: Path, msg: str) -> None:
|
||||||
|
warnings.append(f"WARN {path}: {msg}")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_frontmatter(text: str):
|
||||||
|
lines = text.splitlines()
|
||||||
|
if not lines or lines[0].strip() != "---":
|
||||||
|
return None, text
|
||||||
|
for j in range(1, len(lines)):
|
||||||
|
if lines[j].strip() == "---":
|
||||||
|
fm = "\n".join(lines[1:j])
|
||||||
|
body = "\n".join(lines[j + 1:])
|
||||||
|
return yaml.safe_load(fm) or {}, body
|
||||||
|
return None, text # unterminated
|
||||||
|
|
||||||
|
|
||||||
|
def is_date(value) -> bool:
|
||||||
|
if isinstance(value, datetime.date):
|
||||||
|
return True
|
||||||
|
return isinstance(value, str) and bool(DATE_RE.match(value))
|
||||||
|
|
||||||
|
|
||||||
|
def as_links(value):
|
||||||
|
"""Normalize a link field's value to a list of strings."""
|
||||||
|
if value is None:
|
||||||
|
return []
|
||||||
|
if isinstance(value, str):
|
||||||
|
return [value]
|
||||||
|
if isinstance(value, list):
|
||||||
|
return [v for v in value if isinstance(v, str)]
|
||||||
|
return None # wrong shape
|
||||||
|
|
||||||
|
|
||||||
|
def discover(root: Path, scope: dict) -> list[Path]:
|
||||||
|
files: set[Path] = set()
|
||||||
|
for pattern in scope.get("include", []):
|
||||||
|
files.update(root.glob(pattern))
|
||||||
|
result = []
|
||||||
|
for f in sorted(files):
|
||||||
|
rel = f.relative_to(root).as_posix()
|
||||||
|
if any(fnmatch.fnmatch(rel, pat) for pat in scope.get("exclude", [])):
|
||||||
|
continue
|
||||||
|
if f.is_file():
|
||||||
|
result.append(f)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def check_fields(path: Path, meta: dict, spec: dict, root: Path) -> None:
|
||||||
|
for field in spec.get("required", []):
|
||||||
|
if field not in meta or meta[field] is None:
|
||||||
|
err(path, f"missing required field '{field}'")
|
||||||
|
for field, rule in (spec.get("fields") or {}).items():
|
||||||
|
if field not in meta:
|
||||||
|
continue
|
||||||
|
value = meta[field]
|
||||||
|
if value is None:
|
||||||
|
if not rule.get("nullable"):
|
||||||
|
# required-check already covers required fields;
|
||||||
|
# a present-but-null optional field is fine unless typed link
|
||||||
|
pass
|
||||||
|
continue
|
||||||
|
if "enum" in rule and value not in rule["enum"]:
|
||||||
|
err(path, f"'{field}: {value}' not in enum {rule['enum']}")
|
||||||
|
if "pattern" in rule and not re.match(rule["pattern"], str(value)):
|
||||||
|
err(path, f"'{field}: {value}' does not match {rule['pattern']}")
|
||||||
|
kind = rule.get("kind")
|
||||||
|
if kind == "date" and not is_date(value):
|
||||||
|
err(path, f"'{field}: {value}' is not a YYYY-MM-DD date")
|
||||||
|
if kind == "bool" and not isinstance(value, bool):
|
||||||
|
err(path, f"'{field}: {value}' is not a boolean")
|
||||||
|
if kind == "str" and not isinstance(value, str):
|
||||||
|
err(path, f"'{field}' must be a string")
|
||||||
|
|
||||||
|
|
||||||
|
def check_links(path: Path, meta: dict, link_fields: list, root: Path,
|
||||||
|
inbound: set) -> None:
|
||||||
|
for field in link_fields:
|
||||||
|
if field not in meta:
|
||||||
|
continue
|
||||||
|
links = as_links(meta[field])
|
||||||
|
if links is None:
|
||||||
|
err(path, f"'{field}' must be a string or list of strings")
|
||||||
|
continue
|
||||||
|
for link in links:
|
||||||
|
if link.startswith(EXTERNAL_PREFIXES):
|
||||||
|
continue
|
||||||
|
target = (root / link)
|
||||||
|
if not target.is_file():
|
||||||
|
err(path, f"'{field}' link target missing: {link}")
|
||||||
|
else:
|
||||||
|
inbound.add(target.resolve())
|
||||||
|
|
||||||
|
|
||||||
|
def check_body_links(path: Path, body: str, root: Path, inbound: set) -> None:
|
||||||
|
# strip fenced code blocks and inline code spans so mermaid, code
|
||||||
|
# samples, and literal link examples in backticks aren't scanned
|
||||||
|
body = re.sub(r"```.*?```", "", body, flags=re.S)
|
||||||
|
body = re.sub(r"`[^`\n]*`", "", body)
|
||||||
|
candidates = [m.group(1) for m in INLINE_LINK_RE.finditer(body)]
|
||||||
|
for raw in (m.group(1) for m in HTML_SRC_RE.finditer(body)):
|
||||||
|
# srcset may list "path 2x, path2 1x" pairs — take each path token
|
||||||
|
for part in raw.split(","):
|
||||||
|
candidates.append(part.strip().split()[0])
|
||||||
|
for link in candidates:
|
||||||
|
if link.startswith(EXTERNAL_PREFIXES) or link.startswith("#"):
|
||||||
|
continue
|
||||||
|
link = link.split("#", 1)[0]
|
||||||
|
if not link:
|
||||||
|
continue
|
||||||
|
target = (path.parent / link).resolve()
|
||||||
|
if not target.is_file():
|
||||||
|
err(path, f"inline link target missing: {link}")
|
||||||
|
else:
|
||||||
|
inbound.add(target)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_rules(path: Path, rel: str, meta: dict, spec: dict) -> None:
|
||||||
|
for rule in spec.get("rules", []):
|
||||||
|
if rule == "superseded_requires_pointer":
|
||||||
|
if meta.get("status") == "superseded" and not meta.get("superseded_by"):
|
||||||
|
err(path, "status 'superseded' requires 'superseded_by'")
|
||||||
|
elif rule == "waiting_requires_reason":
|
||||||
|
if meta.get("status") == "waiting" and not meta.get("wartegrund"):
|
||||||
|
err(path, "status 'waiting' requires 'wartegrund'")
|
||||||
|
elif rule == "slug_matches_filename":
|
||||||
|
if meta.get("slug") is not None and str(meta["slug"]) != path.stem:
|
||||||
|
err(path, f"slug '{meta['slug']}' does not match filename")
|
||||||
|
elif rule == "done_iff_in_done_dir":
|
||||||
|
in_done = "/done/" in f"/{rel}"
|
||||||
|
if (meta.get("status") == "done") != in_done:
|
||||||
|
err(path, "status 'done' <-> file in docs/design/done/ mismatch")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
|
||||||
|
schema = yaml.safe_load((root / "schema.yaml").read_text(encoding="utf-8"))
|
||||||
|
link_fields = schema.get("link_fields", [])
|
||||||
|
types = schema.get("types", {})
|
||||||
|
inbound: set = set()
|
||||||
|
wiki_pages: list[tuple[Path, dict]] = []
|
||||||
|
|
||||||
|
# Root documents: inline links must resolve; no frontmatter required.
|
||||||
|
for rel in schema.get("scope", {}).get("link_only", []):
|
||||||
|
path = root / rel
|
||||||
|
if not path.is_file():
|
||||||
|
continue # e.g. STATUS.md before first generation
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
_meta, body = parse_frontmatter(text)
|
||||||
|
check_body_links(path, body if _meta is not None else text,
|
||||||
|
root, inbound)
|
||||||
|
seen_ids: dict[tuple[str, str], Path] = {}
|
||||||
|
in_progress: list[Path] = []
|
||||||
|
artifacts = discover(root, schema.get("scope", {}))
|
||||||
|
|
||||||
|
for path in artifacts:
|
||||||
|
rel = path.relative_to(root).as_posix()
|
||||||
|
meta, body = parse_frontmatter(path.read_text(encoding="utf-8"))
|
||||||
|
if meta is None:
|
||||||
|
err(path, "missing or unterminated YAML frontmatter")
|
||||||
|
continue
|
||||||
|
if not isinstance(meta, dict) or "type" not in meta:
|
||||||
|
err(path, "frontmatter has no 'type'")
|
||||||
|
continue
|
||||||
|
t = meta["type"]
|
||||||
|
if t not in types:
|
||||||
|
err(path, f"unknown type '{t}'")
|
||||||
|
continue
|
||||||
|
spec = types[t]
|
||||||
|
expected_dir = spec.get("dir", ".")
|
||||||
|
actual_dir = str(Path(rel).parent.as_posix())
|
||||||
|
if expected_dir == ".":
|
||||||
|
if actual_dir != ".":
|
||||||
|
err(path, f"type '{t}' must live in repo root")
|
||||||
|
elif not (actual_dir == expected_dir
|
||||||
|
or actual_dir.startswith(expected_dir + "/")):
|
||||||
|
err(path, f"type '{t}' must live under {expected_dir}/")
|
||||||
|
fn_pattern = spec.get("filename")
|
||||||
|
if fn_pattern and not re.match(fn_pattern, path.name):
|
||||||
|
err(path, f"filename does not match {fn_pattern}")
|
||||||
|
check_fields(path, meta, spec, root)
|
||||||
|
if "id" in (spec.get("fields") or {}) and meta.get("id") is not None:
|
||||||
|
artifact_id = str(meta["id"])
|
||||||
|
if not path.name.startswith(f"{artifact_id}-"):
|
||||||
|
err(path, f"id '{artifact_id}' does not match filename prefix")
|
||||||
|
key = (t, artifact_id)
|
||||||
|
if key in seen_ids:
|
||||||
|
err(path, f"duplicate {t} id '{artifact_id}' "
|
||||||
|
f"(also in {seen_ids[key].name})")
|
||||||
|
else:
|
||||||
|
seen_ids[key] = path
|
||||||
|
check_links(path, meta, link_fields, root, inbound)
|
||||||
|
check_body_links(path, body, root, inbound)
|
||||||
|
apply_rules(path, rel, meta, spec)
|
||||||
|
if t == "wiki-page" and meta.get("area") != "index":
|
||||||
|
wiki_pages.append((path, meta))
|
||||||
|
if t == "issue" and meta.get("status") == "in-progress":
|
||||||
|
in_progress.append(path)
|
||||||
|
|
||||||
|
# link-only files: inline links are checked, frontmatter not required
|
||||||
|
already = {p.resolve() for p in artifacts}
|
||||||
|
for pattern in schema.get("scope", {}).get("link_only", []):
|
||||||
|
for path in sorted(root.glob(pattern)):
|
||||||
|
if not path.is_file() or path.resolve() in already:
|
||||||
|
continue
|
||||||
|
meta, body = parse_frontmatter(path.read_text(encoding="utf-8"))
|
||||||
|
if meta is None:
|
||||||
|
body = path.read_text(encoding="utf-8")
|
||||||
|
check_body_links(path, body, root, inbound)
|
||||||
|
|
||||||
|
if len(in_progress) > 2:
|
||||||
|
names = ", ".join(p.name for p in in_progress)
|
||||||
|
err(root / "docs/issues", f"WIP limit exceeded: "
|
||||||
|
f"{len(in_progress)} issues in-progress (max 2): {names}")
|
||||||
|
|
||||||
|
for path, _meta in wiki_pages:
|
||||||
|
if path.resolve() not in inbound:
|
||||||
|
warn(path, "orphan wiki page — nothing links to it")
|
||||||
|
|
||||||
|
for line in errors + warnings:
|
||||||
|
print(line)
|
||||||
|
print(f"validate: {len(errors)} error(s), {len(warnings)} warning(s)")
|
||||||
|
return 1 if errors else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in New Issue
Block a user