diff --git a/AGENTS.md b/AGENTS.md index 58e3b0f..ab0af87 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -82,6 +82,7 @@ What a file is called says who it is for and how it is loaded. This is a rule, n | `instructions/.md` | Agents | By link, or on explicit request | | `instructions//SKILL.md` | Agents | By the harness, once published | | `types/.md` | Agents + validator | Via `tools/wikitool types describe`. Split by `root:`: a page type-spec (`root: kb`) belongs to the instance and ships as `.template`; one describing a stack artifact ships verbatim | +| `docs/.md` | Agents and humans | By link, or on explicit request - never automatically, and never as instruction | | `INDEX.md` | Both | Generated - never hand-edited | A stage may carry both a `README.md` and a `CONTRACT.md`: different readers, different @@ -89,6 +90,16 @@ documents. What it may not carry is the same content twice - a README that resta contract is a second copy that drifts. `docs verify` enforces the specific case that already happened once: no README may hold a copy of the `wikitool` command table. +**`docs/` carries no normative sentence.** It holds why the stack is built the way it is - +background a session consults in passing, not a rule it must follow. Anything that would bind +belongs in a `CONTRACT.md` instead, which is what keeps invariant 8 intact here: `docs/` is +never a second place a rule could live, only prose about rules that live elsewhere. That is also +why nothing verifies its content - there is no rule in it to check. It has no frontmatter, no type, no index, no lint, no decay, no provenance, and no +`COLLECTION.md` - which [kb/CONTRACT.md § Collections](kb/CONTRACT.md#collections) forbids +outside `kb/` anyway, but the point holds independently: `docs/` stays a plain directory of +prose, invisible to everything `tools/wikitool` does except `dist export`, which copies it +verbatim. A fresh instance needs the reasoning as much as this one does. + ## Personalization `USER.md` and `SOUL.md` are read at session start, if the runtime has not already injected @@ -145,7 +156,8 @@ input schema + compiler output derived (gitignored) work/ tracked scratch, deleted when the run closes ``` -Alongside it, not part of it: `instructions/` (what agents are told to do) and this file. +Alongside it, not part of it: `instructions/` (what agents are told to do), `docs/` (why the +stack is built the way it is - see [File naming](#file-naming)), and this file. **By stage** - read the contract for the stage you are writing in: diff --git a/CHANGES.md b/CHANGES.md index 0282f22..317c467 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -20,6 +20,57 @@ their date-only headings. --- +## 4.3.0 - 2026-09-03 - docs/ als ausgelieferter Hintergrund-Ort; Decision-Seiten bleiben in kb/, Decay-Skip fuer concept_type: decision + +**Author:** Torben Nehmer + +Gitea #38: `dist export` lieferte bislang keine einzige `kb/`-Seite aus - eine frische Instanz +bekam den Stack, aber keinen Grund für seine Form. Die dokumentierte `adr-NNN-`-Konvention in +`kb/concepts/COLLECTION.md` existierte zudem nur auf Papier: keine der sieben +`concept_type: decision`-Seiten folgte ihr, und `confidence_decay()` lief bedingungslos über sie +- ein Kategorienfehler, weil Zeitablauf eine Entscheidung nicht falscher macht, nur Supersession +tut das. + +**Neu:** `docs/` - ein inertes Verzeichnis für Stack-Hintergrund (warum der Stack so gebaut ist, +nicht was diese Instanz entschieden hat). Keine Frontmatter, kein Typ, kein Index, kein Lint, +keine Decay, keine Provenance, keine `COLLECTION.md`. `dist export` liefert es verbatim aus, wie +`instructions/` und `types/`. Befüllung folgt in Gitea #45. + +**Verworfen, nach Prüfung:** ein Umzug der sieben Decision-Seiten nach `decisions/`. Der +Subtyp-Floor aus #28 verlangt mindestens eine Seite je deklariertem `concept_type`, und ein +Umzug hätte `decision` auf null gebracht; dazu zeigen 89 Wikilinks aus `kb/` sowie +tool-eigene Frontmatter-Arrays auf die sieben, und `links.py`/`xref add` kennen kein Ziel +außerhalb `kb/`. Die sieben bleiben in `kb/concepts/`, ebenso ein zweiter, separat erwogener +Rename (`docs verify` → `parity verify`) - der wäre nur nötig gewesen, wenn ein Befehl auf das +Verzeichnis `docs/` wirkt, und keiner tut das. + +**Geändert:** +- `confidence_decay()` überspringt `concept_type: decision` strukturell (kategorische Ausnahme, + nicht als Brücke gebaut - Begründung im Docstring). +- `kb/concepts/COLLECTION.md` § Decisions ersetzt die tote ADR-Vorlage durch die real gelebte + Form: eine Entscheidung ist eine gewöhnliche Concept-Seite, organische Prosa, kein + `adr-NNN-`-Präfix, `**Status:**` optional, Supersession per `supersedes`-Link. +- `kb/CONVENTIONS.md` § Naming und `instructions/kb-profiles.md` (Profil `german`) korrigiert - + beide dokumentierten noch die verworfene `adr-NNN-`-Namensregel. +- `AGENTS.md` § File naming und § Routing: `docs/`-Zeile, plus die Regel, dass `docs/` keinen + normativen Satz trägt (das hält Invariante 8 heil - was binden würde, gehört in einen + Contract). +- `tools/CONTRACT.md`: Klarstellung, dass `docs verify` Dokumentations-Parität prüft, nicht das + `docs/`-Verzeichnis, sowie `docs/` in der `dist export`-Zeile ergänzt. + +Additiv und in beide Richtungen drop-in: eine bestehende Instanz ohne `docs/` exportiert +weiterhin identisch (leerer `_copy_tree`-Treffer), eine Instanz mit `docs/` bekommt es ab jetzt +mitgeliefert. Kein Feld, kein Kommando ändert sein Verhalten für bestehenden Inhalt. + +**Migration:** none required. + +Berührt: `tools/chemenu/commands/confidence_decay.py`, `tools/chemenu/commands/dist_cmd.py`, +`tools/chemenu/tests/test_confidence_decay.py`, `tools/chemenu/tests/test_dist_cmd.py`, +`kb/concepts/COLLECTION.md`, `kb/CONVENTIONS.md`, `instructions/kb-profiles.md`, `AGENTS.md`, +`tools/CONTRACT.md`. + +--- + ## 4.2.0 - 2026-09-03 - Korpus-Kuratierungsrichtlinie: Untergrenzen und Leitplanke für reaktive Fixes **Author:** Torben Nehmer diff --git a/VERSION b/VERSION index 6aba2b2..8089590 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.2.0 +4.3.0 diff --git a/instructions/kb-profiles.md b/instructions/kb-profiles.md index 7dc177a..3b608fa 100644 --- a/instructions/kb-profiles.md +++ b/instructions/kb-profiles.md @@ -68,7 +68,7 @@ stack's hardcoded behaviour until the conventions file existed. |---|---| | `language:` | `de` | | `sections:` | `Beziehungen` / `Siehe auch` / `Fußnoten` | -| Naming | Human-readable titles with spaces; singular for entities; `adr-NNN-` for decisions; `X vs Y` for comparisons | +| Naming | Human-readable titles with spaces; singular for entities; a decision named like any other concept, no `adr-NNN-` prefix; `X vs Y` for comparisons | | Tone | Wikipedia register, with a German buzzword and filler list | | Relationship labels | `hängt ab von` · `verwendet` · `implementiert` · `erweitert` · `ersetzt` · `steht in Konflikt mit` · `benötigt` · `erzeugt` · `konsumiert` · `besitzt` · `pflegt` · `läuft auf` · `verwandt mit` | | Confidence rubric | 0.5 base, +0.2 per supporting source (max +0.6), recency and source-quality bonuses; hedge with "möglicherweise"/"kann" below 0.6, "unsicher"/"unbestätigt" below 0.4 | diff --git a/kb/CONVENTIONS.md b/kb/CONVENTIONS.md index 26bcd0f..7f84531 100644 --- a/kb/CONVENTIONS.md +++ b/kb/CONVENTIONS.md @@ -62,7 +62,8 @@ There is no `## Siehe auch` region any more. It was the reciprocal half of a bid - Human-readable titles with spaces: `Hybrid Search.md`, `Gitea Actions.md` - not kebab-case. - Singular for entities: `ha-core.md`, not `ha-cores.md`. - Comparison pages read as a comparison: `Go vs Rust.md`. -- ADRs are prefixed: `adr-001-use-go-modules.md`. +- A decision (`concept_type: decision`) is named like any other concept - no `adr-NNN-` prefix. + See [kb/concepts/COLLECTION.md § Decisions](concepts/COLLECTION.md#decisions). - Prefer readability over convention when the two conflict. What to name a thing: projects use their repository or common name; systems a descriptive diff --git a/kb/concepts/COLLECTION.md b/kb/concepts/COLLECTION.md index 30cd3ea..c62dbea 100644 --- a/kb/concepts/COLLECTION.md +++ b/kb/concepts/COLLECTION.md @@ -27,19 +27,27 @@ tone, relationship labels, the confidence rubric. Neither is restated here. `concept` (`tools/wikitool types describe concept`). -## Decisions and ADRs +## Decisions -An architectural decision is a concept page, prefixed as -[kb/CONVENTIONS.md § Naming](../CONVENTIONS.md#naming) says. It records: +An architectural decision is an ordinary concept page with `concept_type: decision` +(`tools/wikitool types describe concept`) - not a separate format, and not a separate location. +There is no `adr-NNN-`-prefixed filename and no dedicated directory: naming follows +[kb/CONVENTIONS.md § Naming](../CONVENTIONS.md#naming) like every other concept, and the page +lives in `kb/concepts/` like every other concept. -- **Context** - what forced a decision. -- **Decision** - what was chosen. -- **Consequences** - what this costs, not only what it buys. -- **Status** - proposed / accepted / deprecated / superseded. -- Links to every entity the decision affects. +The body is organic prose under this collection's usual sections, not a fixed template. What it +still has to carry: what was decided, what forced the decision, what it costs (not only what it +buys), and a link to every entity the decision affects. A `**Status:**` line is optional - most +decision pages in this instance carry none, because the page's own prose already says whether the +decision stands. -A superseded ADR is never deleted or rewritten. The new one declares `supersedes` pointing at -it; the old one needs no edge back, because its inbound view renders the replacement. +A decision superseded by a later one is never deleted or rewritten. The new page declares +`supersedes` pointing at it; the old one needs no edge back, because its inbound view renders the +replacement. + +`concept_type: decision` is also the one subtype [kb/CONTRACT.md](../CONTRACT.md)'s confidence +machinery treats differently: `confidence decay` skips it structurally, because elapsed time does +not falsify a decision - only a later decision superseding it does. ## Authorised labels diff --git a/tools/CONTRACT.md b/tools/CONTRACT.md index 28c55d8..2685385 100644 --- a/tools/CONTRACT.md +++ b/tools/CONTRACT.md @@ -68,10 +68,10 @@ tools/wikitool --help | `instructions sync [--force]` | Publish every `instructions//SKILL.md` into `.agents/skills/` and `.claude/skills/` as **copies**, and delete published skills whose source is gone. Both targets are gitignored, so a fresh clone runs this once - see `instructions/bootstrap.md`. Re-running is also how a drifted copy is repaired: the source always wins. `--force` is required only to replace a target directory that is not a published skill at all (no `SKILL.md` in it) | | `instructions verify` | Check the instruction layer: flat instructions validate against `types/instruction.schema.yaml`, each `SKILL.md` carries the frontmatter its harness reads, every published copy is byte-identical to its source, no instruction is left that nothing references, and nothing under `instructions/dev/` is referenced from outside it (a `` block is exempt - see [instructions/CONTRACT.md](../instructions/CONTRACT.md)). Missing *every* copy is reported as "run sync", not as drift - that is a clean checkout | | `instructions list [--json]` | List the flat instructions with their descriptions. This is how the layer is discovered; `search` deliberately covers `kb/` only | -| `docs verify` | Check the docs that mirror the code: every CLI command documented here (and vice versa), every directory under `kb/` has a `COLLECTION.md` and no directory outside it does, every collection declaring `profile:` and a `required_by_stack:` that agrees with the stack's own list, `kb/CONVENTIONS.md` naming all three tool-owned section headings if it exists at all, every stage contract present, no pre-migration `type: entity` blocks left in the contracts, and the `.gitignore` canaries clear in both directions (nothing ignored under `raw/`/`kb/`, everything ignored under `reports/` and the published skill directories) | +| `docs verify` | Check the docs that mirror the code: every CLI command documented here (and vice versa), every directory under `kb/` has a `COLLECTION.md` and no directory outside it does, every collection declaring `profile:` and a `required_by_stack:` that agrees with the stack's own list, `kb/CONVENTIONS.md` naming all three tool-owned section headings if it exists at all, every stage contract present, no pre-migration `type: entity` blocks left in the contracts, and the `.gitignore` canaries clear in both directions (nothing ignored under `raw/`/`kb/`, everything ignored under `reports/` and the published skill directories). The name is about documentation parity, not about the `docs/` directory - it neither reads nor requires one, the same way `kb/` predates the collection it now checks | | `eval sessions [--json]` | List the sessions that have a trace under `reports/telemetry/`, most recent first. Read-only and exempt from the Iteration Budget Gate | | `eval score [--session ] [--json] [--markdown out.md] [--save] [--fail-on-error]` | Score one traced session: structural state from `lint`'s own checks (L1) plus trajectory rules over the trace (L2) - was a refused call repeated unchanged, was a gate flag passed without that gate having refused anything, did a publish of `kb/` pages go unlogged. Defaults to the current session. `--save` writes `reports/evals//.{json,md}`. Read-only over `kb/` and exempt from the budget; see [../EVALS.md](../EVALS.md) | -| `dist export [--dry-run] [--source-repo U] [--source-commit SHA] [--release-url U] [--update-url U]` | Write a contentless, distributable copy of this repo's machinery into an empty `` directory: `AGENTS.md`/`README.md`/`EVALS.md` with any `...` region removed, `instructions/` (minus `instructions/dev/`), `types/` (the `root: kb` page type-specs and their schemas re-keyed as `.template`, the stack's own verbatim), `tools/` (no venv/caches), the `.github/hooks/`+`.vibe/` session-tracing config plus `.claude/settings.json`, `kb/CONTRACT.md` (no pages, no areas), empty `raw/{articles,documents,notes,assets}/`, `VERSION`, `USER.md.template`/`SOUL.md.template` plus `kb/CONVENTIONS.md.template` and each collection's contract re-keyed as `kb//COLLECTION.md.template` (the templates ship; the filled `USER.md`/`SOUL.md`/`kb/CONVENTIONS.md`/`kb//COLLECTION.md`/`types/.md` never do - all of them bind their instance and none are the stack's to decide, and `find_leaks` refuses a plan carrying one), and a generated `.wikitool-release.json` stamp (version, export date, origin, and a sha256 per exported file - the base a later upgrade would compare against). The four origin options only fill stamp fields: `export` never calls git and cannot discover them. Refuses a non-empty target, and a tree with no `VERSION`. See `instructions/setup-instance.md`. One-way: there is no command that reconstructs a distributed instance into a dev instance - work on the stack in the origin repo (or a new dev instance exported from it) instead | +| `dist export [--dry-run] [--source-repo U] [--source-commit SHA] [--release-url U] [--update-url U]` | Write a contentless, distributable copy of this repo's machinery into an empty `` directory: `AGENTS.md`/`README.md`/`EVALS.md` with any `...` region removed, `instructions/` (minus `instructions/dev/`), `types/` (the `root: kb` page type-specs and their schemas re-keyed as `.template`, the stack's own verbatim), `docs/` verbatim, `tools/` (no venv/caches), the `.github/hooks/`+`.vibe/` session-tracing config plus `.claude/settings.json`, `kb/CONTRACT.md` (no pages, no areas), empty `raw/{articles,documents,notes,assets}/`, `VERSION`, `USER.md.template`/`SOUL.md.template` plus `kb/CONVENTIONS.md.template` and each collection's contract re-keyed as `kb//COLLECTION.md.template` (the templates ship; the filled `USER.md`/`SOUL.md`/`kb/CONVENTIONS.md`/`kb//COLLECTION.md`/`types/.md` never do - all of them bind their instance and none are the stack's to decide, and `find_leaks` refuses a plan carrying one), and a generated `.wikitool-release.json` stamp (version, export date, origin, and a sha256 per exported file - the base a later upgrade would compare against). The four origin options only fill stamp fields: `export` never calls git and cannot discover them. Refuses a non-empty target, and a tree with no `VERSION`. See `instructions/setup-instance.md`. One-way: there is no command that reconstructs a distributed instance into a dev instance - work on the stack in the origin repo (or a new dev instance exported from it) instead | | `version show [--json]` | Print this instance's stack version and where it came from (development tree, or a distribution with its export date and origin). Bare `wikitool version` is an alias for this. Read-only, offline, and **exempt from the Iteration Budget Gate** | | `version check [--url U] [--timeout S] [--json]` | Ask the origin's release feed whether a newer stack exists, and whether the step crosses a compatibility boundary (`state: current\|update\|migration\|ahead`). **The only command in `wikitool` that makes a network call** - never reached implicitly from another command, needs no key, times out, and reports an unreachable feed as an error rather than as "up to date". The feed is `$WIKITOOL_UPDATE_URL`, else the release stamp's, else the built-in origin; `$WIKITOOL_UPDATE_TOKEN` is only needed if that feed is not readable anonymously. Read-only and exempt from the budget gate | | `version notes [--version X.Y.Z]` | Print one version's `CHANGES.md` entry, for use as release notes (default: this tree's `VERSION`). Read-only and exempt from the budget gate | diff --git a/tools/chemenu/commands/confidence_decay.py b/tools/chemenu/commands/confidence_decay.py index 60b8d28..7a18203 100644 --- a/tools/chemenu/commands/confidence_decay.py +++ b/tools/chemenu/commands/confidence_decay.py @@ -11,6 +11,14 @@ Keeping the undecayed anchor in `confidence_base` is what makes repeated runs idempotent - decaying the stored `confidence` in place (the pre-2026-08-13 behavior) compounded on every run, because the elapsed-months factor kept growing while the multiplicand had already shrunk. + +Pages with `concept_type: decision` are skipped structurally, not as an +interim measure. The formula models staleness - a claim that nobody has +re-checked in a while becomes less trustworthy - and a decision is not a +claim about the world that time can falsify. What retires a decision is a +later decision superseding it, never elapsed months on its own; that is a +category the decay formula does not have a term for, so it does not apply +one. """ from __future__ import annotations @@ -118,6 +126,8 @@ def confidence_decay( missing_base = [] for title, page in sorted(pages.items()): + if page.frontmatter.get("concept_type") == "decision": + continue confidence = page.frontmatter.get("confidence") if confidence is None: continue diff --git a/tools/chemenu/commands/dist_cmd.py b/tools/chemenu/commands/dist_cmd.py index a917eef..54453d2 100644 --- a/tools/chemenu/commands/dist_cmd.py +++ b/tools/chemenu/commands/dist_cmd.py @@ -347,6 +347,12 @@ def build_plan(origin: Optional[Origin] = None) -> dict[str, PlannedFile]: for hook_dir in HOOK_DIRS: plan.update(_copy_tree(config.ROOT / hook_dir, hook_dir, frozenset())) + # docs/ is stack background - why the stack is built the way it is - and + # ships verbatim like instructions/ and types/: it carries no page, no + # frontmatter, and (AGENTS.md § File naming) no normative sentence, so + # there is nothing instance-owned in it to split off as a .template. + plan.update(_copy_tree(config.ROOT / "docs", "docs", frozenset())) + # `kb/CONTRACT.md` is stack-owned and ships verbatim; everything beside it # under `kb/` is the instance's own and ships only as a `.template`. That is # the personalization split (`USER.md`/`SOUL.md`) one directory down, and @@ -492,7 +498,8 @@ def export_command( ): """Export a contentless, distributable copy of this repo's machinery: AGENTS.md/README.md (dev-instance-only marker blocks removed), - instructions/ (no instructions/dev/), types/, tools/ (no venv/caches), + instructions/ (no instructions/dev/), types/, docs/ verbatim, + tools/ (no venv/caches), the .github/hooks/+.vibe session-tracing config plus .claude/settings.json, kb/CONTRACT.md plus a COLLECTION.md.template per collection and kb/CONVENTIONS.md.template (no pages, no areas), empty diff --git a/tools/chemenu/tests/test_confidence_decay.py b/tools/chemenu/tests/test_confidence_decay.py index b02003b..84cdab4 100644 --- a/tools/chemenu/tests/test_confidence_decay.py +++ b/tools/chemenu/tests/test_confidence_decay.py @@ -5,7 +5,7 @@ import pytest from chemenu import config from chemenu.commands import confidence_decay from chemenu.commands.confidence_decay import FLOOR, compute_decay -from chemenu.frontmatter_io import read_page +from chemenu.frontmatter_io import read_page, write_page def test_no_decay_at_zero_months(): @@ -68,3 +68,22 @@ def test_decay_skips_pages_without_a_base(decay_wiki): confidence_decay.confidence_decay(apply=True) frontmatter, _ = read_page(decay_wiki / "entities/systems/aurora.md") assert frontmatter["confidence"] == 0.9 + + +def test_decay_skips_decision_pages(decay_wiki): + """A decision is not falsified by elapsed time, only by a later decision + superseding it - `concept_type: decision` is a categorical skip, not + something an old `modified` date should ever decay (Gitea #38).""" + decision_path = decay_wiki / "concepts" / "some-decision.md" + write_page( + decision_path, + { + "type": "types/concept.md", "concept_type": "decision", + "tags": [], "created": "2015-01-01", "modified": "2015-01-01", + "related": [], "sources": [], "confidence": 0.9, "confidence_base": 0.9, + }, + "\n# some-decision\n", + ) + confidence_decay.confidence_decay(apply=True) + frontmatter, _ = read_page(decision_path) + assert frontmatter["confidence"] == 0.9 diff --git a/tools/chemenu/tests/test_dist_cmd.py b/tools/chemenu/tests/test_dist_cmd.py index 75c2181..29a4acd 100644 --- a/tools/chemenu/tests/test_dist_cmd.py +++ b/tools/chemenu/tests/test_dist_cmd.py @@ -135,6 +135,10 @@ def repo(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: f"\n# conventions template\n", encoding="utf-8" ) + docs_dir = root / "docs" + docs_dir.mkdir() + (docs_dir / "why-gates-are-code.md").write_text("# Why gates are code\n", encoding="utf-8") + for relative in ("raw/CONTRACT.md", "reports/CONTRACT.md", "work/CONTRACT.md"): path = root / relative path.parent.mkdir(parents=True, exist_ok=True) @@ -156,6 +160,11 @@ def test_plan_never_includes_commonplace(repo): assert "commonplace" not in combined +def test_plan_ships_docs_verbatim(repo): + plan = dist_cmd.build_plan() + assert plan["docs/why-gates-are-code.md"].content == "# Why gates are code\n" + + def test_plan_never_includes_instructions_dev(repo): """instructions/dev/ - flat dev-only instructions and the nested skill that switches a session into tool-development mode - is pruned