Layers¶
Which files in a corpus belong to the framework, which belong to the corpus, and what happens to each when a newer
framework arrives. new writes them the first time and update writes them again,
and both read the same declaration.
A corpus is one repository of knowledge records kept in git. The template is the repository or folder a corpus takes the framework from. A layer says who owns a file, and what a difference from upstream means.
The two halves¶
The framework reaches a corpus by two routes. kac itself comes from nuget.org, and
dotnet tool update KnowledgeAsCode.Tool moves it. Everything else comes from the template .corpus.yaml points at,
and update moves that.
A corpus can run a new tool over an old copy of the framework's files, or the reverse, and neither is a fault on its
own. The template's manifest states the oldest tool that can read it in minimum-tool, which is the one place the two
are held together. An older tool meets that key and stops, because half-reading a template it cannot understand would be
worse.
One file, one layer¶
The template's manifest.yaml declares the rules, and the first matching rule wins. A rule with no to: lands its file
on the path it was read from, which is how .schema/ reaches a corpus root. to: sends a file somewhere else, which is
how template/knowledge-as-code/ lands at a corpus's own knowledge-as-code/.
| Layer | What happens |
|---|---|
overlay |
Written wherever the two copies differ. This is framework property and an edit to it is drift. |
seed |
Written when absent. Written again only under update-policy: full. |
removed |
Deleted. A tombstone in the manifest, so a removal is stated rather than inferred. |
withheld |
Never written. The template's own machinery. |
Drift is a file in the overlay layer that no longer matches the upstream it came from. A manifest and a descriptor answer it, so a necessary deviation does not look like an accident. The mechanism is separable argues why the split is worth the trouble.
Seed files¶
A type's root page and its _template.md arrive with the framework's wording, and get rewritten in the corpus's own
domain. Refreshing them on every run would open each update with three dozen files to revert by hand.
So update-policy: cautious in .corpus.yaml is the default, and writes a seed only where
the corpus has none. full refreshes them and hands the reconciliation to the diff. --policy overrides either for one
run.
A seed belongs to the corpus once written, so retiring one is the corpus's own call.
A seeded record that moved¶
The template seeds a record at the top of its type's folder, as policies/devi-deviations.md. A corpus is
free to file that record deeper, and a type may read the folder below it as the record's category, so
policies/governance/devi-deviations.md is an ordinary place for it to end up.
Compared by path alone, that corpus reads as having no such seed. The update would offer a copy at the seeded path, and
accepting it leaves two records with one id, which kac validate then fails on id-unique.
So kac looks for a seed absent from its path by the id it carries, anywhere under its type's folder. A match is the
corpus's copy of that record, and the update leaves it alone.
full leaves it alone too, which is the one place a moved record parts company with a seed sitting where it was sent. A
record's relative links are written for the depth it was seeded at, so the template's copy written a folder down leaves
every ../ short by one, and kac validate fails the lot on link-resolves. Rewrite such a record from the template
by hand, and fix the links as you go.
Deletions¶
A file missing from the template is not evidence it was dropped. It is as likely to be evidence of a mistake upstream.
Only layer: removed deletes, and it deletes exactly what it names.
skip:¶
A path listed there is neither read nor written, in either direction. It is the one way to say that you own a file the overlay would otherwise reclaim on every run:
skip:
- path: .plugin/hooks/breadcrumb
reason: Patched for our proxy.
update steps over each one and reports what it stepped over. The reason is for whoever opens the file next, and it is
the only thing standing between a deliberate divergence and one nobody remembers making.
A shared plugin tree¶
plugin.from in .corpus.yaml sends bundle to one tree elsewhere, so a corpus that names it
keeps none of that tree and an update writes none of it. Its own .plugin/.claude-plugin/plugin.json is a seed and
arrives all the same, because that manifest gives the plugin its name and lists the components that corpus declares.
A corpus adopting the key with the old copies still on disk is told. update reports each one as a file the template
sends nothing to, and --check fails on it, because a corpus's own file wins the merge and a leftover would go on
shipping after every upstream change.
CI starters¶
A manifest rule may declare ci:, naming the system its files serve, and new writes the matching starter alone. Which
system builds a repository is that repository's own answer, so an update leaves a starter the corpus does not have where
it is. A corpus that wants one copies it across by hand.
A GitHub Actions workflow reaching a corpus built elsewhere is worse than unread. On github.com it runs uninvited.
Three files no template can send¶
.corpus.yaml is composed, never copied. No template can carry a descriptor without carrying somebody else's name
in it. So new builds the file from the answers it was given and stamps it with the upstream: block: the URL, the
path within it, the ref followed, the commit resolved, the template's version and the date. That block is what update
reads later.
shortcode: always arrives bare. new neither asks for it nor invents one, because a shortcode cannot be changed once
another corpus has cited it. path:, ref: and commit: arrive bare too wherever the take could not answer them,
which is every --from naming a folder, since a folder resolves no commit.
README.md is written, because the template's own is withheld. The template's README describes the template rather
than a corpus, so a corpus that copied everything would arrive with no README at all. What new writes is short: the
corpus's name, what it holds, and how to run the tool against it.
It arrives with the markers for the block it may hold, and a line saying so. It is the one page a corpus may decline a block on. A README written without them would decline on every new corpus's behalf, and nobody would have chosen that.
The breadcrumb hook arrives with its execute bit. .plugin/hooks/breadcrumb is run rather than read, and a hook
arriving without its mode bit fails silently on Unix.
--check¶
update --check computes the plan, prints what would change, writes nothing, and exits non-zero if anything would.
It also reports the reverse. A file the corpus keeps, where the rules call the area overlay, that the template sends
nothing to, is a framework change made in the wrong tree. It would reach no other corpus, and nothing in this one reads
as though anything is missing, so the check is the only place it surfaces. Move it upstream, or claim it with a skip:
entry.
This is what proves the framework's own repository, where each corpus under examples/ holds a materialised copy of
what the template sends. A file whose destination is where it was already read from is shared with every corpus there
rather than copied into each. .schema/ and the two writing skills are the rules in that position.
Where to go next¶
The corpus descriptor is the reference for every key new writes and update reads.