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
This commit is contained in:
1 parent
3c9d669729
commit
ee24b6e5b8
20 files changed
+507
-38
No files matched your search
+37
@@ -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
|
||||||
|
|
||||||
|
<!-- wikitool:bumps -->
|
||||||
|
- Typ `project` und Collection `kb/gtd/`: das Vorhaben als eigene Seitenart
|
||||||
|
<!-- /wikitool:bumps -->
|
||||||
|
|
||||||
|
### 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
|
## 6.2.0 - 2026-09-19 - Entity-Subtyp project nach codebase umbenannt
|
||||||
|
|
||||||
**Author:** Torben Nehmer
|
**Author:** Torben Nehmer
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ chemenu/
|
|||||||
│ ├── concept.md # Concept type config + template (+ .guidance.md)
|
│ ├── concept.md # Concept type config + template (+ .guidance.md)
|
||||||
│ ├── source.md # Source type config + template (+ .guidance.md)
|
│ ├── source.md # Source type config + template (+ .guidance.md)
|
||||||
│ ├── comparison.md # Comparison 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`
|
│ ├── instruction.md # Instruction type - lives outside kb/ via `root: repo`
|
||||||
│ └── lint-report.md # Contract-only: describes reports/, owns no directory
|
│ └── lint-report.md # Contract-only: describes reports/, owns no directory
|
||||||
├── kb/ # OUTPUT: compiled knowledge. A namespace, not a collection
|
├── kb/ # OUTPUT: compiled knowledge. A namespace, not a collection
|
||||||
@@ -103,7 +104,7 @@ chemenu/
|
|||||||
│ ├── log.md # Generated chronological audit log
|
│ ├── log.md # Generated chronological audit log
|
||||||
│ ├── provenance.md # Generated raw-file reverse index
|
│ ├── provenance.md # Generated raw-file reverse index
|
||||||
│ ├── entities/ # COLLECTION.md + INDEX.md + areas below
|
│ ├── entities/ # COLLECTION.md + INDEX.md + areas below
|
||||||
│ │ ├── projects/
|
│ │ ├── codebases/
|
||||||
│ │ ├── systems/
|
│ │ ├── systems/
|
||||||
│ │ ├── tools/ # own INDEX.md once past 50 pages
|
│ │ ├── tools/ # own INDEX.md once past 50 pages
|
||||||
│ │ ├── technologies/
|
│ │ ├── technologies/
|
||||||
@@ -123,7 +124,11 @@ chemenu/
|
|||||||
│ │ ├── notes/
|
│ │ ├── notes/
|
||||||
│ │ ├── trackers/
|
│ │ ├── trackers/
|
||||||
│ │ └── unclassified/
|
│ │ └── 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
|
├── work/ # WORKSHOP: one directory per multi-session run, tracked
|
||||||
│ └── CONTRACT.md # Run keys, required files, how a run closes
|
│ └── CONTRACT.md # Run keys, required files, how a run closes
|
||||||
├── reports/ # DERIVED: lint reports, traces, eval scores. Gitignored
|
├── reports/ # DERIVED: lint reports, traces, eval scores. Gitignored
|
||||||
@@ -467,6 +472,7 @@ The LLM will create and maintain:
|
|||||||
- Entity pages in `kb/entities/`
|
- Entity pages in `kb/entities/`
|
||||||
- Concept pages in `kb/concepts/`
|
- Concept pages in `kb/concepts/`
|
||||||
- Comparison pages in `kb/comparisons/`
|
- Comparison pages in `kb/comparisons/`
|
||||||
|
- Project (Vorhaben) pages in `kb/gtd/`
|
||||||
- Lint reports, session traces and eval scores in `reports/` (gitignored)
|
- Lint reports, session traces and eval scores in `reports/` (gitignored)
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|||||||
+3
-2
@@ -80,12 +80,13 @@ resolves against it by name.
|
|||||||
|
|
||||||
| Collection | Holds | Contract |
|
| 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/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/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/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/<name>` and write a `kb/<name>/COLLECTION.md` with the two fields above. Collections
|
`mkdir kb/<name>` and write a `kb/<name>/COLLECTION.md` with the two fields above. Collections
|
||||||
are discovered by contract presence, so no code change is needed. A collection only becomes
|
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
|
*writable* once some type-spec declares a matching `base_dir:`. Renaming or dropping one is the
|
||||||
|
|||||||
+3
-2
@@ -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
|
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
|
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
|
(`types/entity.md`, `types/concept.md`, `types/source.md`, `types/comparison.md`,
|
||||||
parts that become page text: each one's `## Template` block, and the `layout:` titles that head a
|
`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
|
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
|
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.
|
[kb/CONTRACT.md](CONTRACT.md#language-and-identifiers) makes inside a page, applied one level up.
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ outbound:
|
|||||||
concepts: [implements, exemplifies, rests-on, applies-when, operates-on, invokes, authored, alternative-to, see-also]
|
concepts: [implements, exemplifies, rests-on, applies-when, operates-on, invokes, authored, alternative-to, see-also]
|
||||||
sources: [evidenced-by, defined-in, see-also]
|
sources: [evidenced-by, defined-in, see-also]
|
||||||
comparisons: [compares-with, see-also]
|
comparisons: [compares-with, see-also]
|
||||||
|
gtd: [see-also]
|
||||||
required_by_stack: false
|
required_by_stack: false
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<!-- Generated by `wikitool index rebuild`. Do not hand-edit. -->
|
||||||
|
|
||||||
|
# kb/gtd/ - Index
|
||||||
|
|
||||||
|
0 page(s). Regenerated by `wikitool index rebuild`.
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ The page tables live in a generated `INDEX.md` inside each collection, linked be
|
|||||||
- **Comparisons:** 1
|
- **Comparisons:** 1
|
||||||
- **Concepts:** 80
|
- **Concepts:** 80
|
||||||
- **Entities:** 72
|
- **Entities:** 72
|
||||||
|
- **Gtd:** 0
|
||||||
- **Sources:** 29
|
- **Sources:** 29
|
||||||
- **Last Updated:** 2026-09-19
|
- **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) |
|
| `comparisons/` | 1 | [comparisons/INDEX.md](comparisons/INDEX.md) |
|
||||||
| `concepts/` | 80 | [concepts/INDEX.md](concepts/INDEX.md) |
|
| `concepts/` | 80 | [concepts/INDEX.md](concepts/INDEX.md) |
|
||||||
| `entities/` | 72 | [entities/INDEX.md](entities/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) |
|
| `sources/` | 29 | [sources/INDEX.md](sources/INDEX.md) |
|
||||||
|
|
||||||
### concepts/
|
### concepts/
|
||||||
|
|||||||
+1
-1
@@ -170,7 +170,7 @@ tools/wikitool <command> --help
|
|||||||
| `instructions sync [--force]` | Publish every `instructions/<name>/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 sync [--force]` | Publish every `instructions/<name>/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 `<!-- dist:strip-start/end -->` 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 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 `<!-- dist:strip-start/end -->` 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 |
|
| `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 `<!-- dist:strip-start/end -->` 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 `<!-- dist:strip-start/end -->` 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 (`<!-- wikitool:toc -->` ... `<!-- /wikitool:toc -->`, 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 `<name>.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 |
|
| `docs toc [--apply]` | Create, refresh or remove the generated table-of-contents region (`<!-- wikitool:toc -->` ... `<!-- /wikitool:toc -->`, 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 `<name>.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
|
### Telemetry
|
||||||
|
|||||||
@@ -58,25 +58,35 @@ ANY_DESTINATION = "any"
|
|||||||
# and its schema requiring `raw_files:` - not the directory, not the title
|
# and its schema requiring `raw_files:` - not the directory, not the title
|
||||||
# prefix, and not a word of its prose or its template.
|
# 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
|
# 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.
|
# would be the stack reaching into a file it does not own.
|
||||||
STACK_REQUIRED_TYPES = ("source",)
|
STACK_REQUIRED_TYPES = ("source", "project")
|
||||||
STACK_REQUIRED_TYPE_FIELDS = {"source": ("raw_files",)}
|
STACK_REQUIRED_TYPE_FIELDS = {"source": ("raw_files",), "project": ("state",)}
|
||||||
|
|
||||||
|
|
||||||
def stack_required_collections() -> tuple[str, ...]:
|
def stack_required_collection_owners() -> dict[str, str]:
|
||||||
"""Collection names an instance may not rename or drop.
|
"""Which stack-required type writes into each stack-required collection -
|
||||||
|
`{collection name: type name}`.
|
||||||
|
|
||||||
**Derived, not listed.** The required collection is whichever one the
|
**Derived, not listed.** The required collection is whichever one the
|
||||||
required type writes into - so an instance that legitimately renames
|
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:`,
|
`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.
|
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
|
from chemenu.type_resolver import resolver
|
||||||
|
|
||||||
names: list[str] = []
|
owners: dict[str, str] = {}
|
||||||
for type_name in STACK_REQUIRED_TYPES:
|
for type_name in STACK_REQUIRED_TYPES:
|
||||||
try:
|
try:
|
||||||
type_path = resolver.find_type_by_name(type_name)
|
type_path = resolver.find_type_by_name(type_name)
|
||||||
@@ -88,8 +98,14 @@ def stack_required_collections() -> tuple[str, ...]:
|
|||||||
except (ValueError, OSError):
|
except (ValueError, OSError):
|
||||||
continue
|
continue
|
||||||
if base_dir:
|
if base_dir:
|
||||||
names.append(str(base_dir).strip("/"))
|
owners.setdefault(str(base_dir).strip("/"), type_name)
|
||||||
return tuple(dict.fromkeys(names))
|
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]:
|
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
|
root = kb_dir if kb_dir is not None else config.KB_DIR
|
||||||
issues: list[str] = []
|
issues: list[str] = []
|
||||||
|
|
||||||
required = stack_required_collections()
|
owners = stack_required_collection_owners()
|
||||||
|
required = tuple(owners.keys())
|
||||||
can_label = collections_that_can_carry_labelled_edges()
|
can_label = collections_that_can_carry_labelled_edges()
|
||||||
present = {path.name for path in iter_kb_collections(root)}
|
present = {path.name for path in iter_kb_collections(root)}
|
||||||
for name in required:
|
for name in required:
|
||||||
if name not in present:
|
if name not in present:
|
||||||
issues.append(
|
issues.append(
|
||||||
f"kb/{name}/ is missing - it is where the stack-required `source` type writes, "
|
f"kb/{name}/ is missing - it is where the stack-required `{owners[name]}` type "
|
||||||
f"and `sources coverage`, `[^cite-id]` resolution and `kb/provenance.md` all "
|
f"writes, and wikitool depends on that collection existing by name"
|
||||||
f"depend on those pages existing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for collection in iter_kb_collections(root):
|
for collection in iter_kb_collections(root):
|
||||||
|
|||||||
@@ -41,10 +41,11 @@ def kb_root(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
|||||||
kb.mkdir()
|
kb.mkdir()
|
||||||
monkeypatch.setattr(config, "ROOT", tmp_path)
|
monkeypatch.setattr(config, "ROOT", tmp_path)
|
||||||
monkeypatch.setattr(config, "KB_DIR", kb)
|
monkeypatch.setattr(config, "KB_DIR", kb)
|
||||||
# Which collection the stack requires is *derived* from where the required
|
# Which collections the stack requires are *derived* from where each
|
||||||
# `source` type writes, so these tests need the shipped `types/` reachable -
|
# required type (`source`, `project`) writes, so these tests need the
|
||||||
# a fixture tree without one derives an empty requirement and would assert
|
# shipped `types/` reachable - a fixture tree without one derives an empty
|
||||||
# against a rule that is not running. See conftest.use_shipped_type_specs.
|
# requirement and would assert against a rule that is not running. See
|
||||||
|
# conftest.use_shipped_type_specs.
|
||||||
use_shipped_type_specs(monkeypatch)
|
use_shipped_type_specs(monkeypatch)
|
||||||
conventions.reset_cache()
|
conventions.reset_cache()
|
||||||
yield kb
|
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):
|
def test_a_correct_declaration_reports_nothing(kb_root):
|
||||||
_collection(kb_root, "sources", profile="sources", required=True)
|
_collection(kb_root, "sources", profile="sources", required=True)
|
||||||
|
_collection(kb_root, "gtd", profile="none", required=True)
|
||||||
_collection(kb_root, "entities", profile="entities")
|
_collection(kb_root, "entities", profile="entities")
|
||||||
assert kb_collections.declaration_issues(kb_root) == []
|
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):
|
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, "sources", profile="sources", required=True)
|
||||||
|
_collection(kb_root, "gtd", profile="none", required=True)
|
||||||
_authorising(kb_root, "entities", " any: [uses]")
|
_authorising(kb_root, "entities", " any: [uses]")
|
||||||
assert kb_collections.declaration_issues(kb_root) == []
|
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):
|
def test_a_collection_without_outbound_is_not_a_finding(kb_root):
|
||||||
"""Absence is the declaration `kb/sources/` makes: no authored edges here."""
|
"""Absence is the declaration `kb/sources/` makes: no authored edges here."""
|
||||||
_collection(kb_root, "sources", profile="sources", required=True)
|
_collection(kb_root, "sources", profile="sources", required=True)
|
||||||
|
_collection(kb_root, "gtd", profile="none", required=True)
|
||||||
assert kb_collections.declaration_issues(kb_root) == []
|
assert kb_collections.declaration_issues(kb_root) == []
|
||||||
@@ -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)
|
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():
|
def test_legacy_type_blocks_are_absent():
|
||||||
assert docs_verify.check_legacy_type_blocks() == []
|
assert docs_verify.check_legacy_type_blocks() == []
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from chemenu import config, kb_collections
|
from chemenu import config, kb_collections, type_resolver
|
||||||
|
from chemenu.type_resolver import TypeResolver
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -65,3 +66,37 @@ def test_vendored_commonplace_contracts_are_ignored(repo):
|
|||||||
|
|
||||||
def test_a_clean_tree_has_no_strays(repo):
|
def test_a_clean_tree_has_no_strays(repo):
|
||||||
assert kb_collections.stray_collection_contracts() == []
|
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()
|
||||||
@@ -77,6 +77,33 @@ def test_new_entity_creates_page_with_expected_frontmatter(monkeypatch, kb_dir):
|
|||||||
assert "# gateway.example.net" in body
|
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):
|
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:`,
|
"""Gitea #109 stops materializing an optional field's schema `default:`,
|
||||||
but `tags`/`related`/`sources` are optional arrays with no `default:` at
|
but `tags`/`related`/`sources` are optional arrays with no `default:` at
|
||||||
|
|||||||
@@ -268,8 +268,8 @@ def test_find_type_by_name_resolves_short_names():
|
|||||||
def test_list_type_specs_finds_every_type_spec():
|
def test_list_type_specs_finds_every_type_spec():
|
||||||
names = {fm.get("name") for _, fm in resolver.list_type_specs()}
|
names = {fm.get("name") for _, fm in resolver.list_type_specs()}
|
||||||
assert names == {
|
assert names == {
|
||||||
"type-spec", "entity", "concept", "source", "comparison", "lint-report", "instruction",
|
"type-spec", "entity", "concept", "source", "comparison", "project", "lint-report",
|
||||||
"type-guidance",
|
"instruction", "type-guidance",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ def test_types_list_finds_all_current_type_specs():
|
|||||||
rows = json.loads(result.output)
|
rows = json.loads(result.output)
|
||||||
names = {row["name"] for row in rows}
|
names = {row["name"] for row in rows}
|
||||||
assert names == {
|
assert names == {
|
||||||
"type-spec", "entity", "concept", "source", "comparison", "lint-report", "instruction",
|
"type-spec", "entity", "concept", "source", "comparison", "project", "lint-report",
|
||||||
"type-guidance",
|
"instruction", "type-guidance",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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 `<label>: <page title>` -
|
||||||
|
the label drawn from instructions/link-taxonomy.md and authorised per
|
||||||
|
destination by kb/gtd/COLLECTION.md's `outbound:` block - or a bare page
|
||||||
|
title for an edge whose label has not been declared yet.
|
||||||
|
sources:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: Source page titles that support claims on this page
|
||||||
|
provenance:
|
||||||
|
type: string
|
||||||
|
enum: [sourced, general, mixed]
|
||||||
|
default: general
|
||||||
|
description: >-
|
||||||
|
Provenance classification. `general` is the common case: a project page
|
||||||
|
is usually written by the operator from their own participation, not
|
||||||
|
from an ingested source.
|
||||||
|
summary:
|
||||||
|
type: string
|
||||||
|
description: 1-line summary for index.md
|
||||||
|
minLength: 1
|
||||||
|
required:
|
||||||
|
- type
|
||||||
|
- state
|
||||||
|
- responsibility
|
||||||
|
- created
|
||||||
|
- modified
|
||||||
|
- provenance
|
||||||
|
- summary
|
||||||
|
additionalProperties: false
|
||||||
+14
-8
@@ -67,7 +67,7 @@ frontmatter before anyone drew it:
|
|||||||
|
|
||||||
| Type-spec | Describes | Owned by | Ships as |
|
| Type-spec | Describes | Owned by | Ships as |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `root: kb` (`entity`, `concept`, `source`, `comparison`) | A page **this instance** writes | The instance | `types/<name>.md.template` plus its `.schema.yaml.template`, adopted by a rename |
|
| `root: kb` (`entity`, `concept`, `source`, `comparison`, `project`) | A page **this instance** writes | The instance | `types/<name>.md.template` plus its `.schema.yaml.template`, adopted by a rename |
|
||||||
| `root: repo` (`instruction`), no `base_dir` (`lint-report`), and `type-spec`/`type-guidance` themselves | A stack artifact | The stack | Verbatim |
|
| `root: repo` (`instruction`), no `base_dir` (`lint-report`), and `type-spec`/`type-guidance` themselves | A stack artifact | The stack | Verbatim |
|
||||||
|
|
||||||
A page type-spec's frontmatter configuration and its `## Template` body are therefore the
|
A page type-spec's frontmatter configuration and its `## Template` body are therefore the
|
||||||
@@ -104,13 +104,19 @@ exactly as it binds the four shipped ones: a new page type is instance-owned end
|
|||||||
settles who may change it, not which language each half is written in - and it may declare its
|
settles who may change it, not which language each half is written in - and it may declare its
|
||||||
own `guidance:` file if it wants the same shape, though nothing requires it to.
|
own `guidance:` file if it wants the same shape, though nothing requires it to.
|
||||||
|
|
||||||
**What the stack still requires of the type layer is one line.** There must be a type-spec
|
**What the stack still requires of the type layer is one line per type, and there are two of
|
||||||
declaring `name: source` whose schema requires `raw_files:` — the whole `raw/` → `kb/`
|
them today.** There must be a type-spec declaring `name: source` whose schema requires
|
||||||
provenance path (`sources coverage`, `[^cite-id]` resolution, `kb/provenance.md`) asks
|
`raw_files:` — the whole `raw/` → `kb/` provenance path (`sources coverage`, `[^cite-id]`
|
||||||
`page.kind == "source"`, so without it nothing resolves. `docs verify` checks exactly that and
|
resolution, `kb/provenance.md`) asks `page.kind == "source"`, so without it nothing resolves.
|
||||||
nothing beyond it: not the directory, not the title prefix, not a word of the prose. Which
|
There must equally be one declaring `name: project` whose schema requires `state:` — the weekly
|
||||||
collection is stack-required is *derived* from where that type writes rather than listed
|
review asks `page.kind == "project"` and reads `state:` to tell an ongoing initiative with no
|
||||||
separately, so renaming it stays consistent instead of tripping a hardcoded name.
|
next action from one that is deliberately paused or over, so without it nothing to review
|
||||||
|
resolves either. `docs verify` checks exactly that, for each, and nothing beyond it: not the
|
||||||
|
directory, not the title prefix, not a word of the prose. Which collection is stack-required is
|
||||||
|
*derived* from where that type writes rather than listed separately, so renaming it stays
|
||||||
|
consistent instead of tripping a hardcoded name. The anchor stays this small on purpose — the
|
||||||
|
page type-specs belong to the instance, so anything more would be the stack reaching into a file
|
||||||
|
it does not own (`tools/chemenu/kb_collections.py`'s `STACK_REQUIRED_TYPES`).
|
||||||
|
|
||||||
**Quality goal:** a type-spec is the single source of truth for its type. No structural fact
|
**Quality goal:** a type-spec is the single source of truth for its type. No structural fact
|
||||||
about a page type may be restated anywhere else — not in `AGENTS.md`, not in a skill, not in
|
about a page type may be restated anywhere else — not in `AGENTS.md`, not in a skill, not in
|
||||||
|
|||||||
Reference in new issue
Block a user