feat: a subtype gets its own page skeleton from types/<type>.<value>.md (#117)
Files changed: - AGENTS.md - CHANGES.md - README.md - VERSION - docs/ownership-and-templates.md - instructions/evolve-subtypes.md - instructions/setup-instance.md - instructions/subtype-templates.md - instructions/upgrade-instance.md - tools/CONTRACT.md - tools/chemenu/commands/dist_cmd.py - tools/chemenu/commands/docs_verify.py - tools/chemenu/commands/new_page.py - tools/chemenu/tests/test_dist_cmd.py - tools/chemenu/tests/test_docs_verify.py - tools/chemenu/tests/test_new_page.py - tools/chemenu/tests/test_toc.py - tools/chemenu/tests/test_type_resolver.py - tools/chemenu/toc.py - tools/chemenu/type_resolver.py - types/concept.decision.md - types/entity.guidance.md - types/entity.md - types/entity.person.md - types/type-spec.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
This commit is contained in:
1 parent
c261b8f4ca
commit
d8cb494d58
25 files changed
+747
-51
No files matched your search
@@ -102,6 +102,7 @@ What a file is called says who it is for and how it is loaded. This is a rule, n
|
||||
| `instructions/<name>/SKILL.md` | Agents | By the harness, once published |
|
||||
| `types/<name>.md` | Agents + validator | Via `tools/wikitool types describe`. Split by `root:`: a page type-spec (`root: kb`) belongs to the instance and ships as `.template`; one describing a stack artifact ships verbatim |
|
||||
| `types/<name>.guidance.md` | Agents + validator | Via `tools/wikitool types describe`, composed with the `types/<name>.md` it documents. Stack-owned regardless of the type-spec's own `root:` - it ships verbatim and is optional, present only where the type-spec declares `guidance:` |
|
||||
| `types/<name>.<subtype>.md` | Agents + `new` | Never as instruction: `wikitool new` copies it as the page skeleton for that one subtype value, in place of the type-spec's `## Template` block. Page material in the KB language, no frontmatter; its name is its only declaration, and `guidance` is reserved. Owned like the type-spec beside it, so it ships as `.template` |
|
||||
| `docs/<name>.md` | Agents and humans | By link, or on explicit request - never automatically, and never as instruction |
|
||||
| `INDEX.md` | Both | Generated - never hand-edited |
|
||||
|
||||
|
||||
+31
-1
@@ -59,7 +59,7 @@ concern - readable here, never shipped as something to parse.
|
||||
|
||||
---
|
||||
|
||||
## 8.0.0-beta.30 - 2026-10-03 - lint: a wikilink wrapped across a line break is its own finding; rename and rm see it
|
||||
## 8.0.0-beta.31 - 2026-10-03 - new: a subtype gets its own page skeleton from types/<type>.<value>.md
|
||||
|
||||
**Author:** Torben Nehmer
|
||||
|
||||
@@ -102,6 +102,7 @@ concern - readable here, never shipped as something to parse.
|
||||
- publish keeps a closing trailer block of --message last, so git reads Co-Authored-By again (#149)
|
||||
- Stack-Entwicklung in drei Phasen: stack-dev (Design), stack-build, stack-close - Übergabe über den Tracker, kein Modellwechsel in der Sitzung
|
||||
- raw fetch: a sanctioned intake for a URL into incoming/
|
||||
- new: a subtype gets its own page skeleton from types/<type>.<value>.md
|
||||
|
||||
**Low impact**
|
||||
- version bump no longer points at version release in its output
|
||||
@@ -143,6 +144,35 @@ concern - readable here, never shipped as something to parse.
|
||||
- lint: a wikilink wrapped across a line break is its own finding; rename and rm see it
|
||||
<!-- /wikitool:bumps -->
|
||||
|
||||
### new: a subtype gets its own page skeleton from types/<type>.<value>.md
|
||||
|
||||
A type-spec carried one `## Template` block for every value of its subtype field, so `wikitool
|
||||
new` scaffolded a person with `Version`, `Sprache/Technik` and `Repository`, and a decision with
|
||||
"Wann zu verwenden" instead of context and consequences - and authors rebuilt those pages by
|
||||
hand. Now a file `types/<type>.<value>.md` beside the type-spec is the skeleton for pages whose
|
||||
subtype field holds `<value>`: plain markdown in the KB language, no frontmatter, its name the
|
||||
only declaration. It replaces the block whole; every subtype without such a file, and every type
|
||||
without `subtype_field:`, scaffolds byte-identically to before. `guidance` is reserved for
|
||||
`<type>.guidance.md`. The stack ships two: `types/entity.person.md` (Rolle, Zugehörigkeit,
|
||||
Wirkungszeitraum, Beiträge) and `types/concept.decision.md` (Kontext, Entscheidung, Alternativen,
|
||||
Konsequenzen, Status).
|
||||
|
||||
`docs verify` reports a subtype template whose `types/<type>.md` is no type-spec with
|
||||
`subtype_field:`, whose value the field's enum does not allow, or which carries frontmatter -
|
||||
each would otherwise be a file nothing reads. Subtype templates never get a table-of-contents
|
||||
region, since `new` copies them into every page. They are page material of an instance-owned type,
|
||||
so `dist export` ships them as `.template`, `find_leaks` refuses one under its own name, and
|
||||
`dist adopt` takes them; no export carries one unsuffixed, so no `dist upgrade` writes over an
|
||||
adopted one. A new manual instruction, `instructions/subtype-templates.md`, is the interview that
|
||||
finds subtypes whose pages depart from their template (at least three pages, the same way) and
|
||||
writes the template the user accepts. `types/entity.md` and its guidance no longer speak of
|
||||
"projects" since the subtype became `codebase`.
|
||||
|
||||
**For an existing instance:** nothing changes until it adopts a template - `dist upgrade` lays
|
||||
`types/entity.person.md.template` and `types/concept.decision.md.template` beside the adopted
|
||||
type-specs; `tools/wikitool dist adopt <path>` takes one, leaving it takes none. Both are valid.
|
||||
No page changes (Gitea #117).
|
||||
|
||||
### lint: a wikilink wrapped across a line break is its own finding; rename and rm see it
|
||||
|
||||
A `[[...]]` written across a line break - prose wrapped at a fixed column, with the break
|
||||
|
||||
@@ -103,7 +103,8 @@ chemenu/
|
||||
│ ├── type-guidance.md # Contract for the *.guidance.md files below
|
||||
│ ├── entity.md # Entity type config + template (+ .schema.yaml)
|
||||
│ ├── entity.guidance.md # Its stack-owned authoring prose, shipped verbatim
|
||||
│ ├── concept.md # Concept type config + template (+ .guidance.md)
|
||||
│ ├── entity.person.md # Subtype template: what `new` scaffolds for entity_type=person
|
||||
│ ├── concept.md # Concept type config + template (+ .guidance.md, + concept.decision.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
|
||||
@@ -523,7 +524,7 @@ This wiki is tailored for IT work with:
|
||||
- **Entity types** specific to software development and systems
|
||||
- **Relationship types** like `hängt ab von`, `verwendet`, `implementiert` - the vocabulary is in
|
||||
[kb/CONVENTIONS.md](kb/CONVENTIONS.md), because it is this instance's rather than the stack's
|
||||
- **Templates** for projects, systems, tools, technologies, ADRs
|
||||
- **Templates** per page type, and per subtype where its pages need a shape of their own - a person, a decision record (ADR)
|
||||
- **Guidelines** for documenting technical decisions
|
||||
- **Cross-reference patterns** for code and architecture
|
||||
|
||||
|
||||
@@ -127,8 +127,8 @@ stack-owned file (`types/<name>.guidance.md`) holding exactly the half that used
|
||||
when to use the type, when not to, and mechanism-level advice that holds for every instance. That
|
||||
file ships verbatim and upgrades like any other machinery file, whether or not the type-spec that
|
||||
links it has ever been adopted. `types/type-spec.md` §§ "Who owns a type-spec" and "Anatomy of a
|
||||
type" hold the current shape; `tools/wikitool types describe <name>` composes both files into one
|
||||
answer, so an agent asking for a type's contract never needs to know it comes from more than one
|
||||
type" hold the current shape; `tools/wikitool types describe <name>` composes the type-spec and its
|
||||
guidance into one answer, so an agent asking for a type's contract never needs to know it comes from more than one
|
||||
file. An instance that adopted its type-specs before this split existed takes it as an *offered*
|
||||
migration rather than something an upgrade applies on its own - the same reasoning as any other
|
||||
instance-owned file in the middle category below, spelled out for this one case because it is the
|
||||
@@ -149,8 +149,12 @@ becomes visible once an upgrade is a command rather than a hand-run copy:
|
||||
kb` type-spec's optional `types/<name>.guidance.md` sits: verbatim, even though the type-spec
|
||||
it documents (below) is not.
|
||||
- **`.template`-sourced files** - `USER.md`, `SOUL.md`, `kb/CONVENTIONS.md`, each
|
||||
`kb/<name>/COLLECTION.md`, `ENVIRONMENT.md`, the `root: kb` type-specs - are never written by
|
||||
an upgrade at all. The distribution ships only the `.template` beside them, so the filled file
|
||||
`kb/<name>/COLLECTION.md`, `ENVIRONMENT.md`, the `root: kb` type-specs and their subtype
|
||||
templates `types/<name>.<value>.md` - are never written by an upgrade at all. A subtype
|
||||
template is the guidance split run the other way: the file boundary cut once more, this time
|
||||
to give page material a file of its own, and page material belongs to the instance, so it
|
||||
lands here rather than among the verbatim files - and because it is its own file, a release
|
||||
can ship a new one without touching an adopted type-spec. The distribution ships only the `.template` beside them, so the filled file
|
||||
is out of reach by construction rather than by a rule someone has to remember. The same
|
||||
property has a second face on the way in: when a release ships a `.template` for a type or
|
||||
collection the instance does not have *yet*, the upgrade writes the template and stops - it
|
||||
|
||||
@@ -102,6 +102,11 @@ choice of an instance's starting vocabulary - that is
|
||||
4. **Record it** with `tools/wikitool log append`, describing what moved and why, the same way
|
||||
any other corpus change is logged.
|
||||
|
||||
5. **A newly added value scaffolds with the type's `## Template` block** until it has a subtype
|
||||
template of its own. Whether its pages want a different skeleton is a separate judgment, made
|
||||
against the pages once they exist, by the same ≥3-page rule as above:
|
||||
[subtype-templates.md](subtype-templates.md).
|
||||
|
||||
## Decision points
|
||||
|
||||
- **The catch-all is empty and lint reports nothing?** Nothing to do - an empty catch-all is the
|
||||
|
||||
@@ -146,6 +146,12 @@ one it needs before that.
|
||||
*this* instance writes, so they belong to it: frontmatter, template and language may all
|
||||
be rewritten. `instruction`, `lint-report`, `type-spec` and `type-guidance` describe stack
|
||||
artifacts and arrive unchanged - none of them ships as a `.template` in the first place.
|
||||
The subtype templates beside them (`types/entity.person.md`, `types/concept.decision.md`:
|
||||
the page skeleton `wikitool new` uses for that one subtype instead of the type's
|
||||
`## Template` block) are adopted the same way and are page material like that block, so
|
||||
they are translated with it. Whether this instance wants further ones is a question for
|
||||
later, once pages exist to show it - [subtype-templates.md](subtype-templates.md), not
|
||||
part of this setup.
|
||||
|
||||
A `root: kb` type-spec's generic authoring guidance (when to use the type, when not to)
|
||||
is not part of this adoption at all: it lives in a sibling `types/<name>.guidance.md`
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
type: types/instruction.md
|
||||
name: subtype-templates
|
||||
description: Interview the corpus and the user for page skeletons per subtype - find subtypes whose pages systematically depart from their type's template, propose a types/<type>.<value>.md for each, and write the ones the user accepts.
|
||||
manual: true
|
||||
---
|
||||
# Find the subtypes that need a page skeleton of their own
|
||||
|
||||
A page type carries one `## Template` block for every value of its subtype field, and for most
|
||||
values that is enough. Where a subtype needs a different page shape - a person is not described by
|
||||
a version and a repository, a decision wants its context, alternatives and consequences - authors
|
||||
rebuild every scaffolded page by hand, and the corpus shows it. A **subtype template**,
|
||||
`types/<type>.<value>.md`, gives that subtype its own skeleton: `wikitool new` takes it instead of
|
||||
the block whenever the page's subtype field holds `<value>`. The file's shape and the checks on it
|
||||
are [types/type-spec.md](../types/type-spec.md) § "Anatomy of a type".
|
||||
|
||||
This instruction is the interview that decides which subtypes get one. It reads the pages first
|
||||
and proposes from them, because a template written ahead of the material is a guess every later
|
||||
page is scaffolded into.
|
||||
|
||||
## When to run
|
||||
|
||||
- The user asks for it, by name or by describing the symptom: pages of one kind keep being
|
||||
rebuilt after `wikitool new`.
|
||||
- After [evolve-subtypes.md](evolve-subtypes.md) added a value and its pages have accumulated.
|
||||
- After an upgrade delivered a subtype template as `.template` beside a type already adopted, and
|
||||
the user wants to know whether to take it.
|
||||
|
||||
Not for changing the `## Template` block every subtype shares - that is an edit to the type-spec
|
||||
itself. Not for adding a subtype value - that is [evolve-subtypes.md](evolve-subtypes.md).
|
||||
|
||||
## Steps
|
||||
|
||||
1. **List what there is to examine.** Every type-spec with a `subtype_field:`, every value its
|
||||
schema allows, and which skeleton each value scaffolds today:
|
||||
|
||||
```bash
|
||||
tools/wikitool types list
|
||||
tools/wikitool types describe <type>
|
||||
ls types/
|
||||
```
|
||||
|
||||
A value scaffolds from `types/<type>.<value>.md` if that file exists, otherwise from the
|
||||
type-spec's `## Template` block.
|
||||
|
||||
2. **Hold each value's pages against the skeleton they were scaffolded from.** Find them and read
|
||||
their `##` headings:
|
||||
|
||||
```bash
|
||||
tools/wikitool search --field <subtype_field>=<value>
|
||||
```
|
||||
|
||||
What counts is a departure several pages share: the same template section emptied or deleted,
|
||||
the same section added under the same or an equivalent name, the same section replaced by
|
||||
another. One page's own extra section is that page's business.
|
||||
|
||||
3. **Apply the admission threshold of [evolve-subtypes.md](evolve-subtypes.md): at least three
|
||||
pages of one subtype departing the same way.** A template is admitted after the material has
|
||||
shown its shape, never in expectation of it. A smaller count is only ever an explicit exception
|
||||
the user names, never a reason to lower the threshold.
|
||||
|
||||
4. **Offer a shipped template as the starting point where one is lying ready.** A
|
||||
`types/<type>.<value>.md.template` that was never adopted is the stack's proposal for that
|
||||
subtype. Compare it with what the pages actually do, and propose it unchanged, adapted, or not
|
||||
at all.
|
||||
|
||||
5. **Put each candidate to the user, one at a time:** which pages, what they share, and a draft
|
||||
of the template in the KB language (`kb/CONVENTIONS.md` `language:`) - the same variables and
|
||||
filters the `## Template` block uses ([types/type-spec.md](../types/type-spec.md) § "Template
|
||||
variables"), no frontmatter, no fence, no tool-owned section. The user decides per candidate:
|
||||
accept, change, or reject.
|
||||
|
||||
6. **Write each accepted template, then check it:**
|
||||
|
||||
```bash
|
||||
tools/wikitool dist adopt types/<type>.<value>.md.template # only where step 4 took the shipped one unchanged
|
||||
tools/wikitool docs verify
|
||||
```
|
||||
|
||||
Otherwise write `types/<type>.<value>.md` directly. `docs verify` refuses a file whose type has
|
||||
no `subtype_field:`, whose value the schema does not allow, or which carries frontmatter.
|
||||
|
||||
7. **Leave the existing pages as they are.** A template acts only on the next `wikitool new`;
|
||||
reshaping existing pages to match it is ordinary page editing, decided per page, and not part
|
||||
of this procedure.
|
||||
|
||||
## Decision points
|
||||
|
||||
- **The departures differ from page to page?** Then no template is warranted: a skeleton that
|
||||
fits none of the pages well is not better than the one they already rebuild.
|
||||
- **All subtypes of a type depart the same way?** The `## Template` block itself is wrong, and
|
||||
editing it is the fix - not one subtype template per value.
|
||||
- **The value is `guidance`?** It cannot have a template: `types/<type>.guidance.md` is always the
|
||||
type's guidance file. Rename the value instead, through [evolve-subtypes.md](evolve-subtypes.md).
|
||||
|
||||
## Scope
|
||||
|
||||
Covers every page type that declares `subtype_field:` - in the shipped specs `entity`, `concept`,
|
||||
`source` and `project`. A type without one, such as `comparison`, has a single skeleton by
|
||||
construction. Does not move, rename or rewrite a page.
|
||||
@@ -110,7 +110,12 @@ a further checkout of this one ([bootstrap.md](bootstrap.md)).
|
||||
- adopting it (copying it to the unsuffixed name) is the instance's own act, and where the
|
||||
stack *requires* that type the omission is what step 9's `docs verify` refuses. Step 2's
|
||||
**Breaking Change:** line says when a release is in that shape; step 9 has the repair.
|
||||
`locally changed` is step 6. `removed` matters only if `--prune` is wanted, which is optional
|
||||
One more shape of `new` needs no decision at all: a subtype template
|
||||
`types/<type>.<value>.md.template` beside a type this instance has already adopted. Adopt it
|
||||
(`tools/wikitool dist adopt types/<type>.<value>.md.template`) and `wikitool new` scaffolds
|
||||
pages of that subtype from it; leave it lying and they keep the type's `## Template` block.
|
||||
Both are valid - [subtype-templates.md](subtype-templates.md) is how to judge whether the
|
||||
corpus wants it. `locally changed` is step 6. `removed` matters only if `--prune` is wanted, which is optional
|
||||
and never required.
|
||||
|
||||
6. **Only if a file is reported as locally changed: decide whose file it is, then reconcile it.**
|
||||
|
||||
+8
-5
@@ -220,6 +220,7 @@ Scaffold a new wiki page of any type.
|
||||
- The target's path below the instance root may be at most 160 characters, counted in UTF-16 code units the way Windows counts MAX_PATH, so a Windows checkout without long paths keeps working. A longer one is refused, for every root, naming the length and how much shorter it has to get.
|
||||
- `new` never overwrites: a file already at the target - or one a case-insensitive file system would treat as the same file - is refused for every root, `instructions/` included.
|
||||
- The type-spec drives everything: fields, directory (`base_dir`/`layout`), title prefix, and template. `types list`/`types describe` show what a type requires.
|
||||
- The body skeleton is `types/<type>.<value>.md` when that file exists, `<value>` being the page's subtype field value (e.g. `types/entity.person.md` for `entity_type=person`) - it replaces the type-spec's `## Template` block whole. Without such a file, and for a type with no subtype field, the `## Template` block is used as before.
|
||||
- A schema `default:` is materialized only for a field the schema also lists in `required:`.
|
||||
- `--set` is repeatable, and comma-separated values fill array fields. An element that itself contains a comma is written `\,`, or passed as its own repeated `--set` for that field - repeating an array field appends.
|
||||
- A capture field the type-spec requires (a source's `fidelity`/`authority`) must be passed with `--set`; `new` never guesses it and refuses `unknown` for it.
|
||||
@@ -2292,6 +2293,7 @@ Check the docs that mirror the code.
|
||||
- 1 A command, contract, or type-form mismatch
|
||||
- 1 The `<!-- wikitool:commands -->` region of `tools/CONTRACT.md` is stale
|
||||
- 1 A type-spec's own frontmatter fails its schema
|
||||
- 1 A subtype template `types/<type>.<value>.md` has no type-spec with a `subtype_field:` beside it, names a value outside that field's enum, or carries frontmatter
|
||||
- 1 A shipped `.md`/`.template` cites an issue number
|
||||
- 1 A reference file's table-of-contents region is missing or stale
|
||||
- 1 A reference file's relative markdown link does not resolve to an existing file
|
||||
@@ -2304,6 +2306,7 @@ Check the docs that mirror the code.
|
||||
- A command, contract, or type-form mismatch -> Fix the documentation it names, then re-run
|
||||
- The `<!-- wikitool:commands -->` region of `tools/CONTRACT.md` is stale -> Run `docs contract --apply`, then re-run
|
||||
- A type-spec's own frontmatter fails its schema -> Fix the field, or add a matching line to `types/type-spec.schema.yaml` if the field is legitimately new
|
||||
- A subtype template `types/<type>.<value>.md` has no type-spec with a `subtype_field:` beside it, names a value outside that field's enum, or carries frontmatter -> Rename the file to the type and value it was meant for, delete it, or remove its frontmatter block
|
||||
- A shipped `.md`/`.template` cites an issue number -> Say what was decided instead of pointing at where, or move the pointer behind a `<!-- dist:strip-start/end -->` block
|
||||
- A reference file's table-of-contents region is missing or stale -> Run `docs toc --apply`, then re-run
|
||||
- A reference file's relative markdown link does not resolve to an existing file -> Fix the `../` count or the target's name
|
||||
@@ -2320,7 +2323,7 @@ Check the docs that mirror the code.
|
||||
- Checks the docs that mirror the code. The name is about documentation parity, not the `docs/` directory - it neither reads nor requires one.
|
||||
- Commands: every command has a `cli_contract` record and is listed in `cli_contract.GROUPS`, in both directions; every command's non-hidden flags appear in its record's SYNOPSIS and vice versa; `tools/CONTRACT.md`'s generated `<!-- wikitool:commands -->` region matches what `docs contract` would write; no command's rendered `--help`/`-h` text cites an issue number.
|
||||
- Collections: every directory under `kb/` has a `COLLECTION.md` and no directory outside it does; every collection declares `profile:` and a `required_by_stack:` that agrees with the stack's own list.
|
||||
- Types: every type the stack lists (currently `source` and `project`) has a type-spec of that name whose schema requires the field the stack list names (`raw_files:`/`state:`); every file under `types/` declaring `type: types/type-spec.md` validates against `types/type-spec.schema.yaml`; no pre-migration `type: entity` blocks are left in the contracts.
|
||||
- Types: every type the stack lists (currently `source` and `project`) has a type-spec of that name whose schema requires the field the stack list names (`raw_files:`/`state:`); every file under `types/` declaring `type: types/type-spec.md` validates against `types/type-spec.schema.yaml`; every subtype template `types/<type>.<value>.md` (any `<value>` but `guidance`) sits beside a type-spec declaring `subtype_field:`, names a value that field's schema enum allows, and carries no frontmatter; no pre-migration `type: entity` blocks are left in the contracts.
|
||||
- `kb/CONVENTIONS.md`, if it exists at all, names all three tool-owned section headings; every stage contract is present.
|
||||
- The `.gitignore` canaries clear in both directions: nothing ignored under `raw/`/`kb/`, `incoming/` ignored, everything ignored under `reports/` and the published skill directories.
|
||||
- No `.md`/`.template` file `dist export` would ship cites an issue number. A `<!-- dist:strip-start/end -->` region is exempt: the check reads the export plan's text, from which it is already gone.
|
||||
@@ -2370,7 +2373,7 @@ Create, refresh or remove the generated table-of-contents region.
|
||||
|
||||
- Creates, refreshes or removes the generated table-of-contents region on every reference file over 100 lines: `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.
|
||||
- The scope is computed from those categories rather than listed, so a file added later is in scope without a code change.
|
||||
- Out of scope: every `SKILL.md`, and the human docs (`README.md`, `CHANGES.md`, `EVALS.md`, `INSTALL.md`, `tools/README.md`).
|
||||
- Out of scope: every `SKILL.md`, the human docs (`README.md`, `CHANGES.md`, `EVALS.md`, `INSTALL.md`, `tools/README.md`), and every subtype template `types/<type>.<value>.md` - `new` copies it into a page, so it never carries a region whatever its length.
|
||||
- Dry-run by default (prints which files would change); `--apply` writes. Idempotent: a re-run after an interruption converges rather than doubling a region.
|
||||
- If `docs verify` still reports a stale region after `--apply`, the file's `##` headings changed in between; run it again.
|
||||
|
||||
@@ -2596,9 +2599,9 @@ Write a contentless, distributable copy of this repo's machinery.
|
||||
**NOTES**
|
||||
|
||||
- Writes a contentless, distributable copy of this repo's machinery into an empty or new `<target>` directory.
|
||||
- Ships `AGENTS.md`/`README.md`/`EVALS.md` with any `dist:strip-start`...`dist:strip-end` marker region removed, `instructions/` (minus `instructions/dev/`), `types/` (the `root: kb` page type-specs and their schemas re-keyed as `.template`, the stack's own verbatim), `docs/` verbatim, `tools/` (no venv/caches), the `.github/hooks/`+`.vibe/` session-tracing config plus `.claude/settings.json`, `kb/CONTRACT.md` (no pages, no areas) and `VERSION`.
|
||||
- Ships `AGENTS.md`/`README.md`/`EVALS.md` with any `dist:strip-start`...`dist:strip-end` marker region removed, `instructions/` (minus `instructions/dev/`), `types/` (the `root: kb` page type-specs, their schemas and their subtype templates `types/<type>.<value>.md` re-keyed as `.template`, the stack's own verbatim), `docs/` verbatim, `tools/` (no venv/caches), the `.github/hooks/`+`.vibe/` session-tracing config plus `.claude/settings.json`, `kb/CONTRACT.md` (no pages, no areas) and `VERSION`.
|
||||
- Ships `raw/` and `incoming/` as flat roots, each with a `.gitkeep` and no subdirectories. `incoming/.gitkeep` is trackable and survives becoming a git repository, so a plain clone gets the directory without any bootstrap step.
|
||||
- Ships templates, never the filled files: `USER.md.template`/`SOUL.md.template`, `kb/CONVENTIONS.md.template`, and each collection's contract re-keyed as `kb/<name>/COLLECTION.md.template`. The filled `USER.md`/`SOUL.md`/`kb/CONVENTIONS.md`/`kb/<name>/COLLECTION.md`/`types/<page-type>.md` bind their instance; `find_leaks` refuses a plan carrying one.
|
||||
- Ships templates, never the filled files: `USER.md.template`/`SOUL.md.template`, `kb/CONVENTIONS.md.template`, and each collection's contract re-keyed as `kb/<name>/COLLECTION.md.template`. The filled `USER.md`/`SOUL.md`/`kb/CONVENTIONS.md`/`kb/<name>/COLLECTION.md`/`types/<page-type>.md`/`types/<page-type>.<value>.md` bind their instance; `find_leaks` refuses a plan carrying one.
|
||||
- Writes a generated `.wikitool-release.json` stamp: version, export date, origin, and a sha256 per exported file - the base a later upgrade compares against.
|
||||
- The four origin options only fill stamp fields: `export` never calls git and cannot discover them.
|
||||
- A build and test tool: every release is an export packed as a tarball, and an instance is installed from such a release, never from an export directly.
|
||||
@@ -2649,7 +2652,7 @@ Take shipped templates as this instance's own: copy each to its unsuffixed name.
|
||||
**NOTES**
|
||||
|
||||
- Copies `<name>.template` to `<name>` byte for byte; the template stays where it is, as the base the next `dist upgrade` compares against.
|
||||
- Without a path: every `kb/<collection>/COLLECTION.md.template` and every `types/*.template` (the `root: kb` page type-specs and their schemas).
|
||||
- Without a path: every `kb/<collection>/COLLECTION.md.template` and every `types/*.template` (the `root: kb` page type-specs, their schemas and their subtype templates).
|
||||
- With paths: exactly those templates, each of which has to be one of the set above.
|
||||
- Never overwrites: a target that already exists is reported as kept and left untouched.
|
||||
- Not for `kb/CONVENTIONS.md.template` or the personalization templates - those carry a sentinel and are filled in, not copied.
|
||||
|
||||
@@ -353,15 +353,26 @@ _TYPE_SCHEMA_SUFFIX = ".schema.yaml"
|
||||
|
||||
def _owned_type_stem(relative: str) -> Optional[str]:
|
||||
"""The type stem `relative` (a path under `types/`, no `.template`
|
||||
suffix) names, if it is exactly that type-spec's own `<stem>.md` or
|
||||
`<stem>.schema.yaml` - `None` for anything else under `types/`,
|
||||
including a `<stem>.guidance.md` file. `_plan_types()` and `find_leaks()`
|
||||
both ask this instead of computing their own stem, so the two answer the
|
||||
same question about the same path (AGENTS.md invariant 8)."""
|
||||
suffix) names, if it is that type-spec's own `<stem>.md` or
|
||||
`<stem>.schema.yaml`, or one of its subtype templates `<stem>.<value>.md`
|
||||
(Gitea #117) - `None` for anything else under `types/`, including a
|
||||
`<stem>.guidance.md` file. `_plan_types()` and `find_leaks()` both ask this
|
||||
instead of computing their own stem, so the two answer the same question
|
||||
about the same path (AGENTS.md invariant 8).
|
||||
|
||||
A subtype template is page material of its type-spec, so it belongs to
|
||||
whoever owns that type-spec: re-keyed as `.template` beside a `root: kb`
|
||||
one. Missing it here would ship `entity.person.md` verbatim - a file the
|
||||
next `dist upgrade` overwrites in an instance that has adopted and
|
||||
rewritten it."""
|
||||
from chemenu.type_resolver import split_subtype_template_name
|
||||
|
||||
name = relative.rsplit("/", 1)[-1]
|
||||
if name.endswith(_TYPE_SCHEMA_SUFFIX):
|
||||
return name[: -len(_TYPE_SCHEMA_SUFFIX)]
|
||||
if name.endswith(".md") and not name.endswith(".guidance.md"):
|
||||
if (split := split_subtype_template_name(name)) is not None:
|
||||
return split[0]
|
||||
if name.endswith(".md") and "." not in name[: -len(".md")]:
|
||||
return name[: -len(".md")]
|
||||
return None
|
||||
|
||||
@@ -376,6 +387,10 @@ def _plan_types() -> dict[str, PlannedFile]:
|
||||
it, because the two are one type (see types/type-spec.md § Anatomy) and
|
||||
adopting half of it would leave a spec validated by a file it does not own.
|
||||
|
||||
Its subtype templates `<name>.<value>.md` (Gitea #117) travel the same way,
|
||||
each as its own `.template`: page material of the type, adopted or left
|
||||
lying independently of the type-spec beside it.
|
||||
|
||||
A type-spec's optional `<name>.guidance.md` (Gitea #104) is the opposite:
|
||||
stack-owned even where the type-spec itself is instance-owned, and ships
|
||||
verbatim beside the `.template` - `_owned_type_stem` is what keeps it out
|
||||
@@ -546,7 +561,10 @@ def find_leaks(plan: dict[str, PlannedFile]) -> list[str]:
|
||||
and (owned_stem := _owned_type_stem(relative)) is not None
|
||||
and owned_stem in owned_types
|
||||
):
|
||||
leaks.append(f"{relative} (this instance's page type-spec; ship the .template)")
|
||||
leaks.append(
|
||||
f"{relative} (this instance's page type-spec or subtype template; "
|
||||
"ship the .template)"
|
||||
)
|
||||
elif relative.startswith("instructions/dev/"):
|
||||
leaks.append(f"{relative} (stack-development only)")
|
||||
elif (
|
||||
@@ -588,8 +606,8 @@ def _write_plan(target: Path, plan: dict[str, PlannedFile]) -> None:
|
||||
"new `<target>` directory.",
|
||||
"Ships `AGENTS.md`/`README.md`/`EVALS.md` with any `dist:strip-start`...`dist:strip-end` "
|
||||
"marker region removed, `instructions/` (minus `instructions/dev/`), `types/` (the "
|
||||
"`root: kb` page type-specs and their schemas re-keyed as `.template`, the stack's own "
|
||||
"verbatim), `docs/` verbatim, `tools/` (no venv/caches), the "
|
||||
"`root: kb` page type-specs, their schemas and their subtype templates "
|
||||
"`types/<type>.<value>.md` re-keyed as `.template`, the stack's own verbatim), `docs/` verbatim, `tools/` (no venv/caches), the "
|
||||
"`.github/hooks/`+`.vibe/` session-tracing config plus `.claude/settings.json`, "
|
||||
"`kb/CONTRACT.md` (no pages, no areas) and `VERSION`.",
|
||||
"Ships `raw/` and `incoming/` as flat roots, each with a `.gitkeep` and no "
|
||||
@@ -599,7 +617,8 @@ def _write_plan(target: Path, plan: dict[str, PlannedFile]) -> None:
|
||||
"`kb/CONVENTIONS.md.template`, and each collection's contract re-keyed as "
|
||||
"`kb/<name>/COLLECTION.md.template`. The filled "
|
||||
"`USER.md`/`SOUL.md`/`kb/CONVENTIONS.md`/`kb/<name>/COLLECTION.md`/"
|
||||
"`types/<page-type>.md` bind their instance; `find_leaks` refuses a plan carrying one.",
|
||||
"`types/<page-type>.md`/`types/<page-type>.<value>.md` bind their instance; `find_leaks` "
|
||||
"refuses a plan carrying one.",
|
||||
"Writes a generated `.wikitool-release.json` stamp: version, export date, origin, and "
|
||||
"a sha256 per exported file - the base a later upgrade compares against.",
|
||||
"The four origin options only fill stamp fields: `export` never calls git and cannot "
|
||||
@@ -762,7 +781,8 @@ def adoptable_templates() -> list[Path]:
|
||||
"Copies `<name>.template` to `<name>` byte for byte; the template stays where it is, as "
|
||||
"the base the next `dist upgrade` compares against.",
|
||||
"Without a path: every `kb/<collection>/COLLECTION.md.template` and every "
|
||||
"`types/*.template` (the `root: kb` page type-specs and their schemas).",
|
||||
"`types/*.template` (the `root: kb` page type-specs, their schemas and their subtype "
|
||||
"templates).",
|
||||
"With paths: exactly those templates, each of which has to be one of the set above.",
|
||||
"Never overwrites: a target that already exists is reported as kept and left untouched.",
|
||||
"Not for `kb/CONVENTIONS.md.template` or the personalization templates - those carry a "
|
||||
|
||||
@@ -602,6 +602,63 @@ def check_type_spec_frontmatter() -> list[str]:
|
||||
return issues
|
||||
|
||||
|
||||
def check_subtype_templates() -> list[str]:
|
||||
"""Every subtype template `types/<stem>.<value>.md` is one `new` can
|
||||
actually reach (Gitea #117).
|
||||
|
||||
The file's name is its only declaration, so a typo in it is a template
|
||||
nothing ever scaffolds from and nobody notices - `new` silently falls back
|
||||
to the `## Template` block. Three ways that happens, each reported with
|
||||
the file and the value: `types/<stem>.md` is no type-spec declaring a
|
||||
`subtype_field:` (an orphan), `<value>` is not one the schema's enum allows
|
||||
for that field, or the file carries a frontmatter block, which `new` would
|
||||
copy into the page body as text. Which files count is
|
||||
`split_subtype_template_name`'s answer - the same one `toc` and
|
||||
`dist export` use - so `<stem>.guidance.md` is never one of them.
|
||||
"""
|
||||
from chemenu.frontmatter_io import FRONTMATTER_RE, read_page
|
||||
from chemenu.type_resolver import resolver, split_subtype_template_name
|
||||
|
||||
issues: list[str] = []
|
||||
if not config.TYPES_DIR.is_dir():
|
||||
return issues
|
||||
for path in sorted(config.TYPES_DIR.glob("*.md")):
|
||||
split = split_subtype_template_name(path.name)
|
||||
if split is None:
|
||||
continue
|
||||
stem, value = split
|
||||
relative = rel_path(path)
|
||||
spec_file = config.TYPES_DIR / f"{stem}.md"
|
||||
spec_frontmatter = read_page(spec_file)[0] if spec_file.is_file() else {}
|
||||
subtype_field = spec_frontmatter.get("subtype_field")
|
||||
if spec_frontmatter.get("type") != "types/type-spec.md" or not subtype_field:
|
||||
issues.append(
|
||||
f"{relative}: subtype template for value `{value}`, but types/{stem}.md is no "
|
||||
"type-spec declaring `subtype_field:` - `new` never scaffolds from it"
|
||||
)
|
||||
continue
|
||||
type_path = rel_path(spec_file)
|
||||
try:
|
||||
allowed = resolver.get_enum(type_path, subtype_field)
|
||||
except (ValueError, OSError):
|
||||
# No enum to check against (a free-form subtype field, or no
|
||||
# schema): any value is one a page can carry.
|
||||
allowed = None
|
||||
if allowed is not None and value not in allowed:
|
||||
issues.append(
|
||||
f"{relative}: `{value}` is not a value {type_path}'s schema allows for "
|
||||
f"`{subtype_field}` ({', '.join(map(str, allowed))}) - `new` never scaffolds "
|
||||
"from it"
|
||||
)
|
||||
if FRONTMATTER_RE.match(path.read_text(encoding="utf-8")):
|
||||
issues.append(
|
||||
f"{relative}: subtype template for value `{value}` carries a frontmatter block - "
|
||||
"the whole file is the page body `new` scaffolds, so the block would land in "
|
||||
"every page as text"
|
||||
)
|
||||
return issues
|
||||
|
||||
|
||||
def check_legacy_type_blocks() -> list[str]:
|
||||
issues = []
|
||||
guarded = [
|
||||
@@ -1062,8 +1119,11 @@ def check_breaking_change_for_boundary() -> list[str]:
|
||||
"Types: every type the stack lists (currently `source` and `project`) has a type-spec "
|
||||
"of that name whose schema requires the field the stack list names "
|
||||
"(`raw_files:`/`state:`); every file under `types/` declaring `type: "
|
||||
"types/type-spec.md` validates against `types/type-spec.schema.yaml`; no "
|
||||
"pre-migration `type: entity` blocks are left in the contracts.",
|
||||
"types/type-spec.md` validates against `types/type-spec.schema.yaml`; every subtype "
|
||||
"template `types/<type>.<value>.md` (any `<value>` but `guidance`) sits beside a "
|
||||
"type-spec declaring `subtype_field:`, names a value that field's schema enum allows, "
|
||||
"and carries no frontmatter; no pre-migration `type: entity` blocks are left in the "
|
||||
"contracts.",
|
||||
"`kb/CONVENTIONS.md`, if it exists at all, names all three tool-owned section headings; "
|
||||
"every stage contract is present.",
|
||||
"The `.gitignore` canaries clear in both directions: nothing ignored under "
|
||||
@@ -1099,6 +1159,13 @@ def check_breaking_change_for_boundary() -> list[str]:
|
||||
reaction="Fix the field, or add a matching line to `types/type-spec.schema.yaml` if "
|
||||
"the field is legitimately new",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="A subtype template `types/<type>.<value>.md` has no type-spec with a "
|
||||
"`subtype_field:` beside it, names a value outside that field's enum, or carries "
|
||||
"frontmatter",
|
||||
reaction="Rename the file to the type and value it was meant for, delete it, or "
|
||||
"remove its frontmatter block",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="A shipped `.md`/`.template` cites an issue number",
|
||||
reaction="Say what was decided instead of pointing at where, or move the pointer "
|
||||
@@ -1153,6 +1220,7 @@ def verify():
|
||||
+ check_readmes_have_no_command_table()
|
||||
+ check_collection_contracts()
|
||||
+ check_type_spec_frontmatter()
|
||||
+ check_subtype_templates()
|
||||
+ check_legacy_type_blocks()
|
||||
+ check_ignored_content()
|
||||
+ check_version_changelog()
|
||||
@@ -1207,8 +1275,10 @@ def verify():
|
||||
"`<name>.template` it ships as, where one exists.",
|
||||
"The scope is computed from those categories rather than listed, so a file added later "
|
||||
"is in scope without a code change.",
|
||||
"Out of scope: every `SKILL.md`, and the human docs (`README.md`, `CHANGES.md`, "
|
||||
"`EVALS.md`, `INSTALL.md`, `tools/README.md`).",
|
||||
"Out of scope: every `SKILL.md`, the human docs (`README.md`, `CHANGES.md`, "
|
||||
"`EVALS.md`, `INSTALL.md`, `tools/README.md`), and every subtype template "
|
||||
"`types/<type>.<value>.md` - `new` copies it into a page, so it never carries a region "
|
||||
"whatever its length.",
|
||||
"Dry-run by default (prints which files would change); `--apply` writes. Idempotent: a "
|
||||
"re-run after an interruption converges rather than doubling a region.",
|
||||
"If `docs verify` still reports a stale region after `--apply`, the file's `##` "
|
||||
|
||||
@@ -17,7 +17,9 @@ in `required:` - an optional field's default is a reader-side assumption
|
||||
would turn that assumption into a stated claim instead (Gitea #109).
|
||||
Directory placement for subtype-driven types (in the shipped specs: entity,
|
||||
concept, source and project) also comes from the type-spec, via its `layout:` frontmatter (see
|
||||
`TypeResolver.get_layout`) - not a hand-maintained Python dict.
|
||||
`TypeResolver.get_layout`) - not a hand-maintained Python dict. A subtype whose pages need a
|
||||
different skeleton gets it from a file beside the type-spec, `types/<type>.<value>.md`
|
||||
(`TypeResolver.page_template`), again without a code change here.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -63,10 +65,12 @@ def _default_summary(summary: str) -> str:
|
||||
return summary.strip() or "TODO: add summary"
|
||||
|
||||
|
||||
def _resolve_type_and_get_template(type_path: str, source_dir: Path = None):
|
||||
"""Resolve a type path, load the type-spec, and extract its template."""
|
||||
def _resolve_type_and_get_template(type_path: str, source_dir: Path = None, subtype: Any = None):
|
||||
"""Resolve a type path, load the type-spec, and pick its template: the
|
||||
subtype template `types/<stem>.<subtype>.md` where one exists, otherwise
|
||||
the type-spec's own `## Template` block (Gitea #117)."""
|
||||
type_spec = resolver.load_type_spec(type_path, source_dir)
|
||||
template = resolver.extract_template(type_spec)
|
||||
template = resolver.page_template(type_spec, subtype)
|
||||
return type_spec, template
|
||||
|
||||
|
||||
@@ -260,11 +264,11 @@ def _validate_or_fail(frontmatter: Dict[str, Any], type_path: str, source_dir: P
|
||||
fail(str(exc))
|
||||
|
||||
|
||||
def _load_type_or_fail(type_path: str, source_dir: Path):
|
||||
def _load_type_or_fail(type_path: str, source_dir: Path, subtype: Any = None):
|
||||
"""Resolve a type path and load its type-spec + template, converting an
|
||||
unresolvable/invalid `--type` into the CLI's normal friendly-failure path."""
|
||||
try:
|
||||
return _resolve_type_and_get_template(type_path, source_dir)
|
||||
return _resolve_type_and_get_template(type_path, source_dir, subtype)
|
||||
except ValueError as exc:
|
||||
fail(str(exc))
|
||||
|
||||
@@ -391,6 +395,10 @@ def _ensure_tracker_project(page_title: str, *, resume: bool) -> Optional[str]:
|
||||
"included.",
|
||||
"The type-spec drives everything: fields, directory (`base_dir`/`layout`), title "
|
||||
"prefix, and template. `types list`/`types describe` show what a type requires.",
|
||||
"The body skeleton is `types/<type>.<value>.md` when that file exists, `<value>` being "
|
||||
"the page's subtype field value (e.g. `types/entity.person.md` for `entity_type=person`) "
|
||||
"- it replaces the type-spec's `## Template` block whole. Without such a file, and for a "
|
||||
"type with no subtype field, the `## Template` block is used as before.",
|
||||
"A schema `default:` is materialized only for a field the schema also lists in "
|
||||
"`required:`.",
|
||||
"`--set` is repeatable, and comma-separated values fill array fields. An element that "
|
||||
@@ -608,8 +616,15 @@ def new_page_command(
|
||||
)
|
||||
|
||||
target_dir = _target_dir(type_path, frontmatter)
|
||||
_type_spec, template = _load_type_or_fail(type_path, target_dir)
|
||||
# Validated before the template is picked: the subtype value names a file
|
||||
# under types/, so it has passed the schema's enum before it is used as one.
|
||||
_validate_or_fail(frontmatter, type_path, target_dir)
|
||||
try:
|
||||
subtype_field = resolver.get_subtype_field(type_path)
|
||||
except ValueError as exc:
|
||||
fail(str(exc))
|
||||
subtype = frontmatter.get(subtype_field) if subtype_field else None
|
||||
_type_spec, template = _load_type_or_fail(type_path, target_dir, subtype)
|
||||
|
||||
if "raw_files" in frontmatter:
|
||||
check_raw_files_exist(frontmatter["raw_files"])
|
||||
|
||||
@@ -94,6 +94,9 @@ def repo(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
||||
"---\n\n# Entity Guidance\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
# entity's subtype template (Gitea #117): page material of an instance-owned
|
||||
# type, so it crosses as `.template` like the type-spec beside it.
|
||||
(types_dir / "entity.person.md").write_text("# {name}\n\n## Rolle\n", encoding="utf-8")
|
||||
(types_dir / "instruction.md").write_text(
|
||||
"---\ntype: types/type-spec.md\nname: instruction\ndescription: d\n"
|
||||
"schema: null\nbase_dir: instructions\nroot: repo\n---\n\n# Instruction\n",
|
||||
@@ -347,6 +350,8 @@ def test_find_leaks_is_silent_on_a_clean_plan(repo):
|
||||
# stack's, so shipping it would hand a new instance this one's
|
||||
# authoring language as though the stack had decided it.
|
||||
"types/entity.md",
|
||||
# Its subtype template, likewise (Gitea #117).
|
||||
"types/entity.person.md",
|
||||
],
|
||||
)
|
||||
def test_find_leaks_catches_one_instance_own_data(repo, relative):
|
||||
@@ -425,6 +430,30 @@ def test_guidance_file_ships_verbatim_beside_a_templated_type_spec(repo, monkeyp
|
||||
assert "types/entity.guidance.md.template" not in plan
|
||||
|
||||
|
||||
def test_subtype_template_ships_only_as_template_and_upgrade_never_writes_it(repo, monkeypatch):
|
||||
"""Gitea #117: no export carries a subtype template under its unsuffixed
|
||||
name, so no `dist upgrade` - which writes only what the new stamp lists -
|
||||
can ever write over one an instance has adopted. A guidance file sharing
|
||||
the dotted shape still ships verbatim."""
|
||||
from chemenu.type_resolver import resolver
|
||||
|
||||
monkeypatch.setattr(resolver, "_repo_root", config.ROOT)
|
||||
plan = dist_cmd.build_plan()
|
||||
|
||||
assert "types/entity.person.md.template" in plan
|
||||
assert "types/entity.person.md" not in plan
|
||||
assert "types/entity.guidance.md" in plan
|
||||
assert dist_cmd.find_leaks(plan) == []
|
||||
assert "types/entity.person.md" not in dist_cmd._write_candidates(plan)
|
||||
|
||||
|
||||
def test_this_repos_export_plan_carries_both_example_subtype_templates():
|
||||
plan = dist_cmd.build_plan()
|
||||
for name in ("types/entity.person.md", "types/concept.decision.md"):
|
||||
assert f"{name}.template" in plan
|
||||
assert name not in plan
|
||||
|
||||
|
||||
def test_plan_creates_empty_raw_and_incoming_not_real_content(repo):
|
||||
"""Both flat since Gitea #67: `raw/` addresses a file by its accept date,
|
||||
never by a hand-picked type, so there is nothing left to seed per type."""
|
||||
@@ -612,7 +641,7 @@ def instance(repo):
|
||||
(repo / "kb" / "entities" / "COLLECTION.md").rename(
|
||||
repo / "kb" / "entities" / "COLLECTION.md.template"
|
||||
)
|
||||
for name in ("entity.md", "entity.schema.yaml"):
|
||||
for name in ("entity.md", "entity.schema.yaml", "entity.person.md"):
|
||||
(repo / "types" / name).rename(repo / "types" / f"{name}.template")
|
||||
return repo
|
||||
|
||||
@@ -620,7 +649,10 @@ def instance(repo):
|
||||
def test_adopt_without_a_path_copies_every_collection_and_type_template(instance):
|
||||
dist_cmd.run_adopt([])
|
||||
|
||||
for relative in ("kb/entities/COLLECTION.md", "types/entity.md", "types/entity.schema.yaml"):
|
||||
for relative in (
|
||||
"kb/entities/COLLECTION.md", "types/entity.md", "types/entity.schema.yaml",
|
||||
"types/entity.person.md",
|
||||
):
|
||||
adopted = instance / relative
|
||||
template = instance / f"{relative}.template"
|
||||
assert adopted.read_bytes() == template.read_bytes()
|
||||
|
||||
@@ -910,3 +910,60 @@ def test_verify_raises_when_a_shipped_document_cites_an_issue(monkeypatch):
|
||||
monkeypatch.setattr(docs_verify, "check_no_issue_references", lambda: ["cited"])
|
||||
with pytest.raises(typer.Exit):
|
||||
docs_verify.verify()
|
||||
|
||||
|
||||
# --- subtype templates (Gitea #117) ---------------------------------------------
|
||||
|
||||
|
||||
def test_this_repos_subtype_templates_are_all_reachable():
|
||||
assert docs_verify.check_subtype_templates() == []
|
||||
|
||||
|
||||
def _subtype_tree(tmp_path, monkeypatch, files: dict):
|
||||
"""A copy of the shipped `types/`, plus `files` written into it."""
|
||||
import shutil
|
||||
|
||||
types_dir = tmp_path / "types"
|
||||
shutil.copytree(config.ROOT / "types", types_dir)
|
||||
for name, text in files.items():
|
||||
(types_dir / name).write_text(text, 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))
|
||||
return docs_verify.check_subtype_templates()
|
||||
|
||||
|
||||
def test_an_orphaned_subtype_template_is_reported(tmp_path, monkeypatch):
|
||||
issues = _subtype_tree(tmp_path, monkeypatch, {
|
||||
"gadget.big.md": "# {name}\n", # no such type-spec
|
||||
"comparison.wide.md": "# {name}\n", # a type-spec, but no subtype_field
|
||||
})
|
||||
assert any("types/gadget.big.md" in i and "`big`" in i for i in issues)
|
||||
assert any("types/comparison.wide.md" in i and "`wide`" in i for i in issues)
|
||||
assert len(issues) == 2
|
||||
|
||||
|
||||
def test_a_subtype_template_outside_the_enum_is_reported(tmp_path, monkeypatch):
|
||||
issues = _subtype_tree(tmp_path, monkeypatch, {"entity.persn.md": "# {name}\n"})
|
||||
assert len(issues) == 1
|
||||
assert "types/entity.persn.md" in issues[0] and "`persn`" in issues[0]
|
||||
assert "person" in issues[0] # names what the enum does allow
|
||||
|
||||
|
||||
def test_a_subtype_template_with_frontmatter_is_reported(tmp_path, monkeypatch):
|
||||
issues = _subtype_tree(tmp_path, monkeypatch, {
|
||||
"entity.tool.md": "---\ntitle: x\n---\n# {name}\n",
|
||||
})
|
||||
assert len(issues) == 1
|
||||
assert "types/entity.tool.md" in issues[0] and "frontmatter" in issues[0]
|
||||
|
||||
|
||||
def test_a_guidance_file_is_never_taken_for_a_subtype_template(tmp_path, monkeypatch):
|
||||
"""`<stem>.guidance.md` is reserved - neither the orphan check nor the enum
|
||||
check fires on one, even beside a type-spec without `subtype_field:` and
|
||||
for a value no enum lists."""
|
||||
issues = _subtype_tree(tmp_path, monkeypatch, {
|
||||
"widget.guidance.md": "---\ntype: types/type-guidance.md\nname: widget\n"
|
||||
"description: d\n---\n\n# G\n",
|
||||
})
|
||||
assert issues == []
|
||||
@@ -1095,3 +1095,64 @@ def test_new_project_over_the_path_budget_never_reaches_the_tracker(monkeypatch,
|
||||
assert result.exit_code == 1, result.output
|
||||
assert "160" in result.output
|
||||
assert handler_cls.posted is False
|
||||
|
||||
|
||||
# --- subtype templates (Gitea #117) ---------------------------------------------
|
||||
|
||||
|
||||
def _rendered(type_name: str, field: str, value: str, name: str) -> str:
|
||||
"""What the shipped subtype file renders to for one page, computed from
|
||||
the file itself rather than restated here."""
|
||||
import datetime
|
||||
|
||||
from chemenu import config
|
||||
|
||||
text = (config.TYPES_DIR / f"{type_name}.{value}.md").read_text(encoding="utf-8").strip()
|
||||
return (
|
||||
text.replace("{name}", name)
|
||||
.replace(f"{{{field}|capitalize}}", value.capitalize())
|
||||
.replace("{today}", datetime.date.today().isoformat())
|
||||
)
|
||||
|
||||
|
||||
def test_new_person_scaffolds_the_person_template(monkeypatch, kb_dir):
|
||||
result = _invoke_new(monkeypatch, kb_dir, [
|
||||
"new", "entity", "--name", "Grace Hopper", "--set", "entity_type=person",
|
||||
])
|
||||
assert result.exit_code == 0, result.output
|
||||
_fm, body = read_page(kb_dir / "entities/people/Grace Hopper.md")
|
||||
for codebase_only in ("Version", "Sprache/Technik", "Repository"):
|
||||
assert codebase_only not in body
|
||||
assert body.strip() == _rendered("entity", "entity_type", "person", "Grace Hopper")
|
||||
assert "{" not in body
|
||||
|
||||
|
||||
def test_new_decision_scaffolds_the_decision_template(monkeypatch, kb_dir):
|
||||
result = _invoke_new(monkeypatch, kb_dir, [
|
||||
"new", "concept", "--name", "Flat Subtype Files", "--set", "concept_type=decision",
|
||||
])
|
||||
assert result.exit_code == 0, result.output
|
||||
_fm, body = read_page(kb_dir / "concepts/decisions/Flat Subtype Files.md")
|
||||
assert body.strip() == _rendered("concept", "concept_type", "decision", "Flat Subtype Files")
|
||||
for heading in ("## Kontext", "## Entscheidung", "## Alternativen", "## Konsequenzen"):
|
||||
assert heading in body
|
||||
|
||||
|
||||
def test_a_subtype_without_its_own_file_keeps_the_base_template(monkeypatch, kb_dir):
|
||||
result = _invoke_new(monkeypatch, kb_dir, [
|
||||
"new", "entity", "--name", "libfoo", "--set", "entity_type=codebase",
|
||||
])
|
||||
assert result.exit_code == 0, result.output
|
||||
_fm, body = read_page(kb_dir / "entities/codebases/libfoo.md")
|
||||
assert "**Repository:**" in body
|
||||
|
||||
|
||||
def test_an_invalid_subtype_value_is_refused_before_any_template_is_read(monkeypatch, kb_dir):
|
||||
"""The value names a file under types/, so it passes the schema's enum
|
||||
first - `guidance` is not a valid entity_type and never gets that far."""
|
||||
result = _invoke_new(monkeypatch, kb_dir, [
|
||||
"new", "entity", "--name", "Nope", "--set", "entity_type=guidance",
|
||||
])
|
||||
assert result.exit_code == 1
|
||||
assert "entity_type" in result.output
|
||||
assert not list(kb_dir.rglob("Nope.md"))
|
||||
@@ -229,3 +229,35 @@ def test_strip_region_removes_what_types_describe_would_otherwise_echo():
|
||||
def test_strip_region_leaves_a_body_that_never_had_one_alone():
|
||||
body = "# A type\n\n## X\n\nProse.\n"
|
||||
assert toc.strip_region(body) == body
|
||||
|
||||
|
||||
def test_target_files_leaves_out_subtype_templates(tmp_path, monkeypatch):
|
||||
"""Gitea #117: `new` copies a subtype template into every page it
|
||||
scaffolds, so a region written into one would land in each of them. Out of
|
||||
scope whatever its length - a guidance file, which shares the dotted
|
||||
shape, stays in."""
|
||||
from chemenu import config
|
||||
|
||||
monkeypatch.setattr(config, "ROOT", tmp_path)
|
||||
types_dir = tmp_path / "types"
|
||||
types_dir.mkdir()
|
||||
long_body = _body(150, 6)
|
||||
for name in ("entity.md", "entity.guidance.md", "entity.person.md", "entity.person.md.template"):
|
||||
(types_dir / name).write_text(long_body, encoding="utf-8")
|
||||
|
||||
relatives = {f.relative_to(tmp_path).as_posix() for f in toc.target_files()}
|
||||
|
||||
assert relatives == {"types/entity.md", "types/entity.guidance.md"}
|
||||
|
||||
|
||||
def test_no_shipped_subtype_template_carries_a_toc_marker():
|
||||
from chemenu import config
|
||||
from chemenu.type_resolver import split_subtype_template_name
|
||||
|
||||
templates = [
|
||||
path for path in config.TYPES_DIR.iterdir()
|
||||
if split_subtype_template_name(path.name) is not None
|
||||
]
|
||||
assert templates
|
||||
for path in templates:
|
||||
assert "wikitool:toc" not in path.read_text(encoding="utf-8")
|
||||
@@ -374,3 +374,114 @@ def test_compute_target_dir_resolves_against_repo_root_for_root_repo_types():
|
||||
def test_compute_target_dir_rejects_a_type_with_no_base_dir():
|
||||
with pytest.raises(ValueError, match="base_dir"):
|
||||
resolver.compute_target_dir("types/type-spec.md", {})
|
||||
|
||||
|
||||
# --- subtype templates (Gitea #117) ---------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name, expected",
|
||||
[
|
||||
("entity.person.md", ("entity", "person")),
|
||||
("concept.decision.md", ("concept", "decision")),
|
||||
("entity.guidance.md", None), # reserved: always the guidance file
|
||||
("entity.md", None),
|
||||
("entity.schema.yaml", None),
|
||||
("entity.person.md.template", None),
|
||||
("entity.a.b.md", None),
|
||||
(".person.md", None),
|
||||
],
|
||||
)
|
||||
def test_split_subtype_template_name_reads_the_shape_only(name, expected):
|
||||
from chemenu.type_resolver import split_subtype_template_name
|
||||
|
||||
assert split_subtype_template_name(name) == expected
|
||||
|
||||
|
||||
def test_every_shipped_type_scaffolds_its_base_template_where_no_subtype_file_exists():
|
||||
"""The byte-identity criterion of #117, for every type under `types/`:
|
||||
`new` renders whatever `page_template` returns, so where it returns
|
||||
exactly what `extract_template` returned before, the scaffold is
|
||||
byte-identical. Checked for every enum value without a file of its own,
|
||||
for no value at all, and for every type without a `subtype_field:`."""
|
||||
checked = 0
|
||||
for type_path, frontmatter in resolver.list_type_specs():
|
||||
if not frontmatter.get("base_dir"):
|
||||
continue
|
||||
spec = resolver.load_type_spec(type_path)
|
||||
base = resolver.extract_template(spec)
|
||||
assert resolver.page_template(spec, None) == base
|
||||
field = frontmatter.get("subtype_field")
|
||||
if not field:
|
||||
assert resolver.page_template(spec, "person") == base
|
||||
checked += 1
|
||||
continue
|
||||
for value in resolver.get_enum(type_path, field):
|
||||
stem = type_path[len("types/"):-len(".md")]
|
||||
if (config.TYPES_DIR / f"{stem}.{value}.md").is_file():
|
||||
continue
|
||||
assert resolver.page_template(spec, value) == base, (type_path, value)
|
||||
checked += 1
|
||||
assert checked > 10
|
||||
|
||||
|
||||
def test_page_template_takes_the_shipped_subtype_files():
|
||||
for type_path, value in (("types/entity.md", "person"), ("types/concept.md", "decision")):
|
||||
spec = resolver.load_type_spec(type_path)
|
||||
stem = type_path[len("types/"):-len(".md")]
|
||||
expected = (config.TYPES_DIR / f"{stem}.{value}.md").read_text(encoding="utf-8").strip()
|
||||
assert resolver.page_template(spec, value) == expected
|
||||
assert resolver.page_template(spec, value) != resolver.extract_template(spec)
|
||||
|
||||
|
||||
def _widget_types(tmp_path, *, subtype_field: bool):
|
||||
from chemenu.type_resolver import TypeResolver
|
||||
|
||||
types_dir = tmp_path / "types"
|
||||
types_dir.mkdir()
|
||||
(types_dir / "widget.md").write_text(
|
||||
"---\ntype: types/widget.md\nname: widget\ndescription: A widget type.\n"
|
||||
"schema: null\nbase_dir: widgets\n"
|
||||
+ ("subtype_field: widget_type\n" if subtype_field else "")
|
||||
+ "---\n\n# Widget\n\n## Template\n\n```markdown\n# {name}\n```\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(types_dir / "widget.big.md").write_text("# {name}\n\n## Big\n", encoding="utf-8")
|
||||
(types_dir / "widget.guidance.md").write_text(
|
||||
"---\ntype: types/type-guidance.md\nname: widget\ndescription: d\n---\n\n# G\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
own = TypeResolver(repo_root=tmp_path)
|
||||
return own, own.load_type_spec("types/widget.md")
|
||||
|
||||
|
||||
def test_a_subtype_file_replaces_the_base_whole(tmp_path):
|
||||
own, spec = _widget_types(tmp_path, subtype_field=True)
|
||||
assert own.page_template(spec, "big") == "# {name}\n\n## Big"
|
||||
assert own.page_template(spec, "small") == "# {name}"
|
||||
|
||||
|
||||
def test_a_subtype_file_is_ignored_for_a_type_without_subtype_field(tmp_path):
|
||||
own, spec = _widget_types(tmp_path, subtype_field=False)
|
||||
assert own.page_template(spec, "big") == "# {name}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["guidance", "../widget", "big.md", "", None, 3])
|
||||
def test_a_value_that_cannot_name_a_subtype_file_takes_the_base(tmp_path, value):
|
||||
"""`guidance` is reserved for the guidance file, and a value that is not a
|
||||
single dot-free file-name segment never reaches the file system."""
|
||||
own, spec = _widget_types(tmp_path, subtype_field=True)
|
||||
assert own.page_template(spec, value) == "# {name}"
|
||||
|
||||
|
||||
def test_list_type_specs_counts_no_subtype_template():
|
||||
"""The same set as before #117: a subtype template carries no frontmatter,
|
||||
so it is never a type-spec."""
|
||||
paths = {path for path, _ in resolver.list_type_specs()}
|
||||
assert "types/entity.person.md" not in paths
|
||||
assert "types/concept.decision.md" not in paths
|
||||
assert paths == {
|
||||
"types/comparison.md", "types/concept.md", "types/entity.md", "types/instruction.md",
|
||||
"types/lint-report.md", "types/project.md", "types/source.md", "types/type-guidance.md",
|
||||
"types/type-spec.md",
|
||||
}
|
||||
+16
-2
@@ -23,7 +23,7 @@ precedent first.
|
||||
governs `wikitool` itself. `target_files()` walks every file-naming category
|
||||
AGENTS.md's own table calls agent-loaded: `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
|
||||
file, every `types/*.md` type-spec and guidance file, and every `docs/` page - each together with
|
||||
the `<name>.template` it ships as, where one exists. One rule, and
|
||||
exactly one exception below it - which is the whole point, because a scope
|
||||
carrying several unexplained absences reads as an accident rather than a
|
||||
@@ -61,6 +61,15 @@ other reference file, and `types_cmd` strips the region back out of what
|
||||
`describe` prints: the command already hands over the whole body, so a
|
||||
navigation aid into it would be noise in the output and nothing else.
|
||||
|
||||
**A subtype template under `types/` is not a reference file at all**, which is
|
||||
why it is out of scope rather than a second exception to the rule above.
|
||||
`types/<stem>.<value>.md` is the body skeleton `wikitool new` copies into a page
|
||||
(Gitea #117) - a region written into it would be written into every page
|
||||
scaffolded from it, as an author-facing section nobody may edit. So no file
|
||||
`new` reads as a scaffold ever carries a `wikitool:toc` marker, whatever its
|
||||
length; `type_resolver.split_subtype_template_name` decides which files those
|
||||
are, the same way for this scope as for `dist export` and `docs verify`.
|
||||
|
||||
The threshold's own provenance is worth recording, because the two sources
|
||||
disagree: Codex says 100 lines, Claude Code says 300. This stack took the
|
||||
stricter number.
|
||||
@@ -72,6 +81,7 @@ from pathlib import Path
|
||||
from typing import NamedTuple
|
||||
|
||||
from chemenu import blocks, config, kb_collections, markdown_code
|
||||
from chemenu.type_resolver import split_subtype_template_name
|
||||
|
||||
REGION_NAME = "toc"
|
||||
HEADING_TEXT = "Contents"
|
||||
@@ -159,7 +169,11 @@ def target_files() -> list[Path]:
|
||||
for subdir in ("types", "docs"):
|
||||
directory = config.ROOT / subdir
|
||||
if directory.is_dir():
|
||||
files += sorted(directory.rglob("*.md"))
|
||||
files += sorted(
|
||||
path
|
||||
for path in directory.rglob("*.md")
|
||||
if subdir != "types" or split_subtype_template_name(path.name) is None
|
||||
)
|
||||
files += [
|
||||
template
|
||||
for template in (
|
||||
|
||||
@@ -22,6 +22,38 @@ from chemenu import config
|
||||
from chemenu.frontmatter_io import normalize_dates, read_page, write_page
|
||||
|
||||
|
||||
# The middle part of `types/<stem>.guidance.md`, reserved for a type-spec's
|
||||
# stack-owned authoring prose - never a subtype value with a template of its own.
|
||||
GUIDANCE_PART = "guidance"
|
||||
|
||||
# What a subtype value must look like to name a file `types/<stem>.<value>.md`:
|
||||
# one path segment, no dot (the dot is what separates it from the stem).
|
||||
SUBTYPE_VALUE_RE = re.compile(r"[A-Za-z0-9_-]+")
|
||||
|
||||
|
||||
def split_subtype_template_name(name: str) -> Optional[Tuple[str, str]]:
|
||||
"""`(stem, value)` when a file name under `types/` has the shape of a
|
||||
subtype template, `<stem>.<value>.md` with `<value>` not `guidance`;
|
||||
None for anything else - a type-spec, a schema, a guidance file, a
|
||||
`.template`.
|
||||
|
||||
The stem ends at the first dot: type names match `^[a-z][a-z0-9-]*$`, so
|
||||
they never contain one. Shape only - whether `types/<stem>.md` exists and
|
||||
declares a `subtype_field:` is `docs verify`'s question, not this one's, so
|
||||
`toc`, `dist export` and `docs verify` all classify the same file the
|
||||
same way whether or not it is valid (AGENTS.md invariant 8).
|
||||
"""
|
||||
if not name.endswith(".md"):
|
||||
return None
|
||||
parts = name[: -len(".md")].split(".")
|
||||
if len(parts) != 2:
|
||||
return None
|
||||
stem, value = parts
|
||||
if not stem or value == GUIDANCE_PART or not SUBTYPE_VALUE_RE.fullmatch(value):
|
||||
return None
|
||||
return stem, value
|
||||
|
||||
|
||||
class TypeResolver:
|
||||
"""Resolves and validates type paths against type-spec files."""
|
||||
|
||||
@@ -205,7 +237,41 @@ class TypeResolver:
|
||||
return template
|
||||
|
||||
raise ValueError(f"No template block found in type-spec: {type_spec['path']}")
|
||||
|
||||
|
||||
def subtype_template_path(self, type_spec: Dict[str, Any], subtype: Any) -> Optional[Path]:
|
||||
"""The subtype template `types/<stem>.<subtype>.md` beside a loaded
|
||||
type-spec, or None when there is none - no `subtype_field:`, no value,
|
||||
the reserved value `guidance`, or no such file (Gitea #117).
|
||||
|
||||
The file's name is its only declaration: nothing in the type-spec
|
||||
lists which subtypes have one, so a template is added or dropped by
|
||||
adding or dropping the file. `subtype` is the frontmatter value; one
|
||||
that could not be a file-name segment simply has no template.
|
||||
"""
|
||||
if not type_spec['frontmatter'].get('subtype_field'):
|
||||
return None
|
||||
if not isinstance(subtype, str) or not SUBTYPE_VALUE_RE.fullmatch(subtype):
|
||||
return None
|
||||
if subtype == GUIDANCE_PART:
|
||||
return None
|
||||
spec_path = Path(type_spec['path'])
|
||||
candidate = spec_path.with_name(f"{spec_path.stem}.{subtype}.md")
|
||||
return candidate if candidate.is_file() else None
|
||||
|
||||
def page_template(self, type_spec: Dict[str, Any], subtype: Any = None) -> str:
|
||||
"""The body skeleton `new` scaffolds for one page: the subtype
|
||||
template for `subtype` where one exists, otherwise the type-spec's own
|
||||
`## Template` block (`extract_template`, unchanged).
|
||||
|
||||
A subtype template replaces the base whole - no merging - and is read
|
||||
as plain markdown, stripped the same way an extracted block is, so
|
||||
both sources yield a skeleton of the same shape.
|
||||
"""
|
||||
path = self.subtype_template_path(type_spec, subtype)
|
||||
if path is None:
|
||||
return self.extract_template(type_spec)
|
||||
return path.read_text(encoding='utf-8').strip()
|
||||
|
||||
def _extract_code_block(self, text: str, language: str = None) -> Optional[str]:
|
||||
"""Extract the first code block with the specified language."""
|
||||
# Pattern for fenced code blocks
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# {name}
|
||||
|
||||
**Typ:** {concept_type|capitalize}
|
||||
|
||||
## Definition
|
||||
|
||||
TODO: Die Entscheidung in einem Satz - was gilt, und wofür.
|
||||
|
||||
## Kontext
|
||||
|
||||
TODO: Die Lage, die eine Entscheidung verlangte: das Problem, die Zwänge, wer entschieden hat
|
||||
|
||||
## Entscheidung
|
||||
|
||||
TODO: Was entschieden wurde, und das Argument, das den Ausschlag gab
|
||||
|
||||
## Alternativen
|
||||
|
||||
- TODO: Verworfene Alternative - und warum sie verworfen wurde
|
||||
|
||||
## Konsequenzen
|
||||
|
||||
- TODO: Was aus der Entscheidung folgt, einschließlich ihrer Kosten
|
||||
|
||||
## Status
|
||||
|
||||
TODO: gilt, abgelöst durch [[Nachfolgende Entscheidung]], zurückgenommen usw. - mit Datum
|
||||
@@ -6,12 +6,12 @@ description: When to write an entity page instead of a neighboring type, and how
|
||||
|
||||
# Entity Guidance
|
||||
|
||||
`entity` is the type for concrete things: projects, systems, tools, technologies or people.
|
||||
`entity` is the type for concrete things: codebases, systems, tools, technologies or people.
|
||||
Entities are the primary building blocks of the knowledge graph.
|
||||
|
||||
## When to use
|
||||
|
||||
- Representing a software project or codebase
|
||||
- Representing a codebase - a repository, library or application as code
|
||||
- Documenting a running system, service or infrastructure component
|
||||
- Describing a CLI tool, utility or software library
|
||||
- Recording information about a language, a framework or a protocol
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
---
|
||||
type: types/type-spec.md
|
||||
name: entity
|
||||
description: Base type for entity pages - projects, systems, tools, technologies or people
|
||||
description: Base type for entity pages - codebases, systems, tools, technologies or people
|
||||
schema: types/entity.schema.yaml
|
||||
subtype_field: entity_type
|
||||
base_dir: entities
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# {name}
|
||||
|
||||
**Typ:** {entity_type|capitalize}
|
||||
|
||||
## Beschreibung
|
||||
|
||||
TODO: 1-2 Absätze dazu, wer diese Person oder Organisation ist und wofür sie in diesem Wiki steht.
|
||||
|
||||
## Kerndaten
|
||||
|
||||
- **Rolle:** TODO (falls zutreffend)
|
||||
- **Zugehörigkeit:** TODO (falls zutreffend)
|
||||
- **Wirkungszeitraum:** TODO (falls zutreffend)
|
||||
|
||||
## Beiträge
|
||||
|
||||
TODO: Was diese Person oder Organisation geschaffen, vertreten oder beeinflusst hat - mit Verweisen auf die betreffenden Seiten
|
||||
|
||||
## Historie
|
||||
|
||||
- [{today}] - Page created via wikitool
|
||||
+22
-7
@@ -67,7 +67,7 @@ frontmatter before anyone drew it:
|
||||
|
||||
| Type-spec | Describes | Owned by | Ships as |
|
||||
|---|---|---|---|
|
||||
| `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: kb` (`entity`, `concept`, `source`, `comparison`, `project`) | A page **this instance** writes | The instance | `types/<name>.md.template` plus its `.schema.yaml.template` and any subtype template's `types/<name>.<value>.md.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 |
|
||||
|
||||
A page type-spec's frontmatter configuration and its `## Template` body are therefore the
|
||||
@@ -124,13 +124,23 @@ Python. Adding a type must require no code change.
|
||||
|
||||
### Anatomy of a type
|
||||
|
||||
Each type is at least two files, and a `root: kb` type may be three:
|
||||
Each type is at least two files, and a `root: kb` type may have more:
|
||||
|
||||
| File | Owns |
|
||||
|------|------|
|
||||
| `types/<name>.md` | This instance's configuration: its frontmatter fields as this schema requires them, and the `## Template` block used to scaffold new pages. For a type with no `guidance:` (below), its own prose also carries the authoring contract - when to use the type, when not to |
|
||||
| `types/<name>.schema.yaml` | The machine-checkable half: fields, types, enums, defaults, required-ness, `additionalProperties: false` |
|
||||
| `types/<name>.guidance.md` (optional, `root: kb` only) | The stack-owned authoring contract: when to use the type, when not to, and mechanism-level advice that holds regardless of this instance's own enum values or template text - linked from the type-spec's own `guidance:` field. `types/type-guidance.md` is its contract |
|
||||
| `types/<name>.<value>.md` (optional, any number, only for a type with `subtype_field:`) | A **subtype template**: the page skeleton for pages whose subtype field holds `<value>`, where that subtype needs a different page shape than the `## Template` block gives - a person is not described by a version and a repository. Plain markdown in the KB language, no frontmatter, no fence: the whole file is the skeleton. Its name is its only declaration - nothing in the type-spec lists it. Owned like the type-spec it sits beside, so a `root: kb` type's subtype templates ship as `.template` too |
|
||||
|
||||
`docs verify` holds a subtype template to three things: `types/<name>.md` beside it is a
|
||||
type-spec declaring `subtype_field:`, `<value>` is one that field's schema enum allows (where it
|
||||
has an enum), and the file carries no frontmatter. A misspelt name would otherwise be a template
|
||||
nothing ever reads, and nobody would notice. **`guidance` is reserved** - `types/<name>.guidance.md`
|
||||
is always the guidance file, so a subtype value `guidance` can have no template of its own. A
|
||||
subtype template never gets a table-of-contents region, whatever its length: it is copied into
|
||||
every page scaffolded from it. When an instance needs one is
|
||||
[instructions/subtype-templates.md](../instructions/subtype-templates.md).
|
||||
|
||||
**A `default:` is materialized by `wikitool new` only for a field the schema also lists in
|
||||
`required:`.** On an optional field, `default:` documents what a reader should assume when the
|
||||
@@ -141,9 +151,9 @@ newly scaffolded instruction regardless.
|
||||
|
||||
This file is the self-referential root contract every type-spec is validated against, and
|
||||
`type-guidance.md` is validated against it the same way `lint-report.md` is - itself a
|
||||
non-instantiable, contract-only type. `tools/wikitool types describe <name>` composes all of a
|
||||
type's files into one answer regardless of how many there are; an agent asking for a type's
|
||||
contract never needs to know it came from more than one file.
|
||||
non-instantiable, contract-only type. `tools/wikitool types describe <name>` composes the type-spec, its schema and its guidance into
|
||||
one answer; an agent asking for a type's contract never needs to know it came from more than one
|
||||
file. Subtype templates are not part of that answer - only `wikitool new` reads them.
|
||||
|
||||
### Placement frontmatter
|
||||
|
||||
@@ -206,13 +216,18 @@ No Python change is needed at any step; `wikitool` discovers types by scanning t
|
||||
|
||||
### Template variables
|
||||
|
||||
The `## Template` block is filled from the page's own frontmatter, plus `{name}` and
|
||||
`wikitool new` picks one skeleton per page: `types/<name>.<value>.md` when the page's subtype
|
||||
field holds `<value>` and that file exists, otherwise the `## Template` block. A subtype template
|
||||
replaces the block whole - nothing is merged - so every subtype without a file of its own, and
|
||||
every type without a `subtype_field:`, scaffolds exactly what the block says.
|
||||
|
||||
Either skeleton is filled from the page's own frontmatter, plus `{name}` and
|
||||
`{today}`. Filters render structured fields: `{entities|bullets}`, `{tags|join}`,
|
||||
`{entity_type|capitalize}`, `{entities|table_header}`, `{entities|table_sep}`,
|
||||
`{entities|table_cells}`. `{field|literal text}` falls back to the literal when the field is
|
||||
absent.
|
||||
|
||||
**A template never contains a tool-owned region.** The links and footnotes regions are generated
|
||||
**A template - block or subtype file - never contains a tool-owned region.** The links and footnotes regions are generated
|
||||
between markers by `xref` and `cite`, rendered from frontmatter, and re-rendered on every write -
|
||||
so scaffolding them would create a section an author is forbidden to edit and the tool would
|
||||
replace anyway. See `tools/chemenu/blocks.py`.
|
||||
|
||||
Reference in new issue
Block a user