.corpus.yaml¶
.corpus.yaml sits at the root of a corpus (one repository of knowledge records kept in git). It states what the corpus
is, which types it adopted, where it is published, and where it takes the shared framework from.
new writes the file when the corpus is created, commented key by key. You edit it by hand after that.
update rewrites four of the keys below and never replaces the file. Every other file in a corpus arrives from a
template and update can take it again; this one is the corpus's own.
The longer worked copy is
examples/library/.corpus.yaml.
Read that one while you change yours.
It is not the schema. .schema/ states what a record of each type must have. This file states which of those types
the corpus adopted. It says nothing about their shape.
It is not configuration for a run. It records decisions, and a command reads them. --from, --ref, --path and
--policy each replace a value here for one run only.
Every key¶
| Key | What it states | Who writes it |
|---|---|---|
descriptor-version |
the format of this file | new, then update |
corpus |
the name this corpus publishes under | new |
shortcode |
the word another corpus cites this one by | you |
content-version |
the version of what this corpus knows | new, then you |
display-name |
what a person calls this corpus | you |
description |
what this corpus is, in a sentence | you |
license |
the licence it publishes under | you |
author |
who publishes it, as a name and a URL | you |
publishing-target |
how the corpus is published | new |
publishing.base |
the URL a person opens to browse it | new, then you |
publishing.path-prefix |
where the corpus sits inside its repository | you |
tracker |
where work about this corpus's records is filed | you |
framework |
where to report a problem with the framework | new |
upstream.url, .path, .ref |
where the framework is taken from | new, then you |
upstream.commit, .template-version, .taken-on |
what the last take resolved to | update |
update-policy |
how far an update goes | new |
types |
the types this corpus adopted | new, then update --add-type |
enums |
the ranges the schema leaves to the corpus | you |
consumes |
the corpora this one reads | you, and restore resolves |
export.exclude |
what the export leaves behind | new |
plugin.from |
where the plugin tree is read from | you |
skip |
the files this corpus owns | you |
Identity¶
No key here is called version alone, because three of them say different things: the format of this file, the version
of what the corpus knows, and the template shape it last took.
corpus¶
The name this corpus publishes under.
corpus: knowledge-as-code
An export states it, so a consumer with several exports can tell whose vocabulary it is reading. The folder it vendored the files into may not say.
shortcode¶
The shorthand another corpus cites this one by, as the eng in eng:pol-VURM.TIMEBOX.
shortcode: kac
You choose it, and a corpus citing yours writes what you chose. validate checks the spelling.
The shortcode says what a legal one looks like, and why it never changes.
Leave it out until something cites this corpus, which is the ordinary case. new writes it bare, and an
export from a corpus that has not chosen one states null.
content-version¶
The version of what this corpus knows.
content-version: "0.1.0"
It is semantically versioned. Move the major where a meaning changed or a published URL broke. Move the minor for additions, and the patch for wording. Quote it when one corpus tells another which version of the content it has.
Nothing refuses to load because the number moved. It is a notification.
descriptor-version¶
The format of this file, which the tool owns rather than the corpus.
descriptor-version: 4
update stamps it with the format the tool writes, so a corpus records which shape it last took.
What a reader sees outside the repository¶
These four keys are for a reader who meets this corpus as a package or an installed plugin, not as a repository.
display-name: Knowledge as Code
description: >
The engineering knowledge of one organisation, held as records a person and an agent read the same way.
license: MIT
author:
name: A Person
url: https://github.com/a-person
export writes all four into the export, pack writes them into the package a registry
lists, and bundle writes them into the plugin manifest somebody installs.
Leave a key bare and all three omit it. new writes all four bare, because a licence and an author
supplied by the tool would be inherited instead of chosen.
Where the corpus is published¶
publishing-target¶
How the corpus is published, which decides whether export can build a link at all and which form to build.
publishing-target: github
State it instead of leaving it to be guessed. The export format says what an export writes, and how a record's path and a part's anchor join the base. Every corpus on one target builds the same link, and none of them writes the rule down a second time.
github means the repository is itself the published form, and a record is read rendered on github.com. Write the
repository's own URL, with no /blob on the end. export adds that segment along with the commit.
azure-devops means the corpus lives in Azure Repos and no wiki publishes it. Write the repository's _git URL, as
https://dev.azure.com/{org}/{project}/_git/{repo}.
azure-devops-wiki means an Azure DevOps wiki publishes it. Write the wiki's own URL, as
https://dev.azure.com/{org}/{project}/_wiki/wikis/{wiki}, and nothing after it. Once you open a page, the address bar
shows a numeric page id. A base with one of those in it addresses that page and misaddresses every other record, so
export refuses it and builds no links at all.
This is also the one target whose link is not pinned to a commit. No ?pagePath= URL takes one, so a person following
the link reads whatever the wiki has now. An agent still reads the version the export was built from, because the commit
travels in the manifest instead of in the link.
mkdocs is accepted, and nothing addresses it yet, so an export from a corpus on it has no links. No link is better
than a link built on a convention nobody has settled.
none means the corpus is not published. Its export has no links and says so in its manifest, so a reader is never
handed an address that resolves nowhere. It is the one value that needs no publishing: block.
validate refuses any other value, and names these five in the message.
publishing¶
Where the published corpus is served from, and nothing else.
publishing:
base: https://github.com/paul80nd/knowledge-as-code
path-prefix: examples/library
| Key | What it states | Who writes it |
|---|---|---|
base |
the URL a person opens to browse the corpus | new, then you |
path-prefix |
where the corpus root sits inside that repository | you |
There is one base, whatever the target. An agent reads a record's source from the same place, through a client that authenticates.
Set path-prefix where the repository contains more than the corpus. Do not append that folder to base instead.
export inserts the commit between base and the record's path, so there is no room for it there. Leave the key out
where the corpus is the repository, which is the ordinary case.
Where work is filed¶
tracker¶
Where work about this corpus's records is filed.
tracker:
target: azure-devops
base: https://dev.azure.com/acme/Standards
| Key | What it states | Who writes it |
|---|---|---|
target |
the client that opens a ticket on the tracker | you |
base |
the repository or project the backlog belongs to | you |
Leave the block out where the backlog belongs to what publishing: already names. export derives
the address from that block instead, by the table below. A corpus published on GitHub therefore states nothing: a
repository there has an issue list of its own.
State the block where the backlog and the published form are two places. One Azure DevOps project holds one backlog and many repositories, so a repository URL does not address the backlog.
target is one of github, azure-devops or none, the same three values framework takes. Write
none where the corpus has no tracker at all. validate refuses any other value.
Write the project URL for azure-devops, as https://dev.azure.com/{org}/{project}. A URL naming a repository or a
wiki inside the project is cut back to the project, because that is where the backlog is.
What export derives¶
publishing-target |
The tracker derived from base |
|---|---|
github |
the repository, which is its own issue list |
azure-devops |
the project holding the repository |
azure-devops-wiki |
the project holding the wiki |
mkdocs and none |
none |
Write base alone where your backlog moved inside the platform you publish on, and the client follows from
publishing-target. Write target alone where the corpus has no tracker, and nothing states a base.
State both keys where you state a target publishing-target does not imply. A base is taken from the publishing
block only where the two targets agree. A corpus published on GitHub and filing on Azure Boards is filing somewhere only
it knows, so export writes no base rather than the repository it publishes from.
framework¶
Where to report a problem with the framework itself: kac, the schema, the template or a skill.
framework:
target: github
base: https://github.com/paul80nd/knowledge-as-code
| Key | What it states | Who writes it |
|---|---|---|
target |
the client that opens an issue on the tracker | new |
base |
the repository or project the tracker belongs to | new |
export writes the block into the manifest, so an agent that meets your corpus as an installed plugin
has the address. Without it, the only address that agent has is your own publishing.base.
target takes the same three values tracker takes. Write github for a tracker on github.com, and
azure-devops for one on Azure Boards. Write none, or leave the block out, where you know of no tracker.
azure-devops-wiki and mkdocs, which publishing-target also takes, address no tracker, so
neither belongs here.
new writes both keys when the corpus is created. It takes base from --from where that named a
repository, and from the framework's own repository where it named a folder. It reads target from the host, and writes
none for a host that neither gh nor az boards serves.
Nothing derives this block. Where a corpus publishes says nothing about who maintains what it took.
Three addresses, and telling them apart¶
tracker.base is where a problem with one of your records goes. framework.base is where a problem with kac, the
schema, the template or a skill goes. Each corpus in consumes: brings a third, which is its producer's.
A corpus whose records and framework are maintained by the same team writes one address twice, and
the export format says how a reader tells that case from the other.
Where the framework comes from¶
upstream¶
Where the corpus takes the framework from, and what the last take resolved to.
upstream:
url: https://github.com/paul80nd/knowledge-as-code
path: template
ref: main
commit: 1d5b531d5bae80dc9805dc501ccc6fe6aa2b4141
template-version: 11
taken-on: "2026-08-24"
| Key | What it states | Who writes it |
|---|---|---|
url |
the repository or folder serving the template | you |
path |
the folder inside it holding manifest.yaml |
you |
ref |
the branch or tag to take from, followed on every update | you |
commit |
what the last take resolved to, as a full-length sha | update |
template-version |
the template shape that take was on | update |
taken-on |
the day the framework last came down | update |
update reads url when no --from is passed, and a run with neither has nothing to take. Leave path out where the
manifest sits at the repository root, which is where this project keeps it.
update follows ref and never reads commit back. Together the two say that the corpus tracks a moving line, and
that you can still see exactly what arrived. A template read from a folder resolves no commit, so commit is left as it
stands, and new leaves ref beside it bare.
Real content is the only thing that reveals a schema is wrong, so the corpus that found the problem is often the one best placed to fix it. A change is settled once the repository serving the template accepts it.
upstream is not consumes. upstream is one framework this corpus receives as files and keeps.
consumes is a graph of records this corpus reads and never keeps. A corpus has one upstream, and any number of
consumed corpora.
upstream.url is not framework.base. upstream.url says where kac copied the template from, and
it is often a folder on the same disk. framework.base says where to report a problem with what arrived, and it is
always a tracker.
update-policy¶
How far an update goes. One of cautious or full, and cautious is the default.
update-policy: cautious
A seed file is your own words: a type's root page, its _template.md, the agent guidance. cautious writes a seed file
only where the corpus has none, so an update does not open with three dozen files to revert by hand. full refreshes
them too, and you reconcile the result from the diff.
update writes overlay files either way. They belong to the framework, not to the corpus, and an edit to one is drift.
types¶
The types this corpus adopted.
types:
- adrs
- policies
Validation, index generation and what an update writes all cover the types listed here and no others.
new writes the list, because a corpus it created has already been asked. Omit the key and you have not
declared yet, so kac reads adoption off the folders it finds. A type counts where both halves are there, meaning the
page and the folder.
Once the key is declared, validate checks the corpus against it in both directions. It reports a
type in the list that is not stood up, and a type stood up that is not in the list. A corpus that declares nothing is
asked neither question.
Use update --add-type and update --drop-type to change the list. Each moves the name and the type's
files together. Editing the list by hand leaves the corpus with a type it does not claim, or claiming one it does not
have.
enums¶
The ranges the schema leaves to the corpus.
enums:
platform: [dotnet-tool, static]
Some fields take a value the framework cannot know. What a service is built on is one list in a library and another in a
payments platform, so .schema/services.yaml declares values: $corpus.platform and lists no values of its own. You
write the range here, and validate checks every record against it.
Walk your own deployables, group them by the runtime and framework a contributor has to know, and close the list on what you found. The type page gives the method in full, and each corpus's copy records the values it settled on.
Write each value in lower case. An enum value is a grep target first and prose second. A range with Dotnet-Web in
it refuses dotnet-web from one side and Dotnet-Web from the other, because enum-lowercase refuses the value in the
record. No record can satisfy such a range, which is the same state as a range you never wrote.
A corpus created by new receives the key with nothing under it, because a corpus with no records has no
estate to derive a range from. Write a record with such a field before you write the range, and validate reports
corpus-enum-undeclared once, against .corpus.yaml. Nobody who wrote a record can fix that, so it is reported where
the person who can fix it works.
A value the range does not admit is an ordinary enum failure, reported against the record. The message quotes the
values your descriptor lists, so an author reads back the list they are being checked against.
consumes¶
The corpora this one reads.
consumes:
- corpus: example-engineering
shortcode: eng
version: ^0.1.0
source: https://nuget.pkg.github.com/OWNER/index.json
resolved: "0.1.0"
| Key | What it states | Who writes it |
|---|---|---|
corpus |
the name the producer publishes the package under | you |
shortcode |
the word you cite it by, as the eng in eng:pol-VURM |
you |
version |
the range you mean, as 0.1.0 or ^0.1.0 |
you |
source |
the registry or folder the package is fetched from | you |
resolved |
the version the last restore took | restore |
One organisation often keeps its policies in one corpus and each team's knowledge in another. A team's corpus cites
those policies without keeping a copy, and this block says which corpus it takes them from.
restore fetches each one and unpacks it under .imports/<shortcode>/, which is not committed.
The range says what you meant. resolved says what your build used. Both sit on the entry, so there is no lock file
beside this one. A source is a registry's service index, or a path to a folder holding what a producer built.
restore covers how a range resolves, what a folder source takes, and what a run refuses.
Leave the key out entirely where this corpus stands on its own, which is the ordinary case.
export¶
What the export leaves behind.
export:
exclude: [ ]
It is empty by default, and that is the important part. A record still in draft travels, and so does one whose review date has passed. Each record states its own status, so a consumer reads what the corpus actually has and decides for itself how far to trust it.
Filter here and the corpus's own state becomes invisible downstream. Your consumer sees a smaller, tidier vocabulary and
no sign that anything was withheld. Reach for it where you cannot warn the audience you publish to. Reach for it as well
where a draft would be read as an answer: a fix in draft is a resolution nobody has checked, and fix-lookup hands
one over as it would any other.
Two values are accepted. draft drops a record whose status says so. overdue drops one whose review-by is in the
past.
plugin¶
Where bundle reads the plugin tree from, meaning the skills and hooks an agent installs.
plugin:
from: ../../shared/.plugin
It reads them from .plugin/ at the corpus root by default. Set plugin.from and it reads them from one tree
elsewhere, resolved against the corpus root. Several corpora in a repository then share one copy, and update withholds
the shared half instead of writing it here.
kac never reads the manifest from the shared tree. The manifest gives the name the plugin installs under, so it stays
at .plugin/.claude-plugin/plugin.json in each corpus, along with the components that corpus declares. A file a corpus
writes beside it wins over the shared tree's copy of the same path, so one skill can be overridden.
Omit the key and the corpus keeps its own tree, which is what new creates.
skip¶
The files this corpus owns.
skip:
- path: .plugin/hooks/breadcrumb
reason: Patched for our proxy.
update neither reads nor writes a path listed here, in either direction, so an entry is how you keep a file the
framework would otherwise reclaim on every run. update reports each file it stepped over. Delete an entry once the
file matches the framework again.
Each entry gives a path and a reason. The reason is for whoever opens the file next. Layers says what it separates.
The export format is the page for what a consumer reads out of a corpus.