Metadata¶
Frontmatter is what makes a corpus machine-readable. The validator checks it, the generator builds indexes from it, and an agent greps it to find things. What each type declares is the type's own business. This page is the model every type answers to.
What goes in a record¶
A record holds three kinds of thing, and each has one home.
Frontmatter is metadata about the record: what identifies it, places it in the taxonomy, and describes it as a whole. A field holds one value, or a set of them. An index sorts on it, a citation resolves against it, and an agent greps it.
The body holds the record's parts, where a part needs a rule or an address of its own. What counts as a part is the
type's business, declared under parts: in its schema. The declaration names the section holding them, and says whether
they are rows of a table or headings beneath it. The validator reads them out of the body and checks them.
Two shapes are in use. A policy's obligations are rows of ## Clauses, each with an id written beside it, cited as
pol-VURM.TIMEBOX, so a clause can be reworded without its address moving. A standard's rules are headings, each
opening with its id, so ### PERMS: A job declares the permission it needs is cited as std-CI.PERMS and linked at
#perms-a-job-declares-the-permission-it-needs. A glossary's terms are headings, addressed by the anchor each heading
slugs to, so gls-knowledge-as-code.corpus and a link to #corpus name the same entry.
Everything else is prose, in the section where it belongs. Ask whether anything has to address the piece: a citation that must resolve, a check that must fire. Where nothing does, a table is prose set in columns and a heading is a heading, and the schema says nothing about either.
Derive what the system already knows¶
A field costs every record of its type a column, because a wiki renders frontmatter as a table at the top of the page. So a field is a decision about the reader as much as about the schema.
A field states what is semantically the record's own. decided-on is a real fact about a decision, where the file's
last-modified date is git's business. Three fields are deliberately absent:
| Not a field | Because |
|---|---|
title |
It is the H1, verbatim |
created / updated |
Git knows, and will not forget to update it |
lifecycle |
Follows from tier. A second field could disagree |
type and tier are the deliberate exceptions. The folder gives a record both, and both are written down anyway, so a
record read away from its folder still says what kind of thing it is. The validator checks each against the value the
schema derives.
A derived field¶
A type can also take a field's value from the folder a record is saved in. A field declaring from: sub-path takes the
folders between the type's own folder and the file. So a policy at policies/security/accs-access-by-identity.md gets
category: security with no line of frontmatter saying so. Writing the key by hand is an error, reported as
derived-key, because the folder and the line could then say different things. Folders can nest, so
standards/platform/dotnet/testing.md gets platform/dotnet.
Save a record straight into its type folder and the field is empty, which is a normal state. You start using
categories by making a folder. Everything that reads the field gets the value: the generated index, its sort, and
kac export.
Filling sources¶
sources is a universal optional field. It states where a record's content came from, one entry per source. An entry
takes one of two forms: the address of something a reader can follow, such as a specification, a vendor page or a
ticket, or a description of the population the content was drawn from. Write the address in full. No type renders
sources, so a short form such as owner/repo#123 is a link nowhere.
Name a source where a reader could check the record against something the corpus does not keep. Where the record
states the estate's own facts, leave the field out. reports is the one type that requires an entry.
| Type | What sources holds |
|---|---|
| Data, Glossary, Integration, NFR, Standard, Tool | the published document the record restates: a specification, a regulation, a vendor page, a contract |
| ADR, Offering, Deviation, Explanation, Fix, Postmortem | the material the record was written from: a benchmark, a ticket, a thread, an incident channel |
| Control, Process, Runbook, Service | usually nothing. These records state the estate's own facts, and cite the repository or rule behind them |
| Policy | the minute or the opinion the commitment was written from, and never a framework, which aligns-with names |
| Report | each corpus the report answers for, and the content-version it was true of |
sources and lineage¶
sources sits on the record. lineage sits on the type, in .schema/<type>.yaml, and states what that kind of record
is modelled on.
One document can belong in either, and what you took from it decides which. Take the form, meaning the sections, the
fields, or the rule about superseding, and it belongs to the type's lineage. Take the content, meaning a
threshold, a definition, or a list, and it belongs to this record's sources.
Filling accepts¶
accepts is a universal optional field. It lists the warnings a record keeps, one entry per check, each with the
reason. validate reports an accepted warning as info, so CI stops raising it. The field stays in the record and never
travels in an export.
Accept a warning where the record already says why it stands, such as a gap its Operational notes describe. Leave a
warning standing where it points at work nobody has done, such as a runbook nobody has walked.
validate says which entries it refuses.
Naming a type¶
A type name is singular and its folder and page are plural: an ADR in adrs/, a standard in standards/. Each
record writes the singular name in its type field. Two folders take the singular: data/ is a mass noun, and a
corpus has one glossary/ kept in several files, one per bounded context.
Allocating an id¶
An id is <type-prefix>-<discriminator>. The prefix is singular, since an id names a single record: adr-0017,
std-SECRET. The discriminator names the document, and its shape is the type's id.style.
Numeric discriminators are zero-padded, allocated sequentially, and never reused. Where a record is withdrawn before acceptance, its number is retired. Mnemonics are allocated by meaning rather than in sequence, so there is no next one to take. A slug is the thing's own name.
You may correct a filename. You may not correct an id. Every cross-reference resolves against it. A record whose meaning has moved past its mnemonic is replaced, and the old one retired.
The three id styles¶
The schema sets one per type, and id.width says how long the discriminator runs.
Numbered (adr-0017) suits anything chronological. The id records the order things happened, which is information
none of the others can give.
Slug (svc-billing-api) suits anything with a natural stable name, where a number would be an arbitrary handle for
something already well identified.
Mnemonic (pol-VURM) suits a small, long-lived set that other records cite constantly, so the id says something.
Draw it from the concept, since the wording will change.
id.width is an exact count or a span. width: 4 gives every policy a four-character handle. A span, written as
min: and max:, lets the length follow the concept. Standards set min: 2 and max: 7, so std-PR and
std-SECRET are both well formed. A slug reads neither.
What the filename says¶
filename.carries-id says whether the filename opens with the discriminator. A policy's does, so pol-VURM is
filed as vurm-vulnerability-remediation.md. The mnemonic is upper-case in the id and lower-case in the filename, and
its first letter matches the slug's so the folder still reads alphabetically.
A standard's filename takes nothing of the id. It is a topical slug under a category folder, because one filename cannot say which of several widths its opening segment is.
A filename slug names what the record is. The H1 states what it commits to. So pol-EVER is titled Everything is
in version control and filed as ever-version-control.md.
A filename slug is at most 30 characters, excluding the NNNN- or mnem- prefix where the filename has one. A slug
that will not fit is often a record doing two things, so split or narrow it.
Referring to an id¶
Two separators extend an id. . selects a part. : selects a corpus.
. addresses a part of a record. pol-VURM.TIMEBOX names the policy, then the clause inside it.
gls-knowledge-as-code.corpus names the glossary, then the term. Each citation resolves against the record it names, so
a reference to a part that does not exist fails the build. So does a reference into a type that keeps no parts.
A citation takes three forms, and kac validate resolves each. A code span states the reference. A link carrying the
whole citation takes the reader to it. A link naming the record, with the part id written after the closing bracket,
does the same and reuses one definition:
The clause is `pol-VURM.TIMEBOX`.
The default branch is protected. See [pol-VURM.TIMEBOX].
The default branch is protected. See [pol-VURM].TIMEBOX, and the window in [pol-VURM].WINDOW.
[pol-VURM.TIMEBOX]: vurm-a-title.md#clauses
[pol-VURM]: vurm-a-title.md#clauses
The third form needs one link definition however many clauses of that policy the document cites. Point the
definition at the section containing the parts. The part id has to touch the bracket, so See [pol-VURM]. The
policy... stays a full stop.
A field may require the part. A standard's implements: names the clauses it puts into practice, and a deviation's
departs-from: names the clauses it breaks. kac validate refuses a bare policy id in either field, and on a Covers
line, because it would read as every clause covered.
A field may also be checked against what the prose cites. implements: declares mirrors-citations: Covers, so each
rule closes on a footnote naming the clauses it discharges, and kac validate checks that the union of those lines
equals the field:
### Nowhere else holds a secret
- A repository **MUST NOT** contain a secret, in source or in a pipeline definition.
- A build **MUST NOT** bake a secret into an artefact or an image.
_**Covers:** [pol-SCRT].EMBED, [pol-SCRT].LEAKED, [pol-SCRT].LOGS_
The line is italic with the label bold, and it closes the section it covers. A section covering nothing has no line.
kac validate reports a line that is not italic, names no clause, or names a policy whole.
A field declaring reciprocal: agrees with its counterpart. supersedes and superseded-by are one such pair,
and replaces and successor another. kac validate reports reciprocal where one record points at another and the
other does not point back. Knowledge is a graph says why.
: scopes a reference to the corpus supplying the record. eng:pol-VURM.TIMEBOX reads scope, record, part. A
record the reading corpus keeps is cited bare, and qualifying one is an error. Spelling a
citation says how a scoped reference resolves.
The shortcode¶
It is the half before the colon. The producer declares it as shortcode: in
.corpus.yaml, and every corpus citing that one writes what the producer chose,
so one obligation has one spelling.
A shortcode is immutable, because a rename breaks citations in repositories its owner cannot edit. So declare one
when something is about to cite the corpus. kac new writes the key bare.
It is two to eight characters, lower case, opening on a letter with letters and digits after it. It has no hyphen,
and it is no type's prefix, since std:pol-VURM reads as a standard. kac validate refuses both.
Adding a field¶
First ask whether the content belongs in frontmatter at all, as What goes in a record says. Then check that git, the folder, the H1 or an existing link does not already state the fact.
On a type the corpus declares itself, declare a new field in the type's .schema/<folder>.yaml and add it to that
type's _template.md. Then run kac generate so the generated tables pick it up. The
validator reads the schema, so it needs no change of its own.
On a type the framework ships, a new field is a change to the framework, because kac update reverts a corpus's edit
to a schema file it receives. That includes a field every type has, which goes in .schema/_universal.yaml. A type file
may refine it with a declaration of its own.
Taxonomy is the page for deciding which type a new field belongs to.