diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 06c7d4e..acd46a6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -33,3 +33,19 @@ stillstandspruefung: # Befunde sind kein Betriebsausfall, aber sie sollen sichtbar bleiben. Die rote # Pipeline ist bei uns die Alarmanlage (gitops/CLAUDE.md, TURN-Rotation). 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 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..71385ef --- /dev/null +++ b/AGENTS.md @@ -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. + + + +## 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). diff --git a/CLAUDE.md b/CLAUDE.md index cfc7b9a..9aef12f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,233 +1 @@ -# CLAUDE.md — übergreifende Arbeitskonventionen (kanonisch) - -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.* +Read AGENTS.md — the canonical instruction file for this repository. All rules live there. diff --git a/README.md b/README.md index 760c5c9..653a038 100644 --- a/README.md +++ b/README.md @@ -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) | | `roadmap.md` | Linien, Meilenstein-Kandidaten, Kadenz — GitLab-Milestones halten den Stand | | `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 | Gelesen wird das alles auch gebündelt unter **[axionwiki.lab](https://axionwiki.lab)** — diff --git a/STATUS.md b/STATUS.md new file mode 100644 index 0000000..0880014 --- /dev/null +++ b/STATUS.md @@ -0,0 +1,24 @@ +# STATUS + + + +## 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_ diff --git a/WORKFLOW.md b/WORKFLOW.md new file mode 100644 index 0000000..3b2fe7d --- /dev/null +++ b/WORKFLOW.md @@ -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. diff --git a/docs/aar/template.md b/docs/aar/template.md new file mode 100644 index 0000000..b23ff59 --- /dev/null +++ b/docs/aar/template.md @@ -0,0 +1,34 @@ +--- +type: aar +status: open # open | harvested +date: YYYY-MM-DD +related: [] # design docs, issues, ADRs involved +--- + + + +# AAR: Title + +## What was planned / expected + +## What happened + + + +## Why the difference + + + +## Learnings + + + +## Actions + + diff --git a/docs/adr/template.md b/docs/adr/template.md new file mode 100644 index 0000000..9a9d73e --- /dev/null +++ b/docs/adr/template.md @@ -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 +--- + + + +# ADR-0000: Title + +## Context + + + +## Options Considered + + + +## Decision + + + +## Consequences + + + + diff --git a/docs/design/2026-08-11-neckbeard-migration.md b/docs/design/2026-08-11-neckbeard-migration.md index 30a879a..748662b 100644 --- a/docs/design/2026-08-11-neckbeard-migration.md +++ b/docs/design/2026-08-11-neckbeard-migration.md @@ -1,6 +1,6 @@ --- type: design -status: gate-3 +status: gate-4 date: 2026-08-11 size: L 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 benannt, aus dem management-Scope abgeleitet; `area:authentik` liegt 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. diff --git a/docs/design/template.md b/docs/design/template.md new file mode 100644 index 0000000..32e1508 --- /dev/null +++ b/docs/design/template.md @@ -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 +--- + + + +# Design: Title + +## Gate 1 — Product + +**Problem.** + +**Acceptance criterion.** + +**Non-goals.** + +**Announcement.** + +**Mockups.** + +> **STOP — awaiting Gate 1 approval.** + +## Gate 2 — Architecture + +**Inputs read.** + +**System fit.** + +**Constraints.** + +**Options & trade-offs.** + +**New ADRs.** + +> **STOP — awaiting Gate 2 approval.** + +## Gate 3 — Program Design + +**Files.** + +**Signatures.** + +**Call stack.** + +**Test assertions.** + +**Boundaries — DO NOT CHANGE.** + +**Shakiest calls.** + +> **STOP — awaiting Gate 3 approval.** + +## Gate 4 — Vertical Slices + + + +### Slice 1 — Tracer bullet +- [ ] Task: … — files: … — action: … — verify: … — done: … + +**Evidence:** +**Status:** + +> **STOP — slice review.** + +### Slice 2 — … + +### Handoff + + + +## Gate 5 — Closeout (AAR) + +**Planned vs. actual.** + +**Why the difference.** + +**Learnings.** + +**Harvested.** + +**Open uncertainties.** + + diff --git a/docs/issues/template.md b/docs/issues/template.md new file mode 100644 index 0000000..8adbf4b --- /dev/null +++ b/docs/issues/template.md @@ -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 +--- + + + +# Issue-0000: Title + +## Problem / Motivation + + + +## Acceptance + + + +## Notes + + diff --git a/docs/sources/regelwerk/karpathy-guidelines.md b/docs/sources/regelwerk/karpathy-guidelines.md new file mode 100644 index 0000000..3eae5de --- /dev/null +++ b/docs/sources/regelwerk/karpathy-guidelines.md @@ -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. + diff --git a/docs/sources/upstream/neckbeard-v0.1.1/AGENTS.md b/docs/sources/upstream/neckbeard-v0.1.1/AGENTS.md new file mode 100644 index 0000000..f651a80 --- /dev/null +++ b/docs/sources/upstream/neckbeard-v0.1.1/AGENTS.md @@ -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. diff --git a/docs/sources/upstream/neckbeard-v0.1.1/CLAUDE.md b/docs/sources/upstream/neckbeard-v0.1.1/CLAUDE.md new file mode 100644 index 0000000..9aef12f --- /dev/null +++ b/docs/sources/upstream/neckbeard-v0.1.1/CLAUDE.md @@ -0,0 +1 @@ +Read AGENTS.md — the canonical instruction file for this repository. All rules live there. diff --git a/docs/sources/upstream/neckbeard-v0.1.1/HERKUNFT.md b/docs/sources/upstream/neckbeard-v0.1.1/HERKUNFT.md new file mode 100644 index 0000000..90f24fc --- /dev/null +++ b/docs/sources/upstream/neckbeard-v0.1.1/HERKUNFT.md @@ -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 `` | +| `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 | diff --git a/docs/sources/upstream/neckbeard-v0.1.1/WORKFLOW.md b/docs/sources/upstream/neckbeard-v0.1.1/WORKFLOW.md new file mode 100644 index 0000000..3b2fe7d --- /dev/null +++ b/docs/sources/upstream/neckbeard-v0.1.1/WORKFLOW.md @@ -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. diff --git a/docs/sources/upstream/neckbeard-v0.1.1/schema.yaml b/docs/sources/upstream/neckbeard-v0.1.1/schema.yaml new file mode 100644 index 0000000..ac54a8e --- /dev/null +++ b/docs/sources/upstream/neckbeard-v0.1.1/schema.yaml @@ -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 diff --git a/docs/sources/upstream/neckbeard-v0.1.1/scripts/gen_status.py b/docs/sources/upstream/neckbeard-v0.1.1/scripts/gen_status.py new file mode 100644 index 0000000..e829371 --- /dev/null +++ b/docs/sources/upstream/neckbeard-v0.1.1/scripts/gen_status.py @@ -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("") + 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()) diff --git a/docs/sources/upstream/neckbeard-v0.1.1/scripts/validate.py b/docs/sources/upstream/neckbeard-v0.1.1/scripts/validate.py new file mode 100644 index 0000000..f935d44 --- /dev/null +++ b/docs/sources/upstream/neckbeard-v0.1.1/scripts/validate.py @@ -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()) diff --git a/docs/sources/upstream/neckbeard-v0.1.1/templates/aar-template.md b/docs/sources/upstream/neckbeard-v0.1.1/templates/aar-template.md new file mode 100644 index 0000000..b23ff59 --- /dev/null +++ b/docs/sources/upstream/neckbeard-v0.1.1/templates/aar-template.md @@ -0,0 +1,34 @@ +--- +type: aar +status: open # open | harvested +date: YYYY-MM-DD +related: [] # design docs, issues, ADRs involved +--- + + + +# AAR: Title + +## What was planned / expected + +## What happened + + + +## Why the difference + + + +## Learnings + + + +## Actions + + diff --git a/docs/sources/upstream/neckbeard-v0.1.1/templates/adr-template.md b/docs/sources/upstream/neckbeard-v0.1.1/templates/adr-template.md new file mode 100644 index 0000000..9a9d73e --- /dev/null +++ b/docs/sources/upstream/neckbeard-v0.1.1/templates/adr-template.md @@ -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 +--- + + + +# ADR-0000: Title + +## Context + + + +## Options Considered + + + +## Decision + + + +## Consequences + + + + diff --git a/docs/sources/upstream/neckbeard-v0.1.1/templates/design-template.md b/docs/sources/upstream/neckbeard-v0.1.1/templates/design-template.md new file mode 100644 index 0000000..32e1508 --- /dev/null +++ b/docs/sources/upstream/neckbeard-v0.1.1/templates/design-template.md @@ -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 +--- + + + +# Design: Title + +## Gate 1 — Product + +**Problem.** + +**Acceptance criterion.** + +**Non-goals.** + +**Announcement.** + +**Mockups.** + +> **STOP — awaiting Gate 1 approval.** + +## Gate 2 — Architecture + +**Inputs read.** + +**System fit.** + +**Constraints.** + +**Options & trade-offs.** + +**New ADRs.** + +> **STOP — awaiting Gate 2 approval.** + +## Gate 3 — Program Design + +**Files.** + +**Signatures.** + +**Call stack.** + +**Test assertions.** + +**Boundaries — DO NOT CHANGE.** + +**Shakiest calls.** + +> **STOP — awaiting Gate 3 approval.** + +## Gate 4 — Vertical Slices + + + +### Slice 1 — Tracer bullet +- [ ] Task: … — files: … — action: … — verify: … — done: … + +**Evidence:** +**Status:** + +> **STOP — slice review.** + +### Slice 2 — … + +### Handoff + + + +## Gate 5 — Closeout (AAR) + +**Planned vs. actual.** + +**Why the difference.** + +**Learnings.** + +**Harvested.** + +**Open uncertainties.** + + diff --git a/docs/sources/upstream/neckbeard-v0.1.1/templates/issue-template.md b/docs/sources/upstream/neckbeard-v0.1.1/templates/issue-template.md new file mode 100644 index 0000000..8adbf4b --- /dev/null +++ b/docs/sources/upstream/neckbeard-v0.1.1/templates/issue-template.md @@ -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 +--- + + + +# Issue-0000: Title + +## Problem / Motivation + + + +## Acceptance + + + +## Notes + + diff --git a/schema.yaml b/schema.yaml new file mode 100644 index 0000000..1baa49c --- /dev/null +++ b/schema.yaml @@ -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 diff --git a/scripts/gen_status.py b/scripts/gen_status.py new file mode 100644 index 0000000..65df437 --- /dev/null +++ b/scripts/gen_status.py @@ -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("") + 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()) diff --git a/scripts/pruefe_upstream_drift.py b/scripts/pruefe_upstream_drift.py new file mode 100644 index 0000000..6152cef --- /dev/null +++ b/scripts/pruefe_upstream_drift.py @@ -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 = "" + +# (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()) diff --git a/scripts/validate.py b/scripts/validate.py new file mode 100644 index 0000000..d411618 --- /dev/null +++ b/scripts/validate.py @@ -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())