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¶
-
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 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.
Changed¶
-
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.
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.