Added
-
spans-more-than-one-servicewarns where one service delivers the whole of an offering. An offering naming
one service inimplemented-byrestates the service record beside it. The rule is guarded on the field, so a
corpus that declinedservicesis not warned about one it cannot fill. -
A rule expression can count a field's entries.
entries('implemented-by')answers how many, where
present()answers whether. A scalar counts as one, and an absent field as none, so a rule that must not fire on
an absent field guards withpresent()first.min-items:states the same floor as an error. Use the fact where
the shortfall is worth a warning and not a failure.docs/design/expressions.mdlists it. -
An integration travels to a consumer.
.schema/integrations.yamldeclares anexport:block, sokac export
writes a file per integration record.What it doesandFailure modestravel atfull.Contract,Commercials
andContactsstay behind, because each describes owning the account.docs/design/export.mdnow listsdataas
the one type declaring no block. -
An explanation travels to a consumer.
.schema/explanations.yamldeclares anexport:block, sokac export
writes a file per explanation record.Where the detail livesis the one section it declares, so it is the only one
that travels, atfull. The rest of the body is free-form and no schema can name it.docs/design/export.mdno
longer listsexplanationsamong the types declaring no block. -
A postmortem travels to a consumer.
.schema/postmortems.yamldeclares anexport:block, sokac export
writes a file per postmortem record.Summary,Root cause,Contributing factorsandWhat went welltravel at
full, andImpactas its opening paragraph.TimelineandActionsstay behind, because a timeline names one
estate's clocks and each action links a work item the reader cannot open.docs/design/export.mdno longer lists
postmortemsamong the types declaring no block. -
An offering travels to a consumer.
.schema/offerings.yamldeclares anexport:block, sokac export
writes a file per offering record. Every section travels atfull, becauseWhere the detail livesis the only
place an offering states its work items and the rest of the record is short by design.docs/design/export.mdno
longer listsofferingsamong the types declaring no block. -
feature-file-repowarns where a feature file names a repository no implementing service does. A path in
feature-filesstarts with its repository, spelled as that repository's service spellsrepo:. The check compares
that first segment against the services inimplemented-byand names the ones it found. It warns rather than fails,
because a regression pack can live in a repository no service claims. Nothing reads the rest of the path until
feature-file-orphansruns. -
A service names the NFRs that bind it.
.schema/services.yamldeclaresnfrs:, and it travels in the services
export.nfrs.applies-toand bothnfrs:fields declarereciprocal:, so an NFR and the record it binds each name
the other, andvalidatereports either end that does not.
Changed
-
An offering says who it is for.
Who it is foris a required section, and it travels in the export. ITIL 4
defines a service offering by the consumer group it serves, and that group is what decides where one offering ends
and the next begins. TheWhy it existsguidance in each_template.mdno longer asks for the audience, because
the new section holds it. An existing offering gains one heading. -
An offering that is
livestates an NFR.nfrsis required once the status reacheslive: a customer
already has the offering, and nothing else on the record says how well it has to work. A corpus that declined
nfrsis not asked, and adopting the type starts the obligation with no edit to.schema/offerings.yaml. -
offeringsnames its prior art.lineagesaid "None that fits" and leftalignmentanddivergenceempty.
It now names ITIL 4 Foundation 2.3.2, the service offering, and states what the type takes from ITIL, the GOV.UK
Service Manual and Backstage, and where it parts from each. Both values render into every adopting corpus's
knowledge-as-code/lineage.md.docs/framework/lineage.mdrecords that ITIL is paywalled, beside the rows that
already were. -
The
capabilitytype is nowoffering. A record lands inofferings/asofr-borrowing, the page beside
it isofferings.md, and the identity line readsOffering:. ITIL 4 calls this document a service offering and
defines it by the consumer group it serves, which is what this type always meant.Capabilitymeant something else
to two of its likely readers: ArchiMate uses it for an ability an organisation possesses, and SAFe for functionality
below an epic. A corpus that adoptedcapabilitiesrenames the folder and the page. It changes each record's
type:,id:and identity line, and rewrites everycap-reference in the records of other types. It then writes
offeringsovercapabilitiesintypes:and deletes.schema/capabilities.yamlby hand.kac update --drop-type capabilities
refuses that last step, because the template no longer declares the name. The export isofferings@1, so a consumer
seescapabilitiesstop andofferingsstart. The template version moves to 18, andkac newstamps
template-version: 18. -
kac validateno longer asks for a field no record in the corpus can fill. A field whoseref:names only
types nothing there supplies is dropped from the required pass, and arequired-when:on such a field never fires.
A type an import publishes counts as supplied, so a standard citing a producer's policy clause is still asked for in
a corpus adopting nopoliciesof its own. Adopting one of those types starts the obligation with no edit to
.schema/. A field withallow-literal:is fillable without them, so it is still asked for.ref-resolvesis
unchanged: a value a record does write is held to the same standard as before. -
A postmortem records what ended the incident, and all three of the lessons.
Resolutionand two further
sections,What went wrongandWhere we got lucky, joinWhat went well, and all five travel in the export.
Google SRE groups the three lessons under oneLessons Learnedheading; each is declared on its own here, so
required-sectionasks for it andempty-sectionrefuses a bare one. An existing postmortem gains three headings. -
A postmortem may name more than one root cause. The template said "Resist listing several". Google SRE writes
Root causesin the plural and PagerDuty records contributing factors and no root cause at all, so the guidance now
says to name more than one where more than one stands out. Nothing about the section changes. -
A postmortem states when service came back, and
durationis checked against it.restored-atis a third
timestamp, required once a postmortem is published, anddurationis now an ISO 8601 duration such asPT4H20M.
duration-matches-the-momentsfails a value the two moments refuse, and its message carries the span they give, so
the fix is a paste.restored-not-before-occurredfails service coming back before it went. Nothing requires
restored-atto followdetected-at: an incident can recover before anybody notices it. -
A rule expression can ask for the time between two moments.
span('occurred-at', 'restored-at')answers with an
ISO 8601 duration in hours, minutes and seconds, and with nothing where either field is absent, is not a moment, or
where the second is the earlier.docs/design/expressions.mdcarries it in the table of what an expression may call. -
A postmortem states when an incident began and when it was noticed, to the second.
occurred-onand
detected-onare nowoccurred-atanddetected-at, and each takes a UTC timestamp as2026-09-07T20:18:00Z. The
gap between the pair is what the pair is for, and it is usually measured in minutes.-onnames a date everywhere
else in the schema, so the names moved with the type. A corpus holding postmortem records renames both keys and
writes a time into each:validatereports the old spelling asunknown-key. -
Where the detail livesand the frontmatter say the same thing, andvalidatechecks it.implemented-byand
nfrsdeclaremirrors-section: Where the detail lives, sorelated-matches-sectionreports either end naming an
id the other does not. That section is a bulleted list rather than a headerless table, which is the formservices
already uses for the same shape and the one a screen reader can read. It lost itsTested byline, which restated
feature-filesand had already drifted from it, and itsDecided inline, which no field ever backed. -
kac newseeds an offering template and type page that name no tracker. Both said functional detail lives in
Azure DevOps epics. They now describe a work item and leave the tracker to the corpus. Inside the corpus the
template links only toservices, so one that adopted neitheradrsnornfrsno longer receives a definition
into a folder it does not have. The type page also states the floor the type has: an offering whose
implemented-bynames one service is a synonym for that service. Both files seed, so an existing corpus keeps the
wording it was created with. -
A report's
generated.atdates the content's last meaningful change..schema/reports.yamldescribed it as
"the moment the content last changed", dropping the qualifier [OKF v0.2] states, while the framework moves the stamp
for neither a hand-raisedsourcesversion nor theImported:bullet an imported entry's raise rewrites. The
description and the field'snotes:are read by whoever maintains a corpus's schema, andkac updatetakes both
down. No generated block changes: theschema-reportstable prints the description ofgenerateditself and none
of its entries. -
kac newseeds report guidance the schema accepts.reports.mdtold you to verify a report you had just
written, whichno-self-verificationrejects. It now says to write yourself intogenerated.by, leaveverified
empty, and ask somebody else to read it. Both it andreports/_template.mdalso say whichsourcesentry a
hand-raise touches: the corpus the report answers for comes first and its raise edits frontmatter alone, and raising
an imported entry also means editing theImported:bullet under## Limitsand adding averifiedentry. The
files seed, so an existing corpus keeps the wording it was created with. -
kac newseeds the example policy aspolicies/devi-deviations.md. The file was
devi-deviations-are-recorded.md, which restated the record's title. A policy filename names what the policy is, and
the H1 states the intent..schema/policies.yamland thepolicies.mdtype page both state the rule. The file
seeds, so an existing corpus keeps the name it was created with. The template version moves to 17, andkac new
stampstemplate-version: 17.
Removed
ado-epicsis gone fromofferings, and with it theintvalue type. A work item id assumed one tracker,
and a corpus planning on GitHub issues had nowhere to put the equivalent. Work items are now links in the
Where the detail liveslist, labelled the way the corpus's own tracker labels them.ado-epicswas the only field
in the taxonomy declaredof: int, soint-formatguarded nothing andtype: intandof: intare no longer
values a schema may declare.kac checksprints one check fewer.
Fixed
- An index column heading spells an initialism in capitals.
kac generateheaded thetheir-slacolumn Their sla,
becauseidwas the only field name it read as an initialism. That column now heads Their SLA. Every other field
heads a column in sentence case, as it did.