From ee24b6e5b8c686589db033d60b4812668a744096 Mon Sep 17 00:00:00 2001 From: Torben Nehmer Date: Sat, 19 Sep 2026 21:13:24 +0200 Subject: [PATCH] stack: Typ `project` und Collection `kb/gtd/` (#123) Files changed: - CHANGES.md - README.md - VERSION - kb/CONTRACT.md - kb/CONVENTIONS.md - kb/entities/COLLECTION.md - kb/gtd/COLLECTION.md - kb/gtd/INDEX.md - kb/index.md - tools/CONTRACT.md - tools/chemenu/kb_collections.py - tools/chemenu/tests/test_conventions.py - tools/chemenu/tests/test_docs_verify.py - tools/chemenu/tests/test_kb_collections.py - tools/chemenu/tests/test_new_page.py - tools/chemenu/tests/test_type_resolver.py - tools/chemenu/tests/test_types_cmd.py - types/project.md - types/project.schema.yaml - types/type-spec.md --- CHANGES.md | 37 ++++++++ README.md | 10 ++- VERSION | 2 +- kb/CONTRACT.md | 5 +- kb/CONVENTIONS.md | 5 +- kb/entities/COLLECTION.md | 1 + kb/gtd/COLLECTION.md | 78 ++++++++++++++++ kb/gtd/INDEX.md | 6 ++ kb/index.md | 2 + tools/CONTRACT.md | 2 +- tools/chemenu/kb_collections.py | 42 ++++++--- tools/chemenu/tests/test_conventions.py | 12 ++- tools/chemenu/tests/test_docs_verify.py | 71 +++++++++++++++ tools/chemenu/tests/test_kb_collections.py | 37 +++++++- tools/chemenu/tests/test_new_page.py | 27 ++++++ tools/chemenu/tests/test_type_resolver.py | 4 +- tools/chemenu/tests/test_types_cmd.py | 4 +- types/project.md | 100 +++++++++++++++++++++ types/project.schema.yaml | 78 ++++++++++++++++ types/type-spec.md | 22 +++-- 20 files changed, 507 insertions(+), 38 deletions(-) create mode 100644 kb/gtd/COLLECTION.md create mode 100644 kb/gtd/INDEX.md create mode 100644 types/project.md create mode 100644 types/project.schema.yaml diff --git a/CHANGES.md b/CHANGES.md index a3e8b16..b207e8b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -59,6 +59,43 @@ concern - readable here, never shipped as something to parse. --- +## 7.0.0-beta.1 - 2026-09-19 - Typ `project` und Collection `kb/gtd/`: das Vorhaben als eigene Seitenart + +**Author:** Torben Nehmer + +**Breaking Change:** docs verify now requires an adopted `project` type-spec (schema requiring `state:`) and its `kb/gtd/` collection - an instance must adopt types/project.md(.schema.yaml) and kb/gtd/COLLECTION.md from their .template before docs verify passes again + +**Migration:** none required - no kb/ page content changes - the fix is the ordinary .template adoption every root:kb type already requires, not a data migration + + +- Typ `project` und Collection `kb/gtd/`: das Vorhaben als eigene Seitenart + + +### Typ `project` und Collection `kb/gtd/`: das Vorhaben als eigene Seitenart + +Gitea #119 (Paket #123): ein neuer Seitentyp `project` fuer das Vorhaben - Ziel, Beteiligte, +dauerhafter Status, offene Schleifen - abgegrenzt gegen das Artefakt (`entity`/`codebase`, seit +6.2.0). `types/project.md`/`.schema.yaml` und die neue Collection `kb/gtd/` (Bereiche `haus/`, +`finanzen/`, `technik/` ueber `responsibility:`) folgen exakt dem Muster, das `entity`/`concept`/ +`source`/`comparison` schon vorgeben - kein Code noetig fuer `wikitool new project`, `types list` +oder den Template-Versand, alles daran ist bereits generisch. + +Neu ist nur eine Zeile Code: `project` tritt neben `source` in +`kb_collections.STACK_REQUIRED_TYPES`, nach demselben "fordern statt besitzen"-Idiom (D16) - ein +Type-Spec `name: project`, dessen Schema `state:` fuehrt, muss existieren, weil der +Wochenrueckblick (#125) sonst nichts hat, wogegen er ein Tracker-Projekt abgleichen kann. Das +macht `docs verify` zum Grenzuebertritt (siehe **Breaking Change** oben): eine Instanz, die die +neue `tools/`-Fassung uebernimmt, ohne `types/project.md.template` und +`kb/gtd/COLLECTION.md.template` zu adoptieren, faellt fortan durch, wo sie vorher bestand. Dabei +aufgefallen und mitkorrigiert: `kb_collections.declaration_issues()`s Meldung fuer eine fehlende +Pflicht-Collection nannte immer `source`, unabhaengig davon, welcher Typ tatsaechlich fehlte - +jetzt benennt sie den Typ, den `stack_required_collection_owners()` tatsaechlich dafuer +verantwortlich macht. `kb/entities/COLLECTION.md` traegt jetzt einen `gtd:`-Block (vorerst nur +`see-also`), ohne den keine Kante von einer Entity auf ein Vorhaben autorisierbar waere - das ist +der Block, auf den #118 wartet. + +--- + ## 6.2.0 - 2026-09-19 - Entity-Subtyp project nach codebase umbenannt **Author:** Torben Nehmer diff --git a/README.md b/README.md index bffd06f..cbd41a8 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ chemenu/ │ ├── concept.md # Concept type config + template (+ .guidance.md) │ ├── source.md # Source type config + template (+ .guidance.md) │ ├── comparison.md # Comparison type config + template (+ .guidance.md) +│ ├── project.md # Project (Vorhaben) type config + template, no guidance file │ ├── instruction.md # Instruction type - lives outside kb/ via `root: repo` │ └── lint-report.md # Contract-only: describes reports/, owns no directory ├── kb/ # OUTPUT: compiled knowledge. A namespace, not a collection @@ -103,7 +104,7 @@ chemenu/ │ ├── log.md # Generated chronological audit log │ ├── provenance.md # Generated raw-file reverse index │ ├── entities/ # COLLECTION.md + INDEX.md + areas below -│ │ ├── projects/ +│ │ ├── codebases/ │ │ ├── systems/ │ │ ├── tools/ # own INDEX.md once past 50 pages │ │ ├── technologies/ @@ -123,7 +124,11 @@ chemenu/ │ │ ├── notes/ │ │ ├── trackers/ │ │ └── unclassified/ -│ └── comparisons/ # COLLECTION.md - comparison pages, no subtype axis +│ ├── comparisons/ # COLLECTION.md - comparison pages, no subtype axis +│ └── gtd/ # COLLECTION.md + INDEX.md + areas below +│ ├── haus/ +│ ├── finanzen/ +│ └── technik/ ├── work/ # WORKSHOP: one directory per multi-session run, tracked │ └── CONTRACT.md # Run keys, required files, how a run closes ├── reports/ # DERIVED: lint reports, traces, eval scores. Gitignored @@ -467,6 +472,7 @@ The LLM will create and maintain: - Entity pages in `kb/entities/` - Concept pages in `kb/concepts/` - Comparison pages in `kb/comparisons/` +- Project (Vorhaben) pages in `kb/gtd/` - Lint reports, session traces and eval scores in `reports/` (gitignored) ## Changelog diff --git a/VERSION b/VERSION index 6abaeb2..97f8009 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.2.0 +7.0.0-beta.1 diff --git a/kb/CONTRACT.md b/kb/CONTRACT.md index 2e3e86d..5467c4d 100644 --- a/kb/CONTRACT.md +++ b/kb/CONTRACT.md @@ -80,12 +80,13 @@ resolves against it by name. | Collection | Holds | Contract | |------------|-------|----------| -| `kb/entities/` | Concrete things: projects, deployed systems, tools, technologies, people | [entities/COLLECTION.md](entities/COLLECTION.md) | +| `kb/entities/` | Concrete things: codebases, deployed systems, tools, technologies, people | [entities/COLLECTION.md](entities/COLLECTION.md) | | `kb/concepts/` | Architectures, patterns, protocols, workflows, decisions, recurring problems | [concepts/COLLECTION.md](concepts/COLLECTION.md) | | `kb/sources/` | One summary page per ingested source, carrying its `raw_files:` provenance | [sources/COLLECTION.md](sources/COLLECTION.md) | | `kb/comparisons/` | Structured comparisons of two or more existing pages | [comparisons/COLLECTION.md](comparisons/COLLECTION.md) | +| `kb/gtd/` | One page per committed initiative (a GTD project): goal, participants, durable status, open loops | [gtd/COLLECTION.md](gtd/COLLECTION.md) | -The four rows above are this instance's collections, not a fixed set. **Adding one:** +The five rows above are this instance's collections, not a fixed set. **Adding one:** `mkdir kb/` and write a `kb//COLLECTION.md` with the two fields above. Collections are discovered by contract presence, so no code change is needed. A collection only becomes *writable* once some type-spec declares a matching `base_dir:`. Renaming or dropping one is the diff --git a/kb/CONVENTIONS.md b/kb/CONVENTIONS.md index 4d9d4d0..6cbca4e 100644 --- a/kb/CONVENTIONS.md +++ b/kb/CONVENTIONS.md @@ -42,8 +42,9 @@ those regions and nothing else. Nothing matches on this text. Pages are written in **German** - the `language:` in this file's own frontmatter, and the one place that value is written down. This binds `kb/`, and inside the page type-specs -(`types/entity.md`, `types/concept.md`, `types/source.md`, `types/comparison.md`) exactly the -parts that become page text: each one's `## Template` block, and the `layout:` titles that head a +(`types/entity.md`, `types/concept.md`, `types/source.md`, `types/comparison.md`, +`types/project.md`) exactly the parts that become page text: each one's `## Template` block, and +the `layout:` titles that head a catalog section. Their authoring guidance around those is instruction to an agent, so it follows the control plane and stays English - the same prose/identifier cut [kb/CONTRACT.md](CONTRACT.md#language-and-identifiers) makes inside a page, applied one level up. diff --git a/kb/entities/COLLECTION.md b/kb/entities/COLLECTION.md index 484cd01..ec488bb 100644 --- a/kb/entities/COLLECTION.md +++ b/kb/entities/COLLECTION.md @@ -5,6 +5,7 @@ outbound: concepts: [implements, exemplifies, rests-on, applies-when, operates-on, invokes, authored, alternative-to, see-also] sources: [evidenced-by, defined-in, see-also] comparisons: [compares-with, see-also] + gtd: [see-also] required_by_stack: false --- diff --git a/kb/gtd/COLLECTION.md b/kb/gtd/COLLECTION.md new file mode 100644 index 0000000..be76a9c --- /dev/null +++ b/kb/gtd/COLLECTION.md @@ -0,0 +1,78 @@ +--- +profile: none +outbound: + entities: [see-also] + concepts: [see-also] + sources: [see-also] + gtd: [see-also] +required_by_stack: true +--- + +# kb/gtd/ - Collection Contract + +One page per committed initiative (a project in the GTD sense): the goal, the participants, the +durable status, the open loops. This is the half of Muster 4 that `kb/` owns - the other half, +the moment-to-moment task list, lives in the task tracker and is joined to a page here only by +name (`AGENTS.md` invariant 8, § "Two truths about status are forbidden"). + +**Quality goal:** a page here should still make sense once the initiative is over. A reader +should come away knowing what was attempted, who was in it, what was decided, and what was +learned - not a snapshot of what was still open at some point in time. + +Inherits [kb/CONTRACT.md](../CONTRACT.md) for the rules the stack enforces - linking mechanics, +provenance, citation, the confidence machinery - and +[kb/CONVENTIONS.md](../CONVENTIONS.md) for what this instance decided: language, naming forms, +tone, relationship labels, the hedging rule. Neither is restated here. + +**This collection is `required_by_stack`.** A type-spec declaring `name: project` whose schema +requires `state:` must exist (`types/type-spec.md` § "What the stack still requires of the type +layer"), and `kb/gtd/` is whichever collection that type writes into - derived, not hardcoded, so +renaming it stays consistent instead of tripping a stale name. + +## Types offered + +`project` (`tools/wikitool types describe project`). The `responsibility:` field selects the +area: + +| Area | Holds | +|------|-------| +| `haus/` | Household initiatives | +| `finanzen/` | Financial initiatives | +| `technik/` | Technical initiatives outside any tracked codebase's own scope | + +These are areas, not collections: they inherit this contract and carry no `COLLECTION.md` of +their own. The initial three values are this instance's own starting vocabulary +(`types/project.md` § Frontmatter) - not a stack requirement, and free to extend. + +## Two rules unique to this collection + +- **`## Status` is durable, never a momentary state (D7).** The page never summarizes the task + list. "Pilotbetrieb seit 2026-03, zwei Abteilungen angebunden" is a status; "warte auf + Freigabe" is a tracker state and does not belong here. The join between a page and its tracker + project happens at read time, over the normalized title, and is never stored. +- **`## Beteiligte` carries mentions, not links (D28).** One to two lines per person, in prose, + with no `[[wikilink]]` and no page of their own. This is a deliberate, named exception to + `kb/CONTRACT.md` § "Every page should" - a project page with unlinked people in its + `## Beteiligte` section is conforming, not incomplete. A person earns their own page, and the + mention becomes an edge, only once they matter for the knowledge independent of this one + initiative. + +## Authorised labels + +Only `see-also` is authorised in every direction for now. The vocabulary a participation edge +(person -> project) would use is a deliberate later addition, not an oversight - adding it is a +collection-contract change made when that label exists, not a way around a refusal. + +## Outbound linking + +A project page links to the entities and concepts its initiative actually touches - the codebase +it ships, the system it changes, the concept it applies - and to other project pages it depends +on or was split from. + +## What does not belong here + +- A summary of the tracker's current task list. The tracker owns tasks; this page owns the + initiative's durable memory. +- A person's own page reached from `## Beteiligte` - see above. +- An initiative's *artifact* - the codebase, system or tool it is about. That is `entity` + (`kb/entities/COLLECTION.md`), a different page under a different type. diff --git a/kb/gtd/INDEX.md b/kb/gtd/INDEX.md new file mode 100644 index 0000000..9685323 --- /dev/null +++ b/kb/gtd/INDEX.md @@ -0,0 +1,6 @@ + + +# kb/gtd/ - Index + +0 page(s). Regenerated by `wikitool index rebuild`. + diff --git a/kb/index.md b/kb/index.md index d8b9607..96b0acb 100644 --- a/kb/index.md +++ b/kb/index.md @@ -17,6 +17,7 @@ The page tables live in a generated `INDEX.md` inside each collection, linked be - **Comparisons:** 1 - **Concepts:** 80 - **Entities:** 72 +- **Gtd:** 0 - **Sources:** 29 - **Last Updated:** 2026-09-19 @@ -29,6 +30,7 @@ The page tables live in a generated `INDEX.md` inside each collection, linked be | `comparisons/` | 1 | [comparisons/INDEX.md](comparisons/INDEX.md) | | `concepts/` | 80 | [concepts/INDEX.md](concepts/INDEX.md) | | `entities/` | 72 | [entities/INDEX.md](entities/INDEX.md) | +| `gtd/` | 0 | [gtd/INDEX.md](gtd/INDEX.md) | | `sources/` | 29 | [sources/INDEX.md](sources/INDEX.md) | ### concepts/ diff --git a/tools/CONTRACT.md b/tools/CONTRACT.md index ad0cb22..3a3c41c 100644 --- a/tools/CONTRACT.md +++ b/tools/CONTRACT.md @@ -170,7 +170,7 @@ 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, no `SKILL.md` carries a relative markdown link (`sync` copies it to a different depth than the source, so a `SKILL.md` references a target as a repo-root-relative plain path instead - see [instructions/CONTRACT.md](../instructions/CONTRACT.md) § "A skill's outbound reference is a plain path, not a link"), 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 in this file's own § Commands table and, separately, in its § Error contracts table (both directions, checked per table, so a row dropped from one is not hidden by the same name surviving in the other, and only a name's presence in a row is checked, never the rest of that row's text), 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, every file under `types/` declaring `type: types/type-spec.md` validating against `types/type-spec.schema.yaml`, no pre-migration `type: entity` blocks left in the contracts, the `.gitignore` canaries clear in both directions (nothing ignored under `raw/`/`kb/`, `incoming/` ignored, everything ignored under `reports/` and the published skill directories), and no `.md`/`.template` file `dist export` would ship citing an issue number - the tracker exists only in the origin repo, so such a number in a distributed instance is a reference its reader can neither resolve nor recognise as unresolvable (a `` region is exempt: it is already gone from the text the check reads, which is the export plan's, not the working tree's), every reference file `docs toc` covers carrying the current table-of-contents region for its own headings - missing and stale are one check, because the generator is idempotent - and every relative markdown link in one of those same reference files resolving to a file that actually exists (a target's `#anchor` suffix is stripped first; code fences and inline code spans are masked before scanning, so a passage showing link syntax as an example is not mistaken for a real reference). 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 | +| `docs verify` | Check the docs that mirror the code: every CLI command documented in this file's own § Commands table and, separately, in its § Error contracts table (both directions, checked per table, so a row dropped from one is not hidden by the same name surviving in the other, and only a name's presence in a row is checked, never the rest of that row's text), 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, every type the stack lists (currently `source` and `project`) having a type-spec of that name whose schema requires the field the stack list also names (`raw_files:`/`state:`), `kb/CONVENTIONS.md` naming all three tool-owned section headings if it exists at all, every stage contract present, every file under `types/` declaring `type: types/type-spec.md` validating against `types/type-spec.schema.yaml`, no pre-migration `type: entity` blocks left in the contracts, the `.gitignore` canaries clear in both directions (nothing ignored under `raw/`/`kb/`, `incoming/` ignored, everything ignored under `reports/` and the published skill directories), and no `.md`/`.template` file `dist export` would ship citing an issue number - the tracker exists only in the origin repo, so such a number in a distributed instance is a reference its reader can neither resolve nor recognise as unresolvable (a `` region is exempt: it is already gone from the text the check reads, which is the export plan's, not the working tree's), every reference file `docs toc` covers carrying the current table-of-contents region for its own headings - missing and stale are one check, because the generator is idempotent - and every relative markdown link in one of those same reference files resolving to a file that actually exists (a target's `#anchor` suffix is stripped first; code fences and inline code spans are masked before scanning, so a passage showing link syntax as an example is not mistaken for a real reference). 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 | | `docs toc [--apply]` | Create, refresh or remove the generated table-of-contents region (`` ... ``, placed after the title and before the first `##`) on every reference file over 100 lines, in the scope Anthropic's skill-authoring guidance names for a file previewed rather than read in full: `AGENTS.md`, every stage contract, `kb/CONVENTIONS.md`, every `kb/*/COLLECTION.md`, every flat `instructions/**.md` file, every `types/*.md` type-spec, and every `docs/` page - each together with the `.template` it ships as, where one exists. Computed from those categories rather than listed, so a file added later is in scope without a code change. A template is in scope because it is the same document one step earlier in its life: an instance adopts it by copying it back, so a region missing there is a region missing in the adopted file, which is how `kb/CONVENTIONS.md.template` came to grow past the threshold with no region and left every instance adopting it failing `docs verify` at the end of its own setup. `SKILL.md` is the one exception, and the same guidance is why: it places a skill body on the loading level that is read whole when the skill triggers, and aims its own TOC advice at the bundled reference files a skill points *at*. Human docs (`README.md`, `CHANGES.md`, `EVALS.md`, `INSTALL.md`, `tools/README.md`) are out of scope because AGENTS.md § File naming says no agent loads them as instruction. Dry-run by default (prints which files would change); `--apply` writes. `docs verify` checks the result stays current the same way it checks every other generated-from-code copy | ### Telemetry diff --git a/tools/chemenu/kb_collections.py b/tools/chemenu/kb_collections.py index 5330646..f8e02f0 100644 --- a/tools/chemenu/kb_collections.py +++ b/tools/chemenu/kb_collections.py @@ -58,25 +58,35 @@ ANY_DESTINATION = "any" # and its schema requiring `raw_files:` - not the directory, not the title # prefix, and not a word of its prose or its template. # -# That is the whole anchor, and it is deliberately this small: the four page +# `project` is here for the same reason, one layer up: the weekly review +# (`wikitool review`) asks `page.kind == "project"` and reads `state:` to tell +# an ongoing initiative with no next action ("stalled") from one that is +# `dormant`, `completed` or `abandoned` on purpose. Without a `project` type +# declaring `state:` the review has nothing to join a tracker project against. +# +# That is the whole anchor, and it is deliberately this small: the page # type-specs belong to the instance (see types/type-spec.md), so anything more # would be the stack reaching into a file it does not own. -STACK_REQUIRED_TYPES = ("source",) -STACK_REQUIRED_TYPE_FIELDS = {"source": ("raw_files",)} +STACK_REQUIRED_TYPES = ("source", "project") +STACK_REQUIRED_TYPE_FIELDS = {"source": ("raw_files",), "project": ("state",)} -def stack_required_collections() -> tuple[str, ...]: - """Collection names an instance may not rename or drop. +def stack_required_collection_owners() -> dict[str, str]: + """Which stack-required type writes into each stack-required collection - + `{collection name: type name}`. **Derived, not listed.** The required collection is whichever one the required type writes into - so an instance that legitimately renames `kb/sources/` to something else, and says so in the type-spec's `base_dir:`, stays consistent instead of tripping a constant that hardcoded the old name. - A second literal list would only be a copy that drifts. + A second literal list would only be a copy that drifts. The one-to-many + direction (several required types sharing a collection) picks the first + type in `STACK_REQUIRED_TYPES` that claims it - two required types + legitimately sharing one `base_dir:` is not a case that has come up. """ from chemenu.type_resolver import resolver - names: list[str] = [] + owners: dict[str, str] = {} for type_name in STACK_REQUIRED_TYPES: try: type_path = resolver.find_type_by_name(type_name) @@ -88,8 +98,14 @@ def stack_required_collections() -> tuple[str, ...]: except (ValueError, OSError): continue if base_dir: - names.append(str(base_dir).strip("/")) - return tuple(dict.fromkeys(names)) + owners.setdefault(str(base_dir).strip("/"), type_name) + return owners + + +def stack_required_collections() -> tuple[str, ...]: + """Collection names an instance may not rename or drop - see + `stack_required_collection_owners()`, which this derives from.""" + return tuple(stack_required_collection_owners().keys()) def iter_kb_collections(kb_dir: Path | None = None) -> list[Path]: @@ -226,15 +242,15 @@ def declaration_issues(kb_dir: Path | None = None) -> list[str]: root = kb_dir if kb_dir is not None else config.KB_DIR issues: list[str] = [] - required = stack_required_collections() + owners = stack_required_collection_owners() + required = tuple(owners.keys()) can_label = collections_that_can_carry_labelled_edges() present = {path.name for path in iter_kb_collections(root)} for name in required: if name not in present: issues.append( - f"kb/{name}/ is missing - it is where the stack-required `source` type writes, " - f"and `sources coverage`, `[^cite-id]` resolution and `kb/provenance.md` all " - f"depend on those pages existing" + f"kb/{name}/ is missing - it is where the stack-required `{owners[name]}` type " + f"writes, and wikitool depends on that collection existing by name" ) for collection in iter_kb_collections(root): diff --git a/tools/chemenu/tests/test_conventions.py b/tools/chemenu/tests/test_conventions.py index 10e9d13..cf8a07f 100644 --- a/tools/chemenu/tests/test_conventions.py +++ b/tools/chemenu/tests/test_conventions.py @@ -41,10 +41,11 @@ def kb_root(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: kb.mkdir() monkeypatch.setattr(config, "ROOT", tmp_path) monkeypatch.setattr(config, "KB_DIR", kb) - # Which collection the stack requires is *derived* from where the required - # `source` type writes, so these tests need the shipped `types/` reachable - - # a fixture tree without one derives an empty requirement and would assert - # against a rule that is not running. See conftest.use_shipped_type_specs. + # Which collections the stack requires are *derived* from where each + # required type (`source`, `project`) writes, so these tests need the + # shipped `types/` reachable - a fixture tree without one derives an empty + # requirement and would assert against a rule that is not running. See + # conftest.use_shipped_type_specs. use_shipped_type_specs(monkeypatch) conventions.reset_cache() yield kb @@ -153,6 +154,7 @@ def test_a_missing_stack_required_collection_is_reported(kb_root): def test_a_correct_declaration_reports_nothing(kb_root): _collection(kb_root, "sources", profile="sources", required=True) + _collection(kb_root, "gtd", profile="none", required=True) _collection(kb_root, "entities", profile="entities") assert kb_collections.declaration_issues(kb_root) == [] @@ -210,6 +212,7 @@ def test_outbound_on_a_collection_that_cannot_carry_labels_is_a_finding(kb_root) def test_outbound_is_fine_on_a_collection_whose_type_offers_related(kb_root): _collection(kb_root, "sources", profile="sources", required=True) + _collection(kb_root, "gtd", profile="none", required=True) _authorising(kb_root, "entities", " any: [uses]") assert kb_collections.declaration_issues(kb_root) == [] @@ -217,4 +220,5 @@ def test_outbound_is_fine_on_a_collection_whose_type_offers_related(kb_root): def test_a_collection_without_outbound_is_not_a_finding(kb_root): """Absence is the declaration `kb/sources/` makes: no authored edges here.""" _collection(kb_root, "sources", profile="sources", required=True) + _collection(kb_root, "gtd", profile="none", required=True) assert kb_collections.declaration_issues(kb_root) == [] diff --git a/tools/chemenu/tests/test_docs_verify.py b/tools/chemenu/tests/test_docs_verify.py index f15cc91..eb15da4 100644 --- a/tools/chemenu/tests/test_docs_verify.py +++ b/tools/chemenu/tests/test_docs_verify.py @@ -335,6 +335,77 @@ def test_an_unknown_type_spec_field_is_reported(tmp_path, monkeypatch): assert any("widget.md" in issue and "not_a_real_field" in issue for issue in issues) +def _stack_required_type_tree(tmp_path, monkeypatch, project_md: str = "", project_schema: str = ""): + """A `types/` fixture carrying a valid `source` type-spec (copied from the + real repo, so it never drifts from what `docs verify` actually enforces) + plus whatever `project.md`/`project.schema.yaml` the caller supplies - + empty strings mean "write nothing", so a caller can exercise the + type-missing case. `check_stack_required_types()` (Gitea #123) has no + dedicated coverage otherwise: it is only ever exercised indirectly, via a + full `verify()` run against the real repo tree.""" + types_dir = tmp_path / "types" + types_dir.mkdir() + for name in ("type-spec.md", "type-spec.schema.yaml", "source.md", "source.schema.yaml"): + (types_dir / name).write_text( + (config.ROOT / "types" / name).read_text(encoding="utf-8"), encoding="utf-8" + ) + if project_md: + (types_dir / "project.md").write_text(project_md, encoding="utf-8") + if project_schema: + (types_dir / "project.schema.yaml").write_text(project_schema, encoding="utf-8") + monkeypatch.setattr(config, "ROOT", tmp_path) + monkeypatch.setattr(config, "TYPES_DIR", types_dir) + monkeypatch.setattr(type_resolver, "resolver", TypeResolver(repo_root=tmp_path)) + + +_PROJECT_MD = ( + "---\n" + "type: types/type-spec.md\n" + "name: project\n" + "description: Fixture project type\n" + "schema: types/project.schema.yaml\n" + "---\n" +) + + +def test_check_stack_required_types_reports_a_missing_project_type(tmp_path, monkeypatch): + """No `project.md` at all - the type-missing case, worded to name the + type that is missing.""" + _stack_required_type_tree(tmp_path, monkeypatch) + issues = docs_verify.check_stack_required_types() + assert any("name: project" in issue for issue in issues) + + +def test_check_stack_required_types_reports_project_schema_missing_state(tmp_path, monkeypatch): + """A `project` type-spec exists, but its schema does not require `state:` + - the field-missing case, distinct from the type-missing one above.""" + schema = ( + "type: object\n" + "properties:\n" + " type: {type: string, const: 'types/project.md'}\n" + " state: {type: string, enum: [active, dormant, completed, abandoned]}\n" + "required: [type]\n" + "additionalProperties: false\n" + ) + _stack_required_type_tree(tmp_path, monkeypatch, project_md=_PROJECT_MD, project_schema=schema) + issues = docs_verify.check_stack_required_types() + assert any("state" in issue and "project.md" in issue for issue in issues) + + +def test_check_stack_required_types_passes_when_both_types_satisfy_their_field(tmp_path, monkeypatch): + """`source`/`raw_files:` and `project`/`state:` both satisfied - no issues.""" + schema = ( + "type: object\n" + "properties:\n" + " type: {type: string, const: 'types/project.md'}\n" + " state: {type: string, enum: [active, dormant, completed, abandoned]}\n" + "required: [type, state]\n" + "additionalProperties: false\n" + ) + _stack_required_type_tree(tmp_path, monkeypatch, project_md=_PROJECT_MD, project_schema=schema) + assert docs_verify.check_stack_required_types() == [] + + def test_legacy_type_blocks_are_absent(): assert docs_verify.check_legacy_type_blocks() == [] diff --git a/tools/chemenu/tests/test_kb_collections.py b/tools/chemenu/tests/test_kb_collections.py index aa2bba2..e97b383 100644 --- a/tools/chemenu/tests/test_kb_collections.py +++ b/tools/chemenu/tests/test_kb_collections.py @@ -2,7 +2,8 @@ from pathlib import Path import pytest -from chemenu import config, kb_collections +from chemenu import config, kb_collections, type_resolver +from chemenu.type_resolver import TypeResolver @pytest.fixture @@ -65,3 +66,37 @@ def test_vendored_commonplace_contracts_are_ignored(repo): def test_a_clean_tree_has_no_strays(repo): assert kb_collections.stray_collection_contracts() == [] + + +def test_stack_required_collections_includes_gtd_once_project_type_exists(repo, monkeypatch): + """`stack_required_collections()` derives from wherever the required types + write, so `project`'s `base_dir: gtd` makes `gtd` required the moment that + type-spec exists - the same derivation `source` already gets from writing + to `sources` (Gitea #123).""" + real_types_dir = Path(__file__).resolve().parents[3] / "types" + types_dir = repo / "types" + for name in ("type-spec.md", "type-spec.schema.yaml"): + (types_dir / name).write_text( + (real_types_dir / name).read_text(encoding="utf-8"), encoding="utf-8" + ) + (types_dir / "project.md").write_text( + "---\n" + "type: types/type-spec.md\n" + "name: project\n" + "description: Fixture project type\n" + "schema: null\n" + "base_dir: gtd\n" + "---\n", + encoding="utf-8", + ) + monkeypatch.setattr(config, "TYPES_DIR", types_dir) + monkeypatch.setattr(type_resolver, "resolver", TypeResolver(repo_root=repo)) + assert "gtd" in kb_collections.stack_required_collections() + + +def test_stack_required_collections_omits_a_type_with_no_type_spec(repo, monkeypatch): + """No `project.md` at all - the required-types derivation must tolerate a + missing type rather than raising, so a corpus mid-adoption still lints.""" + monkeypatch.setattr(config, "TYPES_DIR", repo / "types") + monkeypatch.setattr(type_resolver, "resolver", TypeResolver(repo_root=repo)) + assert "gtd" not in kb_collections.stack_required_collections() diff --git a/tools/chemenu/tests/test_new_page.py b/tools/chemenu/tests/test_new_page.py index 82866a0..e6d2f1b 100644 --- a/tools/chemenu/tests/test_new_page.py +++ b/tools/chemenu/tests/test_new_page.py @@ -77,6 +77,33 @@ def test_new_entity_creates_page_with_expected_frontmatter(monkeypatch, kb_dir): assert "# gateway.example.net" in body +def test_new_project_creates_page_with_expected_frontmatter(monkeypatch, kb_dir): + """Gitea #123: `state:` is required with a schema `default: active`, so it + must materialize even though the caller never sets it - the same rule + `--set entity_type=...`'s required fields already follow.""" + result = _invoke_new(monkeypatch, kb_dir, [ + "new", "project", "--name", "Testvorhaben", "--set", "responsibility=haus", + ]) + assert result.exit_code == 0, result.output + path = kb_dir / "gtd/haus/Testvorhaben.md" + assert path.exists() + fm, body = read_page(path) + assert fm["type"] == "types/project.md" + assert fm["state"] == "active" + assert fm["responsibility"] == "haus" + assert "# Testvorhaben" in body + for heading in ("Ziel", "Kontext", "Beteiligte", "Status", "Entscheidungen", "Gelerntes"): + assert f"## {heading}" in body + + +def test_new_project_refuses_a_responsibility_outside_the_enum(monkeypatch, kb_dir): + result = _invoke_new(monkeypatch, kb_dir, [ + "new", "project", "--name", "Badvorhaben", "--set", "responsibility=nichtexistent", + ]) + assert result.exit_code == 1 + assert not list(kb_dir.rglob("Badvorhaben.md")) + + def test_new_entity_still_materializes_empty_arrays_for_unset_optional_fields(monkeypatch, kb_dir): """Gitea #109 stops materializing an optional field's schema `default:`, but `tags`/`related`/`sources` are optional arrays with no `default:` at diff --git a/tools/chemenu/tests/test_type_resolver.py b/tools/chemenu/tests/test_type_resolver.py index 1cf6ce4..04549e6 100644 --- a/tools/chemenu/tests/test_type_resolver.py +++ b/tools/chemenu/tests/test_type_resolver.py @@ -268,8 +268,8 @@ def test_find_type_by_name_resolves_short_names(): def test_list_type_specs_finds_every_type_spec(): names = {fm.get("name") for _, fm in resolver.list_type_specs()} assert names == { - "type-spec", "entity", "concept", "source", "comparison", "lint-report", "instruction", - "type-guidance", + "type-spec", "entity", "concept", "source", "comparison", "project", "lint-report", + "instruction", "type-guidance", } diff --git a/tools/chemenu/tests/test_types_cmd.py b/tools/chemenu/tests/test_types_cmd.py index 4c5fd84..83f977d 100644 --- a/tools/chemenu/tests/test_types_cmd.py +++ b/tools/chemenu/tests/test_types_cmd.py @@ -15,8 +15,8 @@ def test_types_list_finds_all_current_type_specs(): rows = json.loads(result.output) names = {row["name"] for row in rows} assert names == { - "type-spec", "entity", "concept", "source", "comparison", "lint-report", "instruction", - "type-guidance", + "type-spec", "entity", "concept", "source", "comparison", "project", "lint-report", + "instruction", "type-guidance", } diff --git a/types/project.md b/types/project.md new file mode 100644 index 0000000..652c42e --- /dev/null +++ b/types/project.md @@ -0,0 +1,100 @@ +--- +type: types/type-spec.md +name: project +description: Structured type for a committed initiative (a GTD project) - goal, participants, durable status, open loops +schema: types/project.schema.yaml +subtype_field: responsibility +base_dir: gtd +page_ref_fields: [related, sources] +layout: + haus: {dir: haus, title: Haus} + finanzen: {dir: finanzen, title: Finanzen} + technik: {dir: technik, title: Technik} +--- + +# Project + +This instance's configuration for the `project` type: its frontmatter fields as this schema +requires them, and the page skeleton `wikitool new project` scaffolds. `project` is instance-owned +end to end - it declares no `guidance:`, so this file's own prose (below) carries its authoring +contract, the same way a type an instance invents for itself always does. +## When to use + +- A committed initiative in the GTD sense: something with a goal, an end state, and open loops - + a household project, a piece of paid work, a piece of volunteer work. + +## When NOT to use + +- **The artifact the initiative is about.** `project` names the *Vorhaben* - the undertaking - not + the thing it produces or touches. A codebase, a deployed system, a tool: that is `entity` with + `entity_type: codebase` (or `system`/`tool`/`technology`). "Ship Chemenu 7.0" is a `project`; + Chemenu the codebase is an `entity`. The two are homonyms, not the same page under two types - + see `kb/entities/COLLECTION.md` for the artifact side of that line. +- A bare task or a day-to-day commitment with no project shape of its own - that lives in the + task provider, not here (`kb/` never mirrors the tracker's momentary state, see below). + +## Frontmatter + +| Field | Required | Use | +|---|---:|---| +| `type` | Yes | `types/project.md` | +| `state` | Yes | One of: active, dormant, completed, abandoned - a durable characterization, not the tracker's momentary state (see `## Status` below) | +| `responsibility` | Yes | `subtype_field`, one of: haus, finanzen, technik - selects the area under `kb/gtd/` | +| `created` / `modified` | Yes | As everywhere | +| `related` | No | Titles of related pages (entities, concepts, sources, other projects) | +| `sources` | No | Titles of the source pages backing this page's claims | +| `provenance` | Yes | sourced, general or mixed - `general` is the common case: a project page is usually written by the operator from their own participation, not from an ingested source | +| `summary` | Yes | One-liner for `kb/index.md` | + +## Authoring guidance + +- **`## Status` is a durable characterization, never a momentary state.** "Pilotbetrieb seit + 2026-03, zwei Abteilungen angebunden" - not "warte auf Freigabe". The page never summarizes the + task list; the momentary state lives in the tracker, and the two are joined only by name, never + synced (`AGENTS.md` invariant 8 - one truth about "status"). +- **`## Beteiligte` carries mentions, not links.** One to two lines per person, in prose, with + **no `[[wikilink]]` and no page of their own.** A person earns a page only once they matter for + the *knowledge*, independent of this project - at that point the mention becomes an edge with a + participation label. This is a deliberate, named exception to `kb/CONTRACT.md` § "Every page + should" (link to the entities it mentions): it is held by this rule, not by a checker, so read + a page here with no person-links as conforming, not as incomplete. + +## Template + +The block below is page material, so it is written in this instance's KB language +(`kb/CONVENTIONS.md` `language:`) rather than in the control plane's English - its headings +become the headings of every page `wikitool new project` scaffolds. + +```markdown +# {name} + +**Status:** {state|capitalize} +**Bereich:** {responsibility|capitalize} + +## Ziel + +TODO: Was ist der angestrebte Endzustand? Woran erkennt man, dass dieses Vorhaben abgeschlossen ist? + +## Kontext + +TODO: Warum jetzt, warum überhaupt - der Hintergrund, der nicht aus dem Titel folgt. + +## Beteiligte + +TODO: Ein bis zwei Zeilen je Person - Erwähnung im Fließtext, kein Wikilink, keine eigene Seite. + +## Status + +TODO: Dauerhafte Charakterisierung des aktuellen Stands - keine Aufgabenliste, kein Momentzustand. + +## Entscheidungen + +TODO: Festlegungen, die dieses Vorhaben getroffen hat, und warum. + +## Gelerntes + +TODO: Was sich im Verlauf gezeigt hat. +``` + +The value behind `**Status:**` and `**Bereich:**` stays the English enum value - that is what +`search --field` filters on. diff --git a/types/project.schema.yaml b/types/project.schema.yaml new file mode 100644 index 0000000..0335e4e --- /dev/null +++ b/types/project.schema.yaml @@ -0,0 +1,78 @@ +# YAML Schema for project type +type: object +properties: + type: + type: string + const: "types/project.md" + description: Must reference the project type-spec + state: + type: string + enum: [active, dormant, completed, abandoned] + default: active + description: >- + Durable characterization of this initiative, not the tracker's momentary + state. `active` and `dormant` both mean the initiative is ongoing - + `dormant` is a deliberate pause, so the weekly review does not flag it as + stalled the way an `active` project with no next action is flagged. + `completed` and `abandoned` both mean it is over; the distinction is for + the record, not for review behaviour. + responsibility: + type: string + enum: [haus, finanzen, technik] + description: >- + Which area of responsibility this initiative belongs to. No default - + the machine must not pick one on the author's behalf. Must match a key + in this type-spec's own `layout:`, or placement falls back to a naively + pluralized directory with no catalog title (see `types/type-spec.md` + § Anatomy of a type). + created: + type: string + format: date + pattern: "^\\d{4}-\\d{2}-\\d{2}$" + description: Creation date in YYYY-MM-DD format + modified: + type: string + format: date + pattern: "^\\d{4}-\\d{2}-\\d{2}$" + description: Last modification date in YYYY-MM-DD format + related: + type: array + items: + oneOf: + - type: string + - type: object + minProperties: 1 + maxProperties: 1 + additionalProperties: + type: string + description: >- + Declared outbound edges. Each entry is either `