feat: slice 1 - the neckbeard framework chain runs end to end

Tracer bullet of the migration design (Gate 4, slice 1): pinned v0.1.1
baseline under docs/sources/upstream/ with provenance note, the
Karpathy block moved verbatim to docs/sources/regelwerk/ (standing
rule mapped onto the sources read-only mechanism), AGENTS.md assembled
from the byte-true upstream sections plus the project section 6
(group rules condensed from the old CLAUDE.md), CLAUDE.md reduced to
the upstream pointer, WORKFLOW.md and all four templates copied,
schema.yaml extended (issue milestone/priority/status columns,
component type, wiki area vision - all flagged in the header),
validate.py and gen_status.py forked with marked extensions,
pruefe_upstream_drift.py added, STATUS.md generated, CI gains the
offline validate job, README directory link defused.

Verified: validate 0 errors 0 warnings (the three pre-existing
directory-link errors are gone), gen_status --check current,
drift check 0 findings, baseline byte-identical to the reference
checkout (10/10 files), four negative tests fire (WIP limit 3x
in-progress, waiting without wartegrund, component slug mismatch,
single-byte drift in WORKFLOW.md). gen_status needs Python >= 3.10
locally (write_text newline) - noted for the design AAR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Thore Cimbal
2026-08-11 12:00:00 +00:00
co-authored by Claude Fable 5
parent 5e46372ea8
commit e36ed337a7
27 changed files with 2311 additions and 235 deletions
+34
View File
@@ -0,0 +1,34 @@
---
type: aar
status: open # open | harvested
date: YYYY-MM-DD
related: [] # design docs, issues, ADRs involved
---
<!-- Copy to docs/aar/YYYY-MM-DD-slug.md. Delete comments when filling in.
Standalone AARs are for incidents and major deviations only —
normal undertakings get their AAR as Gate 5 inside the design doc. -->
# AAR: Title
## What was planned / expected
## What happened
<!-- Facts and timeline, not blame. -->
## Why the difference
<!-- Root cause. For failures, name the routing class:
intent issue / spec issue / code issue. -->
## Learnings
<!-- What future-you should know. Blunt beats polite. -->
## Actions
<!-- Concrete: wiki pages updated (FAQ, Stolpersteine) with links,
framework issues opened, tests added. When all actions are done,
set status: harvested. The refinement session walks all AARs
still marked open. -->
+37
View File
@@ -0,0 +1,37 @@
---
type: adr
id: "0000"
status: proposed # proposed | accepted | superseded
date: YYYY-MM-DD
supersedes: null # path to older ADR, e.g. docs/adr/0002-old.md
superseded_by: null # filled in on the OLD adr when a new one replaces it
related: [] # optional: paths to design docs / issues
---
<!-- Copy to docs/adr/NNNN-slug.md. Delete all comments when filling in. -->
# ADR-0000: Title
## Context
<!-- The situation and the forces at play. Constraints upfront:
deadlines, scale, team knowledge, existing decisions. -->
## Options Considered
<!-- Name each option, even the one you lean toward. Pros/cons per
option; a small dimension table (complexity, cost, maintenance,
familiarity) where it helps. Keep proportional to the decision. -->
## Decision
<!-- The choice, in one or two sentences. -->
## Consequences
<!-- What becomes easier, what becomes harder, what we will need to
revisit. Honest cons included. -->
<!-- Rules: an accepted ADR is never edited — write a new ADR that
supersedes it and set superseded_by here. Lasting directional
decisions only; feature-local choices belong in the design doc. -->
+38 -1
View File
@@ -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`
(§15 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.
+110
View File
@@ -0,0 +1,110 @@
---
type: design
status: gate-1 # gate-1 | gate-2 | gate-3 | gate-4 | gate-5 | done
date: YYYY-MM-DD
size: L # this template is for size L
related: [] # issues, ADRs spawned or read
---
<!-- Copy to docs/design/YYYY-MM-DD-slug.md. Delete comments when filling in.
Fill ONE gate at a time; each gate ends with STOP — do not pre-fill
later gates. Advance `status` only after human approval. -->
# Design: Title
## Gate 1 — Product
**Problem.** <!-- What user problem, for whom. -->
**Acceptance criterion.** <!-- Verifiable. A real number where one
exists; otherwise a concretely checkable outcome. "Works" is not one. -->
**Non-goals.** <!-- What this deliberately does NOT do. The cheapest
scope-creep brake there is. -->
**Announcement.** <!-- 35 sentences: what it is, who it's for, why
it's good. Can't write it? The product isn't understood yet. -->
**Mockups.** <!-- Only if UI is involved: plain-HTML mockups, linked. -->
> **STOP — awaiting Gate 1 approval.**
## Gate 2 — Architecture
**Inputs read.** <!-- Which ADRs and AARs were read; one line each on
why they matter here. -->
**System fit.** <!-- Endpoints, tables/schemas, query outlines,
end-to-end flow as Mermaid. Against the actual codebase. -->
**Constraints.** <!-- Non-functional, proportional to the project:
performance, security, operations, compatibility. "None relevant"
is a valid answer — but say it. -->
**Options & trade-offs.** <!-- Where more than one viable way exists:
name the options, pro/contra each, state the chosen one and WHY.
This is the feature-local decision record. Only lasting, binding
decisions graduate to an ADR below. -->
**New ADRs.** <!-- Lasting decisions discovered here → one ADR each,
linked. None is a valid answer. -->
> **STOP — awaiting Gate 2 approval.**
## Gate 3 — Program Design
**Files.** <!-- Exact paths, new and touched. -->
**Signatures.** <!-- Types and method signatures, no bodies. -->
**Call stack.** <!-- For the main flow(s). -->
**Test assertions.** <!-- What the tests will assert. -->
**Boundaries — DO NOT CHANGE.** <!-- Explicit list. -->
**Shakiest calls.** <!-- The decisions you are least confident about. -->
> **STOP — awaiting Gate 3 approval.**
## Gate 4 — Vertical Slices
<!-- Slice 1 is the tracer bullet: thin end-to-end, runs with mocks.
Then real logic, one testable slice at a time. Per task:
files / action / verify / done. After each slice: evidence,
status, STOP. -->
### Slice 1 — Tracer bullet
- [ ] Task: … — files: … — action: … — verify: … — done: …
**Evidence:** <!-- command output, test run, screenshot ref -->
**Status:** <!-- DONE | DONE_WITH_CONCERNS | NEEDS_CONTEXT | BLOCKED -->
> **STOP — slice review.**
### Slice 2 — …
### Handoff
<!-- The single place session state lives. Overwrite on every handoff;
git keeps the history.
Done slices: …
Open decisions: …
Next step: … -->
## Gate 5 — Closeout (AAR)
**Planned vs. actual.** <!-- What was planned, what happened. -->
**Why the difference.** <!-- Root causes, honestly. -->
**Learnings.** <!-- What future-you should know. -->
**Harvested.** <!-- Wiki pages updated (FAQ, Stolpersteine, …) with
links; framework issues opened, if a rule was missing or wrong. -->
**Open uncertainties.** <!-- Session-handoff answers to: "Which choices
did I make that I'm least confident about?" -->
<!-- After approval: set status: done, move this file to
docs/design/done/, run gen_status.py. -->
+29
View File
@@ -0,0 +1,29 @@
---
type: issue
id: "0000"
status: open # open | in-progress | done | rejected
created: YYYY-MM-DD
related: [] # design docs, ADRs, other issues
---
<!-- Copy to docs/issues/NNNN-slug.md. Delete comments when filling in. -->
# Issue-0000: Title
## Problem / Motivation
<!-- What's wrong or missing, and why it matters. One paragraph. -->
## Acceptance
<!-- When is this issue done? Verifiable, like every other criterion
in this framework. -->
## Notes
<!-- Optional: context, links, findings gathered along the way.
Rules: status is the single source of truth and lives here in the
frontmatter — STATUS.md is generated, never edited. An issue that
starts real work links its design doc in `related`. Closed means
status: done (or rejected, with a one-line reason in Notes) —
the file stays; git is the history. -->
@@ -0,0 +1,68 @@
---
name: karpathy-guidelines
description: Behavioral guidelines to reduce common LLM coding mistakes. Use when writing, reviewing, or refactoring code to avoid overcomplication, make surgical changes, surface assumptions, and define verifiable success criteria.
license: MIT
---
# Karpathy Guidelines
Behavioral guidelines to reduce common LLM coding mistakes, derived from [Andrej Karpathy's observations](https://x.com/karpathy/status/2015883857489522876) on LLM coding pitfalls.
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
## 1. Think Before Coding
**Don't assume. Don't hide confusion. Surface tradeoffs.**
Before implementing:
- State your assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them - don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.
## 2. Simplicity First
**Minimum code that solves the problem. Nothing speculative.**
- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
## 3. Surgical Changes
**Touch only what you must. Clean up only your own mess.**
When editing existing code:
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style, even if you'd do it differently.
- If you notice unrelated dead code, mention it - don't delete it.
When your changes create orphans:
- Remove imports/variables/functions that YOUR changes made unused.
- Don't remove pre-existing dead code unless asked.
The test: Every changed line should trace directly to the user's request.
## 4. Goal-Driven Execution
**Define success criteria. Loop until verified.**
Transform tasks into verifiable goals:
- "Add validation" → "Write tests for invalid inputs, then make them pass"
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
- "Refactor X" → "Ensure tests pass before and after"
For multi-step tasks, state a brief plan:
```
1. [Step] → verify: [check]
2. [Step] → verify: [check]
3. [Step] → verify: [check]
```
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
@@ -0,0 +1,103 @@
# AGENTS.md — Canonical Agent Instructions
Canonical instruction set for any coding agent working in this repository
(Claude Code, GPT-OSS harnesses, others). `CLAUDE.md` points here.
This file is loaded into every session — keep it short. Process details
live in `WORKFLOW.md`; read that when a task begins, not preemptively.
Tradeoff: these rules bias toward caution over speed. For trivial tasks,
use judgment — but say so.
## 1. Operating Rules
### Think before coding
- State your assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them — don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.
### Simplicity first
- Minimum code that solves the problem. Nothing speculative.
- No features beyond what was asked. No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.
- Test: "Would a senior engineer call this overcomplicated?" If yes, simplify.
- Before writing new code, stop at the first rung that holds:
needed at all? → codebase already has it? → stdlib? → platform-native?
→ installed dependency? → one line? → only then: the minimum that works.
(Ladder after ponytail, MIT.)
- Never cut, at any rung: trust-boundary validation, data-loss handling,
security, accessibility.
- Lazy about the solution, never about reading the code first.
### Surgical changes
- Touch only what you must. Match existing style, even if you'd differ.
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- If you notice unrelated dead code, mention it — don't delete it.
- Remove imports/variables/functions that YOUR changes made unused;
leave pre-existing dead code alone unless asked.
- Every changed line must trace directly to the request.
### Goal-driven execution
- Transform tasks into verifiable goals:
"fix the bug" → "write a test that reproduces it, then make it pass".
- For multi-step work, state a brief plan: step → verify, step → verify.
- A task is well-defined only if it names all four:
**files, action, verify, done.** Missing one? The task is too vague — say so.
### Verification before completion
- Never claim something works without evidence: a test run, command
output, a rendered result. "Should work" is not a status.
- Report every task/slice with exactly one status:
`DONE` | `DONE_WITH_CONCERNS` | `NEEDS_CONTEXT` | `BLOCKED`.
- Uncertainty is reported, never swallowed. Flag your shakiest calls.
## 2. Project Initialization (Gate 0)
At session start, read `PROJECT.md`. If it does not exist, initialization
is your first task: before anything else, ask the Gate 0 questions defined
in `WORKFLOW.md` — response language, size-S gate exception (yes/no),
one-line project purpose, audience — write the answers to `PROJECT.md`,
and have `validate.py` accept it. Never guess these answers; ask.
## 3. Workflow
For anything beyond a trivial change, read `WORKFLOW.md` and follow its
gates. At task start, propose a size class (S/M/L); the human confirms
(possibly batched later). **Never advance past a gate without explicit
human approval** — sole exception: size-S tasks, and only if `PROJECT.md`
explicitly grants that exception.
## 4. Repository Map
| Path | Purpose |
|---|---|
| `WORKFLOW.md` | Gate 0 (init) + Gates 15, size classes, debugging path, session handoff, refinement ritual |
| `PROJECT.md` | Per-project answers from Gate 0: language, size-S exception, purpose, audience |
| `STATUS.md` | Generated overview: open issues, active designs, recent ADRs — do not edit by hand |
| `schema.yaml` | Frontmatter schema — single source of truth for artifact structure |
| `docs/adr/` | Architecture Decision Records — binding; never edited, only superseded |
| `docs/design/` | One design doc per undertaking; completed ones move to `done/` |
| `docs/aar/` | Standalone After Action Reviews (incidents, major deviations only) |
| `docs/issues/` | In-repo issues, one file each; status lives in frontmatter |
| `docs/wiki/` | Wiki areas as folders, created on demand — rules in `docs/wiki/index.md` |
| `docs/sources/` | Immutable original sources; wiki pages cite them — read-only for agents |
| `scripts/` | Deterministic tooling: `validate.py`, `gen_status.py` |
Before proposing options (Gate 2), read the relevant ADRs and AARs first —
past decisions and learnings are input, not trivia.
## 5. Artifact Rules
- All artifacts are standard Markdown with YAML frontmatter conforming to
`schema.yaml`. Standard links only (`[text](path.md)`), no wikilinks.
Diagrams as Mermaid. This keeps every artifact portable across LLMs,
GitLab, and Obsidian.
- Never invent frontmatter fields or status values. `validate.py` is
authoritative; if it rejects your artifact, fix the artifact, not the
validator.
- Deterministic jobs (status generation, validation, link checks) are done
by scripts, not by you. If a deterministic job lacks a script, propose
one instead of doing it by inference.
@@ -0,0 +1 @@
Read AGENTS.md — the canonical instruction file for this repository. All rules live there.
@@ -0,0 +1,25 @@
# Herkunft dieser Baseline
Unveränderte Originale aus **neckbeard v0.1.1**, Commit
`823a08cac6b03a47d7e2f661200a49ac6e09d38d` (`main`, sauber), Origin
`https://git.lab/oss-projekte/ai/neckbeard.git` — der Stand, gegen den
der Feldtest (Branch `Neckbeard-v0.1.1-analyse-1`) gemessen hat und aus
dem die Migration übernommen wurde (Design:
`docs/design/2026-08-11-neckbeard-migration.md`).
Zweck: Byte-Baseline für `scripts/pruefe_upstream_drift.py`. Ein
Framework-Upgrade ersetzt diese Dateien bewusst und in einem eigenen
Commit — nie beiläufig.
| Datei hier | Arbeitskopie | Prüfung |
|---|---|---|
| `AGENTS.md` | `/AGENTS.md` | Präfix bis zur Marke `<!-- projektabschnitt -->` |
| `CLAUDE.md` | `/CLAUDE.md` | byte-identisch |
| `WORKFLOW.md` | `/WORKFLOW.md` | byte-identisch |
| `templates/adr-template.md` | `docs/adr/template.md` | byte-identisch |
| `templates/design-template.md` | `docs/design/template.md` | byte-identisch |
| `templates/aar-template.md` | `docs/aar/template.md` | byte-identisch |
| `templates/issue-template.md` | `docs/issues/template.md` | byte-identisch |
| `schema.yaml` | `/schema.yaml` | **erklärt projekterweitert** — nur Diff-Referenz |
| `scripts/validate.py` | `scripts/validate.py` | **erklärt projekterweitert** — nur Diff-Referenz |
| `scripts/gen_status.py` | `scripts/gen_status.py` | **erklärt projekterweitert** — nur Diff-Referenz |
@@ -0,0 +1,140 @@
# WORKFLOW.md — Gates, Sizing, and Rituals
Read this when a task begins, not preemptively. `AGENTS.md` holds the
always-on rules; this file holds the process.
## Size Classes
Propose one at task start; the human confirms — individually, or batched
at the next refinement session.
| Class | Scope | Process |
|---|---|---|
| S | One file / one small change, no design decisions | Direct. AGENTS.md rules only. The one-line go-ahead **before starting is the stop** — waived only if `PROJECT.md` grants the size-S exception. |
| M | Few files, minor decisions, fits one session | Slice plan in chat, no file. **STOP: plan approval before any code.** Then implement; each slice reports evidence and status inline. Gate 5 is a short AAR note in chat, filed to the wiki only if it produced a real learning. |
| L | New feature, multiple files or sessions, real decisions | Full design doc in `docs/design/` following Gates 15 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 15 (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 (35 sentences): what it is, who it's for, why
it's good. If you can't write it, the product isn't understood yet.
- UI involved? Plain-HTML mockups of the affected screens.
**STOP.**
### Gate 2 — Architecture
- Read first: the actual codebase, relevant ADRs, relevant AARs.
Past decisions and learnings are input, not trivia.
- How it fits the real system: endpoints, tables/schemas, query
outlines, the end-to-end flow (Mermaid).
- Constraints: non-functional requirements, proportional to the project.
- Options & trade-offs where more than one viable way exists: pro/contra
each, chosen option, and why. Feature-local decisions stay here.
- Lasting directional decisions discovered here become ADRs (one each),
linked from the design doc.
**STOP.**
### Gate 3 — Program Design
- File locations: exact paths, new and touched.
- Types and method signatures — no bodies.
- Call stack for the main flow(s).
- What the tests will assert.
- Boundaries: an explicit DO NOT CHANGE list.
- Shakiest calls: name the decisions you are least confident about.
**STOP.**
### Gate 4 — Vertical Slices
- Slice 1 is the tracer bullet: a thin end-to-end path that runs
(mocks and stubs allowed). Only then real logic, one testable slice
at a time. Never build layer-by-layer horizontally.
- Every slice lists its tasks; every task names **files, action,
verify, done**.
- Each slice ends with verification evidence, a status
(`DONE` | `DONE_WITH_CONCERNS` | `NEEDS_CONTEXT` | `BLOCKED`),
and a **STOP** for human review before the next slice.
### Gate 5 — Closeout
- AAR section in the design doc: planned / actual / why the
difference / learnings.
- Harvest: learnings useful to future readers go to the wiki
(FAQ, Stolpersteine) with source links. A missing or wrong framework
rule becomes a framework issue or update.
- Good analyses produced along the way may be filed as wiki pages
(with citations) instead of dying in chat history.
- Move the design doc to `docs/design/done/`. Run `gen_status.py`.
## Debugging Path
For bugs and incidents, any size:
1. Reproduce first. No reproduction, no fix.
2. Hypothesize the root cause; verify the hypothesis with evidence
before changing anything.
3. Route the failure before fixing (diagnostic failure routing):
- **Intent issue** — we built toward the wrong goal → back to Gate 1.
- **Spec issue** — the design/plan was wrong → fix the spec
(Gate 2/3), then the code.
- **Code issue** — plan right, code wrong → fix in place.
4. Fix, plus a test that would have caught it.
5. Incidents and major misdiagnoses get a standalone AAR in `docs/aar/`.
## Session Handoff
- When a slice completes, or context quality degrades, write the current
state into the design doc's **Handoff block** — done slices, open
decisions, next step — then start a fresh session that resumes from
the doc. The doc is the memory; the session is disposable.
- End every working session by answering: "Which choices did I make that
I'm least confident about?" File the answer in the design doc.
## Refinement Session
A recurring, human-triggered ritual. Agenda:
1. Batched confirmations: size classes and small approvals queued since
last time.
2. Backlog triage over `docs/issues/`: close, reprioritize, split.
3. AAR harvest: walk recent AARs; update the wiki (FAQ, Stolpersteine);
propose framework changes.
4. Wiki lint (content-level, beyond `validate.py`): contradictions
between pages, claims superseded by newer sources, orphan pages,
missing cross-references, gaps worth a new page or a web search.
5. STATUS review: anything stale or surprising in `STATUS.md`.
## Knowledge Handling (summary)
Full rules live in `docs/wiki/index.md`. The short version:
- Original sources live in `docs/sources/`, immutable — agents read
them, never modify them. Wiki pages cite the sources they draw on.
- Contradictions are resolved or explicitly flagged — never left
silently coexisting.
- If the wiki has no confident answer, say so. Never file a
low-confidence synthesis back as knowledge.
- Git is the changelog. No separate log file.
@@ -0,0 +1,106 @@
# schema.yaml — single source of truth for artifact frontmatter.
# Stage 1 of ADR-0004: scripts/validate.py checks generically against this
# file. Extending the framework's metadata means editing THIS file, not code.
# Agents: never invent fields or status values; propose a schema change.
version: 1
scope:
# Files considered artifacts. Templates and raw sources are exempt.
include:
- "PROJECT.md"
- "docs/**/*.md"
exclude:
- "**/template.md"
- "docs/sources/**"
- "vendor/**"
# Files whose inline links are checked, but which need no frontmatter
# (root-level prose: README, AGENTS, WORKFLOW, generated STATUS, ...).
link_only:
- "*.md"
# Frontmatter fields whose values are links. Values starting with
# http://, https:// or mailto: are treated as external and only
# format-checked; everything else must be a repo-root-relative path
# to an existing file.
link_fields: [related, sources, supersedes, superseded_by]
types:
project:
dir: "."
filename: "^PROJECT\\.md$"
required: [type, language, size_s_exception, purpose, audience]
fields:
language: { enum: [de, en] }
size_s_exception: { kind: bool }
purpose: { kind: str }
audience: { kind: str }
adr:
dir: "docs/adr"
filename: "^\\d{4}-[a-z0-9-]+\\.md$"
required: [type, id, status, date]
fields:
id: { pattern: "^\\d{4}$" }
status: { enum: [proposed, accepted, superseded] }
date: { kind: date }
supersedes: { kind: link, nullable: true }
superseded_by: { kind: link, nullable: true }
related: { kind: links }
rules:
# status: superseded requires superseded_by to point at the successor.
- superseded_requires_pointer
design:
dir: "docs/design"
filename: "^\\d{4}-\\d{2}-\\d{2}-[a-z0-9-]+\\.md$"
required: [type, status, date, size]
fields:
status: { enum: [gate-1, gate-2, gate-3, gate-4, gate-5, done] }
size: { enum: [L] }
date: { kind: date }
related: { kind: links }
rules:
# status: done if and only if the file lives under docs/design/done/.
- done_iff_in_done_dir
aar:
dir: "docs/aar"
filename: "^\\d{4}-\\d{2}-\\d{2}-[a-z0-9-]+\\.md$"
required: [type, status, date]
fields:
status: { enum: [open, harvested] }
date: { kind: date }
related: { kind: links }
issue:
dir: "docs/issues"
filename: "^\\d{4}-[a-z0-9-]+\\.md$"
required: [type, id, status, created]
fields:
id: { pattern: "^\\d{4}$" }
status: { enum: [open, in-progress, done, rejected] }
created: { kind: date }
related: { kind: links }
wiki-page:
dir: "docs/wiki"
filename: "^[a-z0-9-]+\\.md$"
required: [type, area]
fields:
area:
enum:
- index
- architecture
- admin
- deployment
- user-guide
- requirements
- faq
- stolpersteine
sources: { kind: links }
related: { kind: links }
rules:
# Pages other than the index should be linked from somewhere
# (reported as WARNING, not error — see validate.py).
- warn_if_orphan
@@ -0,0 +1,143 @@
#!/usr/bin/env python3
"""gen_status.py — generate STATUS.md deterministically from frontmatter.
Writes STATUS.md (no timestamps output depends only on repo content, so
reruns are diff-clean). With --check, regenerates in memory and fails if
the committed STATUS.md is stale; CI uses this mode.
Usage:
python scripts/gen_status.py [repo-root] # write STATUS.md
python scripts/gen_status.py --check [repo-root] # verify, exit 1 if stale
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
try:
import yaml
except ImportError: # pragma: no cover
sys.exit("gen_status.py needs PyYAML: pip install pyyaml")
H1_RE = re.compile(r"^#\s+(.*)$", re.M)
def parse(path: Path):
lines = path.read_text(encoding="utf-8").splitlines()
if not lines or lines[0].strip() != "---":
return None, ""
for j in range(1, len(lines)):
if lines[j].strip() == "---":
meta = yaml.safe_load("\n".join(lines[1:j])) or {}
body = "\n".join(lines[j + 1:])
return meta, body
return None, ""
def title(body: str, fallback: str) -> str:
match = H1_RE.search(body)
return match.group(1).strip() if match else fallback
def collect(root: Path, subdir: str, wanted_type: str):
items = []
base = root / subdir
if not base.is_dir():
return items
for path in sorted(base.rglob("*.md")):
if path.name == "template.md":
continue
meta, body = parse(path)
if not isinstance(meta, dict) or meta.get("type") != wanted_type:
continue
rel = path.relative_to(root).as_posix()
items.append((rel, meta, title(body, path.stem)))
return items
def render(root: Path) -> str:
issues = collect(root, "docs/issues", "issue")
designs = collect(root, "docs/design", "design")
adrs = collect(root, "docs/adr", "adr")
aars = collect(root, "docs/aar", "aar")
out: list[str] = []
out.append("# STATUS")
out.append("")
out.append("<!-- Generated by scripts/gen_status.py — do not edit. -->")
out.append("")
open_issues = [i for i in issues
if i[1].get("status") in ("open", "in-progress")]
closed = len(issues) - len(open_issues)
out.append(f"## Issues ({len(open_issues)} open, {closed} closed)")
out.append("")
if open_issues:
out.append("| Issue | Status | Title |")
out.append("|---|---|---|")
for rel, meta, name in open_issues:
out.append(f"| [{meta.get('id', '?')}]({rel}) "
f"| {meta.get('status')} | {name} |")
else:
out.append("_none open_")
out.append("")
active = [d for d in designs if d[1].get("status") != "done"]
out.append(f"## Active design docs ({len(active)})")
out.append("")
if active:
out.append("| Design | Gate | Title |")
out.append("|---|---|---|")
for rel, meta, name in active:
out.append(f"| [{Path(rel).stem}]({rel}) "
f"| {meta.get('status')} | {name} |")
else:
out.append("_none active_")
out.append("")
out.append(f"## ADRs ({len(adrs)})")
out.append("")
if adrs:
out.append("| ADR | Status | Title |")
out.append("|---|---|---|")
for rel, meta, name in adrs:
out.append(f"| [{meta.get('id', '?')}]({rel}) "
f"| {meta.get('status')} | {name} |")
else:
out.append("_none_")
out.append("")
open_aars = [a for a in aars if a[1].get("status") == "open"]
out.append(f"## Open AARs ({len(open_aars)})")
out.append("")
if open_aars:
for rel, _meta, name in open_aars:
out.append(f"- [{name}]({rel})")
else:
out.append("_none — nothing awaiting harvest_")
out.append("")
return "\n".join(out)
def main() -> int:
args = [a for a in sys.argv[1:] if a != "--check"]
check = "--check" in sys.argv[1:]
root = Path(args[0]) if args else Path.cwd()
content = render(root)
status = root / "STATUS.md"
if check:
current = status.read_text(encoding="utf-8") if status.is_file() else ""
if current != content:
print("gen_status --check: STATUS.md is stale — "
"run scripts/gen_status.py and commit the result")
return 1
print("gen_status --check: STATUS.md is current")
return 0
status.write_text(content, encoding="utf-8", newline="\n")
print(f"wrote {status}")
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,254 @@
#!/usr/bin/env python3
"""validate.py — deterministic artifact validation against schema.yaml.
Checks (errors, exit 1):
* frontmatter present, parseable, `type` known
* file location and filename match the type's rules
* required fields, enums, patterns, dates
* link fields: repo-root-relative targets exist (http/https/mailto skipped)
* inline markdown links in bodies resolve (relative to the file)
* per-type rules: superseded_requires_pointer, done_iff_in_done_dir
Warnings (exit 0):
* wiki pages (except index) with no inbound link anywhere
Usage: python scripts/validate.py [repo-root]
"""
from __future__ import annotations
import datetime
import fnmatch
import re
import sys
from pathlib import Path
try:
import yaml
except ImportError: # pragma: no cover
sys.exit("validate.py needs PyYAML: pip install pyyaml")
DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
INLINE_LINK_RE = re.compile(r"\]\(([^)\s]+)\)")
HTML_SRC_RE = re.compile(r"(?:src|srcset)=\"([^\"]+)\"")
EXTERNAL_PREFIXES = ("http://", "https://", "mailto:")
errors: list[str] = []
warnings: list[str] = []
def err(path: Path, msg: str) -> None:
errors.append(f"ERROR {path}: {msg}")
def warn(path: Path, msg: str) -> None:
warnings.append(f"WARN {path}: {msg}")
def parse_frontmatter(text: str):
lines = text.splitlines()
if not lines or lines[0].strip() != "---":
return None, text
for j in range(1, len(lines)):
if lines[j].strip() == "---":
fm = "\n".join(lines[1:j])
body = "\n".join(lines[j + 1:])
return yaml.safe_load(fm) or {}, body
return None, text # unterminated
def is_date(value) -> bool:
if isinstance(value, datetime.date):
return True
return isinstance(value, str) and bool(DATE_RE.match(value))
def as_links(value):
"""Normalize a link field's value to a list of strings."""
if value is None:
return []
if isinstance(value, str):
return [value]
if isinstance(value, list):
return [v for v in value if isinstance(v, str)]
return None # wrong shape
def discover(root: Path, scope: dict) -> list[Path]:
files: set[Path] = set()
for pattern in scope.get("include", []):
files.update(root.glob(pattern))
result = []
for f in sorted(files):
rel = f.relative_to(root).as_posix()
if any(fnmatch.fnmatch(rel, pat) for pat in scope.get("exclude", [])):
continue
if f.is_file():
result.append(f)
return result
def check_fields(path: Path, meta: dict, spec: dict, root: Path) -> None:
for field in spec.get("required", []):
if field not in meta or meta[field] is None:
err(path, f"missing required field '{field}'")
for field, rule in (spec.get("fields") or {}).items():
if field not in meta:
continue
value = meta[field]
if value is None:
if not rule.get("nullable"):
# required-check already covers required fields;
# a present-but-null optional field is fine unless typed link
pass
continue
if "enum" in rule and value not in rule["enum"]:
err(path, f"'{field}: {value}' not in enum {rule['enum']}")
if "pattern" in rule and not re.match(rule["pattern"], str(value)):
err(path, f"'{field}: {value}' does not match {rule['pattern']}")
kind = rule.get("kind")
if kind == "date" and not is_date(value):
err(path, f"'{field}: {value}' is not a YYYY-MM-DD date")
if kind == "bool" and not isinstance(value, bool):
err(path, f"'{field}: {value}' is not a boolean")
if kind == "str" and not isinstance(value, str):
err(path, f"'{field}' must be a string")
def check_links(path: Path, meta: dict, link_fields: list, root: Path,
inbound: set) -> None:
for field in link_fields:
if field not in meta:
continue
links = as_links(meta[field])
if links is None:
err(path, f"'{field}' must be a string or list of strings")
continue
for link in links:
if link.startswith(EXTERNAL_PREFIXES):
continue
target = (root / link)
if not target.is_file():
err(path, f"'{field}' link target missing: {link}")
else:
inbound.add(target.resolve())
def check_body_links(path: Path, body: str, root: Path, inbound: set) -> None:
# strip fenced code blocks and inline code spans so mermaid, code
# samples, and literal link examples in backticks aren't scanned
body = re.sub(r"```.*?```", "", body, flags=re.S)
body = re.sub(r"`[^`\n]*`", "", body)
candidates = [m.group(1) for m in INLINE_LINK_RE.finditer(body)]
for raw in (m.group(1) for m in HTML_SRC_RE.finditer(body)):
# srcset may list "path 2x, path2 1x" pairs — take each path token
for part in raw.split(","):
candidates.append(part.strip().split()[0])
for link in candidates:
if link.startswith(EXTERNAL_PREFIXES) or link.startswith("#"):
continue
link = link.split("#", 1)[0]
if not link:
continue
target = (path.parent / link).resolve()
if not target.is_file():
err(path, f"inline link target missing: {link}")
else:
inbound.add(target)
def apply_rules(path: Path, rel: str, meta: dict, spec: dict) -> None:
for rule in spec.get("rules", []):
if rule == "superseded_requires_pointer":
if meta.get("status") == "superseded" and not meta.get("superseded_by"):
err(path, "status 'superseded' requires 'superseded_by'")
elif rule == "done_iff_in_done_dir":
in_done = "/done/" in f"/{rel}"
if (meta.get("status") == "done") != in_done:
err(path, "status 'done' <-> file in docs/design/done/ mismatch")
def main() -> int:
root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
schema = yaml.safe_load((root / "schema.yaml").read_text(encoding="utf-8"))
link_fields = schema.get("link_fields", [])
types = schema.get("types", {})
inbound: set = set()
wiki_pages: list[tuple[Path, dict]] = []
# Root documents: inline links must resolve; no frontmatter required.
for rel in schema.get("scope", {}).get("link_only", []):
path = root / rel
if not path.is_file():
continue # e.g. STATUS.md before first generation
text = path.read_text(encoding="utf-8")
_meta, body = parse_frontmatter(text)
check_body_links(path, body if _meta is not None else text,
root, inbound)
seen_ids: dict[tuple[str, str], Path] = {}
artifacts = discover(root, schema.get("scope", {}))
for path in artifacts:
rel = path.relative_to(root).as_posix()
meta, body = parse_frontmatter(path.read_text(encoding="utf-8"))
if meta is None:
err(path, "missing or unterminated YAML frontmatter")
continue
if not isinstance(meta, dict) or "type" not in meta:
err(path, "frontmatter has no 'type'")
continue
t = meta["type"]
if t not in types:
err(path, f"unknown type '{t}'")
continue
spec = types[t]
expected_dir = spec.get("dir", ".")
actual_dir = str(Path(rel).parent.as_posix())
if expected_dir == ".":
if actual_dir != ".":
err(path, f"type '{t}' must live in repo root")
elif not (actual_dir == expected_dir
or actual_dir.startswith(expected_dir + "/")):
err(path, f"type '{t}' must live under {expected_dir}/")
fn_pattern = spec.get("filename")
if fn_pattern and not re.match(fn_pattern, path.name):
err(path, f"filename does not match {fn_pattern}")
check_fields(path, meta, spec, root)
if "id" in (spec.get("fields") or {}) and meta.get("id") is not None:
artifact_id = str(meta["id"])
if not path.name.startswith(f"{artifact_id}-"):
err(path, f"id '{artifact_id}' does not match filename prefix")
key = (t, artifact_id)
if key in seen_ids:
err(path, f"duplicate {t} id '{artifact_id}' "
f"(also in {seen_ids[key].name})")
else:
seen_ids[key] = path
check_links(path, meta, link_fields, root, inbound)
check_body_links(path, body, root, inbound)
apply_rules(path, rel, meta, spec)
if t == "wiki-page" and meta.get("area") != "index":
wiki_pages.append((path, meta))
# link-only files: inline links are checked, frontmatter not required
already = {p.resolve() for p in artifacts}
for pattern in schema.get("scope", {}).get("link_only", []):
for path in sorted(root.glob(pattern)):
if not path.is_file() or path.resolve() in already:
continue
meta, body = parse_frontmatter(path.read_text(encoding="utf-8"))
if meta is None:
body = path.read_text(encoding="utf-8")
check_body_links(path, body, root, inbound)
for path, _meta in wiki_pages:
if path.resolve() not in inbound:
warn(path, "orphan wiki page — nothing links to it")
for line in errors + warnings:
print(line)
print(f"validate: {len(errors)} error(s), {len(warnings)} warning(s)")
return 1 if errors else 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,34 @@
---
type: aar
status: open # open | harvested
date: YYYY-MM-DD
related: [] # design docs, issues, ADRs involved
---
<!-- Copy to docs/aar/YYYY-MM-DD-slug.md. Delete comments when filling in.
Standalone AARs are for incidents and major deviations only —
normal undertakings get their AAR as Gate 5 inside the design doc. -->
# AAR: Title
## What was planned / expected
## What happened
<!-- Facts and timeline, not blame. -->
## Why the difference
<!-- Root cause. For failures, name the routing class:
intent issue / spec issue / code issue. -->
## Learnings
<!-- What future-you should know. Blunt beats polite. -->
## Actions
<!-- Concrete: wiki pages updated (FAQ, Stolpersteine) with links,
framework issues opened, tests added. When all actions are done,
set status: harvested. The refinement session walks all AARs
still marked open. -->
@@ -0,0 +1,37 @@
---
type: adr
id: "0000"
status: proposed # proposed | accepted | superseded
date: YYYY-MM-DD
supersedes: null # path to older ADR, e.g. docs/adr/0002-old.md
superseded_by: null # filled in on the OLD adr when a new one replaces it
related: [] # optional: paths to design docs / issues
---
<!-- Copy to docs/adr/NNNN-slug.md. Delete all comments when filling in. -->
# ADR-0000: Title
## Context
<!-- The situation and the forces at play. Constraints upfront:
deadlines, scale, team knowledge, existing decisions. -->
## Options Considered
<!-- Name each option, even the one you lean toward. Pros/cons per
option; a small dimension table (complexity, cost, maintenance,
familiarity) where it helps. Keep proportional to the decision. -->
## Decision
<!-- The choice, in one or two sentences. -->
## Consequences
<!-- What becomes easier, what becomes harder, what we will need to
revisit. Honest cons included. -->
<!-- Rules: an accepted ADR is never edited — write a new ADR that
supersedes it and set superseded_by here. Lasting directional
decisions only; feature-local choices belong in the design doc. -->
@@ -0,0 +1,110 @@
---
type: design
status: gate-1 # gate-1 | gate-2 | gate-3 | gate-4 | gate-5 | done
date: YYYY-MM-DD
size: L # this template is for size L
related: [] # issues, ADRs spawned or read
---
<!-- Copy to docs/design/YYYY-MM-DD-slug.md. Delete comments when filling in.
Fill ONE gate at a time; each gate ends with STOP — do not pre-fill
later gates. Advance `status` only after human approval. -->
# Design: Title
## Gate 1 — Product
**Problem.** <!-- What user problem, for whom. -->
**Acceptance criterion.** <!-- Verifiable. A real number where one
exists; otherwise a concretely checkable outcome. "Works" is not one. -->
**Non-goals.** <!-- What this deliberately does NOT do. The cheapest
scope-creep brake there is. -->
**Announcement.** <!-- 35 sentences: what it is, who it's for, why
it's good. Can't write it? The product isn't understood yet. -->
**Mockups.** <!-- Only if UI is involved: plain-HTML mockups, linked. -->
> **STOP — awaiting Gate 1 approval.**
## Gate 2 — Architecture
**Inputs read.** <!-- Which ADRs and AARs were read; one line each on
why they matter here. -->
**System fit.** <!-- Endpoints, tables/schemas, query outlines,
end-to-end flow as Mermaid. Against the actual codebase. -->
**Constraints.** <!-- Non-functional, proportional to the project:
performance, security, operations, compatibility. "None relevant"
is a valid answer — but say it. -->
**Options & trade-offs.** <!-- Where more than one viable way exists:
name the options, pro/contra each, state the chosen one and WHY.
This is the feature-local decision record. Only lasting, binding
decisions graduate to an ADR below. -->
**New ADRs.** <!-- Lasting decisions discovered here → one ADR each,
linked. None is a valid answer. -->
> **STOP — awaiting Gate 2 approval.**
## Gate 3 — Program Design
**Files.** <!-- Exact paths, new and touched. -->
**Signatures.** <!-- Types and method signatures, no bodies. -->
**Call stack.** <!-- For the main flow(s). -->
**Test assertions.** <!-- What the tests will assert. -->
**Boundaries — DO NOT CHANGE.** <!-- Explicit list. -->
**Shakiest calls.** <!-- The decisions you are least confident about. -->
> **STOP — awaiting Gate 3 approval.**
## Gate 4 — Vertical Slices
<!-- Slice 1 is the tracer bullet: thin end-to-end, runs with mocks.
Then real logic, one testable slice at a time. Per task:
files / action / verify / done. After each slice: evidence,
status, STOP. -->
### Slice 1 — Tracer bullet
- [ ] Task: … — files: … — action: … — verify: … — done: …
**Evidence:** <!-- command output, test run, screenshot ref -->
**Status:** <!-- DONE | DONE_WITH_CONCERNS | NEEDS_CONTEXT | BLOCKED -->
> **STOP — slice review.**
### Slice 2 — …
### Handoff
<!-- The single place session state lives. Overwrite on every handoff;
git keeps the history.
Done slices: …
Open decisions: …
Next step: … -->
## Gate 5 — Closeout (AAR)
**Planned vs. actual.** <!-- What was planned, what happened. -->
**Why the difference.** <!-- Root causes, honestly. -->
**Learnings.** <!-- What future-you should know. -->
**Harvested.** <!-- Wiki pages updated (FAQ, Stolpersteine, …) with
links; framework issues opened, if a rule was missing or wrong. -->
**Open uncertainties.** <!-- Session-handoff answers to: "Which choices
did I make that I'm least confident about?" -->
<!-- After approval: set status: done, move this file to
docs/design/done/, run gen_status.py. -->
@@ -0,0 +1,29 @@
---
type: issue
id: "0000"
status: open # open | in-progress | done | rejected
created: YYYY-MM-DD
related: [] # design docs, ADRs, other issues
---
<!-- Copy to docs/issues/NNNN-slug.md. Delete comments when filling in. -->
# Issue-0000: Title
## Problem / Motivation
<!-- What's wrong or missing, and why it matters. One paragraph. -->
## Acceptance
<!-- When is this issue done? Verifiable, like every other criterion
in this framework. -->
## Notes
<!-- Optional: context, links, findings gathered along the way.
Rules: status is the single source of truth and lives here in the
frontmatter — STATUS.md is generated, never edited. An issue that
starts real work links its design doc in `related`. Closed means
status: done (or rejected, with a one-line reason in Notes) —
the file stays; git is the history. -->