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