Files changed: - CHANGES.md - EVALS.md - VERSION - instructions/wiki-lint/SKILL.md - instructions/wiki-manage/SKILL.md - tools/CONTRACT.md - tools/chemenu/blocks.py - tools/chemenu/commands/lint.py - tools/chemenu/evals/scorecard.py - tools/chemenu/lint_core.py - tools/chemenu/tests/test_blocks.py - tools/chemenu/tests/test_evals.py - tools/chemenu/tests/test_lint.py - tools/chemenu/tests/test_pipeline_l0.py - types/type-spec.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
20 KiB
type, name, description, schema
| type | name | description | schema |
|---|---|---|---|
| types/type-spec.md | type-spec | Authoring and validation contract for path-valued Chemenu type specifications | types/type-spec.schema.yaml |
Type spec
A type-spec doc is the authoring and validation contract for one Chemenu artifact type. Artifact frontmatter stores the repo-relative path to this doc in type:, and this file serves several consumers differently: an author writing a new instance reads it for what to write, someone unfamiliar with the type reads it for what to expect from an instance before opening one, and the validator and the type-conformance reviewer read it to check conformance.
Contents
Required Frontmatter
type:types/type-spec.md. The root type spec is self-referential and uses the same value.name: short human-facing type name. This is also the type's logical "kind" - tooling resolves an instance'stype:path to thisnameinstead of hardcoding a type-path-to-kind mapping.description: retrieval description for the type contract.schema: repo-relative path to the.schema.yamlfile that validates artifacts of this type, ornullwhen the type has no schema.subtype_field(optional): the frontmatter field name on instances of this type that carries a subtype/category (e.g.entity_type), or omitted if this type has no such field.guidance(optional): repo-relative path to atypes/type-guidance.md-shaped document holding this type's stack-owned authoring prose - see § "Anatomy of a type" below. Meaningful only on aroot: kbtype-spec; omit for a type an instance writes entirely for itself.
Validation Contract
Two systems check conformance to a type:
- The deterministic validator checks the symbolic half: frontmatter fields, enums, path patterns, and syntactic body shape (required headings, section counts, date formats) — whatever the named
.schema.yamlcan statically specify. It parses structure; it never judges meaning. - The type-conformance review gate checks the semantic half: everything about an instance that is not mechanically checkable — a
description's routing quality, a title's claim-shape, whether the body's claims hold up — judged against this file's body, read as natural-language authoring instructions applied by an LLM reviewer.
Do not restate a schema rule in body text. The validator already enforces it, so the restatement adds no protection — it only spends the reviewer's judgment re-confirming something already guaranteed, instead of on the properties only a reviewer can check.
A type-spec's own frontmatter is checked the same way a page's is: docs verify validates every file under types/ declaring type: types/type-spec.md against types/type-spec.schema.yaml. A frontmatter field used here needs a matching line in that schema, or docs verify fails.
Writing Shape
- Write the body as a content contract: checkable properties of a conforming instance, in a form the type-conformance gate can apply directly. Prefer "what a conforming instance contains or claims" over "how to produce one" — production process is invisible to the reviewer, so it belongs in a skill or instruction, linked rather than embedded.
- State what the type is for and when an author should choose it over a neighboring type.
- Document the frontmatter fields (required and optional) as a reference for authors; the schema is the sole source of truth for requiredness.
- When
schemais non-null and implies body structure beyondtype/name/description, include a## Templatean authoring agent can copy.
The type layer
types/ is the repo's global type surface: one set of type-specs serving every collection
under kb/. It is deliberately not a collection and carries no COLLECTION.md — there is
no per-collection type surface, and tools/wikitool docs verify fails if a contract appears
here.
Who owns a type-spec
types/ holds two kinds of file, and the line between them is root: — already in the
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 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 - and any subtype
template beside it (§ "Anatomy of a type") - are therefore the instance's to rewrite, and an
upgrade does not take that back. Its generic authoring prose is the opposite: where the type-spec declares guidance:, that prose lives in a separate, stack-owned
types/<name>.guidance.md (§ "Anatomy of a type" below) that ships verbatim and is overwritten
by an upgrade like any other machinery file - the type-spec it documents does not have to be
re-adopted, or even touched, for that improvement to arrive. A type-spec that declares no
guidance: (a type an instance writes entirely for itself) has no stack half to receive; it is
described from its own body alone, exactly as every type-spec worked before this split existed.
An existing instance that adopted its type-specs before guidance: existed takes this as an
offered migration (instructions/CONTRACT.md)
rather than something an upgrade applies on its own - see instructions/migrations/ for the
document.
Ownership and language still do not line up with each other, and that stays true even though
the authoring prose now has its own file rather than sharing one with the template. Owning the
type-spec means the instance may rewrite its frontmatter and its ## Template body; a guidance
file it links is never the instance's to rewrite at all - it is the stack's, the same way
tools/CONTRACT.md is. What each part is written in still follows what the line is for, per
AGENTS.md § File naming, independent of which file it lives in or who
owns that file:
Part of a root: kb type's contract |
Lives in | Audience | Language |
|---|---|---|---|
| When to use / not to use, authoring guidance | The guidance: file, where declared |
An agent writing a page | The control plane's — English |
| The frontmatter table documenting this instance's own fields | The type-spec itself | An agent writing a page | The control plane's — English |
The ## Template body, and the layout: titles that head a catalog section |
The type-spec itself | The page itself | The instance's KB language (kb/CONVENTIONS.md language:) |
| A subtype template's whole text | types/<name>.<value>.md |
The page itself | The instance's KB language |
Field names, enum values, dir: values, type: paths |
Either file's frontmatter | The machine | Neither — identifiers, never translated |
That is the same prose/identifier cut kb/CONTRACT.md
makes inside a page, applied one level up. It binds a type-spec an instance writes for itself
exactly as it binds the four shipped ones: a new page type is instance-owned end to end, which
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.
What the stack still requires of the type layer is one line per type, and there are two of
them today. There must be a type-spec declaring name: source whose schema requires
raw_files: — the whole raw/ → kb/ provenance path (sources coverage, [^cite-id]
resolution, kb/provenance.md) asks page.kind == "source", so without it nothing resolves.
There must equally be one declaring name: project whose schema requires state: — the weekly
review asks page.kind == "project" and reads state: to tell an ongoing initiative with no
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
about a page type may be restated anywhere else — not in AGENTS.md, not in a skill, not in
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 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.
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
field is absent - writing it into every scaffolded page turns that silent assumption into a
stated claim instead. instruction.obligation is the field this was fixed for: a migration-only
axis, unrelated to an ordinary instruction, whose default: required used to land in every
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 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
Beyond the required fields above, a type-spec declares where its instances are written.
Never choose a directory by hand - root plus base_dir plus layout decide placement,
and tools/wikitool new writes the file.
| Field | Required | Use |
|---|---|---|
base_dir |
Yes for instantiable types | Directory where instances are written, e.g. entities. Relative to the root named by root: |
root |
No | Which root base_dir resolves against: kb (default, config.KB_DIR) or repo (config.ROOT) |
layout |
No | Maps each subtype to its dir and index title |
title_prefix |
No | Prepended to the page title, e.g. Source - |
page_ref_fields |
No | Fields whose entries are page titles, e.g. [related, sources]. lint checks they resolve; rename/rm rewrite them |
root: kb is the default because it is what every knowledge type wants, and because keeping
placement kb-relative is what lets a test point config.KB_DIR at a fixture and be certain
nothing can write into the real kb/. root: repo is opt-in, for types whose artifacts are
legitimately not knowledge: instruction is the worked example - an agent-directed procedure
under instructions/, outside the raw/ -> kb/ provenance rules entirely.
layout is what turns a subtype field into areas, and it is optional in both directions on
purpose. Declaring one makes every instance land in a directory named by its subtype and gives
that directory its index title; leaving it off keeps the collection flat, however many subtypes
the schema allows (comparison is the worked example - it has no subtype_field at all, so
there is nothing for a layout to key on). The choice for a type that does carry a subtype
field is whether the split helps: the catalog shards per area, so areas are how a large
collection stays readable, but a field whose values are lopsided - one dominant value plus a
handful of rare ones - would produce one big area and a scatter of splinters, which is not an
improvement. wikitool lint's unsharded_collections finding says which case a collection is
in once it grows past the shard threshold, and whether a layout: is already declared for the
split it recommends.
A lopsided field is a reason to fix the field, not a reason to leave layout off forever -
source was exactly this case: 25 of 29 pages carried one value because the schema
handed out that value as a default whenever nobody chose one, not because the material was
genuinely that uniform. Once the default was removed and the pages reclassified by what they
actually were, the field split into six areas with none of them small. Declare layout for
every enum value once it does: a value missing from it still gets a directory (subtype_dir
falls back to naively pluralizing the value's own name), but that directory carries no title in
kb/index.md, which is a silent gap rather than a deliberate flat collection. Leaving a whole
type's layout off is for the case in the paragraph above - no subtype field to key on at all
- not for a subtype field one has simply not gotten around to mapping. A
dir:must be a single path segment, because an area is as deep as a page goes.
A type-spec without base_dir is a contract only and cannot be instantiated;
wikitool new <name> refuses it. lint-report is the worked example: its artifacts are
generated into reports/, so it describes a file format without owning a place anything writes
to.
Adding a type
- Write
types/<name>.mdwith the frontmatter above and a## Templatefenced block. - Write
types/<name>.schema.yamldeclaring every field, withadditionalProperties: false. - Confirm it is discovered:
tools/wikitool types list, thentools/wikitool types describe <name>. - Scaffold with
tools/wikitool new <name> --name "..." --set field=value.
No Python change is needed at any step; wikitool discovers types by scanning this directory.
Template variables
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 - 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.
Placeholder text in a template starts with the token TODO - as the whole line or after a
list marker, checkbox, table cell or bold field label (TODO: ..., - TODO: ...,
- **Version:** TODO (falls zutreffend), | TODO |), with the token followed by the line end,
whitespace, :, (, ) or |. It is a contract, not a habit: wikitool lint reports a ##
section whose non-blank lines on a page are still all such lines as Unfilled Template Sections
(advisory), and a template that marks its placeholders any other way - an instance-owned one
included - leaves its unwritten sections invisible to that finding. A single placeholder beside
written lines is not reported, so a field whose value is genuinely unknown may keep its marker.
Ownership boundary
| Owned here | Owned by kb/CONTRACT.md |
Owned by kb/CONVENTIONS.md and the collection contracts |
|---|---|---|
| Frontmatter fields, enums, defaults, required-ness | Provenance and citation mechanics | Quality goal and tone |
| Directory placement and title prefix | Linking mechanics and the orphan check | Naming conventions |
| Body skeleton (template) | The prose/identifier rule | Relationship vocabulary |
| When to use / not use this type | — | The KB language value, and its section-heading names |
If a rule would be identical for every type and every instance, it belongs in
kb/CONTRACT.md. If every instance would answer it differently, it belongs in
kb/CONVENTIONS.md. If it is identical for every page in one collection, it belongs in that
collection's COLLECTION.md.
What does not belong here
- Page instances. Nothing under
types/is content. - A
COLLECTION.md.types/is a framework surface, not a collection. - Anything already enforced by a
.schema.yamlrestated in prose as if it were separately authoritative.
Template
---
type: types/type-spec.md
name: {type-name}
description: "{Retrieval description for this type contract}"
schema: {types/{type-name}.schema.yaml or null}
---
# {Type name}
{What this type is for, and when to choose it over a neighboring type.}
## Frontmatter
| Field | Required | Use |
|---|---:|---|
| `type` | Yes | `{this type spec's own repo path}` |
| `name` | Yes | {short human-facing type name} |
| `description` | Yes | {retrieval description for instances of this type} |
| `schema` | Yes | {repo-relative path to schema.yaml or null} |
## Template
```markdown
{a copyable frontmatter + body skeleton matching the schema}
```
Relevant Notes:
- This is the root type-spec that all other type-specs reference
- Type-specs form the structural backbone of the wiki's type system
- All validation flows through type-spec definitions