Changelog¶
What changed in each published version of
kac.
This page covers the tool, published to nuget.org as
KnowledgeAsCode.Tool. The same repository holds the schema, the
framework's documentation and the pages a corpus starts from. Those travel as a template with a version of its own,
which manifest.yaml declares and kac new stamps into every corpus it creates. A change there is recorded here where
somebody running kac can observe it, and nowhere otherwise.
The format follows Keep a Changelog. Versions sit below 1.0.0 while the command surface may still change shape.
A push to main publishes whenever kac.csproj names a version nuget.org does not already hold, and that publish tags
the commit and opens a release carrying the section for that version. A change lands its entry under ## Unreleased
first, and whoever owns the branch decides whether it ships now or waits for the rest of what it belongs to.
Unreleased¶
Added¶
kac validatewarns on a deviation from a clause that binds nobody. A departure from aSHOULD, aCOULDor aMAYtakes a reason in the change, and no deviation.departure-needs-nonenow warns wheredeparts-fromnames a policy clause stating noMUSTorMUST NOT, or a standard's rule none of whose bullets states one. An inherited clause is judged on thelevelits export states. Acloseddeviation is silent. Drop such an entry, or name the clause that binds.kac validatechecks that two clauses sharing an id signpost each other. Two policies binding one obligation give it one clause id, and each clause closes with a[pol-X].IDsignpost to the other.pair-one-waynow fails a clause that a clause of the same id signposts, where it does not signpost back.shared-id-unsignpostedwarns on an id two policies use with no signpost either way, once for the two. A scoped signpost and aretiredpolicy form no pair.part-refalready fails a signpost to a clause that does not exist. Add the missing signpost, or give one clause another id.
Changed¶
kac validateresolves areposentry by the form ofrepos-basenow. An entry is<repo>or<group>/<repo>, spelt as the host spells it, spaces and capitals included. Against an Azure DevOps organisation,mirrors-repo-linksresolves<project>/<repo>to<base>/<project>/_git/<repo>. Against a project it resolves<repo>to<base>/_git/<repo>. It warns on an entry in the other form, and readsdev.azure.com/{org}and{org}.visualstudio.comas one organisation. It percent-decodes each link and suggests one percent-encoded. Pointrepos-baseat the organisation where repositories sit in several projects.feature-file-repocompares as many opening segments of a feature path as an entry has.kac report coveragelists only the controls that check something now. TheControlscolumn named every control verifying a rule inCovered by, so anot-enforcedcontrol read as coverage. It now leaves out a control whosemechanismisnot-enforced, and one whosestatusis not its type'sin-force:value, such asplanned. An inherited control is judged on themechanismandstatusits export states.## Limitssays what the column leaves out. Regenerate a coverage report you keep as a record.kac reportleaves the imported version out of## Limitsnow. TheImported:bullet stated each consumed corpus at itscontent-version, andsourcesstated the same version in the frontmatter. Raising an imported entry by hand meant editing both, and nothing caught a bullet left behind. The bullet now states the shortcode and the corpus, sosourcesis the one place the version is written, and a raise leaves the body untouched. Delete the version from the bullet of a report you keep as a record.- The deviation and offering templates ask for two things they left out. A deviation's
How it closesasks what closing it costs, in staff time and anything that has to be bought, as prose and with no new field. An offering whose status is notliveopens with that status and what a customer can do today, so its first sentence does not read as a working product.kac newwrites both.kac updateleaves a template you already hold alone, so copy the wording by hand where you want it. kac reportlinks every record id it cites to that record's page now. A report named each record as bare code, so a reader could not open an inherited clause it listed. Each id is now a link, and a cited part follows it as code, as[`eng:pol-A11Y`][eng:pol-A11Y]`.UPFRONT`. A local record links by its path fromreports/, and an imported one to the page its producer published. An import publishing no page stays bare code, and## Limitsnames it. The link definitions sit at the foot. Regenerate a report you keep as a record.
Removed¶
publishing-targetno longer takesmkdocs.kacaccepted the value and built nothing for it, so a corpus naming it exported without links and could not tell it had asked for an unbuilt target. Nowkac new --publishing mkdocsrefuses the flag, andvalidatereportsdescriptor-targetagainst a.corpus.yamlnaming it. Each message lists the targets that work:azure-devops,azure-devops-wiki,githubandnone. A corpus onmkdocswritesnoneinstead, and its export changes only in thetargetit states.
0.43.0 - 2026-10-28¶
Changed¶
raise-finding,request-deviationandharvest-findingskeep each platform's commands in a file of their own.github.mdandazure-devops.mdsit beside eachSKILL.md, and a session reads only the one its tracker'stargetnames. On Azure DevOps,harvest-findingsnow has a command for every step: it reads a work item and posts a Markdown comment throughaz rest, reads a body stored as HTML, and opens adraftwithaz repos pr create. The template version moves to 26, andkac updatewrites the six new files.
Fixed¶
kac report coveragecounts the clauses a corpus inherits without adoptingpolicies. It looked for the clause type in the corpus's own schema, which has only the types the corpus adopted. So a consumer decliningpoliciesreported 0 clauses and gave no reason.reportnow reads an imported type as clauses where its lines state aclauseand alevel, the keyspolicy-lookupreads. A producer that renamedpoliciesstill reports, and an imported glossary's terms stay out. Regenerate a coverage report you keep as a record.list-orderpasses over a report'ssourcesin the orderkac reportwrites it. The run lists the corpus the report answers for first, then each import, andvalidatewarned wherever an import sorted ahead of the corpus. A list field may now declareorder: as-written, andlist-orderpasses over that list.sourcesdeclares it in.schema/reports.yaml, whichkac updatewrites.order:on a field that is not a list reportsschema-dispatch, and a value other thanalphabeticaloras-writtenreportsschema-unreadable.raise-findingandrequest-deviationfile an Azure DevOps work item whose description stays Markdown.az boards work-item createcannot mark a description as Markdown, so Azure DevOps read the body as HTML and lost the fences around itskac-findingorkac-deviationblock. Each skill now writes a JSON patch file and posts it withaz rest, which sets/multilineFieldsFormat/System.DescriptiontoMarkdown.az restsigns in throughaz login. Where Azure DevOps refuses that sign-in, the skill prints the body and namesaz login.
0.42.0 - 2026-10-28¶
Added¶
kac newandkac bundlesay when a corpus has no tracker to file on. A corpus publishing tomkdocsornonestates notracker:block, soraise-findingandrequest-deviationprint each issue for you to file.newnow closes on a line saying so, and that atracker:block in.corpus.yamlsets one.bundleprints the same warning wherever the plugin ships either skill, and still builds.validatesays nothing, because a corpus that never bundles never files. Getting started names setting a tracker as a step before you publish a plugin.
Changed¶
slug-lengthsays how much shorter the filename has to be. It printed the slug's length and the limit, and left the arithmetic to you. It now printsslug '<slug>' is 38 characters; rename the file to bring it within 30, 8 shorter., with the slug's real length, the type'sslug-maxand the difference between them.- Every
_template.mdstates the slug limit and the order of a list. Its Frontmatter guidance says the slug is 30 characters at most, and that each list field's entries go in alphabetical order, so a record written as it asks passesslug-lengthandlist-order.knowledge-as-code/contributing.mdstates both once.kac updatewrites the newcontributing.md. It leaves a template you already hold alone, so add the paragraph by hand, or runkac update --policy fullwhere you have not edited your templates. kac newandkac updatewrite you as the owner of each record they seed.adr-0001andpol-DEVIarrived owned by the framework's author, and a corpus published them under that name. Both commands readgit config user.emailand writehuman:and the part before the@. Where git has no email, they use the name you logged in as, and say so.kac newasks for the owner with that default, and--owneranswers it. Both print the owner they wrote. The framework's glossary keeps its owner.
0.41.0 - 2026-10-27¶
Added¶
kac restoresends a personal access token as Basic auth whereKAC_REGISTRY_USERis set. Azure Artifacts takes a personal access token in no other form, so a restore from your own machine was refused. SetKAC_REGISTRY_USERto any name, andKAC_REGISTRY_TOKENto a token with the Packaging (Read) scope. WhereKAC_REGISTRY_USERis unset,kacsends the token as a bearer token, which is what a pipeline's own token needs. A refused read and an empty version listing each name both variables.
Changed¶
corpus-retrieval,raise-findingandrequest-deviationsay what they hand you withoutghoraz. Each names the client it runs at the top, and its fallback section lists the cases that section covers: a client that is not installed, and a session that cannot run a command.raise-findingandrequest-deviationprint the whole body for you to file by hand, andcorpus-retrievalasks you for the section it needs.- Each lookup skill lists what travelled.
kac new,kac updateandkac bundlesend lookup skills that answer "which standards are there?" and its like for policies, glossary terms, controls, processes and fixes.standards-lookupreads the build index, and the others group their flat file or list their folder. Each counts an inherited record, answers with ids and titles, and reads the record to test whether a sentence is already stated. A question about what nothing covers goes toask-the-corpus.
Fixed¶
controls-lookupdeclares the reports it reads. It answers a question about the whole corpus from a report, so itsplugin.jsonentry listsreports@2underoptional. Where the export includes reports at another shape,kac bundlerefuses the plugin until the entry names that shape. Add"optional": ["reports@2"]to thecontrols-lookupentry of yourplugin.json.- Both starter pipelines run
kac restorebeforevalidate. A new corpus can declareconsumes:with no edit to its pipeline. Whereconsumes:is empty, the step prints that there is nothing to restore and succeeds. The GitHub starter grantspackages: readand passesgithub.tokenasKAC_REGISTRY_TOKENto the restore and validate steps, sovalidatechecks each lock against the feed. The Azure starter passes$(System.AccessToken). Each header comment names the feed permission a package from another repository or project needs. - The template's build-index hook prints the build index to each subagent, and again after a compaction. It
printed once for each session id, so a subagent never received it, and a compaction kept the id and silenced it.
The hook now keeps a marker for each agent, named
<session_id>.<agent_id>for a subagent. A newSessionStartentry matchingcompactruns it withreset, which deletes the session's markers, so each agent's next tool call prints it again.
0.40.0 - 2026-10-27¶
Added¶
kac validatereports its findings on the pull request under CI. Under GitHub Actions each error and warning is also a::erroror::warningannotation, and under Azure Pipelines a##vso[task.logissue]line. On a pull request with findings,validateposts one comment per corpus listing them, and later runs update it. It posts only where the step hasGITHUB_TOKENwithpull-requests: write, orSYSTEM_ACCESSTOKEN, and a failed post leaves the exit code alone. Without the token, a run with findings prints a note and a clean run prints nothing. An info finding and--jsonwrite neither, and a local run is unchanged.kac newpins itself in.config/dotnet-tools.json. The tool manifest names the release of thekacthat rannew, andkac updatenever compares, writes or reports it. Both starter pipelines install .NET SDK 10.0.402, rundotnet tool restoreand thendotnet tool run kac, so each can take the tokenvalidateposts with. The GitHub starter grantspull-requests: writeand passesGITHUB_TOKENto the validate step, and the Azure starter passesSYSTEM_ACCESSTOKEN. The starter'sdependabot.ymlgains anugetentry, which proposes each newkac.- A record accepts a warning, with a reason.
accepts:is a universal field listing acheckand areasonfor each warning a record keeps.kac validatereports each accepted warning as info, readingaccepted:and the reason, so CI neither annotates nor comments on it. An entry naming an error, an info or an unknown check is the erroracceptance-check, and one matching no warning is the warningacceptance-stale. The field never travels in an export. kac validate --since <ref>compares each record with its state at that revision. Records pair byid, so a moved file still compares.immutable-after-acceptedandimmutable-after-publishedare errors on a change in substance to a decided ADR or a published postmortem. A reflow, a link target, a typo or a move ofstatus,superseded-by,review-byoracceptspasses.retire-not-deletewarns on a record deleted while in force. A ref git cannot resolve exits 1. Without the flag, one line names the checks skipped. Both starter pipelines fetch the full history and pass--sinceon a pull request.kac validate --since <ref>keeps clause ids and rule ids.clause-id-keptis an error where a policy wasactiveat the revision and a clause id it had there has gone, andrule-id-keptis the same for a standard's rule ids. The message names the id and says to restore it or replace the record with a new one. A part added or reworded passes, and so does a new title. A part whose id no longer parses is left to the id-format checks. A record that was notactiveat the revision changes freely, and that includes a draft madeactivein the same change.
Changed¶
- A required key in an entry needs a value.
kac validatereportsentry-keywhere a record's object entry writes a required key with no value, such asreason:inaccepts:orresource:insources:. A template's entries are exempt. - A tool can replace several others.
replacesintoolstakes a list of tool ids, one per line, so one tool can take over from several. Each tool it lists names it assuccessor, andkac validatereportsreciprocalwhere one does not. A single id written on the line itself is now alisterror.version:underexport:in.schema/tools.yamlmoves to 4. - A standard's rule is cited by its id, and a control names the rules it checks. Each rule heading opens with an id,
as
### PERMS: A job declares the permission it needs, andkac validatereportspart-id-formaton one without. The export'spartis that id andtitledrops it, whileanchorstays the slug of the whole heading.verifiesincontrolstakes rule ids, asstd-CI.PERMS, and a bare standard id is aref-resolveserror.reciprocalfolds a rule id to its standard.version:underexport:moves to 3 in.schema/standards.yamland to 2 in.schema/controls.yaml. kac report coveragenames the rule.Covered bylists the rules whoseCoversline cites the clause, andControlslists the controls naming one of them. The review viewkac bundlewrites lists each control under the rule it names.- A runbook, a process and a manual control say who did it.
last-rehearsedandlast-verifiedtake the last run, as{ at: "2026-10-20", by: human:alex.doe }, ornever. A runbook statesmethod,liveortabletop, in place ofrehearsal-method, andkac validatereportswalk-method-statedon a walk without one.bynames a person or an agent, never a process. A rule expression reads one key of an object field by a dotted path, asdays_since('last-rehearsed.at').version:underexport:moves to 3 in.schema/runbooks.yamland.schema/controls.yaml, and to 2 in.schema/processes.yaml.
Removed¶
changelog-begins-at-activeandchangelog-on-material-changeleave thestandardsschema. Neither ran, and a standard's type page no longer lists them as declared and not yet enforced.
Fixed¶
kac export,kac bundle,kac report --outandkac validate --jsonwrite the same bytes on Windows as on Linux. Indented JSON and the body of a report ended their lines with CRLF on Windows, mixed with the LF written around them. Each line now ends with LF on every platform.- A path
kacreports uses/on Windows.kac report --out reports\clause-coverage.mdprinted the backslashes and lost the grey folder prefix. Everywroteline, and each refusal ofreport --out, now prints/. kac newandkac updatedelete their template clone on Windows. Git marks the files under.git/objectsread-only, and Windows refused to delete them, so each run left its clone in the temp folder.
0.39.0 - 2026-10-23¶
Added¶
kac exportstates each type's in-force status in the manifest. Each entry undertypeshasinForce, thestatusvalue its schema declares asin-force:, or null for a type with nostatusrange. An inherited type states its producer's value, and two corpora stating different values for one type stop the export.kac bundlereads it for the review view, which namedactivefor a standard whatever the schema said. An export without the key still bundles, and its review view says the status is not stated.formatVersionstays at 4.
Fixed¶
kac bundlenames the status a control is in force at in the review view. The view said a control checks a rule only where its status isactive, whatever the controls schema declared asin-force:. It now readsinForceon the controls entry of the export's manifest, as it does for standards. Where the entry states none, the view says so, says no control is known to check a rule, and leaves every obligation to the reviewer.present()andentries()read a filled-in object field. On a report holdinggenerated:with its keys,present('generated')read false andentries('generated')read 0. An unquoted{{…}}placeholder still reads as absent.kac validatereports one under an object field, or in a list of objects, as it does under any other field. It failed there with anInvalidCastException.kac bundlenames the version a plugin keeps when the corpus states none. Where.corpus.yamlhad nocontent-version,bundlewarned that the plugin had no version and nothing would install it. The plugin kept the version its ownplugin.jsonstates, so the warning was wrong. It now names that version. Setcontent-versionand export again to choose the version a plugin publishes.kac validatereports a ruleseverity:it cannot read. A value other thanerrororwarning, such aserororError, read as no severity at all. A rule nothing implements then passed as an intention, unreported, and anexpr:rule was told it had no severity.schema-dispatchnow reports the value against the rule's line in its.schema/file.kac report --outrefuses a corpus whose.corpus.yamlstates nocorpus:. The report'ssourcescited the corpus by its absolute path, so a committed report named one machine's folder. It writes nothing now, and says to write the key. A report printed without--outcites that corpus asthis corpus.kac newremoves a reference definition naming a declined type's page. A seed linking a type page by reference, such as[deviation][deviations]with[deviations]: ../deviations.md, kept that definition in a corpus that declined the type. The corpus failedlink-resolveson its firstvalidate. The definition now goes, and each link citing it keeps its words, as an inline link already did.kac updatenames the CI system each withheld file belongs to. A corpus holding none of the template's CI files was told it withheld three continuous integration starters. One of the three is.github/dependabot.yml, which runs no build. The line now counts the files and names their systems, such asgithubandazure-devops.kac exportwrites a section's reference links inline. A section using[resilience][pol-RECV]travelled without the definition at the foot of its record, so the link lost its target. Each full, collapsed or shortcut reference, and each image reference, now arrives as an inline link to the target its definition states. A reference with no definition arrives as written.kac exportwrites a part line's reference links inline. A part's lead, asides and bullets, such as a term's definition or a rule's obligations, kept a reference such as[pol-TRUS]without the definition at the foot of its record, so the link had no target. Each now arrives as an inline link to that target, as one in a section does. A record's last part, where it holds only asides, no longer exports those definitions as its lead.kac exportwrites a relative link as the address of the page it points at. A link such as../policies/x.mdin a section or a part line was relative to the producer's record, so a consumer's reader could not follow it. Where the corpus has an address, a target resolving to a Markdown file it contains now arrives as that file's address. Any other target arrives as written. A record inherited from a package exported by 0.38.0 or earlier keeps its relative targets until that producer publishes a new version.kac exportkeeps the indent of a block inside a list item. A fence inside a numbered item lost the indent of its opening line, so a renderer closed the list there and showed the code outside it. A paragraph in the item lost its indent when unwrapped. Both now arrive indented as written.kac exportkeeps a fence that holds a blank line.exportsplit a section at each blank line before it looked for a fence. The lines after a blank line inside a code block were then joined onto one line. A fence now arrives as written up to its closing line, or to the end of its section where it has none.kac report coveragelists only a deviation in force. TheDeviationscolumn listed every deviation citing a clause indeparts-from:, so a closed one read as a departure that still stands. A deviation now appears only while itsstatusis the value its type declares inin-force:, which isactive.## Limitssays so.kac validatereports a clause opening with a keyword in a code span. A clause such as`SHOULD` gopassedclause-modalas though it opened with the keyword. A code span names a keyword, so that clause states no obligation.clause-modalnow reports it as opening with no modal. It reports a boldMUSTfollowed by`NOT`too, whichkac exportread asMUST NOT.kac exportrefuses a consumed type that lost record files. Where none arrived, the type and its part lines left the export unreported. Where some did, the rest went missing unreported.exportnow stops and names the type and its producer when fewer files arrive than the producer's manifest counts, or part lines arrive with none. A type with no records, lines or counts is still left out.
0.38.0 - 2026-10-23¶
Fixed¶
kac validatereports a frontmatter finding on the line its key sits on. A finding about one value, such asenum,date-format,field-pattern,id-formatorempty-optional-key, named the line above it, in the text output and in--jsonalike. An editor or a CI annotation opened the previous field. A finding about one entry of a list, such asdependency-criticalityorlist-order, now names that entry. A finding about the whole record, such asrequired-field,unknown-keyorkey-order, stays on the opening---.kac exportjoins a wrapped line that opens with a code span. A paragraph whose later line started with a backtick, such as`kac`, travelled with the source's line breaks in it. Of the lines opening with a backtick or a tilde, only a fence keeps a paragraph as written: three or more backticks, or three or more tildes. A~~~fence was joined before, and now arrives as written. The export changes wherever a record has either.kac validaterefuses a part line reading a list or an object field. Anexport.parts.line:key whose source isfront.<field>takes one value. A field declaredtype: listortype: objectwas read as nothing, andkac exportwrotenullunder that key on every line of the flat file.schema-shapenow reports the source against the type file, whether the type declares the field or_universal.yamldoes, such astags. Name a field with one value, or drop the key. The record file still has the field whole.kac exportwrites a part line arriving by two routes once. A corpus consuming both a producer and that producer's own producer met the grandparent's part lines twice, stamped alike. The parts file had both copies, and the manifest'spartscount included both, so the breadcrumb overstated what the corpus holds. Each line is now written and counted once, as a record file already was.kac validatereads an em dash as an absent value. A field written astarget: —passed every check, thoughbare-keysays an absent value is never an em dash. A required field with nopattern:validated clean with nothing filled in.bare-keynow reports the spelling, andrequired-fieldreports the field. A quoted"—"counts, as""does. A dash beside other text, such as— tbc, is a value. Thebare-keymessage and the checks table onknowledge-as-code/metadata.mdnow list''beside"", so runkac generateto update that page.kac validatereports a citation footnote written above a deeper heading inside a part. A standard'sCoversline placed above an H4 inside an H3 rule passedmirrors-citations, though the rule went on below it. In a type declaringparts:, a heading now closes a footnote's section only at or above the part level, anywhere in the record. The next rule, the next H2 and the end of the file still close it, and the line passes there.kac exportrefuses a part line whose record file did not travel with it. A consumed corpus could send a line naminggp:gls-oldwithoutglossary/gp/gls-old.json, andexport,validateandbundleall passed. A lookup skill following that line opened a file nobody received.exportnow writes nothing, and names the type, the line's record and the file it expected. Re-export and re-pack the corpus that sent the line, then runkac restore.kac validatereports a rule's config against the schema.max-words:orpostures:on a rule that never reads it passed in silence, and so didmax-words: five, which read as absent. A misspelt posture inpostures:reported every correct policy as wrong: 108 errors across 40 records in the engineering example.schema-dispatchnow reports each against the rule's line in its.schema/file, and a rule whose value it cannot use checks no record until the value is fixed. Spell a posture as the register's##heading spells it.kac validatereads a list of objects as filled in.present()andentries()in a rule'sexpr:read a field declaredof: objectas empty, sopresent('aligns-with')was false on a policy that filled it in. Both now count every entry of a list, whatever its shape. A valuerequired-fieldreads as absent, such as~,nullor—, is absent topresent()too.kac validaterefuses a key on a list of objects that would check nothing.ref:,min-records:,mirrors-section:,mirrors-citations:andreciprocal:each read a list entry as one plain value. On a field declaredof: object, each passed in silence and checked no record.schema-shapenow reports each against the type file, or against_universal.yaml. Drop the key, or declare the fieldof: id.kac validatereads a modal in a code span as a word the clause names.clause-compoundwarned on a policy clause such as "MUST record a deviation before departing from aMUSTorMUST NOT", reading the backticked keyword as a second modal. The template'spol-DEVIseed has that wording, so every corpuskac newcreated warned from its firstvalidate. A clause row now drops its code spans before looking for a second modal, as a rule's bullets already did. A second modal written plain or bold still warns.
0.37.0 - 2026-10-21¶
Changed¶
kac generateprints the type relations once, as a labelled graph.knowledge-as-code/taxonomy.mdno longer has thetypes-edgestable. Each edge intypes-graphis labelled with its field, and a reciprocal pair is one edge labelled with both, as-- superseded-by / supersedes -->. Thetypes-lineagetable onknowledge-as-code/lineage.mdkeepsTypeandNearest prior art.alignmentanddivergencestay in each type's schema underlineage:. Runkac updateto take both pages, which removes the oldtypes-edgesmarkers.kaccites BCP 14 for the bold keywords. Thenot-normativewarning fromkac validate, and the descriptions ofnot-normativeandentry-requirementinkac checks, say "BCP 14" where they said "RFC 2119".kac generatewrites the same descriptions into the checks tables onexplanations.mdandglossary.md. Runkac update, thenkac generate, to take them.
0.36.0 - 2026-10-21¶
Added¶
kac bundlewrites the views a plugin manifest declares. A component with aviewkey is generated into the plugin from the export, at itspath. Each view lists the rules under their standard and its status.build-indexgives each rule's title, under a count of the rules the corpus states.reviewgives each rule's obligations, and the standard's controls with their mechanism and status. It opens on facts: what each keyword and status means, and that a control checks only the obligations its record's What it checks section names, where it isactiveand runs asciorruntime-alert. A type under a component's newoptionalkey is read where the export has it, and its absence trims nothing.kac bundlerefuses a view it does not write, and a view path the plugin tree orcorpusRootalready uses.kac bundlerenders what the build-index hook prints. Where the hook directory and the build index both ship, it writeshooks/build-index.json, which the template's newPreToolUsehook prints at a session's first tool call, whichever tool that is. The hook prints it once a session, marking the session under${CLAUDE_PLUGIN_DATA}. An index over the 10,000 characters Claude Code passes to a session is replaced by its rule and standard counts and its path, andkac bundlewarns. The template moves to version 20.standards-review, the skill that reviews a change against the review view. It runs in a subagent of its own, readsviews/review.mdwhole, and cites the rule behind each finding: a broken MUST as a violation, a departure from a SHOULD as a judgement call. It leaves out only what an activeciorruntime-alertcontrol checks.kac bundleships it wherever the export carries standards. The template moves to version 21.kac updatewrites the skill and leaves yourplugin.jsonalone, andkac bundlerefuses a skill no component declares, so add:
{
"path": "skills/standards-review",
"requires": [ "standards@2" ],
"optional": [ "controls@1" ],
"note": "Reviews a change in a forked subagent against views/review.md, read whole. It takes a rule's link from corpus/standards/rules.jsonl, and reads a control from corpus/controls/ where the export carries controls."
}
ask-the-corpus, the skill for a question with no known place in the corpus. It sends a known need to its lookup, and reads deviations and reports from files beside it.kac bundleships it wherever a lookup ships. The breadcrumb names skills by the roleannouncegives them:"router","lookup"or"write-back".truestill reads as"lookup". The breadcrumb now says the records are the organisation's own knowledge, names the router, and asks a session to report a change withraise-finding. The template moves to version 22. Copy itsask-the-corpusentry and the two newannouncevalues into yourplugin.json.ask-the-corpusanswers "something is broken, what do I do first" from a runbook. A newrunbooks.mdbeside it matches the failure against each runbook's exportedSymptoms, and quotes its id, title andImpact. The steps never travel, so it fetches them throughcorpus-retrieval. Where the fetch fails, it gives the runbook's link and states no steps of its own.fix-lookupreads the same file where no fix matches, or something is broken now. The template moves to version 23. Add"runbooks@2"tooptionalin theask-the-corpusandfix-lookupentries of yourplugin.json.
Changed¶
- Every bundled skill states what it is for, the intent of the type it reads, and the reason for each rule it
gives. A MUST is the baseline for the work, and a SHOULD is the usual way, which the work may leave by giving its
reason. A skill sets out an order only where the order is the content: filing a finding, requesting a deviation, and
reviewing a change. Each description names each case that triggers it once.
process-lookupstates the type of each field it describes, and readstypesbefore it says what is missing. A trial over 22 cases found the rewrittenstandards-lookupcomplies and cites as often as the skill it replaced.
0.35.0 - 2026-10-16¶
Changed¶
- The universal checks print once, on
knowledge-as-code/metadata.md.kac generatewrites them into a newchecks-universalblock there. Each type page's checks table lists only the checks its type adds, under a link to that block. Its frontmatter table leaves out a universal field the type takes unchanged, and keepstier,statusand any field the type refines.kac validatereports ametadata.mdwithout the new markers, and the message nameskac update, which writes the framework's copy of the page. Runkac update, thenkac generate.
0.34.0 - 2026-10-16¶
Added¶
-
A seeded line that lost its link is listed.
kac newandkac updateprint each line of a seeded page that linked a type this corpus declined, by file and line number, with the types it named. The link is gone and the words stay, so the sentence may still send a reader to a type the corpus does not have. Read each line and rewrite it for your corpus. A line linking into a declined type's folder is dropped whole, and is not listed. -
A template link into a folder the corpus lacks is reported.
kac validatewarns where a_template.mdlink carries a placeholder and points into a folder this corpus does not have, such as../policies/{{a}}.mdin a corpus with nopolicies/.link-resolvesstill skips the placeholder itself. A record written from the template would faillink-resolvesonce an author filled it in. The check id istemplate-folder-resolves.
Changed¶
fallback-requiredreads each row of an integration's Failure modes table.kac validatewarns where a row leaves its fallback cell empty, and where the section has no table. WriteNone.in the cell where nothing takes over. Before, the rule looked for the word fallback anywhere in the section, and the template's own column header supplied it, so a record copied from the template always passed.kac newandkac update --add-typedrop a template's links to the types you declined. A template line citing a record of a declined type is removed, with the link definition behind it. Before, the definition arrived intact, and every corpus that declined a type received a template offering a link to a folder it does not have. A link to a declined type's root page is still unlinked and its text left standing.kac bundlerefuses a skill or hook thatplugin.jsondoes not declare. A directory underskills/orhooks/in the plugin tree that nometadata.componentsentry claims stops the run, and the message names it. Before, it shipped even where the export lacked the types it reads, because no trim could reach it. Declare it with the types it reads inrequires. A file outside those two directories, such as a README or a licence, still ships undeclared.
0.33.0 - 2026-10-14¶
Added¶
- An ADR whose decision rests on a descriptive record is reported.
decisions-cite-no-descriptive-recordruns onadrs.kac validatewarns where a link inDecisionorConsequenceslands on a record of a descriptive type, such as a service or a glossary. That record changes as the estate does, and an ADR is not rewritten once accepted. A link inContext,Alternatives Considered,RelatedorReferencespasses. The check id isdecision-cites-descriptive. - A draft deviation past its review date is reported.
draft-expiryruns ondeviations.kac validatewarns where a record is stilldraftand itsreview-byhas passed, so a departure nobody finished accepting does not sit unnoticed. The message offers three fixes: accept it with a newreview-by, set a new date, or delete the record.expirystill reports anactiverecord alone, and aclosedone is reported by neither. - A term two glossaries define, linked from neither, is reported.
redefinitions-are-linkedruns onglossary, in place of the unrunredefinitions-are-reciprocal.kac validatewarns where two glossaries in one corpus define the same term and neither entry links to the other at the term's anchor. It reports against the glossary further down itsnarrowschain, because a glossary every corpus shares cannot link back. The check id isredefinition-unlinked. - Each line of an export's flat files has its link.
kac exportwriteslinks.humanon every line ofterms.jsonl,clauses.jsonl,rules.jsonlandframeworks.jsonl, built for the corpus's publishing target. It is the same shape a record file has. A reader quotes it, and no longer fills inhumanTemplate, whose Azure DevOps wiki form needs.mdremoved and every/encoded. It isnullwhere the corpus publishes nowherekaccan address.
Changed¶
- The documentation site is split into four sections, one for each reader. Use it, The framework, Schema reference
and Maintaining
kaceach open on who the section is for, and most pages have a new address.kac newwrites the new address of the corpus descriptor page into.corpus.yaml, andkac updateupdates framework files to cite the new addresses. Every old address redirects to the page that replaced it. - Every sentence after the first in a message opens with a capital.
kac new,kac updateand the descriptor checks inkac validatestarted a message's later sentences in lower case, as innothing was written. They now writeNothing was written, as the record checks already did. A script matching the old wording needs the capital.
Removed¶
- A corpus no longer receives
.schema/CLAUDE.md. That file is guidance for changing the framework's own schema, and it points at the tool's source and tests, which a corpus does not hold.kac newno longer writes it, andkac updatedeletes the copy an existing corpus holds unlessskip:in.corpus.yamlnames it..schema/README.mdstill arrives, and says where the rules for changing a schema file are written. The template version moves to 19, andkac newstampstemplate-version: 19. carried-in-full-by-digestis gone from theglossaryschema. It no longer appears on the glossary type page under Declared, not yet enforced. Nothing ran it, and the digest it protected is not generated. The type page and_template.mdnow ask for one paragraph of definition, usually one or two sentences, where they asked for one sentence. Most definitions in the example corpora already run to two.raiser-does-not-verifyis gone from thefixesschema. It no longer appears on the fixes type page under Declared, not yet enforced. Nothing ran it, because no field on a fix records who raised it. The type page still says that a session never verifies its own fix.
0.32.0 - 2026-10-10¶
Added¶
- A list written inside a rule's bullet is refused.
kac validatefails a bullet under a standard's rule heading that holds a list of its own, and reports it once at the outermost nested list. One bullet is one obligation, so a point nested inside it is cited by nothing and exported nowhere. Write each point as a bullet of its own with its own modal. The check id ispart-nested, and a type declaring no modals, as a glossary does, may still nest a list. - A modal written outside
Rulesis reported.binds-only-under-rulesruns onstandards.kac validatewarns where a bullet in any other section names one of the type's declared keywords, in bold or in plain capitals, and quotes the bullet back. No citation reaches a rule written there, so the message offers the two fixes: move the bullet under aRulesheading, or write the keyword in backticks where the bullet only names one. A paragraph is never reported, nor a bullet inside a block quote. The check id ismodal-outside-rules, and thestandards/_template.mda corpus starts from teaches the form. - A runbook whose resolution never says when to stop is reported.
outcome-statedruns onrunbooks.kac validatefails aResolutionsection with no line openingConfirmed when, so a reader whose steps worked learns when the incident is over. The check asks only that the line is present. Prose wraps at 120 columns, so a rule testing the last line would fail a closing sentence running over two. The runbook_template.mdstates where the line goes.failure-route-statedserves the other reader, whose step did not work. - A runbook nobody has walked in 92 days is reported.
staleness-loudruns onrunbooks.kac validatewarns onlast-rehearsed: "never", and on a date more than 92 days past, whicheverrehearsal-frequencythe record states. That floor is where this rule parts fromstalenessonprocesses, which measuresquarterlyandannualalone. A walk is a drill, or a read-through where the fault cannot be caused. The newrehearsal-methodfield records which, asliveortabletop, and is required oncelast-rehearsedstates a day. - A glossary definition that repeats its own term is reported.
definitions-do-not-repeat-the-termruns onglossary.kac validatewarns where the first sentence of an entry's definition names the term its heading states, and quotes the sentence back. Code spans and link targets are dropped first, so an entry definingExportas whatkac exportwrites is passed over. A later sentence may use the term, because by then the reader has the meaning. The check id isdefinition-circular, and ISO/IEC Directives, Part 2, clause 16.5.6 is the source. - A requirement written into a glossary entry is reported.
entries-state-no-requirementruns onglossary.kac validatewarns on a bold RFC 2119 keyword anywhere inside an entry, and names the term carrying it. Bold is what binds under BCP 14, so a keyword written in plain capitals is a term being named and is passed over. The obligation belongs in a standard, which the message says. The check id isentry-requirement, and the glossary type page states which of Part 2's rules run and which are the author's. - A service graded above something it depends on is reported.
dependency-criticalityruns onservices.kac validatewarns where a record'scriticalityoutranks thecriticalityof a record itsdepends-oncites, and quotes the id and both grades. The comparison rests onordered: true, a new key on a shared enum in.schema/_enums.yamlsaying its values rank, most significant first.examples/librarycarries one such edge, and the record already explains it.
Changed¶
- A rule's obligations export one entry per bullet.
kac exportwritesobligationson each line ofstandards/rules.jsonlas a list, one entry per bullet under the rule's heading. Each entry stateslevel, the bold modal the bullet binds with, andtext, the bullet as Markdown with its wrapped lines joined. A consumer filters onlevel. Thestandardsexport moves to shape version 2, and any type declaring modals can take thepart.bulletsline source.
Removed¶
not-load-bearingis gone fromexplanations. The rule asked that no decided-tier document cite an explanation as the authority for a constraint. No field declaresref: explanations, so such a citation is a markdown link, and a link does not say whether it is background or an authority.explanations.mdloses its Declared, not yet enforced table, which held this rule alone. Every corpus'sknowledge-as-code/taxonomy.mdgains the principle as prose, in thedecidedtier note.
0.31.0 - 2026-09-23¶
Added¶
- A diagnosis branch that strands the reader is reported.
escalation-requiredruns onrunbooks. Every branch under## Diagnosislinks to#resolutionor#escalation, or ends on the wordcontinueto fall through to the next question. The last list in the section has nothing to fall through to, socontinuethere is reported as well. One branch of the tree reaches#escalation, for the reader the tree does not answer.kac validatefails each branch at fault underdiagnosis-dead-end, quoting it, and the tree that never escalates underdiagnosis-no-escalation. ADiagnosiswritten as prose has no branches and is passed over. The runbook_template.mdteaches the form. - A standard's changelog runs newest first.
kac validatewarns where an entry is dated after the one above it, and states both dates. A bullet that does not open on an ISO date before a colon is passed over, which is how a template placeholder stays out of it. The rule ischangelog-newest-firstand it reports underchangelog-order. - A control states the day somebody reads it again.
controlsdeclaresreview-by, required on every control.frequencysays how often the check runs, andreview-bysays when somebody reads the record against it. The field travels in the export afterapplies-to,kac generatelists it on the type page, andcontrols-lookuptells a reader to check it besidestatus. - A review date that has passed is reported on every type that states one.
control-in-date,policy-in-date,standard-in-date,nfr-in-date,fix-in-date,glossary-in-dateandexplanation-in-daterun.kac validatewarns wherereview-byis earlier than the day of the run, as it already did for a tool, a data record and a deviation. A warning does not fail the build. - A type says which
statusvalue means a record is in force.in-force:sits besidevalues:in.schema/<type>.yaml, and every type that states a range declares it. The value is notactivethroughout: an ADR settles ataccepted, an NFR atagreed, an offering and a service atlive, a tool atapproved, a postmortem atpublished.kac generatewrites it into thestatusrow of the type page, so an author reads the settled value beside the range.kac validatefails a type that states a range and names no value, anin-force:the range does not include, and anin-force:on a field that is not an enum. - A
type: idwith noref:is reported.kac validatefails a schema whose field declarestype: id, orof: idon a list, and states no folders for its ids to resolve against. The reference pass selects a field on itsref:before it reads the type, so the value reached no check while the type page rendered the field as a citation. An id on an entry key is reported whatever the key declares, at any depth, because that pass reads a field's own value and never walks anentry:block. Both are reported underschema-dispatch. - A framework citation that never became a link is reported.
alignment-unlinkedruns.kac validatefails a policy whoseAlignmentcell writes[UK GDPR].Art.5(1)(e)with no link definition behind it. Markdown leaves the brackets on the page as text, so the cell states no mapping. Reported once per label. While the definition is missing,alignment-rollupdoes not reportaligns-withas claiming that framework.
Removed¶
constraint-consistencyis gone fromnfrs. The rule compared an NFR'stargetagainst thetheir-slaof each integration inconstrained-by. Both fields are free text, so there was no pair of numbers to compare. Thetheir-slanote in.schema/integrations.yamlno longer gives the rule as the reason to quote a contract, andconstrained-byis unchanged.- Three rules nothing could run are gone from the schema.
store-has-serviceondata,terms-are-singularonglossaryandblamelessonpostmortemsno longer appear on a type page under Declared, not yet enforced.kacchecks closed word lists. Each of these three needs a judgement it cannot make: a store written as prose, an English plural, a personal name. The type page and the_template.mdfor each type already tell an author what to write, andref-resolvesalready reports adatarecord whoseowned-bynames no service. - A corpus states which area path inside an Azure DevOps project its work is filed under.
tracker:andframework:in.corpus.yamleach take anarea, written below the project and separated by/. One project gives one backlog to any number of corpora, so without it every corpus's findings land in one queue.kac exportwrites it on all three trackers inmanifest.json, and the two filing skills setSystem.AreaPathfrom it.areaapplies toazure-devopsalone, so a newdescriptor-areacheck fails one stated elsewhere and the export drops it. Nothing derives it: only the corpus knows which area inside a shared project is its own.descriptor-versionmoves to 6, and the export'sformatVersionstays at 4.
Fixed¶
raise-findingandrequest-deviationfile on the tracker rather than where the corpus publishes. Both readtrackerandsources[].trackerfrommanifest.json, and neither readspublishingany more. On Azure DevOps the two differ:publishing.baseis a repository inside a project and the backlog is the project, so every work item went to an address with no backlog. Both also decode a percent-encoded project name before passing it toaz, soEngineering%20Standardsreaches it asEngineering Standards. Nothing changes for a corpus on GitHub, where a repository is its own issue list and the two addresses are one string.clause-compoundreads a whole modal word.kac validateno longer warns on a clause such as**MUST** be MUSTERED before the shift, where a longer word only begins with a modal.MAYinsideMAYBEandSHALLinsideSHALLOWdid the same. A second modal is still all the check reads. Two obligations written under one modal pass, and the check's description and the row on a policy's type page now say so.label-canonicalreads the label a reader sees.kac validatefails a shortcut or collapsed reference whose use site mis-cases the label, such as[STD-ci]behind a[std-CI]:definition. Markdown matches the two case-insensitively, so the page rendered an id no record carries and every check passed. A full reference such as[the workflows standard][ci-standard]displays its own words instead, so its label is now reported at the definition alone and no longer quoted at a line where nobody can read it.
0.30.0 - 2026-09-21¶
Added¶
- A service states how a live problem reaches a person.
servicesdeclaresmonitoring-output, required once a service isliveordeprecated. Its values arealert,ticket,logandnone.kac validatewarns throughcritical-service-is-alertedwhere aliveordeprecatedservice gradedcriticalstatesnone. The field travels in the export besidecriticality, andkac generatelists it in the services index after that column. - A service states the interfaces it offers.
servicesdeclaresinterfaces, a list of the contract formats this service publishes and the address each contract sits at. Each entry takes atype, drawn from aninterface-typerange the corpus states underenums:, and acontractURL. The field is optional on every service, because no machine-readable format exists for a command line tool or a web page. It travels in the export besiderepos. - A corpus states where its code repositories are addressed from.
.corpus.yamltakesrepos-base, a URL areposentry on a service is appended to.kac validatewarns throughmirrors-repo-linkswhere areposentry is not linked underWhere it lives, and where that section links a repository under the base thatreposdoes not state. Both directions are reported, and a corpus stating no base runs no check. The value travels in the export and in the plugin manifest, once for the corpus and once for each corpus it consumes, so a consumer resolves an entry it inherited against the estate that wrote it.descriptor-versionmoves to 5, andkac updatestamps it. - A runbook whose resolution gives no route out is reported.
failure-route-statedruns.kac validatefails a runbook whoseResolutionsection links nowhere to#escalation, so a reader whose step did not work has somewhere to go. The schema asked this ofDiagnosisalone. A reader in the diagnosis is still choosing, and a reader in the resolution is already acting on the estate. The rule matches the whole section, so a step with a route of its own satisfies it as the closing line does. - A tool states what the package declares apart from what somebody concluded.
toolsdeclareslicence-declared, which keeps a string SPDX has no expression for, such as npm'sSEE LICENSE IN README.md.licencestates the concluded value.kac validatewarns throughconclusion-states-a-reasonwhere the two differ and no## Licence and obligationssection says how the conclusion was reached. Both fields travel in the export, andversion:underexport:in.schema/tools.yamlmoves to 3. - An NFR states how long a condition holds before its alert is raised.
nfrsdeclaresalert-after, optional on every target. The value is the alert's configured delay, such as15 minutes, and never a detection time measured after an incident.kac validatewarns throughalert-after-needs-an-alertwhere a target stating one binds no service that raises an alert or a ticket, following an offering'simplemented-byto the services behind it. The field travels in the export besidewindow, andversion:underexport:in.schema/nfrs.yamlmoves to 5.
Changed¶
-
licenceon a tool is required.kac validatefails a tool record that states none, throughrequired-field. An entry nobody has screened statesNOASSERTION, so the register shows the gap. A corpus that has adoptedtoolsfills the field in every record before it takes this schema. -
An optional field's range is asked for from the first document that states it.
kac validatereportedcorpus-enum-undeclaredfor every field drawing onenums:as soon as the corpus held one record of the type, so a corpus was asked for a range it had no use for. A required field is still asked from the first record of its type, and a_template.mdstates a field as a record does. A range the corpus has already written is still held to lower case whether or not anything states the field. - A range asked for inside an object entry names the path a record writes it at.
corpus-enum-undeclarednow reports'interfaces.type' on a servicewhere it reported'type' on a service, which read as the record's owntypekey. - A runbook closes its resolution on a fixed line. The
_template.mdthatkac newsends now endsResolutionwith a line openingConfirmed whenand closing with a link to the escalation. Before, it asked for the confirmation in any form and gave no route out. Therunbooks.mdbeside it now tells an author to write that line, having said nothing aboutResolutionat all.outcome-statedis declared and does not run, sokac generatelists it under Declared, not yet enforced on the type page.
0.29.0 - 2026-09-21¶
Added¶
-
A process past its rehearsal cadence is reported.
stalenessnow runs.kac validatewarns on a process whoselast-rehearsedis"never", and on aquarterlyorannualone more than 92 or 366 days past that date. The rule was declared and ran on nothing, so a corpus could say every procedure was rehearsed on a cycle and be asked for no evidence.on-changeandper-releasestate an event, which no window measures, so a process using either is reported only whilelast-rehearsedis"never". -
days_since('field')measures a date field against the day of the run. An expression could already compare two dates a record states, and ask whether one had gone by. Neither asks how old a record is. The fact answers zero where a field is absent, is not a date, or names a day the run has not reached, so a rule comparing it against a threshold leaves an absent value torequired-field. -
Every rule in a standard is held to a keyword in bold capitals.
part-modalreads the bullets under each rule heading and reports one that names none ofMUST,MUST NOT,SHOULD,SHOULD NOTorMAY, one that leaves a keyword in plain text, and a heading gathering no bullet at all. BCP 14 gives a keyword its meaning only in capitals, and this corpus makes bold the thing a reader skims, so a keyword carrying one of the two bound nobody and nothing said so. A type declares its own set inparts.binding:andparts.advisory:, which a heading source may now carry: a glossary declares none and is asked nothing. -
A standard names the standards it rests on.
depends-ontakes standard ids, andkac validatechecks each one resolves. ISO/IEC Directives Part 2 makes normative references a clause of every document, and a folder cannot say thatplatform/dotnet/testing.mdis no use withoutcommon/testing.md. The field travels in the export. -
A superseded standard names its replacement.
superseded-bytakes a standard id and is required wherestatusissuperseded, withsupersedesthe same edge read the other way. Both travel in the export, so a reader arriving from an old citation is sent to the standard that replaced it. -
A policy in force says the day it came into force.
active-fromtakes the day, quoted, andkac validaterequires it wherestatusisactive. NIST SP 800-53 Rev. 5's-1controls and ISO/IEC 27001 clause 5.2 both ask a policy to record its approval, andownersays who without saying when.review-not-before-activefails areview-byearlier than it. The date stays out of the export, andpolicy-lookuplists it among what stayed behind.kac newsends a_template.mdcarrying the field, and a corpus already holding anactivepolicy owes the date at its nextkac update. -
An integration says how the estate would leave it.
Exitis a section, andexit-requiredwarns where acriticalorimportantintegration still in use has none. Article 30(3)(f) of DORA asks an ICT contract for an exit strategy, and the section travels in the export.kac newsends a_template.mdnaming it. -
A retired integration names what took over.
successortakes an integration id,replacesis the same edge read the other way, andkac validatechecks both ends.retired-has-successorwarns where aretiredintegration names none. Both fields travel in the export. -
An integration on trial says what would end the trial.
Trial criteriais a section, andtrial-criteria-requiredwarns where atrialintegration has none. The section stays out of the export, because a consumer readsstatus. -
A control that never stops says so.
frequencytakescontinuous. A control whosemechanismisruntime-alerthas to carry a frequency, and none ofper-prthroughannualis true of an alert rule that evaluates without stopping. -
A manual control says when it last ran.
last-verifiedtakes the day, quoted, or"never", andkac validaterequires it wheremechanismismanual. An automated check leaves a run in its own logs, and a periodic human check leaves nothing otherwise. It travels in the export, andkac newsends a_template.mdnaming it. -
A data document says whether it holds personal data.
personal-datatakesnone,personalorspecial-category, andkac validaterequires it.retentionis now required from this field rather than fromclassification, so data that is confidential and personal is asked for a retention period. -
A data document says who the data is about.
data-subjectstakes the categories of people, in the corpus's own words, andkac validaterequires it wherepersonal-datais anything butnone. GDPR Article 30(1)(c) asks for the categories of data subjects, and the NIST Privacy Framework asks the same as ID.IM-P3. -
A data document says where the data is kept.
regiontakes a cloud region or a place, andkac validaterequires it. TheWhere it livestable asked for it and nothing read the answer. -
A data document says what the data is for.
Purposeis a required section. GDPR Article 30(1)(b) asks for the purposes of the processing, and the NIST Privacy Framework asks the same as ID.IM-P5. -
The
Flowstable says where each recipient processes the data.kac newsends a_template.mdwith the column.regioncovers the owning service, so a transfer out of the country is recorded beside the recipient that makes it, which is what GDPR Article 30(1)(e) asks about. -
A data document says when someone checks it again.
review-bytakes the day, quoted, andkac validaterequires it. It travels in the export, which movesdata's shape to version 2.classification,personal-data,retentionandregionare claims about a store that nothing in a corpus checks, and no field asked anybody to confirm them. The ICO asks you to review retained personal data regularly and to justify how often, and NIST SP 800-53r5 RA-2 asks that a categorisation be revisited.classification-in-datewarns once the date has passed. -
A deviation rates the risk it leaves.
risktakeshigh,mediumorlow, andkac validaterequires it. The rating is the risk left once what compensates is working, andhigh-risk-review-windowwarns where ahighrating setsreview-bymore than six months afteraccepted-on. -
A deviation says what can still go wrong.
What the risk isis a required section, and it travels in the export. The sections beside it said what limits the risk and never what remains of it. -
A deviation can name who does the work.
assigned-totakes a person or a post, and stays out of the export.owneraccepts the risk and this field closes it, which are one person in a small estate and two in a large one. -
A rule can measure a gap in days.
days('a', 'b')counts whole days between two date fields, wherespan()answers in hours between two timestamps.docs/design/expressions.mdis the reference. -
An explanation says what it covers.
What this coversis a required section, and it travels in the export. It states what the record covers and what it leaves to another record, which is what keeps the residual type bounded.kac validatereportssectionsagainst an explanation without it, andkac newsends a_template.mdcarrying it. -
A fix says what you were running.
Environmentis a required section, and it travels in the export. It states the tool, the operating system and the versions, which is how a reader tells whether the fix is theirs.applies-totakes service ids alone, and a fix about a laptop or a pinned tool version has no service to name.kac validatereportssectionsagainst a fix without it, andkac newsends a_template.mdcarrying it. -
A rule can ask whether a key is written once.
entries_unique('field', 'key')reads one key across the objects a field holds and answers false where two of them state the same value. True where the field is absent.docs/design/expressions.mdis the reference. -
An NFR says which quality it commits to.
characteristicis required, and takesaccuracy,availability,capacity,latency,recovery,scalabilityorthroughput. The values sit at ISO/IEC 25010's subcharacteristic level, so a corpus can list the qualities it has promised nothing about. It travels in the export, indexes beside the target, andkac newsends a_template.mdnaming it. -
An NFR states the period its figure is read over.
windowtakes the period, such asmonthlyorrolling 4 weeks.kac validaterequires it wherecharacteristicisavailability,capacity,latencyorthroughput, because a rate or a percentile only means something over a stated period. A recovery, scalability or accuracy target binds each event or each value, so it needs none. It travels in the export. -
An NFR says why the figure is that one.
Why this numberis a required section, and it travels in the export. It states what fixes the figure and what the target leaves out, which the three records inexamples/paymentswere each stating insideTarget.kac validatereportssectionsagainst an NFR without it. -
An agreed NFR says when it was agreed.
agreed-ontakes the day, quoted, andkac validaterequires it wherestatusisagreed. An agreed target is a commitment somebody accepted, and nothing recorded when. It travels in the export. -
A process says when it is rehearsed again.
rehearsal-frequencyis required, and it takes a newon-changebesideper-release,quarterlyandannual. ComplianceForge HCGF reviews a procedure when its technology, its steps or its people change. NIST SP 800-53 Rev. 5's-1controls ask for a frequency and the events beside it. Nothing running on a schedule can measureon-change, soprocess-lookuptells a reader to ask what has moved.kac newsends a_template.mdcarrying the field. A corpus already holding a process writes a value into every one of them, becausekac updatereaches the schema and never a record. -
A process says how to back out.
If it goes wrongis a required section, where it was optional. The type page already promised a rollback, and the section stays out of the export, because a reader who has to back out is holding the record.kac newsends a_template.mdnaming it. -
A runbook says who is affected.
Impactis a required section, and it travels in the export besideSymptoms. The Google SRE workbook asks a playbook to explain the severity and the impact of the alert it answers, andseveritysaid only how urgent.kac newsends a_template.mdnaming it. -
A runbook says who to tell.
Communicationis a required section. AWS Well-Architected OPS07-BP04 puts stakeholders and a communication plan on a playbook, and PagerDuty asks a responder for status updates throughout.Escalationsays who to wake, and this says who is waiting. The section stays out of the export, because a reader telling anybody is holding the record.kac newsends a_template.mdnaming it. -
A runbook lists its tools apart from its permissions.
requires-toolstakes what the reader installs. AWS Well-Architected OPS07-BP03 gives a runbook a column for each, and two of the three runbooks inexamples/dog-foodingwere writing a tool intorequires-access. It travels in the export, andkac newsends a_template.mdnaming it. -
A service says what sort of component it is.
component-typeis required, and takes its range from the corpus asplatformdoes. Backstage, OpsLevel and Cortex each require this word of a catalogue entry. It travels in the export, and a corpus adoptingserviceswritesenums.component-typein.corpus.yamlbefore its first record validates. -
A tool entry names the packages it approves.
packagestakes one entry per package, each a package URL with the version range approved for it. CycloneDX 1.6 and SPDX 2.3 both identify a component that way, anddrift-against-manifestshad nothing to match a manifest entry against.Spectre.ConsoleandSpectre.Console.Cliare one decision at two ranges, which the singleversionsstring could not state. It travels in the export. -
A tool entry links the project it names.
homepageis required and takes anhttps://address. SPDX gives every package a home page, and the OpenSSF guide to evaluating open source software opens by asking whether you have the project you think you have. No entry inexamples/dog-foodinglinked its own project. -
A tool entry carries two dates.
decided-onsays when the current status was settled, andreview-bysays when somebody checks the entry again. Both are required and both travel in the export. Every entry inexamples/dog-foodingwas writing the first into the body as prose, and nothing stated the second, so an approval taken once governed new work for ever.review-in-datewarns oncereview-byhas passed. -
A deprecated or rejected tool says what went wrong with it.
exit-states-a-reasonwarns where## Statusis absent, so somebody proposing the same package in two years finds the evaluation rather than repeating it. -
A tool entry may record an accessibility assessment.
Accessibilityis a declared section and travels in the export.std-A11Yinexamples/dog-foodingrequires an assessment in the tool record of any component rendering a governed surface, and two entries were carrying one under a heading the type did not declare, which the export dropped. -
datarecords travel in the export..schema/data.yamldeclares anexport:block at version 1, sokac exportwrites one file per data document andkac bundleships them with the plugin.classification,personal-data,data-subjects,retention,regionandflows-totravel as frontmatter, andPurpose,Entities,Where it lives,Classification,RetentionandFlowstravel whole. A service'sdata-storescited records no consumer could read. Every type in the taxonomy now declares a block.
Changed¶
-
An NFR says whether anybody is held to its target.
statustakes a fourth value,aspirational, for a figure somebody settled that the estate is not measured against. The Google SRE workbook keeps such an objective measured and tracked, and exempts it from the error budget policy a miss otherwise triggers;What we do about a breachis where that exemption is written.nfrspublishes atexport.version4, because a reader that tookagreedanddraftto be the only live values would now read an aspirational target as binding. -
An NFR records what it is achieving now.
Current actualwas optional and is now required, so the reading sits beside the target in every record. It stays out of the export, because an export has no way to say how old a measurement has since become. -
An NFR states what a breach costs and what the response to one is, in two sections.
If it is breachedwas one required section, and a record could answer with either half.What a breach costsandWhat we do about a breachare both required, sorequired-sectionreports the one a record left out. The Google SRE workbook keeps an error budget apart from the error budget policy that spends it.nfrspublishes atexport.version3, because a reader taking the consequence out ofIf it is breachedwould now find no section under that name. -
An integration id starts
itg-, notint-.kac validaterejectsint-sendgrid, andref-resolvesreports each citation still pointing at one.intis a reserved word in C# and in most languages a maintainer reads, andint-also matchesconstraint-andprint-, so a search for an integration id returns mostly lines that are not one.integrationspublishes atexport.version2, because a reader matching^int-on an exported id would now match nothing. Rename the id in every integration record, and in every citation of it.kac generatethen rewritesintegrations/_index.md, whose heading readsIntegration Index (ITG). -
policiesstates what it takes from current practice.lineagewas measured against ComplianceForge HCGF's policy layer, NIST SP 800-12 Rev. 1 and NIST SP 800-53 Rev. 5.alignmentsaid a policy pushes its mandatory language down to the standard beneath it, and 206 of the 243 clauses inexamples/engineeringare MUST or MUST NOT.alignment,divergenceandcollisionnow describe a policy that binds in its own right, andkac generatewrites all three into every adopting corpus'sknowledge-as-code/lineage.md. -
versionson a tool is gone, and a version range sits on the package instead.packagesstates the range for each package it approves, so a family chosen together can be pinned apart.toolspublishes atexport.version2, because a reader that took a range out ofversionswould now find none there. The tool index listsreview-bywhere it listedversions. -
Statuson a tool is optional, and says why the tool sits where it is. It was required, and carried the status and the date the status was set, which the frontmatter already states.exit-states-a-reasonasks for it of adeprecatedor arejectedentry, which are the two stances that owe a reader an explanation. -
toolsstates what it takes from current practice.lineagewas measured against the Thoughtworks Technology Radar, CycloneDX 1.6, SPDX 2.3 and the OpenSSF guide to evaluating open source software.alignmentclaimed named technologies sorted by stance, and the index sorts by category.divergenceclaimed binding version ranges, and the field was optional, unchecked and empty on three of 13 entries. Both now describe the schema as it stands, anddivergencerecords theassessring this register does not have. -
targeton an NFR no longer carries the measurement window. The window iswindow, so a consumer reads the figure and the period apart.nfrspublishes atexport.version2, because a reader that took the period out oftargetwould now find none there. -
requires-accesson a runbook takes a permission and no longer a tool. A tool isrequires-tools, so a reader sees what to install apart from what somebody has to grant them.runbookspublishes atexport.version2, because a reader that took a tool out ofrequires-accesswould now find none there. -
A runbook says how urgent it is.
severityis required, where it was optional. The Google SRE workbook asks a playbook to explain the severity of the alert it answers, andImpactstates the other half. -
A runbook says when it is rehearsed again.
rehearsal-frequencyis required, where it was optional. No runbook anywhere set it, so the declaredstaleness-loudrule could only ever fire on alast-rehearsedof"never". Nothing running on a schedule can measureon-change, which the field accepts. A corpus that already has a runbook writes a value into every one of them, becausekac updatecopies the schema and never a record. -
runbooksstates what it takes from current practice.lineagewas measured against the Google SRE book, the Google SRE workbook's On-Call chapter and AWS Well-Architected OPS07-BP03 and BP04.alignmentnamed a statistic and no field or section, and now names the five it is true of.divergencerecords that AWS writes a playbook to find the cause and a runbook to resolve it, where this type is both on one page.collisionsaid the industry uses runbook and playbook interchangeably, which is true of Google and not of AWS.kac generatewrites all three into every adopting corpus'sknowledge-as-code/lineage.md. -
nfrsstates what it takes from current practice.lineagewas measured against ISO/IEC 25010:2023 and the Google SRE workbook's Implementing SLOs.alignmentnames the four things the workbook asks an objective for, anddivergencenames the error budget it keeps and this type does not take.kac generatewrites both into every adopting corpus'sknowledge-as-code/lineage.md. -
A glossary term carries the other names it answers to.
**Also:**gives a second name for the same thing, an acronym or an abbreviation, and**Avoid:**gives a name the glossary has dropped. Both travel in the export asalsoandavoid, so a search reaches a term by a name its heading does not carry. SKOS calls the first analtLabeland DITAglossentrycalls it aglossAlt, and neither had anywhere to go here before. -
parts.asides:replacesparts.aside:, and takes a list. A type declares every bold label its parts may open a block with, andexport.parts.line:addresses one aspart.aside.<Label>.part.asideon its own is gone. Against a schema still naming either,kac validatereportsschema-unknown-keyfor the block key andschema-dispatchfor the line source. It reportsschema-shapeagainst a label the type'sparts.asides:does not declare. -
glossarystates what it takes from current practice.lineagewas measured against Evans, Domain-Driven Design, SKOS and DITAglossentry.prior-artnow names all three, andalignmentanddivergencesay what the type takes from each and where it parts from them.kac generatewrites both into every adopting corpus'sknowledge-as-code/lineage.md. -
manual-periodicis nowmanual. The value held a cadence inside a method name, wherefrequencystates the cadence beside it.kac validatereportsenumagainst a control still carrying the old value, and a corpus rewrites it by hand. -
classificationgrades confidentiality alone. The field takespublic,internalandconfidential.personalandspecial-categorymove topersonal-data, because a category of data is not a grade of sensitivity.kac validatereportsenumagainst a data document still carrying either value inclassification, and a corpus rewrites it by hand. -
datastates what it takes from current practice.lineagewas measured against GDPR Article 30, the NIST Privacy Framework v1.0 and ISO/IEC 27002:2022.alignmentclaimed transfers and security measures, which the type asks for nowhere. Both now name what the type takes and where it parts from them.kac generatewrites both into every adopting corpus'sknowledge-as-code/lineage.md. -
controlsstates what it takes from current practice.lineagewas measured against NIST SP 800-53A Rev. 5, the OSCAL assessment models and the CIS Controls Assessment Specification.alignmentanddivergencenow name what the type took from each and where it parts from them.kac generatewrites both into every adopting corpus'sknowledge-as-code/lineage.md. -
A deviation's
owneris a person, never a post. The field takeshuman:alex.doealone, where every other type also admitsrole:. One individual accepts a risk and a post cannot.kac validatereportsfield-patternagainst a deviation still naming a role, andassigned-tois where a post belongs. -
deviationsstates what it takes from current practice.lineagewas measured against NIST SP 800-37 Rev. 2, the PCI DSS v4.0 compensating controls worksheet and the FedRAMP POA&M template.alignmentclaimed a bounded period and tracked remediation, which the type asks for nowhere. Both now name what the type takes and where it parts from them.kac generatewrites both into every adopting corpus'sknowledge-as-code/lineage.md. -
An explanation can explain a standard, a process or an ADR.
explainstook a service or an offering alone, and a subject is not always a deployable thing. The export'sshapeVersionfor the type moves from 1 to 2, because a reader that resolved every id in the services or offerings folder would now be wrong. -
explanationsstates what it takes from current practice.lineagewas measured against Diátaxis, the Good Docs Project's concept template and arc42.alignmentclaimed discursive prose and weighed alternatives, which the type asks for nowhere. Both now name what the type takes and where it parts from them.kac generatewrites both into every adopting corpus'sknowledge-as-code/lineage.md. -
fixesstates what it takes from current practice.lineagewas measured against the KCS v6 article, the ServiceNow known error form and ITIL Problem Management.alignmentnames the four sections KCS asks for, anddivergencenames the workaround ITIL expects and this type does not take.kac generatewrites both into every adopting corpus'sknowledge-as-code/lineage.md. -
verifiedsays who has checked a record, not how often. An actor is written once, and a check they make again moves theaton the entry they have. The Open Knowledge Format keeps the list for independent checks, a human sign-off beside a nightly process, and git already keeps when each actor checked before.kac validatereportsone-verification-per-actoragainst afixor areportnaming one actor twice, and a corpus trims each list by hand. The trust tier reads the actors and never the count, so no tier moves. -
processesstates its lineage against EPA QA/G-6. The type declared ComplianceForge HCGF and the Diátaxis how-to guide.prior-artadds Guidance for Preparing Standard Operating Procedures.alignmentanddivergencestate what the comparison found: the review frequency and date EPA QA/G-6 asks for, the experience a procedure does not state, and what git andowneranswer for instead of a revision number and a signature. The row no longer claims the rehearsal date has no precedent. Acollision:entry names what ITIL and BPMN mean by process. All three render intoknowledge-as-code/lineage.md. -
reportsstates what it takes from current practice.lineagewas measured against the Open Knowledge Format v0.2, SARIF 2.1.0 and NIST OSCAL Assessment Results 1.1.3.alignmentnow names the OKF keys and the actor forms the schema declares.divergenceadds the three verification rules OKF has no equivalent for, thestatusvaluesactiveandstale, and the staleness read from each source'scontent-versioninstead of OKF'sstale_after. Both render intoknowledge-as-code/lineage.md. -
repoon a service is nowrepos, and takes a list. OpsLevel lets one component name several repositories, and an asset surface is defined in one repository and filled from another. Each entry matches^[a-z0-9-]+$, whererepowas held to no pattern, so a corpus spelling one asorg/nameor a URL owes a second edit.servicespublishes atexport.version2, because a reader that took a string out ofrepowould now find nothing there.feature-file-reporeads every entry of the list. -
servicesstates what it takes from current practice.lineagewas measured against the Backstage software catalog, OpsLevel and Cortex.alignmentsaid the type states the APIs a component consumes, and no field does.alignmentanddivergencenow describe the type as it stands, andkac generatewrites both into every adopting corpus'sknowledge-as-code/lineage.md.
Fixed¶
meta/type.schema.jsonofferspart.citations.<Label>. The editor's copy of the line-source vocabulary never had a pattern for it, so it markedstandards.yaml'scovers: part.citations.Coversas invalid while the build stayed green.MetaSchemaTestsnow holds that enum to the list the exporter fills.
0.28.0 - 2026-09-17¶
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, wherepresent()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 exportwrites a file per integration record.What it doesandFailure modestravel atfull.Contract,CommercialsandContactsstay 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 exportwrites 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 exportwrites a file per postmortem record.Summary,Root cause,Contributing factorsandWhat went welltravel atfull, 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 listspostmortemsamong the types declaring no block. -
An offering travels to a consumer.
.schema/offerings.yamldeclares anexport:block, sokac exportwrites 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 infeature-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 untilfeature-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 declinednfrsis 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'sknowledge-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'stype:,id:and identity line, and rewrites everycap-reference in the records of other types. It then writesofferingsovercapabilitiesintypes:and deletes.schema/capabilities.yamlby hand.kac update --drop-type capabilitiesrefuses 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 newstampstemplate-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, sorequired-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 requiresrestored-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-onanddetected-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-byandnfrsdeclaremirrors-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 formservicesalready uses for the same shape and the one a screen reader can read. It lost itsTested byline, which restatedfeature-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 whoseimplemented-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, leaveverifiedempty, 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 wasdevi-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 newstampstemplate-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 theWhere 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.
0.27.0 - 2026-09-16¶
Added¶
-
The documentation site lists every skill, under Skills. Sixteen of them across three trees, and which tree a skill lives in decides who can read it and what moving it costs. The three tables are generated from the files that already decide the split:
plugin.jsonfor what travels inside a plugin,manifest.yamlfor what travels into a corpus, and the.claude/skills/directory for what stays here.SkillReferenceTestsfails a stale table, so the page cannot drift from the manifests the way a hand-written one would. Nothing aboutkacchanged. -
kac newandkac updatesend a corpus thewriting-a-reportskill. It joinstechnical-writingandwriting-a-recordin the overlay, under one rule: a writing skill travels where it governs a surface a corpus holds. A corpus can adoptreports, so the skill that says how to fill a report's judgement cells travels with it.writing-in-the-toolandwriting-the-docsdescribe C# and a documentation site no corpus has, andi-want-toroutes to this repository's own processes where a corpus reads its own throughprocess-lookup, so all three stay behind.reports/_template.mdgains the four verdicts as well, for an author who opens the template and loads no skill. Before this the wordsCovered,Covered by its pair,GapandOut of scopewere stated in one file that never left this repository, so a corpus adoptingreportswas told to answer the judgement cells and nowhere told with what.manifest.yamlmoves to 15, andkac updatestampsupstream.template-version. -
kac newandkac updatesend a corpus theharvest-findingsskill, which triages filed findings and drafts the record one asks for.raise-findingfiles an observation as an issue and stops, so nothing moves it afterwards.triagereads every finding without akac:triagedlabel, sorts each into one of five routes, shows a person the table and writes nothing until they agree.draft <issue>takes akac:route-recordfinding and opens a pull request carrying the record. Both invocations write: to the tracker, and to the records themselves. So the reader is the corpus's own maintainer, and the skill lands in the corpus's working tree under.claude/skills/, beside the writing skills. No plugin ships it, because a consumer holds a frozen export and not the source. It reads thetracker,frameworkandpublishingblocks of.corpus.yamlfor the backlog, for where the tool is reported and for where the record lives. A framework finding filed on a tracker other than the framework's own is drafted as a comment and copied by hand, because an Azure DevOps user has no GitHub account.manifest.yamlmoves to 16, andkac updatestampsupstream.template-version. -
kac validaterefuses a target the descriptor names and the tool cannot act on, under a newdescriptor-targetcheck.publishing-targettakes the five values the link rules are written for.tracker.targetandframework.targettakegithub,azure-devopsornone, because a wiki and a documentation site publish records and hold no backlog. The message names the key, the value and the list it takes. Onlykac newheld its flags to these lists before, and every one of the keys is written by hand after that, so a misspelling read as a corpus that publishes nowhere and files nowhere. -
.corpus.yamlstates where work about its records is filed, under a newtracker:key. It takes atargetand abase, the same pairframework:takes. A corpus that states no block gets the tracker itspublishing:block implies, so a corpus on GitHub configures 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, andkac exportderives the project from it.manifest.jsonstates a tracker three times: for the corpus, for the framework, and for every entry insources. Each one gains anid, which is the target and the base normalised, so a caller comparing two of them compares two strings and never parses a URL.baseandidare bothnullwherever a block addresses no backlog, so a base is never written beside a target that cannot use it.descriptor-versionmoves to 4, andkac updatestamps it. No skill reads the key yet. -
.corpus.yamlstates where to report a problem with the framework, under a newframework:key. It takes atargetand abase, the same pairpublishing-targetandpublishing.basetake.kac exportwrites the block intomanifest.json, beside theidevery tracker there gains, so the address travels to whoever installs the corpus as a plugin. Itsbaseis written the way every tracker's is, so an Azure DevOps base is the project holding the backlog. Before this, the only address an export carried was the corpus's ownpublishing.base.upstream.urlwas never one: it says where the template was copied from, and that is often a folder.kac newwrites the block, taking the base from--fromwhere that names a repository and from the framework's own repository where it names a folder.descriptor-versionmoves to 3, andkac updatestamps it. No skill reads the key yet. -
A
fixtakesstatus: draft, and a draft states no verification.verifiedis required of every other status, so a session can write a fix from an observation nobody has checked andkac validatepasses. The type's export shape moves tofixes@2, becauseverifiedarrives as null on a draft. A corpus listingdraftunderexport.exclude:in.corpus.yamlwithholds a draft, which is how an unchecked resolution stays out offix-lookup. -
fix-lookup, the skill that says whether a problem has already been solved here. It searchessymptom-keywords, which a fix over-fills with error text and the words somebody arrives with, then reports the Symptom, Cause and Resolution the corpus settled. It states the record's derivedtruston every answer, so a caller can tell a resolution a person checked from one an agent ran. A fix declares no part, so the skill searchescorpus/fixes/and the record is the unit. It ships in every corpus'splugin.jsonand leaves the bundle wherever the export carries no fix. Take it withkac update --from <template>. -
raise-findingsearches the fixes before it files anything about a problem. Seven sessions meeting the same problem should not open seven issues. Where the plugin has nofix-lookup, the corpus declined thefixestype and the finding is the only route. Take it withkac update --from <template>.
Changed¶
-
A verification taken before
generated.atreaches no trust tier.kac exportderivedtrustfromverifiedalone, so a report somebody read in March still shipped ashuman-reviewedafter an agent rewrote its verdicts in September. Authorship now passes to whoever answers those cells, which movesgenerated.atpast every reading of the words before them, so the exporter leaves an older entry out. A moment that will not parse still counts, becausetimestamp-formatalready reports it against the record. -
A report's
generatedsays who wrote the content, and authorship passes to whoever edits it.kac reportwrote itself intogenerated.byand left it there, so a report an agent finished credited the tool with the agent's verdicts.bynow names whoever wrote the content a reader meets, which the Open Knowledge Format defines it as and illustrates with a person. Two keys survive the handover:reportstates the report the run used, ascoverage, so a record says whichkac reportname regenerates it, andtoolkeeps the version that wrote the mechanical half. Both are optional, and a report nobody produced with the tool states neither.generated-by-a-produceris renamedgenerated-by-a-known-actorand admits all three actor forms, sohuman:alex.doeis now a legal author. -
A report states no verification until somebody reads it.
verifiedwas required with at least one entry, so an agent finishing a report had to name a verifier to get pastkac validate, and the two worked corpora each named an agent that does not exist. The field now followsfixes: a draft states none, and every other status states one. A report with no entry is the Open Knowledge Format's unverified tier, whichkac exportalready ships astrust. -
An accepted ADR is edited in place, and only a changed decision needs a superseding ADR.
immutable-after-acceptedallowed a typo fix, a link correction and a status transition, and nothing else. That list left out an edit changing no decision. A sentence that no longer matched the decision the ADR already stated read as forbidden. The rule now asks whether the decision changed, andadrs.mdsays to name the edit in the commit message.immutable-after-publishedtakes the same shape for a postmortem, where a new understanding is a new postmortem. Both are still declared and do not run, because telling a changed decision from a correction needs git history. The Decided tier note in_tiers.yamldrops "never rewritten" for the same reason, so a corpus'sknowledge-as-code/taxonomy.mdchanges when you regenerate it.kac update --from <template>takes all three, andkac generaterewrites the blocks under them.adrs.mdandpostmortems.mdseed a corpus, so the Immutability paragraph on each stays that corpus's own to reword. -
kac exportreports every reason it refuses in one run. A type the corpus has not adopted, anexport-excludekey it cannot act on, a consumed corpus nothing is restored for, a consumed corpus at another export format and two corpora disagreeing about a type are each one line, and all of them print before the run stops. A shape or fidelity line now ends with what to do about it. Before, the first of them stopped the run on its own. -
A finding
raise-findingfiles is the observation, and proposes no record. Itskac-findingblock dropsidandexpires, which were a proposed discovery's own fields, and gainslooks-like. That key takes the type the observation resembles,frameworkwhere what was noticed is the tool, the schema or a skill, ornone. Whoever triages the issue gets the hint and keeps the decision.corpus,source,confidence,provenance,applies-toandtagsare unchanged, and so are the body's three headings. Take it withkac update --from <template>.
Removed¶
- The
discoveriestype and theobservedtier. A corpus no longer keeps an unverified observation. One goes into whatever tracker that corpus already uses, and what the corpus keeps is the answer somebody settled and verified.kac newwrites one type fewer, andkac validatecounts one template fewer.fixesandstandardsdroppromoted-from, so a record that still has that key failsunknown-key;sourcesstates where the content came from instead. Aschema-shapemessage naming the tiers now offers four. Take it withkac update --from <template>, which deletesdiscoveries.md, the folder and the schema file.
Fixed¶
-
kac exportpublishes a grandparent's record files down a chain three corpora deep. A consumer read only the record files sitting directly in an imported type's directory. The records its producer had inherited were dropped, and their part lines reached the next corpus naming files that never arrived.kacnow reads the folders a producer filed those records in, and files each record under the corpus that wrote it. A corpus consuming both a producer and that producer's own producer meets one record twice, and writes and counts it once.sourcesalready listed the grandparent, and still does. -
kac validateresolves a citation into a corpus reached through another.gp:pol-OLDwas reported as a shortcode this corpus consumes nothing under. The reader behindvalidateskipped the folders an export files an inherited record in, and filed what it did read under the corpus it arrived through. A record is now found by the shortcode of the corpus that wrote it, whether this corpus declared that corpus inconsumes:or not.eng:pol-OLDcorrespondingly stops resolving, which is whatref-resolvesalready says about a record eng does not have. While a declared import is unrestored, a shortcode nothing here knows is left toimport-restoredinstead of drawing a second finding telling the reader to declare it. -
kac report coveragecredits a grandparent's standard against the clause it implements. A record file keeps the ids its own corpus wrote, and the edge was scoped to the corpus the record arrived through. The clause read as a gap and the standard covered nothing, because the clause lines are scoped to the corpus that wrote them.
0.26.0 - 2026-09-13¶
Added¶
-
kac validatechecks that a quoted clause still says what it is quoted as saying. A control quotes the clause it verifies, and nothing until now compared the two.clause-quoted-faithfullytakes every double-quoted span on a line that also cites a clause, and reports it as an error where the cited clause no longer contains those words. Whitespace is collapsed on both sides, so a quotation wrapped across two lines is read whole. A quoted span on a line citing nothing is left alone, and so is a citation into a corpus you consume: an export sends a record's ids and fields, not its wording..schema/controls.yamldeclares the rule, so take it withkac update --from <template>. -
A fix travels in an export.
.schema/fixes.yamldeclares anexport:block at shape 1, sokac exportwrites one JSON per fix. The record carriessymptom-keywords, which is what a lookup searches on. It carries Symptom, Cause, Resolution and Why it happens whole, because a resolution read without its cause is half an answer. It carriesverified, and the record'strustis derived from that list. Three things stay behind:How we found it, which names commands a consumer cannot run,promoted-from, which names a discovery that travels nowhere, andowner. Take the schema withkac update --from <template>, and adopt the type withkac update --add-type fixes. -
kac report --out <path>writes the report to a file. Without it the report still goes to standard output, so a caller piping one loses nothing. With itkacwrites the file after it has read the corpus, which is what keeps a run off the console encoding and out of the way of a shell holding the same path open. A path a file already occupies is refused and nothing is written, because a finished report holds verdicts somebody wrote. -
Every record can say where its content came from.
.schema/_universal.yamldeclaressources, an optional list whose entries carry a requiredresource. Aresourcenames something a reader can follow, such as a ticket URL, or the population the content was drawn from.sourcesis what the Open Knowledge Format calls the same list, andresourceis its one required key. No type carries the universal field into an export, so a record a consumer holds stands up without the ticket behind it.reportskeeps its ownsources, which requires the field and adds theversioneach corpus was at. Take the field withkac update --from <template>.
Changed¶
-
The prose in
.schema/is rewritten to the writing rules. Everydescription:,notes:,message:and comment in the schema was rewritten against them. A record author reads a shorter field description in the## Metadatatable, and a plainer sentence fromkac checksand from a rule that fails. No check id, severity,expr:, pattern, threshold or export shape changed. Reasoning that had grown into anotes:now sits on the design site, under Checks, Shaping a type and Reports, and thenotes:cites it. Runkac generateafter taking the schema withkac update --from <template>. -
The
## Metadatatable says what a field is, where it used to describe the schema. A field declaring onlynotes:fell back to them for its table cell, so maintainer commentary was published to whoever writes a record. Every field now declares adescription:. An ADR'ssuperseded-byreads "The ADR that replaces this one." where it read "CI reconciles both directions, so a one-sided supersession fails the build." Runkac generateafter taking the schema withkac update --from <template>. -
Every check message opens lower case. Twenty-three rule messages in
.schema/opened with a capital, wherekacprints a message mid-line after the check id. Several also ran to four or five sentences. Each now opens lower case and states what is wrong, then what to write instead. -
.schema/states the reasoning local to a field and cites the site for the rest. Anotes:had grown into a multi-paragraph design argument in forty-six places, and much of it repeated a documentation page. The prose in.schema/is a third shorter. The per-type export choices and the framework-register rules now sit at https://paul80nd.github.io/knowledge-as-code/design/shaping-a-type/, which each type file cites. -
kac report frameworkssays more beside each framework's table. Every reference row carries aCitationscount, so a reference one clause cites reads as1without counting the cell next to it. Each framework's section opens on the standing the register files it under, linked to the register entry that placed it, which is the line a reader would otherwise scroll back to the totals table for. -
Each report writes its own
## Limits. Both printed one wording, written for a reading of clause coverage.frameworksnow says that anAlignmentcell stays in the corpus that wrote it, so it counts the citations written here, and that a citation records the naming rather than a clause meeting what it cites. -
The five lookup skills drop their search procedure.
kac new,kac updateandkac bundlesend skills that name no search tool and no search flags. A trial ran three variants ofpolicy-lookupover five questions: the skill as it shipped, one without the tool name, and one without the search section at all. Every variant found and cited every governing clause. Each run of the shipped skill spent tool calls hunting a Grep tool the session did not hold. What stays is what an agent cannot work out for itself: the file map, the field table, and the warning that a field name likestatusmatches every line of the file. -
glossary-lookup,policy-lookupandstandards-lookupanswer the near miss. Each carries a section for a subject the corpus has not written down that sits beside one it has. "Password rotation" meets a policy about rotating secrets, and the skill now says to name the nearest clause as the nearest one and leave the reading to its owner. -
confirmedis nowverified, it takes any actor, and the export carries the trust tier derived from it. The field is renamed onfixesandreports, which is what the Open Knowledge Format calls the same list. It no longer refuses an agent: a session that reproduced a symptom and ran the resolution has checked something real, andverified-by-a-known-actoradmits it, named with its version the way the tool names itself. That rule still refuses arole:, because a post cannot read an answer. Who is in the list decides the record's tier, which each record file now ships astrust: an empty list isunverified, agents alone aremachine-confirmed, and onehuman:actor ishuman-reviewed. A type whose export does not nameverifiedcarriestrustasnull. One actor is still refused, andno-self-verificationis the new rule and reports asself-verification: a report may not be verified by the producer itsgenerated.bynames. A fix declaresraiser-does-not-verifyand nothing runs it, because nothing on a fix names who raised it.kac reportwritesverified: []where it wroteconfirmed: []. A corpus that adopted either type renames the key in every record and in its_template.md, and takes the new schema withkac update --from <template>. Thereportstype'sshapeVersionmoves to 2, so a consumer reading records of that type reads the new key. -
The
faqtype is nowfix, and itsFixsection is nowResolution. A record lands infixes/asfix-0001, the page beside it isfixes.md, andkac validateholds the record to Symptom, Cause and Resolution. The type and its third section no longer share a word. A corpus that adoptedfaqsrenames the folder and the page, changes each record'stype:,id:, identity line andFixheading, then writesfixesoverfaqsintypes:and deletes.schema/faqs.yamlby hand.kac update --drop-type faqsrefuses that step, because the template no longer declares the name.kac update --from <template>then takes the new schema file. The type's page and its_template.mdare seeds, so a corpus keeps the copies it has, andvalidatenames every line of them still saying FAQ.
Fixed¶
-
kacprints UTF-8 on Windows. Standard output took whatever code page the machine was installed with, so a clause citing§9reached the reader as a replacement character while the same text in the record was intact. Every command writes through the same stream.kac reportis where it showed, because a report quotes citation text back. -
kac reportwrites frontmatter a parser accepts.owner: human:is not valid YAML, so a generated report metfrontmatter-parsesover the whole document rather than a message naming what to fill in.id,ownerandverifiednow arrive empty, andkac validatereportsrequired-fieldagainst each one. -
kac reportcalls a fresh report a draft. The schema requiresstatusand no run wrote it, so every generated report failedkac validateuntil somebody added the field by hand. The frontmatter now carriesstatus: draft. -
id-matches-filenamenames the two things that disagree. It printed the whole id where it meant the id's own slug, so a record filed asreports/rpt-clause-coverage.mdwas told thatrpt-clause-coveragedoes not matchrpt-clause-coverage. It now readsid 'rpt-clause-coverage' carries slug 'clause-coverage', and the filename carries 'rpt-clause-coverage'.The number and the mnemonic branches take the same wording. -
An unhandled fault goes to stderr. Spectre's own handler wrote one to stdout, so
kac report coverage > out.mdput the message inside the report and left the console silent. Every verb's own refusal already went to stderr, and this joins them. -
The
reportpage taught a filename that does not validate. Its example wrotereports/rpt-clause-coverage.md, and a record whose filename repeats the id prefix failsid-matches-filename. The filename carries the question the report answers, and the prefix belongs to theid. -
sectionsis described as the object it is.policy-lookupandstandards-lookuptold a reader to read two or three things fromsectionswithout saying it is keyed by heading, and a reader parsing it as a list gets nothing back.
0.25.0 - 2026-09-09¶
Added¶
- Seven more types travel in an export.
.schema/declares anexport:block at shape 1 onadrs,deviations,nfrs,reports,runbooks,servicesandtools, sokac exportwrites one JSON per record for each of them.deviationscarriesowner, because a register that says what was excused without saying who excused it is not a register.runbookscarriesSymptomsand no steps, for the reasonprocessescarries no steps.discoveriesholds everything back still, and its schema file says why. A standard now carriesderived-fromtoo, so the ADR behind a rule resolves for a consumer holding both. Take them withkac update --from <template>. kac validatewarns where an optional field is written with no value.empty-optional-keyreports a bare key on a field the schema does not require, because it says exactly what leaving the key out says. A required field is the other case, andrequired-fieldstill reports that one. A field declaringrequired-when:is exempt. The templates a corpus starts from now carry the required fields alone, and each one names the optional fields it leaves out. Take those withkac update --from <template>.- A plugin component can declare itself standalone.
metadata.componentsinplugin.jsontakes"standalone": trueon a component whoserequiresis empty.kac bundletrims an empty-requirescomponent when every component reading a type has gone, because such a component exists to support those. A standalone one supports nothing and serves the reader, so it now survives that sweep. Take it withkac update --from <template>. raise-finding, the skill that files what a session noticed about a corpus. The plugin's export is frozen, so an issue on the repository that published it is the only route back. The body carries the keys adiscoveriesrecord needs, and the skill asks before it files, every time. Take it withkac update --from <template>.request-deviation, the skill that asks a clause's owner to accept a departure from it. The body carries the keys adeviationsrecord needs, and leavesownerandaccepted-onto the reply, because the individual accepting the risk is what the request asks for. It files inside the organisation holding the plugin and nowhere else, and it asks before it files. Take it withkac update --from <template>.- A control travels in an export.
.schema/controls.yamldeclares anexport:block at shape 1, sokac exportwrites one JSON per control carryingverifies,mechanism,frequency,evidenceand the three sections a control holds. A control declares no part, so no flat file is written and the record is the unit. Take it withkac update --from <template>. controls-lookup, the skill that says what proves a rule. It answers what checks a standard, where the evidence lives, and which standards nothing claims.mechanism: not-enforcedis what makes the last of those answerable. It reads eachverifiesentry to see whether the control named a record or one rule inside it, and refuses a per-rule figure where only records were named. Take it withkac update --from <template>.- A corpus states the ranges the framework cannot know. A field declaring
values: $corpus.<name>in.schema/draws its range fromenums:in.corpus.yaml, so one schema above several catalogues stands behind the list each of them wrote.services.platformis the first field to use it: what a service is built on is one list in a library and another in a payments platform.kac validatereportscorpus-enum-undeclaredonce against.corpus.yamlwhere a corpus holds a record carrying such a field and has stated a range no record can satisfy, meaning none at all or one carrying a value that is not lower case. An out-of-range value stays an ordinaryenumfailure quoting the corpus's own values.kac newopens the block and leaves it empty. Take it withkac update --from <template>.
Changed¶
.corpus.yamlis at descriptor format 2, andservices.platformno longer carries a range of its own. A corpus holding services statesenums.platformin its descriptor beforekac validatepasses. Derive the values from your own deployables and close the list on what you found, which is what the type's page has always asked for. There is no migration:kac updatestamps the format and writes no values, because only the corpus can say what its estate runs on.
Fixed¶
- An export field declared as an object carries its keys. A field such as a report's
generatedreached a consumer asnull, because only a list of objects was read. It now travels as an object carrying the keys itsshape:orentry:block names. kac reportstamps the release without the commit behind it.generated.bytakes the Open Knowledge Format's<producer>/<version>form, and the value carried the build metadata as well, askac/0.24.0+24dcea21945982d92104c78a854465207d644ad6. It now readskac/0.24.0. A regenerated report no longer shows a moved commit where the tool's version stood still.- A discovery can be promoted to a standard.
promoted-tonames an FAQ or a standard and declaresreciprocal: promoted-from, but onlyfaqscarried that field, so promoting to a standard failedreciprocaland adding the key to the standard failedunknown-key.standardsnow declarespromoted-fromas well, optional and pointing back at the discovery. Take it withkac update --from <template>, which brings the schema down with it. label-canonicalcatches a shortcut label that leads to a record it does not name. The check compared a label against the canonical spelling of its own id, so[std-BOGUS]defined as../standards/workflows.mdpassed:link-resolveswas happy with the path, and the reader was shown an id no record carries.kac validatenow holds a label to the id in the frontmatter of the record it resolves to, which reaches a label the id styles do not recognise at all. A template is exempt, since its definitions demonstrate the form under labels nobody has chosen yet. The row this check gets on a type page is reworded to match, so runkac generateafter upgrading.
0.24.0 - 2026-09-08¶
Added¶
-
kac report <name>prints a report over the corpus and everything it imports. Two reports ship.kac report coveragenames every policy clause and what discharges it, with the deviations departing from it, the controls behind each covering standard, and any clause elsewhere sharing its key.kac report frameworksnames every external framework reference the clause tables cite, the standing the register files each framework under, the clauses citing each one, and how many rest on a single citation. Each row of both carries an emptyNote, for whoever confirms the report. Output is markdown on standard output, so a caller pipes it where they want it. Every run stampsgeneratedandsourcesinto the frontmatter it writes, naming the tool version, the moment, and thecontent-versioneach corpus answered at. The tool printscoveredanduncoveredand never splits a gap from something out of scope, because only a person can tell those apart. -
A standard's
implements:and itsCoverslines reach a consumer. The record carriesimplements, and each rule line carriescovers, holding the clause ids that rule discharges. A corpus inheriting the policies it answers to can now count its own coverage: before this, it saw what its own standards covered and nothing that arrived with the policies.part.citations.<Label>is the export source behind the rule line, and it takes the ids from the labelled footnote closing a part. Neither addition movesstandards@1, because a reader written against the shape before them is still correct. -
framework-uncitedfails a framework on the register that no clause cites. The register is the list of frameworks an estate has taken a standing against, so an entry nothing reaches is a standing nobody acts on, and it reads as coverage to whoever is looking for evidence. It is the third checkalignment-rollupreports under. The register is found by following a clause's own link, so a corpus whose clauses cite nothing has none in view. A finding lands on the policy that reached the page and names the page the entry is deleted from. -
policies/frameworks.jsonltravels in the export. One line per external framework reference, naming the standing the register files it under, the clauses citing it, the policies holding those clauses, and the page and anchor the register entry sits at. A type names the file withframeworks:in itsexport:block and the exporter fills the keys, because a reference is read from a clause's cell and from the register the cell links to rather than from any field a type declares. Navigation stays one way:clauses.jsonlis unchanged, and a clause still carries no framework. Bothpolicies@2andformatVersionstand, because a reader written against the shape before this is still correct. -
report-stalewarns where a report answers for a version the corpus has left behind. Every other record is about the estate, so a corpus that moved leaves it as true as it was; a report is about the corpus, and the same change can make it wrong with nothing in the record showing it. Eachsourcesentry is held against the version in front of the reader: the descriptor's owncontent-version, or the version a consumed corpus's restore resolved to. A warning, because the report may well still hold, and whoever owns it either confirms that and raises the version by hand or runs it again. -
reportsis a knowledge type the framework ships. A finished report is a record: it has an owner, a person confirms it before it is published, and a reader browsing the corpus finds it beside everything else.generatednames what produced the content and when,sourcesnames each corpus it answers for and thecontent-versioneach was at, andconfirmednames every person who has checked it since. Sections are free-form, because a report's headings follow the question it answers. Take it withkac update --add-type reports. -
A field may hold one object, and a shared shape may say what it holds.
type: objectdeclares a value that is one mapping, and its keys are held to their own declarations exactly as a list's object entries are._shapes.yamljoins_enums.yamlas a shared block, declaring an object shape a field takes whole withshape: <name>. Nothing narrows a shape at the point of use, so a type holding one of its keys to a narrower value writes a rule.event, an actor doing something at a point in time, is the shape that ships. This moves the template to version 8, so a corpus takes_shapes.yamlwithkac update. -
entries_match('field', 'key', 're')joins the expression facts. It reads one key inside every object a field holds: each entry of a list of them, and the one anobjectfield holds. It is true where the field is absent and true where an object omits the key, because presence isrequired-field's question andentry-key's, so a rule written on top of it reports one fault once. -
A whole number is a field type the tool checks.
type: int, andof: inton a list, are read byint-format: plain decimal with an optional leading sign, and within what a 64-bit number holds. A separator or a base prefix is refused rather than decoded, because YAML reads1_000and0x1fas numbers of its own and an author should not have to know which spellings the parser admits.ado-epicson a capability is the field this reaches, and its entries were checked by nothing before.
Changed¶
- An FAQ's
confirmed.byis held to a person by a rule rather than by a pattern. The finding moves fromfield-patternon the entry's own line toconfirmed-by-a-personagainst the record, and the message says why a post, an agent and a team alias are each refused.confirmednow takes the sharedeventshape, whosebyis a plain string, and who may confirm is the FAQ type's own question to ask.
Fixed¶
-
kac updatewrites the descriptor's upstream block with one space after each colon. It padded every key it stamped to a column, andyamllint --strictrefuses that under itscolonsrule, so a corpus running the linter its template ships went red oncommit,template-versionandtaken-onthe moment it updated. The next run repairs a descriptor an earlier one aligned. -
A field's
type:andof:are held to what the tool dispatches. Either naming a value no check reads now failsschema-dispatchwhen the schema loads, sotype: tiemstampis reported rather than loading and holding the field to nothing. An entry key answers to the same vocabulary, to whatever depth anentry:block nests, because its value goes back through the same checks. The types aredate,enum,id,int,list,stringandtimestamp; a list's entries areid,int,objectandstring.boolwas offered bymeta/type.schema.jsonand dispatched by nothing, as wasof: date, and both are gone from it. -
An
of:on a field that is not a list is reported. It is read from a list's entries and nowhere else, so a scalar carrying one states a shape its value can never take.values:,min-items:andmin-records:were already held to the field type they are read against, andof:now joins them. -
An unquoted placeholder in a record is reported. YAML reads
owner: {{owner}}as a flow mapping rather than as text, so the value reached no check at all and the record validated clean.bare-keynow reports it and names the quoting that fixes it, andrequired-fieldreports the field missing where the type requires it. A template still reports the same spelling undertemplate-fields, which answers for the documents copied from it. The two seed recordskac newwrites carried the mark, and now name an owner.
0.23.0 - 2026-09-07¶
Added¶
-
A schema field can hold a moment.
type: timestampsits besidetype: datein a type'sfields:block, and takes2026-09-07T20:18:00Z: UTC, to the second, unquoted. A date is a day and is written quoted, because YAML rereads an unquoted one as a datetime and a reader's own zone then shifts the day it shows. AZinstant carries its zone in the value, so no reread moves it.timestamp-formaterrors on a value written in another shape, and on one naming a moment the calendar does not have. -
A field naming a person carries an actor prefix.
ownertakeshuman:alex.doefor a person, orrole:head-of-engineeringfor a post. Exactly one person holds a post, so a role keeps answerability with one human and survives a handover that leaves every record naming the previous holder wrong.confirmed.byon an FAQ anddeciderson an ADR takehuman:alone: each records who performed an act, and a post cannot perform one. A bare name, an agent, a session id and a team alias all failfield-pattern, so the tier boundary between a discovery and an FAQ is checked rather than described. The three prefixes are OKF v0.2's, whose trust tiers key offhuman:the same way. A corpus created before this rewrites the field in each record it holds, andkac validatenames the ones still bare. -
Every record carries its own
type. The universal schema requires the field, directly afterid. Its value is the singular type name the record's folder declares:standardinstandards/,adrinadrs/. A record read away from its folder therefore says what it is.type-matches-foldererrors where the field and the folder disagree. Every_template.mdcarries the line, sokac newwrites it. A corpus created before this adds the line to each record it holds, andkac validatenames the ones that are missing it.
Changed¶
- An FAQ records every confirmation, rather than the last one.
confirmedreplacesconfirmed-byandconfirmed-onwith a list, one entry per confirmation and oldest first:- { at: 2026-09-07T20:18:00Z, by: human:alex.doe }. The moment and the person are one entry, so they are edited together and neither can be left behind. A reader asking when the answer was last checked, by whom, and who checked it before that now has all three. The shape is OKF v0.2's. A corpus holding FAQs written before this rewrites the two keys as one entry per record, andkac validatenames the ones still carrying the old pair.
0.22.0 - 2026-09-07¶
Added¶
- The framework declares a
deviationstype. A deviation records a knowing departure from a policy or a standard: the clauses it departs from, the person who accepted the risk, the day they accepted it, and the day somebody looks at it again.departs-fromnames those clauses one by one and refuses a bare policy or standard id, because a bare id claims a departure from every clause the rule carries.kac update --add-type deviationstakes it, andkac newoffers it beside the rest. Three rules run over a record.review-after-acceptanceerrors where the review date falls on or before the acceptance date, so a deviation cannot expire as it is written.not-open-endedwarns where the record reads as a standing departure rather than a bounded one.expirywarns where a record is stillactiveon a day itsreview-byhas gone by, and stays a warning so a late review never makes deleting the record the cheapest way to a green build. - A rule expression can call
today(). It answers with the day the run happens, as an ISO date, so a rule compares it against a date field under the string comparison the grammar already uses between two dates. The day is read once for the whole run, so a corpus validated across midnight cannot answer one way for its first record and another for its last.
Changed¶
- The template's shape is at version 7. It carries the
deviationspage, its index and its record template, so a corpus runningkac updateis offered the type.
Fixed¶
- A field may name a part spelled unlike the record holding it.
id-formatread the whole entry as one id, sostd-ERRORS.a-failure-says-what-happenedfailed: a standard's record id carries a mnemonic and its rules are heading slugs. The check now reads the record and leaves the part toref-resolves, which is what answers for whether the part exists.
0.21.0 - 2026-09-07¶
Added¶
- A corpus adopting
processesnow publishes them.kac exportwrites one JSON per process carrying its frontmatter, itsWhen to use thistrigger and itsPrerequisites, and stops there.StepsandVerificationstay in the record, because a procedure is followed whole and in order against the version in force rather than against a copy taken on an earlier day. The type declares no parts, so nothing writes a flat file for it andmanifest.jsonreportspartsFile,recordKey,partKey,idKeyandseeAlsoKeyas null besidepartsat zero. kac bundleships two more skills.process-lookupfinds the procedure written for a planned task and reads its trigger before deciding it is yours. It is trimmed where the export carries no processes.corpus-retrievalreaches a record's published source and builds a link to it, namingghandaz devops invokeas the clients that authenticate to each platform and saying what to do where neither reaches. The three skills that shipped before now hand their link building to it.
Changed¶
- A component your plugin manifest declares with an empty
requiresnow travels only where a component that reads a type did. It reads no export itself, so it is there to support the ones that do, and a plugin shipping it alone would carry a skill supporting nothing a reader can reach.bundle.jsongives the reason asno component it supports survived, and a run that trims every component warns as it did before. A file no component claims is unchanged: it needs no declaration and travels whatever the corpus adopted. -
The template's shape is at version 6. It carries the two skills above, so a corpus running
kac updatereceives them. -
kac checksasks you for the half of a drifted checks table you hold. Where the reader-facing table and your.schema/_checks.yamldisagree, every line of the report nameson-type-page:in that file and says where it sits. The row beside it is in the tablekacships, which your corpus holds no copy of, so the report says whose it is rather than naming a source file you cannot open.
Fixed¶
- The lookup skills
kac bundleships now state the type of every field they describe.obligations,definitionandnotare one string of markdown holding the record's bullets, and a skill calling any of them a list sent a reader looping over a string. A field with no value arrives asnullbeside a key that is still there, so test the value rather than the key.shortcodeis the one key a line can be missing outright.
0.20.0 - 2026-09-01¶
Added¶
kac exportcarries the corpora your corpus consumes. Every type a producer exported travels, so a consumer receives types it never adopted and a citation into them resolves. Their parts merge into one flat file per type, and their records are filed under the shortcode of the corpus that wrote them. An inherited line carries that shortcode onid, onrecordand on everyseeAlsovalue, and again undershortcode; a line with none is your own. The manifest gainssources, one entry per corpus inherited, each holding the publishing block its producer wrote, because a record of theirs is read at their commit in their repository.kac packseals all of it, so a third corpus inherits the chain.kac exportrefuses rather than writing a hole. It stops with the reason and no files where a declared import has not been restored, where a consumed corpus is at an export format this build does not read, where two corpora export one type at different shapes or section fidelities, and where one corpus arrives twice at two versions.
Changed¶
-
The breadcrumb names the corpus each count belongs to. A merged export holds several corpora's records under one type, so a type now gets a line for your own records and another for each corpus you consume, reading
standards (from eng). 37 entries across 12 records: …. A type you wrote none of gets no line under your own name. The closing line warns against answering from memory rather than naming what a word means, because a corpus may ship any of the three lookup skills or none of them. -
glossary-lookup,policy-lookupandstandards-lookupreadsources. Each builds a link and a fetch from the publishing block of the corpus that wrote the line, reached through the line'sshortcode, rather than from yours. Each says what a prefixed id means and where an inherited record's file sits.standards-lookupstates that one file already holds the whole union, and reads what stayed behind offtypesin the manifest rather than naming a fixed four.kac updatebrings the three down. -
A type's manifest entry names two more of its part line's keys.
idKeyandseeAlsoKeyjoinrecordKeyandpartKey, so a corpus merging that type stamps the keys the producing type actually named.formatVersionmoves to 4, so runkac exportagain beforekac bundle, which refuses an export built to another shape. -
kac validatereports aCoversline that names nothing.mirrors-citationsnow reports a labelled footnote gathering no citation the field could carry, against the line. A line naming only ids of types the field does not point at gathers nothing either. A section carrying no line at all stays silent, which is how a rule discharging no clause is written. kac validatereports aCoversline a space left out of italic. Markdown will not read an emphasis mark with a space against it, so_**Covers:** [pol-SCRT].EMBED _is not the form and the marks reach the page.mirrors-citationsreports the line, and its citations count as before, so a standard covering six clauses gets one finding rather than six against its frontmatter. A line that is bold alone is still the labelled prose form and is passed over.kac validatetells aCoversline naming a record whole to name the part. Where the field declarespart-required:,mirrors-citationsreports the line in the wordsref-resolvesuses, naming the target type's own word for a part:this 'Covers' line names 'pol-EVER' whole, and 'implements' names a clause. It used to report that the field did not list the id, which sent an author to put a bare policy id there and meetref-resolvesrefusing it on the next run. A field that admits a bare id keeps the message it had.
0.19.0 - 2026-08-31¶
Added¶
- A standard exports its rules, and a corpus adopting standards ships a skill that reads them. The type declares
parts:over the H3 headings under## Rules, sokac exportwrites one line per rule tostandards/rules.jsonl, carrying the obligations in the markdown the standard wrote them in. Each record travels beside them with its Summary and its conformance checklist.kac bundleincludes the newstandards-lookupskill wherever the export carries standards, and trims it where it does not.kac newships the skill in the plugin tree, andkac updatesends it to a corpus already created. - A field can be held to the citations its prose gathers. A type declares
mirrors-citations: <Label>beside the field'sref:, andkac validatereports drift in both directions between the field and the labelled lines. A line is written in italic with the label bold, and closes the section whose citations it gathers, so a line standing in the middle of one is reported where it sits. A standard'simplements:declares it: each rule closes on_**Covers:** …_naming the clauses it discharges, so the frontmatter says which obligations the standard answers and each rule says which of them it answers. The obligations under a rule then carry no clause citation of their own, andkac exportdrops the footnote before it takes them, so a part carrying nothing else travels with no obligations rather than with a coverage line standing where its words belong.kac validatealso reports the key declared with noref:to resolve against.kac newships the form in the standards template. - A standard may carry a
Sources and further readingsection. It names the external documents the standard defers to, each marked normative or informative. A rule built on somebody else's conventions then says where the rest of it lives.kac newships the section in the standards template, and a standard deferring to nothing deletes it.
Changed¶
- A standard's rules sit under
###headings, andkac validatereports a Rules section with none. The heading is what the rules beneath it hold a reader to, and it is the address a citation and an export both carry, sopart-nonenow reaches standards as it already reached glossaries. A standard whose rules are a bare bullet list gains one heading.kac newships the grouping in the standards template. - A tool's
categoryis the folder it sits in undertools/, as a policy's and a standard's already were. It was the one of the three still written by hand, and the only field in the schema carrying nodescription:. A record that writes the key now failsderived-key: delete the line and file the record under the folder you want it to name. It is no longer required, so a tool filed directly intools/simply has no category, which is what the other two do.kac newships the template without the key.
Fixed¶
kac validatereports a schema declaringof: objectwith noentry:block, rather than ending in a stack trace. The schema pass already names that fault asschema-shape. A record filling such a field reached the entry check first and took the run down, so the message never printed.- Declining
kac update --drop-typesaysupdate, where it used to saynew. The message is the tail of the command that printed it, and the one it named was a command the reader had not run. - A rule whose
expr:names a number too large for a whole number is reported, rather than ending in a stack trace.words() < 99999999999reached the parser's integer literal and overflowed past the exception the schema load catches.kac validatenow names the number, its position and the rule. kac restorerefuses a package that unpacks to more than 256MB, or that holds a single entry over 16MB. The path each entry names was already held inside the import folder, and what it unpacks to was not, so a malformed package was read whole into memory instead. Both caps count the bytes actually read, because a zip entry's declared size is the package's own claim about itself.- A part id written against a link takes no delimiter that closes an emphasis.
_[pol-SCRT].EMBED_read the citation aspol-SCRT.EMBED_, because the id was measured off the source rather than off the text markdown makes of it.part-refreported a clause nobody could write.
0.18.0 - 2026-08-31¶
Added¶
- A type's
id.widthtakes amin/maxspan as well as an exact count. A mnemonic drawn from a concept rather than cut to a length can then admit bothstd-PRandstd-SECRETunder one declaration.kac validatereports an id outside the span asid-formatand names both ends. An exactwidth: 4behaves as it always has. filename.carries-id: falsekeeps a type's id out of its filenames. Its records are filed by topic alone, and nothing then reads the head of a filename as an id:id-matches-filenamestays silent,slug-lengthmeasures the whole stem, and a link to the file is a link rather than a citation.kac validaterefuses the three spans it cannot act on. One beside a filename that still carries the id, becausesecret-handling.mdwould otherwise bind to whichever id its first segment happens to spell. One on anumberedtype, which pads to a single width so that ids sort. One whosemin:sits above itsmax:, which no id can meet.- A field can require the part of the record it points at. A type declares
part-required: truebeside the field'sref:, andkac validatereports an id there that names the record whole. The message uses the target type's own word for a part, so a field pointing at policies asks for a clause.kac validatealso reports the key declared with noref:to resolve against, and one pointing at a type that keeps no parts.
Changed¶
- Standards take mnemonic ids.
std-0001becomesstd-VCS. A number records the order things were created, and a reader meeting one in a control'sverifies:learns nothing. Filenames are untouched, because a standard is already named for its rule area. A corpus that wants its numbered standards back claims.schema/standards.yamlwith askip:entry in.corpus.yaml, which stopskac updatereplacing it. kac generateheads a table per folder in a type's index. A type that declares a field withfrom: sub-pathgroups its index rows on the first folder below the type, so a policy folder holding Delivery, Governance, Operations and Security reads as four tables instead of one long list. A record filed deeper joins the table its first folder heads. A type whose records all sit directly in its folder gets the single table it has always had.implements:on a standard names clauses.implements: [ pol-EVER ]becomes one entry per clause the standard puts into practice, aspol-EVER.BRANCH. The bare id claimed the whole policy, so a standard discharging six of eight clauses read to a coverage report as full cover and the other two disappeared. The shorthand is refused rather than admitted beside the list, because it is a keystroke shorter than the honest form. A corpus that wants the old reading back claims.schema/standards.yamlwith askip:entry in.corpus.yaml, which stopskac updatereplacing it.- The derived column is dropped from a table that repeats it. Every row under a heading of Security carries
security, so the Category column says nothing there and is left out. A record filed deeper keeps it, becauseplatform/nodeunder a heading of Platform is the one placenodeis written down. A corpus using no folders at all loses a column that was empty in every row.
Fixed¶
- A schema key spelled
nooroffnow switches its behaviour off.on-type-page: noin_checks.yamlread ason-type-page: true, because only the exact wordfalsewas taken, and the check was then written onto a type page the schema had excused it from. Both spellings of each answer are read. kac updatefinds a seeded record the corpus filed in a sub-folder. A record's folder sets its category, so a corpus files a seeded policy underpolicies/governance/and still holds it. Compared by path, the corpus read as holding none, andupdateoffered a second copy at the seeded path. Accepting it left two records carrying one id, whichkac validatethen failed onid-unique. A seed absent from its path is now looked for by the id it carries, anywhere under its type's folder, and a match is left alone under--policy fullas well as undercautious. A record's relative links are written for the depth it was seeded at, so there is no copyfullcould write a folder down that would resolve.
0.17.0 - 2026-08-29¶
Added¶
- A field can be derived from the folder a record sits in. A type declares
from: sub-pathon a field, andkacreads its value from the folders between the type's own folder and the file.policies/security/accs-access-by-identity.mdcarriescategory: securitywithout a line of frontmatter saying so, andstandards/platform/node/testing.mdcarriesplatform/node. A record saved straight into its type folder gets an empty value, so you start using categories by making a folder. The value reaches the generated index, its sort, andkac export. derived-keyreports a derived field written by hand. The key is declared, sounknown-keyadmits it and cannot say that the value comes from the path. Delete the line, and file the record in the folder you want it to name.schema-shapereports a type whosefolder:is not the name of the file declaring it. A document's type is read from the folder it sits in, and that lookup uses the schema file's name, so the two disagreeing left every record of the type unread whilegeneratewrote into the folder nobody was reading. The two names now have to agree.
Changed¶
- A standard's
axisfield is gone, andcategoryreplaces it. Nothing readaxisbut one index column, which repeated the folder already shown in each row's link. The composition model stays: the rule-set binding a piece of work is the union of the folders that apply to it. Deleteaxis:from every standard. - A policy's
categoryis read from its folder rather than from its frontmatter. It is no longer a required enum ofsecurity,delivery,operationsandgovernance. Move each policy into the folder its category named, delete thecategory:line, and the exported value is unchanged. The set of folders is now the corpus's own.
Fixed¶
kac validatejudges a#fragmentagainst the headings alone. It read a record's frontmatter block as a heading, sofragment-resolvesaccepted a link naming an anchor no renderer offers. A link into a record is now held to the headings that record carries.
0.16.0 - 2026-08-28¶
Changed¶
-
kac exportcarries a field its type declares as a list. Every such field was written asnullon every record, which reads exactly as a record that holds nothing. A list now travels as a JSON array, and an entry the type declares as an object carries the keys that declaration names. A list a record left empty staysnull, beside the field it never wrote.docs/design/export.mdstates the shape. -
The glossary exports
tags. It is the first field to travel as a list. A consumer holding a vendored glossary can filter its records by subject without reading eachScope. The key lands onglossary/<record>.json, and a term line interms.jsonlcarries notags. -
kac newno longer sends a corpus a link to a type it declined. A type's root page and its_template.mdname the other types and link to them, which is what makes a full corpus navigable and what left a corpus adopting a subset holding dead links. Each page is now unlinked as it is written: a reference to a declined type keeps its own wording and loses its link, soThat is a [service](services.md).arrives asThat is a service.
The same happens on kac update --add-type, for the page that arrives, and on kac update --policy full, which now
holds a seed to the template as this corpus would have received it rather than as it was authored. Without that a full
update wrote the links back.
This reaches the pages a corpus receives once and then owns. A framework document is shared word for word, and
framework-names-types goes on holding it to naming a type rather than linking to one.
A link into another type's folder has no such repair, because its text names a record. Two seed pages defined one as a
reference link, which reached a corpus whole, so glossary.md and frameworks.md now name the record without linking
it.
-
kac update --add-typesays what the arriving page does not get. The new page links to the types the corpus holds. The pages already there name it without linking, because each was written while the type was still declined, and changing them is the corpus's own call. -
kac validateno longer refuses a schema for naming a type the corpus declined. A field'sref:and a type'sversus:each name a type, and a corpus adopts as many types as it has use for..schema/standards.yamlalone reaches four other types, throughref:on four fields and aversus:naming one of them again, so a corpus adopting standards and nothing else met fiveschema-dispatcherrors on a schema it had just been sent. Both declarations are now left alone where no schema covers the type. Nothing is rendered, andkac update --add-typestarts the reference without an edit to.schema/.
What a record is held to does not soften with it. ref-resolves goes on asking that a cited id exists, and a field
whose every declared type this corpus turned down now admits nothing rather than everything. It names the types the
declaration wanted, since a type nothing covers has no label to read:
'derived-from' points at 'std-0002', which is a Standard. The field points at 'adrs', which this corpus did not
adopt.
With this and the unlinking above, kac new adopting any single type writes a corpus that validates and exits 0.
kac update --drop-typeasks before it deletes. Giving up a type deletes its page and leaves every page still naming it holding a dead link. The run says so, says thatkac validatereports the ones it can reach, and waits for an answer. The question takes no by default.--yesanswers it in advance, and a run with no terminal and no--yesrefuses rather than guessing.
0.15.0 - 2026-08-28¶
Added¶
kac validatesays when an import has fallen behind what its source publishes.kac restorekeeps the version aconsumes:entry locked for as long as the range still admits it, which is what makes a restore reproducible and is also how a corpus sits on a version nobody meant it to sit on. Sovalidateasks each source what it holds now, once per run, and reports three new checks against.corpus.yaml.
import-behind is a warning: a newer version sits inside the declared range, and kac restore takes it.
import-capped is information: a newer version is published and the range holds it back, which is a decision the
corpus already made. import-unreachable is information too, for a source this run could not ask, so a lock reads
as unchecked rather than as current. None of the three fails the build, because failing on somebody else's release
would turn every downstream red the day a governance corpus ships.
A source answering with no versions at all reports as unreachable rather than as current, because a registry answers a
private feed's anonymous reader exactly as it answers a package nobody has published. A corpus with no
consumes: block reads no source and builds no client, and every other check still reads the working tree alone.
-
A third severity,
info.kac validatecounts it in its summary line and in--json, wheresummary.infosis new, andkac checkstallies it apart from the warnings. Neither a warning nor an info changes the exit code. A check declaresseverity: infoin.schema/_checks.yaml.docs/design/checks.mdcovers it. -
A corpus says who it is, and
packandbundlestop inventing it. Four new keys in.corpus.yaml:display-name,description,licenseandauthor.kac exportcarries them in a newaboutblock,kac packwrites them into the package a registry lists, andkac bundlewrites them into the plugin manifest somebody installs.
A plugin's identity is now generated rather than copied. name, version, displayName, description,
author, homepage, repository, license and keywords are all written from the corpus, and a key the corpus
declared nothing for is removed rather than left standing. author is the exception, filed under the corpus's own
name where it named nobody, because the format asks for one and claude plugin validate --strict fails a manifest
carrying none. .plugin/.claude-plugin/plugin.json keeps only what the corpus declares: metadata.corpusRoot and
metadata.components, plus any key this tool has never heard of. A manifest copied from a template no longer
publishes under the template author's name, licence and repository.
keywords are the types the export carried, so a plugin never advertises a type its corpus declined. kac new
writes the four keys bare, because a value supplied there would be inherited rather than chosen.
-
kac exportnames the two keys that address a part. Each type's manifest entry gainsrecordKeyandpartKey, naming which key of a part line says which record it belongs to and which part of that record it is. A type names its own keys, so a consumer holding a corpus with a type it never adopted had no way to read them and had to assume a spelling. Both are absent where the type keeps no parts, aspartsFileis.docs/design/export.mdcovers it. -
kac validateresolves a reference across a corpus boundary. A citation carrying a producer's shortcode, aseng:pol-VURM.TIMEBOX, resolves against the exportkac restoreunpacked under.imports/. It is read in prose and in a field declaring aref:, soimplements: eng:pol-VURM.TIMEBOXnames one clause rather than a whole policy, and both halves are held to existing. Local records and imported ones go through one lookup, so a corpus is not judged more loosely for having imported the record it cites.
Each side keeps its own spelling. A record the reading corpus holds is cited bare, one it imported carries the shortcode, and writing either the other way is refused naming the spelling to write.
A new import-restored check fails a corpus declaring an import that is not on disk, and names kac restore. Every
citation into that shortcode then stays quiet, so a run that has not restored reports one line rather than one per
reference. docs/cli/validate.md documents both.
kac restorefetches the corpora a corpus declares it consumes. A newconsumes:block in.corpus.yamlnames each producing corpus, the shortcode it is cited by, the version range it is wanted at and the source it comes from.restoreresolves each range, fetches the packagekac packsealed, and unpacks it under.imports/<shortcode>/, which the template now gitignores. The version each range resolved to is written back onto its own entry, so.corpus.yamlstays the one description of what a corpus is.
A source: names a registry's service index or a folder of packages. A folder holds the same sealed package a
registry serves, so a corpus consuming a sibling in its own repository needs no registry, no token and no release. A
path is relative to the corpus declaring it, as upstream.url is.
A range says 1.2.0 or ^1.2.0 and nothing else, and a caret never takes a prerelease. A lock the range still admits
is taken without asking the registry, so two restores of an unchanged descriptor write the same bytes. A run says what
it fetched, at which version, and which corpora were already current.
A shortcode two entries both claim is refused naming both, as is a corpus two entries both consume, as is a package
whose own manifest is cited by a different shortcode from the one declared. KAC_REGISTRY_TOKEN in the environment
carries a bearer token for a private feed. docs/cli/restore.md documents the verb, and docs/corpus-descriptor.md
the block.
0.14.0 - 2026-08-27¶
Added¶
azure-devops-wikiand a newazure-devopstarget build links.kac exportaddressedgithubalone. A corpus publishing to an Azure DevOps wiki now gets a?pagePath=link per record, and one publishing to Azure Repos without a wiki gets a?path=&version=GC<sha>link.kac new --publishing azure-devopsaccepts the new target and fills its base in from adev.azure.comremote, in either the SSH or the HTTPS spelling. A wiki base has to be typed in, because a repository's remote says nothing about which wiki publishes it.
A wiki link is not pinned to a commit, because no ?pagePath= URL takes one. An agent still reads the version the
export was built from. docs/corpus-descriptor.md sets out both targets.
The azure-devops link form and the anchor an Azure DevOps wiki resolves for a heading carrying punctuation are both
unconfirmed against a live organisation. The wiki's page path, its anchor parameter and its rejection of a base
carrying a page id are confirmed.
kac packseals an export into a versioned package. It reads.dist/export/and writes one file to.dist/package/, named for the corpus and itscontent-version. The file is a.nupkg, which is a zip carrying a small XML manifest a registry reads to name and version it, and both GitHub Packages and Azure DevOps Artifacts store one. Everything undercorpus/inside it is the export, byte for byte, so nothing reading the result needs a NuGet client. Two runs over one export produce identical bytes.
The command refuses a corpus that has not declared corpus:, content-version: and shortcode: in .corpus.yaml,
naming the one that is missing. It publishes nothing: pushing the file is your pipeline's step, and docs/cli/pack.md
carries the command for it.
-
kac pack --repository <URL>names where the corpus's source lives. Some registries read that URL to decide which repository a package belongs to, and GitHub Packages refuses a package naming none when the token pushing it is scoped to a repository. The element is left out where the flag is not given, because the export states where a record is published and that is a different address. -
A
policy-lookupskill travels in the plugin.kac bundleships it besideglossary-lookup, and a corpus carrying no policies has it trimmed. It readspolicies/clauses.jsonl, answers from a clause'slevelrather than from the modal in its wording, and says which of the four levels it found. What an external framework obliges stayed behind with the register that explains it, so the skill names that gap rather than filling it. -
A component says whether the breadcrumb names it.
"announce": trueon a manifest entry puts that skill in the breadcrumb's last line, and the default leaves it out. The line exists to create a question a session would not think to put, so a skill somebody asks for by name does not earn it. A corpus adding a second skill setsannounceon the one worth introducing. -
plugin.fromin.corpus.yamlreads the plugin tree from one shared folder. Several corpora in a repository keep one copy of the skills and hooks between them instead of a copy each.kac bundlemerges that tree with the corpus's own.plugin/, where a file the corpus holds wins, andkac updatewithholds the shared half rather than writing it back. The manifest is never taken from the shared tree: it names the plugin, so it stays at.plugin/.claude-plugin/plugin.jsonin each corpus. Omit the key and nothing changes. A corpus adopting the key with the old copies still on disk has each one reported as a file the template sends nothing to, because a corpus's own file wins the merge and a leftover would go on shipping after every upstream change. -
A corpus created before this declares no component for the new skill.
kac updatewrites the skill, and leaves.plugin/.claude-plugin/plugin.jsonalone because the manifest is the corpus's own. A path no component owns ships unconditionally, so add the component yourself to have it trimmed where the type is not adopted:
{
"path": "skills/policy-lookup",
"requires": [ "policies@2" ],
"note": "Reads a clause from corpus/policies/clauses.jsonl and the owning policy beside it."
}
0.13.0 - 2026-08-26¶
Added¶
-
A list field's entries can be objects. A field declaring
of: objectnames its entry's keys in anentry:block, written with the vocabulary a field is written with. Each key is held to its owntype:,pattern:andrequired:.entry-shapereports an entry that is not a mapping, andentry-keyan entry carrying a key the field does not declare or missing one it requires. -
alignment-rollupholds a policy'saligns-withto its clause table. Both directions: a binding framework reference in anAlignmentcell and not in the roll-up, and one in the roll-up that no clause cites. The message names the reference and the side it is missing from. -
The roll-up carries the frameworks that bind. A rule declares
postures:, naming the standings that oblige a summary as the corpus's framework register heads them. A clause may cite a framework filed under any other standing, for provenance, and the roll-up leaves it behind.framework-posturereports a clause citing a framework the register does not place at all, once per framework rather than once per clause. -
A corpus rule can read the corpus's files.
CorpusRuleContextcarries the tree, for the rule whose question is answered by a page no record links into the graph. A framework register is that case: it holds no frontmatter, so it is no record, and it is the only place a standing is written down. -
part-refreads a part id written beside a link.[pol-EVER].BRANCHcitespol-EVER.BRANCH, so a document citing six clauses of one policy carries one link definition rather than six. The part id has to sit against the closing bracket, so a full stop closing a sentence after a link is still a full stop. A corpus already writing this form may see errors it did not before.
Changed¶
-
.corpus.yamltakes onebasewhere it tookhuman-baseandraw-base. Write the URL a person opens to browse the corpus: the GitHub repository with no/blobon the end, the Azure Repos_gitURL, or the wiki's own URL. A raw-content host was a GitHub idea that no other target has, and it never served the human case. Edit thepublishing:block by hand: nothing migrates it, and a descriptor still carrying the old keys exports without links. -
An export's
publishingblock dropsrawTemplateand carriesbaseandpathPrefix.humanTemplatestays. An agent reading a record's source joinspathPrefixahead of the record'spathand asks a client that authenticates to the target, rather than fetching a bare URL. Only GitHub ever served raw source anonymously, and only for a public repository.formatVersionmoves from 2 to 3, sokac bundleandkac packrefuse every export built before this. Rebuild withkac export.
A record's links loses its raw half for the same reason. No type's shapeVersion moves: that object is written
for every type by the exporter rather than declared by any one type's export: block.
The glossary-lookup and policy-lookup skills both tell an agent to fetch the file rather than substitute into a
template, and to say so plainly where it holds no client for the target.
-
An index column holding a list renders its entries. A column naming a list field read the value as a scalar and wrote an empty cell, so
aligns-withon a policy index had been blank since the column was added. A column naming a list of objects renders what names each entry, which foraligns-withis the framework. -
A policy's
aligns-withis grouped by framework. It was a flat list of strings held to an ISO 27001 pattern, which is why no other framework could appear in it. Each entry now carries aframework:and theclauses:reached inside it, and any framework may. A corpus holding policies rewrites the field. -
A clause line carries no
alignment.policiesmoves toexport.version: 2. A framework reference resolves through the corpus's ownframeworks.md, which no consumer receives, so the mapping reached one without what says what it is worth.
0.12.0 - 2026-08-26¶
Fixed¶
-
part-refreads a citation written as a link. A corpus cites a part as a code span, and as a link carrying the citation as its text or as its label. Only the code span was resolved. A link naming a clause or a term that does not exist passedkac validate, because a link resolves against a page and the page carries whichever part the citation claimed. Every form now reports underpart-ref, and a link spelling the separator as a colon is reported as one, so a corpus using the link form may see errors it did not before. -
A type index links a record through the category folder holding it.
kac generatewrote the filename alone, so a record filed under a category below the type's folder was linked as though it sat beside the index. Standards are filed that way by declaration, and every link to one was dead.
0.11.0 - 2026-08-25¶
Added¶
- A policy's clauses travel in an export.
.schema/policies.yamldeclares anexport:block, sokac exportwritespolicies/clauses.jsonland one file per policy beside the glossary's. Each clause line carrieslevel, holding the modal the clause opens with, so a consumer tells aMUSTfrom aCOULDwithout parsing the words.Purposetravels as its opening paragraph, andScopeandExceptionstravel whole.
Fixed¶
-
A part's
anchoris read from where its type takes its parts. A heading-sourced type carries the part id, which is a heading's slug and its anchor alike. A table-sourced type carried that id too, and no fragment resolves to an authored clause id. It now carries the slug of the section holding the table, so a link built from a clause line lands on the table. -
A carried section leaves the link reference definitions behind. They sit in a block at the foot of a record, which puts them inside whichever section is written last, so
kac exportjoined them onto the end of that section's prose. A consumer read a run of paths that nobody sees on the page. A glossary's export is unchanged, becauseScopeis never a glossary's last section. -
kac exportleaves a clause table in the order its author wrote it. Every type's parts were sorted on their text, which is right for a glossary and wrong for a table grouped by binding level: an advisory clause could reach a consumer ahead of the obligations. A heading-sourced type's parts still sort alphabetically.
0.10.0 - 2026-08-25¶
Added¶
-
A section can travel cut down.
export.sections:in.schema/<type>.yamltakessummaryandreferencealongsidefull.summarycarries the section's opening paragraph.referencecarries the key with no words under it, leaving a consumer the record's ownpathandlinksto follow.kac validateno longer refuses either. -
The export manifest states the fidelity each section travelled at. Every entry under
typescarries asectionsobject naming its sections and how much of each one travels, so a consumer can tell a cut section from a whole one.
Changed¶
kac validatereports a reduced fidelity againstexport.parts:alone. A part line carriesfull, becauseline:already names key by key what of a part travels. A type declaringsummaryorreferencethere is still reported as declaring a fidelity nothing carries.
0.9.0 - 2026-08-25¶
Added¶
-
A type declares the keys of its own export line.
export.parts.line:in.schema/<type>.yamlnames the keys one part writes and the source filling each, drawn from a closed vocabulary covering a part's text, its body, its modal, a frontmatter field and a table column.kac exportreads that declaration and names no key itself, so a second type exporting parts costs no code. Glossary is the type that declares one, and itsterms.jsonlis byte for byte what it was. -
Each type states its own shape version in the export manifest.
export.version:in the schema reaches the manifest asshapeVersionon that type's entry.formatVersioncovers the envelope alone, so a key added to one type's line cannot refuse a consumer reading another. -
kac bundlerefuses a component reading a type at a shape the export does not carry. Arequiresentry may name the shape, asglossary@1. A bareglossaryasks for the type and opens none of its files. Either is trimmed, as before, where the export carries no such type at all. -
kac validatereports aline:that would export nothing. A key with no source, a source nothing fills, afront.naming a field no record carries, acolumn.naming a header the type does not declare, and apart.leadorpart.asideagainst a table row are each an error. So is anexport:block with noversion:.
Changed¶
export.parts:in a type's schema is a block, and the fidelity moves inside it.export.parts: fullbecomesexport.parts.fidelity: fullwithline:beside it.kac validatereports a type file still carrying the older form, naming the fidelity, theline:and theversion:it lacks.
0.8.0 - 2026-08-25¶
Added¶
- A corpus declares the shorthand another corpus cites it by.
.corpus.yamlcarries a top-levelshortcode:, which is theengineng:pol-VURM.TIMEBOX.kac validaterefuses a spelling a citation cannot carry, and one a type has already taken as its id prefix.kac exportstates the declared shortcode in its manifest, so a consumer holding several exports knows which one answers a scoped citation.kac newwrites the key with no value: a shortcode cannot be changed once another corpus has cited it, so it is filled in when one is about to.
Fixed¶
kac validatereads a record whose frontmatter carries a complex key. A key written as a sequence or a mapping is legal YAML and names no field. It was reported as frontmatter that would not parse, which named the wrong fault. It now arrives as an empty key, whichunknown-keyreports against the document that wrote it.
0.7.0 - 2026-08-24¶
Added¶
kac updatetakes a newer framework into a corpus that already has one. It fetches the template.corpus.yamlpoints at, decides file by file what the corpus receives, writes it, and records what it took. Everything it writes stays in the working tree and nothing is committed, sogit diffis the review step.updatecovers the layers, the flags and what each refuses.kac update --checkreports what would change and writes nothing, exiting non-zero where anything would. It answers in both directions: a framework file the corpus holds differently, and a file the corpus keeps where the framework's rules apply that the template sends nothing to.kac update --add-typeadopts a type, and--drop-typegives one up. Adopting writes the type's schema, root page and template, and adds the name totypes:. Giving one up refuses where the folder still holds records, naming the count.kac update --policy cautious|fulloverridesupdate-policy:for one run.cautiouswrites a seed only where the corpus has none.fullholds every seed to the template and hands the reconciliation to the diff.updatestampsupstream.commitalongside the template version and the date. A template read from a folder resolves no commit, and the key is then left as it stands.
Removed¶
kac mechanismis gone, andkac updatereplaces both its halves. It compared two corpora on identical paths and read a manifest attooling/manifest.yamlthat no corpus held, so no corpus could run it against the framework it actually took.role:in.corpus.yamlis no longer written or read. It said whether a corpus carried the tests that prove the tool, and no corpus does.newstops writing it, and anupdateover a descriptor still carrying it names the key and stops, as it does for any retired key.
Changed¶
- A continuous integration starter is refreshed and never introduced.
newwrites the starter for the system--cinamed, and an update leaves a starter the corpus does not hold where it is. Which system builds a repository is that repository's own answer.
0.6.0 - 2026-08-24¶
Added¶
kac newturns the folder you are standing in into a corpus. It takes the framework from a template repository at a ref, writes what the manifest says a corpus receives, and writes the two files no template can supply:.corpus.yamlandREADME.md. It then runsgenerate,validateandgit add -A, and stops short of committing.newcovers the flags, the defaults and the order it asks in.--fromdefaults to the framework's own repository, and accepts a local path as well as a URL. The template is cloned rather than fetched over HTTP, so a repository needing authentication uses the credential helper you already have. A local path is the offline escape hatch.--yestakes the default for every answer not given. A run with no terminal and a missing answer exits with an error rather than waiting, because a hung pipeline is worse than a failed one.
Changed¶
- A manifest rule may declare
ci:, naming the continuous integration system its files serve.kac new --ciwrites the matching starter and no other, so a corpus built by Azure DevOps no longer receives a GitHub Actions workflow that would run uninvited. minimum-toolin the template manifest moves to0.6.0. A 0.5.0 tool reads that manifest, ignores everyci:in it, and takes both starters.
0.5.0 - 2026-08-24¶
Changed¶
.corpus.yamltakes a new shape.upstream:now sayspath,ref,commit,template-versionandtaken-on, where it saidmechanism-version,synced-fromandsynced-on.accepted-divergences:becomesskip:, and dropssinceandrevisit.update-policy:arrives, defaulting tocautious. Every renamed key is reported by name, with what to write instead, so nothing is misread in silence;upstream.synced-fromwas dropped rather than renamed, and the message says to delete it. The corpus descriptor covers the whole file.mechanism --syncstampsupstream.template-versionandupstream.taken-on, where it stamped three keys. It leavesupstream.commitalone, because a sync reads a directory rather than a git ref and has no commit to record.mechanism --checkreports a template version, where it reported a mechanism version. The number has not moved.
Added¶
- A template manifest reads
to:on a rule, naming where that rule's files land in a corpus. It replaces the pattern's directory prefix, so a template authored in a subdirectory of the repository serving it reaches a corpus's own root. - A template manifest reads
layer: removed, a tombstone naming a file a corpus should delete when it takes a newer framework. Nothing acts on it yet:kac updateis what will. - A template manifest reads
minimum-tool, the oldest tool that can read it. The template is fetched rather than shipped inside the package, so the two version independently.
0.4.0 - 2026-08-24¶
Changed¶
kacfinds a corpus by its.corpus.yaml, where it looked for a.schema/. It then walks up again from the corpus root for the schema to judge that corpus against, so one schema can serve several corpora in one repository. A standalone corpus holds both files at its own root and both walks stop there, which is the ordinary case and is unchanged. A corpus with no descriptor is no longer found: write one, and the corpus descriptor says what goes in it.kacnames.corpus.yamlwhen it cannot find a corpus, and reports separately on a corpus with no schema above it. The second exits 1 rather than crashing on the first schema file it tries to open.kac mechanism --helpreads its two option descriptions as sentences.--checkclosed on a semicolon, and--againstopened on a bare noun phrase. What either flag does has not moved.
0.3.0 - 2026-08-23¶
Added¶
--no-coloron every verb.NO_COLORin the environment asks for the same thing, and the tool already read it. Colour goes either way, and bold stays.
Changed¶
generatewrites a relative link naming the file, where it wrote a root-relative link naming the folder. A block inREADME.mdlinks[ADR](adrs.md), and one inknowledge-as-code/taxonomy.mdlinks[ADRs](../adrs.md). The link resolves wherever the corpus sits, rather than only where a renderer maps a folder to the page inside it. Runkac generateafter upgrading:--checkreports every block carrying the old form until you do.validateandcheckslist in aligned columns, with the severity coloured. Only the message column wraps, so a narrow terminal breaks a sentence and never a check id.checkssplits its count by severity.generatemarks a file it created, and counts what it wrote against the size of the whole plan.exportandbundledim the directory in each path they write, and colour a remark by whether it is advice or an account of the run. Neither changes a word it prints.- A failure is red on stderr. That covers every verb's hard stop, and the heading over a list of what stopped it. What the heading names stays plain beneath it.
--jsonand every exit code answer as before.--jsongoes straight to the stream and never carries colour, whatever the terminal.- Two messages lose a semicolon the house style does not keep. The
filename / slug-lengthrow in every generated checks table, and the meta-test reporting an over-long description. Runkac generateafter upgrading:--checkreports every type page carrying the old wording until you do.
0.2.1 - 2026-08-21¶
Changed¶
- The command line is parsed by
Spectre.Console.Clirather thanSystem.CommandLine. Every verb, option and exit code answers as it did.--helpreflows into Spectre's layout,-vjoins--version, and-?no longer stands for--help. The tool carries one library for reading a command line and asking a question, rather than two.
0.2.0 - 2026-08-20¶
Changed¶
kac indexis nowkac generate. The command writes each type's_index.mdand rewrites the generated blocks in every type page, and only the first of those is an index.--checkis unchanged, and so is everything either half writes. There is no alias: a pipeline or script still namingindexfails until it namesgenerate.
0.1.1 - 2026-08-20¶
Added¶
- An icon on the nuget.org package page.
- A link from the package page to the release notes for the version being installed.
The tool answers exactly as 0.1.0 does. Only what nuget.org shows about it changed.
0.1.0 - 2026-08-20¶
The first published version.
Added¶
kac validateholds a corpus to the schema it carries: frontmatter, identity, structure, clauses, links, the graph and the type setup.kac indexregenerates_index.mdand the generated blocks in each type page.--checkreports what is stale rather than writing it.kac checkslists every check the validator implements, read from the schema rather than from a list in the tool.kac exportwrites the corpus to.dist/export/as data a consumer reads instead of cloning.kac bundleassembles that export and.plugin/into an installable plugin.kac mechanismcompares the shared layers against a reference corpus, or takes them from one.