Rendered from docs/spec/07-distribution-and-federation.md in the Headwater
corpus. Every document on this half of the site is typed by the taxonomy
the descriptor names: corpus.json.
7 — Distribution and federation
One organization defines a documentation method. Many repositories adopt it. It evolves. Everyone must be able to take the evolution without loss of what they customized. Also, the publisher must be able to tell who actually did.
What is shared, and what is not
| Layer | Shared | Owned locally |
|---|---|---|
| Engine | Yes — a versioned dependency | — |
| Taxonomy schema | Yes — a versioned package | Overlays |
| Doctrine (prose that explains the method) | Yes — vendored or linked | Local method notes |
| Corpus content | No | Everything |
| Router / entry point | No — it describes this repository | Yes |
| Control register | Partly — the publisher's obligations are inherited | Local controls and waivers |
This distinction makes the problem tractable: the taxonomy is a package, not a copy. Prior systems vendored checksummed file trees and gated on byte-identity. That works only while nobody needs to customize. Here, customization is expressed as an overlay against a versioned base. Thus an upgrade is a package bump and a re-resolve, not a merge conflict with a file that you were never supposed to edit.
Publishing
A publisher repository declares a taxonomy package:
package: acme/headwater-taxonomy
version: 3.2.0
requires_engine: ">=1.4 <2"
contents:
taxonomy: taxonomy.yml
conformance: conformance.yml # the rules an adopter is evaluated against, and the levels over them
bundles: bundles/ # named overlays that add, selected at init
assemblies: assemblies/ # recipes that publish selected bundles as flattened packages
migrations: migrations/
doctrine: doctrine/ # prose explaining the method, vendored to consumers
templates: templates/
profiles: [service-repo, docs-only, platform] # named overlays that remove
interview: interview.yml # the questions init asks, and the bundle each answer selects
Publishing is a release: a semantic version, a changelog, an integrity digest, and a migration payload for any major bump. Distribution is over the registry or repository that the organization already uses. The engine requires only that it can check the digest of a version that somebody fetched.
headwater taxonomy publish writes the artifact. It is a directory, because the engine carries no archive format and needs none: whatever moves a directory in the organization moves this one. Beside the manifest it writes a release record. The record names every file in the artifact with the digest of its bytes, and it carries one digest over that list. The record does not cover itself, so the digest is over what the artifact holds rather than over the file that states it. So the header of the record is outside the digest. headwater taxonomy vendor does not read the package identity from it. It takes the name, the version and the engine range from the manifest, which the digest does cover. It refuses a record whose header disagrees with the manifest.
Every contents path a publisher writes is read. taxonomy, bundles, assemblies, conformance, migrations, doctrine and templates each reach a verb. A key that no verb reads is a claim that a publisher makes and a consumer never sees. That is the defect requires_engine refuses from the other side, and contents.migrations was it until the payload had a reader. A value under contents names one path, and the kind of that path is the kind its reader opens. taxonomy and conformance each name a file, because each one is read as text. bundles, assemblies, migrations, doctrine and templates each name a directory, because each one is read as a listing. A publish refuses a value of the other kind, and it refuses an empty value before either check. An empty value names the package directory, which no key means. A list or a mapping there is refused, because no verb reads one and the rules that hold a path cannot hold it.
A publish refuses a manifest that names a member the artifact does not carry. An artifact holds files, so a directory with no file in it reaches no consumer, and the walk that carries the bytes carries none. The check reads the staged artifact before the publisher writes anything, and it reports every key rather than the first. A publisher of a package with no migration payload declares no contents.migrations key rather than an empty directory.
A publish refuses a document that references a path the artifact does not carry. A contents key is one kind of reference, and the body of a carried document is the other. The reader opens every member whose bytes are text and resolves each inline Markdown link against the directory of the file that wrote it. A reference that lands inside the artifact names a member, and a member no consumer receives is a dead path. A reference that climbs above the artifact root names the publisher's own tree, which no publish decides. A link inside a code span, a fenced block or an indented block is an example rather than a reference.
A manifest records the references its own prose leaves unresolved, under unresolved_references. The key maps a member the artifact carries to the targets that member names and the artifact lacks. A recorded pair is reported rather than refused, and every other unresolved reference is refused. The identity of an entry is the pair, so a target recorded under one member admits nothing under another. A pair whose member the artifact lacks is refused, because a record that outlives its member admits whatever is published there later. This key states artifact paths, unlike every other path a manifest writes, because it describes the artifact and ships inside it (#619).
A publisher writes a doctrine reference to a document the artifact does not carry as an absolute URL. The two rules above refuse such a reference or record it, and neither one says what the reference should become. A publisher writes doctrine for the tree it keeps, so a relative link there names a layout no consumer receives. Where the publisher's corpus is readable at a URL, the reference is an absolute URL to that corpus. Where it is not, the publisher carries the target in the artifact or drops the reference. unresolved_references then holds what a publisher ships unrepaired, which makes a dead link a decision rather than an accident. This engine opens no URL, so a publisher who moves the corpus breaks every such reference in silence. A rule that reads a doctrine URL is the condition to revisit this ruling (#633).
headwater taxonomy publish is the verb that reads a template. It refuses a template that disagrees with the taxonomy beside it. A template is the file an adopter copies to start a document. Spec 3 draws the boundary for what one holds. The reader opens every *.md under <contents.templates> and under <contents.bundles>/<name>/templates/. It holds each one against the base with every bundle the package ships. That selection is the most any artifact can hold. A bundle adds only, so a value some selection admits is a value the artifact admits. Publish is the only verb that sees a bundle no consumer selected. It is also the last point at which anybody can be stopped. The next reader is the adopter who copies the file.
Five checks, and the file name states the kind. Front matter alone cannot state it, because a homogeneous shelf carries no discriminator. So the file stem names the kind a template teaches. The stem names a kind the package declares, and that kind is not abstract. Every top-level key that names a facet with an enumerated values list carries a value in that list. A facet that some shelf reads as a discriminator carries the stem's own kind name. No facet that the stem's kind forbids is present. No top-level key names a relation the package declares. Q4 puts a relation under a relations: block and nowhere else. A key of that name beside the facets is an edge that no graph reads. The inverse half of a declared relation is a relation name too. A bundle names it under the relation it inverts rather than as a key of its own. A reader that walks the keys alone passes over it. Every failing template is reported rather than the first one. A refusal names the template's own path, the value, the permitted set and what a document carrying that value loses. The refusal blocks the release. A document that resolves to no kind reaches no other check: no rule reads it, and headwater check --strict exits 0 over it.
A placeholder is passed over, and required-facet presence is outside the five checks. A placeholder is a scalar whose trimmed text opens {{ and closes }}. The reader passes over one rather than typing it. Without that rule it would refuse status_since: "{{today}}" on every template in the library. That facet declares a date, and no template carries a date yet. So the reader performs no type checking at all. A template carries placeholders by construction, and typing one asks about a value nobody has written. Required-facet presence is excluded for the same reason read from the other side. A template is a skeleton that an author fills. An absent key is therefore not a defect, and a wrong value is. Five templates of this library carry no id at all, and each of the five is correct.
Front matter that no loader accepts is refused, and a file with no front matter is not a template. The first is refused because the five checks above all read front matter. A template that does not parse would otherwise evade every one of them and report a pass. A quoted placeholder parses, and nothing substitutes into a template. An author deletes the quotes along with the placeholder. The second case holds only where the stem names no kind the package declares. A Markdown file with no --- block is prose beside the templates rather than a document skeleton. Passing over it takes no check away. A stem that names a kind is the template's own claim to teach it. This reader cannot tell a genuine absence from a block a loader failed to see, and a leading blank line is exactly that case. So a kind-named stem with no readable block is refused too, on the same ground as the first case above. The difference is not one this reader can see, and neither may become a bypass.
doctrine is read too, by a different verb. contents.doctrine reaches headwater taxonomy vendor, which resolves the declared path against the fetched artifact and names where the prose lands (#81). Q11 and the first-contact evaluation draw the boundary for doctrine's license. That boundary is about the terms of the prose and not about who opens it.
A contents key states what the engine reads, and not what the artifact carries. Publication takes the package directory whole, and it takes the bundle tree as well where contents.bundles names one outside the package. So the artifact carries a file that no key names. Whole has one exception, and it is a fixtures/ directory at the root of a bundle. A bundle keeps the corpora its publisher measures that bundle against, and no consumer verb opens one. In this repository's own library they are 71 files and 227 KiB of prose that no consumer resolves (#518). So a publish reads them, holds them to the same rules as every other file it meets, and leaves them out of the artifact. The publisher keeps them where they are, because the exception names what leaves the artifact and never what leaves the disk. It is stated over one path and not over a name. So <contents.bundles>/<name>/fixtures/ is skipped, and a fixtures directory elsewhere in the package or deeper inside a bundle travels like any other file. A key that names a directory holding no file points at nothing the artifact carries. The two readings are separate. The block above is a list of what the engine reads, and not an inventory of the artifact.
Publication judges a contents path by where it resolves, and not by the string a manifest writes. A path that leaves the package and returns inside it is not refused. A symlink is judged by its target, so a name with no .. in it can still resolve outside the package. contents.bundles may still resolve outside the package, because publish copies the bundles into the artifact and rewrites that one scalar. The bound on it is the repository rather than the file system. Every other key is held to the package, symlink or not. A file the walk meets that no contents key names is held to the same rule. So an undeclared symlink outside the package is refused as well (#303).
Nothing is written until everything is read. publish holds every path a manifest declares to the tree before it reads one of them, and before it creates the output directory. A declared path that is not there is a refusal, under every key and not only the keys that publication carries. It names the manifest, the key and the declared value, rather than a file system error out of a copy. A file whose name is not UTF-8 is a refusal too, because the release record names every member as text. A write that fails after that returns the output directory to the state the run found it in.
The output path is one that the run can observe, or the publish does not start. It has to be an empty directory or nothing at all. A directory that holds files is refused, and so is a directory that this process cannot read. A link whose target is not there is refused too, because a read of it reports the same result as an absent path. An output path whose last name part ends in ~staging is refused as well, because a publish reserves that name. Every state the undo returns to is therefore a state the run observed, and nothing the undo removes belonged to anybody else. The undo removes the output directory and each directory that the run made above it. It removes the staging directory that this run made beside the output path, and nothing else.
A publish assembles the artifact beside the output path, and it moves the artifact there in one step. The staging directory is the output path with ~staging at the end of its last name part. A publish that stops part-way therefore leaves the output path in the state that it found, and the next publish needs no repair. A publish writes a marker file into the staging directory as its first act. It removes a directory at that path only when the directory carries that marker, so the removal takes nothing that somebody else put there. The removal happens before the publish writes, so a file an earlier run left in the staging directory does not reach the artifact. A directory at that path without the marker is refused, and so is a file at it, and the refusal names that path.
An output path that is a mount point cannot take a rename, so a publish writes into it directly. A publish that stops part-way there can leave files at the output path. It is the one output path where the paragraph above does not hold.
requires_engine is read, and a package outside the range is refused before a source is loaded. The range is a list of comparators over the engine version, written as ">=1.4 <2". A range that the engine cannot read is refused rather than ignored. An unreadable range that reads as no range is a claim that the publisher made and the consumer dropped. An engine that resolved a package built for a later one would produce a lock that nobody can reproduce.
A package states its version in two files, and the two are held to each other. The manifest carries a version key, and the meta-schema requires a version at the root of a taxonomy source. So one value stands in two places, and the artifact carries both, and the release digest covers both. The engine refuses a package whose two declarations disagree, and the refusal names both files and both numbers. It fires when the package loads, so it does not depend on which of the two numbers a consumer pinned. The manifest is what every consumer-facing reader takes the version from: the pin comparison, the lookup that headwater init makes, and the release record. Nothing held the two together until the numbers were measured apart. The disagreement reached the lock, where it stood in two blocks of one file.
A package states its name in two files as well, under two different keys, and the two are held to each other. The manifest carries a package key, and the meta-schema requires a taxonomy key at the root of a taxonomy source. That root may not carry a package key at all, because package is a reserved reference root there. So one name stands in two places under two spellings, and the artifact carries both, and the release digest covers both. The engine refuses a package whose two declarations name different things, and the refusal names both files, both keys and both names. It fires when the package loads, and before the comparison of the versions. A package that is not the one you asked for makes the question of its version moot. Every consumer-facing reader takes the name from the manifest: the lookup under .headwater/packages/, the release record, the vendor target directory and the corpus descriptor. The key at the root of the source reached the lock and the published artifact, and it decided nothing.
A package name is one or more segments separated by /, and each segment opens and closes with a letter or a digit. Between those, a segment holds letters, digits, ., - and _. headwater taxonomy vendor refuses a name outside that grammar, because the name gives the directory this verb creates under .headwater/packages/. A value such as .. or . names a directory that belongs to the adopter rather than to the package. No registry carries a publisher from a name outside the grammar to one inside it.
The name becomes the directory by one substitution, and that substitution is not injective. vendor replaces each / with a -, so acme/my-taxonomy and acme-my/taxonomy both name .headwater/packages/acme-my-taxonomy. Two publishers reach one directory, and neither of them has to be an adversary. headwater taxonomy vendor refuses the second name where the directory holds a different package, and the refusal names both packages and the directory. The lookup under .headwater/packages/ reads the package: of each manifest and never the name of the directory. So an adopter moves the directory aside, and the engine still finds the package that was there.
That rename works once, and the specification states the rest of its cost because the refusal does not. The adopter moves the first package aside, and the second package installs into the cleared path. After that, headwater taxonomy vendor refuses every later artifact of the first package, because each one derives the same directory. That package remains stuck at that directory, and nothing lifts this. The adopter also cannot move the second package aside to work around it. vendor now finds that the first package already resolves there, and it refuses the move. The refusal names the directory where that package resolves (#354).
A package imposes nothing on a derived taxonomy, and that is a constraint rather than a courtesy. An overlay is a patch, so a consumer's resolved taxonomy and lock contain the base content of the package. Terms on the package that a derived work inherits therefore reach an artifact that spec 0 promises is the adopter's own. So the terms of a taxonomy, bundles or profiles path may not condition what a consumer does with the resolved result. The doctrine path is prose that a consumer vendors, and it takes its own terms. Headwater checks none of this, and spec 6 already states that it checks nothing about a license. What the specification states is the requirement that a publisher must meet (Q11).
The migration payload
contents.migrations names a directory inside the package, and each file in it is one transition. A file states the versions it moves between as two ranges, which the one range reader of this engine reads. headwater taxonomy diff selects the file whose two ranges hold the version this repository takes and the version the artifact declares, and whose transition runs forward.
The two ranges do not order the move, and the third condition is not a courtesy. Nothing makes the two ranges disjoint, and a publisher may write from: ">=1 <3" beside to: ">=2 <3". One version can satisfy both ends. The two range readings alone then hold for a move from a version to itself, and for a move down. A payload states the steps for a move up. Selection therefore also requires that the version the artifact declares is above the version this repository takes.
migration:
format: 1
from: ">=1 <2"
to: ">=2 <3"
steps:
- subject: facet_value
facet: status
from: current
to: [settled, provisional]
task: Say whether the argument of this document is closed.
because: One live state held two states that a reader acts on differently.
No key states whether a step is mechanical, and the target list decides it. The apparent shape of a payload is a rename map with a list of judgment tasks beside it. The schema-format walkthrough found the case that breaks that shape: "one old value maps to a set, and the author chooses". Such a step is a rename in every respect but the one that decides whether a program may apply it. So the split of spec 2 runs through a step, and never between two lists.
One target is mechanical, and the engine applies it. Two or more are a closed choice, and the author picks one value out of the set that the publisher closed. No target is a re-statement, and nothing replaces the old value. A step that leaves a choice or that asks for a re-statement carries a task, which is the question the author answers. A task beside one target is refused, because a mechanical step asks nobody anything. A key that this engine reads and drops is the defect that a payload exists to stop.
An absent to and to: [] are two different statements. A reader that made them one would turn a forgotten target into a re-statement task. An absent to is a publisher that did not say, and it is refused. to: [] is a publisher that says that nothing replaces the value.
A step declares no dimension, and its subject fixes one. A remedies key would be a claim that a publisher writes and that nothing measures. The engine holds the mapping instead, and no payload may vary it.
| subject | what it moves | the dimension it is a remedy for |
|---|---|---|
facet_value |
one value of one facet, and every document that carries it | instance_validity, and consequence with it |
kind |
one kind, and every document that the census typed as it | classification |
overlay_address |
one path into the taxonomy, and every entry of the adopter's overlay addressed at or under it | addressability |
consequence stands in the first row because spec 2 rules that one dimension holds the other: "a broken instance_validity breaks consequence too".
Two of the six dimensions have no subject, and each absence is a statement. No rename is a remedy for projection, because a projection that moved is written again by headwater generate rather than edited. No rename is a remedy for identifier. Spec 3 makes an identifier a stable name that survives a move and a rename. A payload that renamed one would move the thing the identifier holds still.
A facet that a kind starts to require has no subject either, and this absence sits inside a dimension that has one. A facet_value step moves an old value to a new one. A facet that no document carried has no old value, so no step of this vocabulary reaches the break. The engine still measures it. instance_validity reports every document that stopped validating, and headwater taxonomy diff names them. What an adopter does with that list is what first contact already defines. headwater infer --owner <name> --write records the pair set as adoption debt, and headwater check then reports each pair as migration-pending. An author supplies the value under an open task, which is the same shape as a re-statement step. The vocabulary holds no fourth subject for this class, and that is a ruling rather than an omission. Such a step names no old value, so it carries no target, and the engine reads a step with no target as a re-statement. headwater taxonomy migrate --apply writes only a mechanical step, so a fourth subject buys one sentence a publisher types and no run applies. Two measurements reopen the ruling. The first is a migration payload of any subject, written against a real upgrade, because no release of headwater/standard has shipped one. The second is another class of break that reaches no subject, because one such class is an absence and two are a gap (#543).
The third subject names a path and not a value. The from of an overlay_address step is an address, and so is every entry of its to. Both are read by the one path production this engine has (spec 2). A step covers each operation of the adopter's overlay whose own address it is a prefix of. The prefix is over segments and never over text, so a step over kinds.play covers kinds.play.purpose and misses kinds.playbook. headwater taxonomy migrate --apply rewrites the address of each entry it covers, and no byte beside it.
The overlay it writes is the adopter's own, and never a bundle. A bundle is package content, and headwater taxonomy vendor replaces a vendored package directory whole. An address rewritten inside a bundle is lost at the next upgrade, and the publisher of the bundle is who moves it. A bundle whose address stopped reaching a declaration reaches the consumer through the addressability dimension instead. An add over a path the new base no longer declares does not fail to address anything, and spec 2 states what it does instead.
The two ends of the wire check different halves, and neither one can check both. headwater taxonomy publish reads every payload before it writes a file. It refuses a step whose source the taxonomy under publication still declares, because such a step renames something that did not move. It refuses a step whose target no taxonomy under publication declares, and a payload whose ranges do not hold the version under publication. headwater taxonomy diff reads the payload out of the artifact and reports what it means for this corpus. A step that names a value which this repository does not hold is reported and never refused. The old taxonomy a consumer holds is the base under its own overlays, and an overlay may have removed that value.
A publisher holds the two halves to two taxonomies, because a consumer selects. A package with bundles ships one taxonomy for each selection. The narrowest is the base alone, and a consumer who selects no bundle resolves it. The widest is the base with every bundle, and spec 2 makes it a taxonomy that resolves. A bundle is add-only, so any subset of bundles commutes. The publisher holds the target to the widest set, and refuses a step whose target no selection can hold. The publisher holds the source to the narrowest set, because a step whose source every selection still declares renames something that moved for nobody. A source that only a bundle declares moved for the consumers of that bundle and for no other, and the payload states no condition. headwater taxonomy publish therefore neither refuses that step nor certifies it, and headwater taxonomy diff reports it against the taxonomy the consumer holds. Two taxonomies answer to that name, and the consumer end reads both. The first is the taxonomy the lock records, which every source of the payload was written against. The second is the candidate under this consumer's own selection. A step whose source the second one still declares is one this consumer must not apply. headwater taxonomy diff names such a step, and headwater taxonomy migrate --apply refuses to write it.
What the report states is measured, and never asserted. The documents that a step names come from the census of the taxonomy this repository takes. The documents that moved come from the two dimensions that a subject names. classification answers with each document that stopped resolving to the kind it had. instance_validity answers with each document that stopped validating. The denominator is the union of the two. Each set comes out of the comparison that decided its own dimension, rather than out of a second pass over it. So the report says how many of the documents that moved lie under a step, and it names every document that lies under none. A payload that names all of them therefore answers to a corpus that its publisher never saw.
Consuming
A consumer declares what it takes and how it differs:
taxonomy:
package: acme/headwater-taxonomy
version: 3.2.0
digest: sha256:6c2f…
profile: service-repo
overlay: .headwater/overlay.yml
Two packages answer this declaration, and the bundles key is what separates them. A composer takes headwater/standard, names the bundles it wants, and owns the selection from then on. A batteries-included adopter takes headwater/starter, which is the flattened form of the starter assembly, and names no bundles at all. A flattened package ships no bundle directory, so a selection against one is refused rather than ignored. package::selected reports this selects N bundles and <package> ships none, and a case in engine/crates/resolve/tests/publish.rs holds that refusal. The two forms differ in who owns the upgrade and in nothing else the engine reads.
Consuming is two steps, and the split is what keeps the network out of the checking loop. headwater taxonomy vendor <location> fetches the published artifact zip and unpacks it. A caller who fetched the artifact by other means passes the directory instead. The verb then checks the artifact against the digest above and installs it. The fetch lives in headwater-fetch, a crate that only the binary links. So no crate that evaluates a check can call it, and inside the binary only the vendor arm does (HW-DR-0075). headwater taxonomy resolve merges the overlay, validates, and writes the lock. The lock is committed. Thus the corpus is checked against a resolved, reviewable, reproducible taxonomy, and CI needs no network to check anything.
What a package digest proves, and what it does not
It proves that the artifact is the one the consumer pinned. vendor recomputes the digest from the bytes on disk and refuses an artifact that does not match. The message names three kinds of divergence. A file that moved, a file the record names that is absent, and a file present that the record names no member for. The third is the one that a comparison over the record alone would miss. A record cannot report a file that it never named. vendor makes this comparison one time, when it installs the artifact. headwater check and headwater taxonomy validate make the same comparison again on every run. So a later edit to the pin or to a vendored file fails the commit gate.
A version number does not name an artifact, and only the digest does. Nothing refuses a second publication of different bytes under a version already published. publish could not refuse one. It writes into an output directory that must be empty, so it never sees a prior record of any version. headwater taxonomy vendor is the one run that holds two published records of one package at once. The installed release.yml and the artifact's own record are both on disk there. Where the two state one version and different digests, the run names both digests and both member counts, installs the artifact, and exits 0. The report reads the version and the digest of the installed record out of its header. The digest covers the member files rather than that header. So an adopter who edits the installed release.yml by hand makes this report silent. It reports and it gates nothing, and the paragraph below states the same seam for the artifact that arrives. What reopens this: a ruling that every source change bumps the version, or an adopter who reports two different artifacts of one version. #784 carries the measurement that chose the report over the refusal.
It does not prove that the publisher wrote the header of the record. The digest covers the member files and not the record that lists them. So vendor takes the package identity from the manifest, and it refuses a header that disagrees. taxonomy diff and taxonomy migrate read the header instead, and they read it against no pin. What those two report about a package rests on the artifact and not on a digest.
The engine never takes the pin from the artifact. A digest that the engine recorded from whatever it had just received would be a pin against itself. So vendor refuses to run when no pin exists, and it names the field to write. The publisher states the digest where a consumer reads it, and the artifact and the digest travel apart.
A digest the caller supplied is the pin, when vendor has matched the artifact to it and no pin is declared. The value came from the publisher by the caller's hand, and not from the artifact, so it is not a pin against itself. taxonomy vendor --expect <digest> first holds the bytes to that value. Only when they match, and .headwater/taxonomy.yml declares no taxonomy.digest, does it write the value there. It edits one line and keeps every other byte. A declared pin is never replaced: a different --expect installs the artifact it names and prints both values, and pin.current then reports the stale pin. A refused or failed run leaves the declaration byte-identical. taxonomy import --expect <digest> still discards the value. The owner ruled this in #1063, because a guide that names one command left a declaration with no pin, and the next clone refused (#641). The write moves no trust. The first fetch still rests on the channel that carried the digest, as HW-OBL-0115 records. What reopens this: a consumer who reports a pin that --expect wrote and that they did not intend to commit.
It does not prove who published the artifact. Nothing here is a signature, so the first fetch rests on the channel that carried the digest. A signature needs a key, a route that distributes the key to an adopter who has never met the publisher, and a rule for revocation. None of the three is decided (Q22), and HW-OBL-0115 holds the question rather than a manifest key that would read as an answer.
A vendored package is not a maintained one. vendor replaces a directory that carries a release record, and it refuses a directory that carries none. A package that a person maintains is a publisher's source, and a consumer command that overwrote one would delete the thing being published. Spec 2 requires customization by overlay and never by fork, so a vendored directory has nothing in it that an adopter should have edited.
What rechecks the installed bytes after the vendor is check, validate and conformance, and not resolve. vendor compares the artifact once, at the moment it installs. headwater check reports a digest that is not the pin as the error taxonomy.pin.diverged, and headwater taxonomy validate refuses it. The reading pin.current recomputes the digest of every installed member on every run, holds it to the digest the consumer pinned, and installs nothing. A stale pin is a gap in that reading, so a plain run reports it and --level L0 makes the exit non-zero. Installed bytes that differ from their release record end the run at every level, because that package carries a rule set nothing can trust. taxonomy resolve reads the version the manifest declares and no digest, so a build that runs it alone finds neither.
The prose a publisher ships lands with the package, at .headwater/packages/<name>/<doctrine>. vendor copies the whole artifact, so the directory arrives whether or not a key names it. contents.doctrine is what turns that arrival into a claim the engine holds. vendor resolves the declared path against the fetched artifact before it installs anything, and it writes nothing to do it. So vendor refuses an artifact that names prose it does not carry, and .headwater/packages/ stays exactly as the run found it. The report then names the installed path, and it states that the directory is prose for a person rather than schema.
That path is the only destination, and doctrine reaches nothing else in an adopter's tree. #381 asked whether a publisher may send the prose somewhere a reader is more likely to look. The adopter's corpus root is one such place, and a documentation directory the adopter names is another. The answer is no. vendor writes .headwater/packages/<name>, .headwater/packages/~staging/<name> and .headwater/packages/<name>~aside, and it writes no other path, which the contract for headwater taxonomy states row by row. Publishing above rules what a doctrine reference becomes, and this ruling is why it must. A relative link inside a doctrine page resolves against .headwater/packages/, and never against the adopter's own corpus. Three reasons hold the ruling, and they are written here so that a later reading does not find them again.
The census. Doctrine is instruction to a person rather than corpus content, and no kind in the base package or in any bundle binds it (HW-DR-0056). So a copy into a corpus root is one untyped file for each file copied. The measurement is this repository. headwater check --root . reports docs/doctrine/maturity-model.md under untyped: no shelf pattern claims this path, and the census counts two untyped files in the whole corpus. One of the two is this repository's own doctrine page. A vendored doctrine directory adds one more such file for every page it carries. The count above is a ratio to the whole corpus rather than a total, because a total goes stale on the next merge.
The upgrade, and the kind. The paragraph below states that a replacement is whole, and a copy outside .headwater/packages/ has no such story. Nothing removes it and nothing replaces it, so the next vendor leaves a stale copy of last version's prose in place. A second destination also asks what the copied file is, and nothing answers, because no shelf claims the path and no kind types the document. The paragraph above on a maintained package rules the last of it. A vendored directory holds nothing an adopter should have edited, and a destination the adopter names is a directory the adopter edits.
The price this ruling accepts. Prose that lands under .headwater/packages/ is prose that no rule reads. Spec 13 measures that gap for a package source rather than for a doctrine directory, and the two share one cause. A doctrine page in an adopter's tree therefore answers to no language regime and to no check. The ruling takes that price. The alternative is the untyped file the census paragraph measures. One unread page costs an adopter less than a corpus that types one file fewer.
What reopens this: a kind for doctrine. A kind gives a copied page a shelf, a language regime and a check. It removes the census cost, and it answers the question of what the copied file is. An adopter who reports that nobody finds the prose under .headwater/packages/ is the evidence that would ask for one.
The replacement never leaves part of a package behind. vendor writes the new package beside the old one, and then puts the new one in place. So .headwater/packages/<name> holds a complete package or nothing at every moment, under a failure and under a run that a person stops. A vendor that fails leaves the package that was installed, and the refusal says which state the directory is in.
A run that a person stops can leave one of two directories behind. .headwater/packages/<name>~aside holds the package that was installed, and the engine still finds it there. .headwater/packages/~staging/<name> holds the copy the run was making. The engine never finds it there. find reads one level of .headwater/packages/ and skips a directory with no manifest beside it, and .headwater/packages/~staging itself carries none (#357). The next vendor of the package removes both. vendor refuses to install either of them as an artifact, so an adopter who finds one removes it.
A third directory a tree can carry is the root itself, at the place engines before 0.2.0 read. HW-DR-0067 moved the root from packages/ to .headwater/packages/, so a tree vendored before that carries a complete package at a path nothing reads. The lookup computes a sentence from the tree in front of it: where a directory stands at packages/, the two refusals the lookup can reach name that directory and say to move what is under it, and where none stands neither refusal mentions one. Without that sentence the adopter is told only that no package declares the name, while the package they installed sits unread beside the answer.
Profiles are publisher overlays
Not every repository holds every shelf. A profile is a named overlay that the publisher ships. It contains remove operations for the shelves that a repository archetype does not have, and it is selected by name in the consumer declaration above. It is not a separate mechanism. The overlay resolver already implements every part of it (dependent-key deletion, confluence, core satisfaction on the result).
An earlier draft listed profiles as their own declaration, and thus kept two names for a subset of one mechanism. The effect is unchanged. A repository never has a rule, glob, or projection that targets a shelf that is not present. Dead configuration is noise that teaches readers to ignore configuration.
Bundles are publisher overlays in the other direction
A bundle is a named overlay that the publisher ships, which holds add operations for optional content. A profile removes what an archetype does not have. A bundle adds what an adopter needs. One mechanism, two conventional directions, and neither one is new.
bundles:
procedure: {requires: []}
standards: {requires: []}
evidence: {requires: []}
proposals: {requires: []}
operations: {requires: [procedure]}
compliance: {requires: [standards, evidence]}
Two rules keep this cheap, and both run on machinery that exists.
A bundle holds no override and no remove. If a bundle needs to change the base, the base declared something that it should not have. Add-only overlays over disjoint addresses commute, so the resolver's static confluence check proves that every subset of bundles resolves. One exception is ruled: a bundle may add into the keys of a bundle that it names in requires (Q67). A dependent add_to into a list of the other bundle does not commute with it, and the dependency orders the pair. A dependent add of a new key commutes as it is. So confluence holds over every order that respects the declared dependencies. The ruled form also makes one subset fail. A selection can hold a dependent bundle and lack a bundle that it names in requires. requires does not add a bundle to a selection (Q40). Without a check, a dependent add_to then finds no list. A dependent add under a missing mapping creates a stub node that no bundle declares. The claim therefore holds for every subset that is closed under requires, and for no other subset that holds a dependent write. The resolver refuses that selection before the merge, for both operations. The refusal names the dependent bundle, the bundle missing from the selection, and the address of the write. The resolver also refuses a cycle in requires among the selected bundles, and it names every bundle in the cycle. headwater taxonomy publish runs that check once per release. It resolves the base with every bundle the package ships, on every release. A migration payload is not a condition of that, and a set that does not resolve is refused (#387).
Every subset of the shipped bundles that is closed under requires resolves, and an adopter can still select a combination that fails. Referential integrity is a separate rule, about the names a declaration reads rather than about the merge. headwater taxonomy publish runs that rule as well, over the selection the artifact it writes carries. A plain publish reads the widest set, and publish --assembly reads the one selection the recipe names. The second reading is total, because a flattened artifact declares no bundles and its consumer has no selection left to get wrong. The adopter's own selection is the one reading no publisher can take, because the adopter has not chosen yet.
A bundle states its intended closure in requires, and Q40 rules that key a label today. The resolver loads only the bundle names in the consumer declaration. Referential integrity refuses a selection that omits content another bundle needs. The label explains the refusal but does not expand the selection. The refusal names every address that reads a missing name. The resolver and the publish both name the bundle that would supply them, by resolving each unselected bundle rather than by reading requires:. Q67 gives the key one reading as a mechanism: a bundle may add into the keys of a bundle that it names there. The resolver reads the key for that purpose and for no other. It applies the bundles in the order of the bundles: list, with one exception. A bundle waits until every bundle that its requires names has applied. Bundles later in the list that are ready apply ahead of it.
This is what fixes the size of the base package. A large base forces bundles and profiles to remove, and remove carries dependent-key deletion and the most failure modes of the three operations. A minimal base lets every bundle stay add-only. The first-run walkthrough derives the base from the core on those terms, and it measures what each of five adopters authors and deletes.
An assembly has two consumption forms
An assembly is a named publisher recipe over one package version. It declares a complete bundle selection and may declare one assembly overlay. The overlay holds connections whose meaning spans two or more selected bundles. No selected bundle owns those connections, and the assembly changes none of its bundle sources. A recipe with no such connection to make declares no overlay key at all. An empty overlay is not glue, and the resolver refuses one.
The recipe below is the one this repository ships, at taxonomy-source/headwater-standard/assemblies/starter/assembly.yml. It declares no overlay.
assembly: starter
package: headwater/starter
version: 0.1.0
from:
package: headwater/standard@4.15.0
bundles: [design-spec, evidence-and-obligation, decision-record]
headwater/standard@4.15.0
|-- base taxonomy
|-- bundle: design-spec ---------------\
|-- bundle: evidence-and-obligation ----+--> assembly: starter
`-- bundle: decision-record -----------/ |-- optional overlay: none declared here
|
|-- composer: pins standard and selects bundles
|
`-- publisher: resolves recipe
-> headwater/starter (flattened)
-> batteries-included consumer
An arrow into the assembly identifies a recipe input, not a package dependency. The composer chooses inputs directly. The publisher resolves the assembly, and the flattened package contains that result.
A composer consumes the recipe inputs. The consumer pins the source package and lists the bundles it wants. The consumer may take the assembly overlay or write a local overlay instead. This form preserves control over the selected parts and their upgrades.
A batteries-included consumer takes a flattened package. The publisher resolves the recipe and emits a complete taxonomy source under the assembly package identity. The package declares no bundle directory, and its consumer selects no bundles. The publisher copies the selected doctrine and templates into namespaced paths in the artifact.
A flattened manifest drops three keys. bundles and assemblies are absorbed: the publisher resolves the selected bundles into the generated taxonomy, and it records the recipe under distribution.derived_from. migrations is discarded, and nothing in the artifact represents it. headwater taxonomy diff selects a payload by the ranges that payload declares, so one written for the source version line has no reader here. The publisher names a discarded key on standard error and says nothing about an absorbed one. Every other member keeps the relative path the source names, and the conformance rule set is one of them.
A flattened package therefore carries no migration payload, including one for its own version line. Nothing writes contents.migrations into a generated manifest, and a recipe declares no payload of its own.
The flattened package is generated output and never an independently authored taxonomy. Its manifest declares distribution.form: flattened and records the recipe under distribution.derived_from. The record names the source package version, the selected bundles, the assembly overlay, and their digests. These fields state provenance and create no runtime dependency.
The publisher compares the flattened source with a fresh resolution of the recipe. The comparison excludes the package name, package version, and derivation record. Every taxonomy declaration must otherwise have the same canonical text. Publication refuses a difference.
The two forms place upgrades on different owners. A composer can change its selection or advance the source package directly. A flattened consumer waits for a new release from the assembly publisher. Both forms use the same migration and compatibility measurements after publication.
The starter kit is an assembly
Spec 0 promises a doctrine starter kit, and spec 2 refers to a base package. These are two artifacts, and an earlier reading of Q3 treated them as one. They answer opposite requirements. The base has to be minimal so that bundles stay add-only. The starter kit has to be opinionated so that a new adopter does not face a blank schema.
headwater/starter is the first assembly. Its recipe is a bundle selection and the doctrine that explains the combination. It may add an assembly overlay where two selected bundles need a connection between them. A composer can take the recipe inputs. A batteries-included consumer can take the flattened headwater/starter package. Nobody is expected to run the base bare.
The selection is design-spec, evidence-and-obligation and decision-record. It is the selection this repository runs on its own corpus, and HW-OBL-0018 records that no measurement of adopters stands behind it yet. The doctrine page that ships beside the recipe says the same thing to the adopter who reads it.
The interview
headwater init composes a bundle selection from answers. It is the first-run surface, and the blank-schema problem is a first-run problem. Five rules govern it, and the walkthrough derives each one.
- It emits an overlay, never a resolved taxonomy. A tool that writes a complete taxonomy file forks the adopter from the base before they write a document. Every later upgrade is then a merge. Spec 2 requires customization by overlay and never by fork, and this is the one place where a breach of that rule stays invisible.
- It is package data, not engine code. Principle 1 puts anything an adopter might want different into the schema. An interview compiled into the engine cannot ship with a third-party package, and a publisher with its own bundles needs its own questions. The interview sits beside profiles and templates in the package. It is not a taxonomy declaration, because it describes the package rather than the corpus, so the count of declarations stays at thirteen.
- It is
headwater inferwith a second evidence source. Q12 makesinferpropose a taxonomy from a tree that already exists. Both emit the same artifact, so they are one command with two inputs. On an empty repository the tree contributes nothing and the interview asks everything. The blank-schema case is thus the degenerate one rather than a special one. - It emits three artifacts, and one read of the tree produces all three. The overlay above. A report of what the tree holds that no proposed shelf or kind explains. And the adoption payload, which is the set of findings that the proposal expects to fail.
infercannot weaken the base to fit the corpus, and that is structural rather than a rule to police. A bundle selection is add-only, and an add-only overlay carries no operation that removes a base rule. - Every question is about the corpus, and none is about the taxonomy. "Do you write runbooks?" needs no model in the reader's head. "Do you want a
procedurepurpose?" needs the whole of spec 2 first. Each answer selects a bundle or supplies a value that the corpus alone holds, and no answer exposes a declaration name. - A question that an existing ruling answers is deleted, and a question that no ruling can answer is asked. Spec 3 rules that an identifier always carries a namespace, so the interview never asks whether the adopter wants identifiers. It asks what the namespace is. No package can supply that value. A package that named one would give it to every corpus that adopts it, and a stand-in such as
reponames nobody at all. The interview is the first moment at which the owner of the corpus is present to answer. It is the one answer that selects no bundle, and it lands in the overlay asidentifier_schemes.<scheme>.namespaceon every scheme the selection reaches.
The interview asks only what changes the selection, and the one value that no package can hold. Everything else waits for a corpus that taxonomy audit can measure, because a day-one guess about facet orthogonality is worse than a day-thirty measurement of it.
The cost is the one that overlays already carry, one level up. A resolved taxonomy is an artifact that nobody authored directly, and an interview adds a step where nobody authored the answers as configuration either. So the generated overlay carries a comment above each block that names the question and the answer which produced it. Re-running init re-asks with the current answers as defaults and rewrites the same blocks. An adopter who changes their mind edits an answer, not a taxonomy.
The invariant core
A package declares a core: the semantics that an overlay may extend but never remove or redefine (spec 2). Without one, "the same taxonomy" is not a meaningful claim. If a consumer may override anything, two consumers of one package can share no structure at all.
The core is semantic, not lexical. It constrains roles and purposes, never names or paths. A consumer may rename every shelf, relocate every directory, and replace every identifier pattern and every lifecycle value, and still satisfy the core. The condition: after resolution, some facet still has the state role, some kind still serves the rationale purpose, and lineage remains expressible and lifecycle-sensitive.
The identifier namespace is lexical and the core does not carry it. identifier integrity requires one on every resolved scheme (spec 2), and the core has no form that could ask for the same thing. The value is the consumer's own, because a package that named one would give it to every corpus that adopts it. The rule is lexical because the alternative is unrecoverable rather than merely untidy.
That is what makes the package a workable boundary object. It is plastic enough to adapt to local practice, and strong enough to keep a common identity across sites. Local form is fully negotiable. Shared meaning is not.
Satisfaction is evaluated on the resolved taxonomy. The engine does not forbid particular overlay operations. The engine rejects an overlay when the result fails a core requirement. The rejection names that requirement and the operation that removed its last satisfier.
Conformance checks the core, not the whole taxonomy. A consumer that renamed and rearranged everything, but kept the core, is conformant, and the report should say so. This is the difference between a method and a monoculture.
Upgrading
headwater taxonomy diff <artifact> --to 4.0.0
reports, against the local corpus rather than in the abstract:
- what changed in the base.
- measured compatibility across the engine's six dimensions — classification, instance validity, consequence, projection, identifier, addressability (spec 2).
- which overlay entries the change invalidates (an override that addresses a removed path is an error, not a silent no-op). This is the
addressabilitydimension, reported here at the grain that a consumer can act on. - whether the new base still satisfies the core under the local overlay.
- which local documents violate the new schema.
- which migration steps apply, split into mechanical and judgment-bearing. A step names documents or overlay entries, and the report names every document that no step names.
The verb takes a directory, and --to states which version that directory is expected to be. This verb opens no socket, so the artifact is a directory that the caller already fetched. Only taxonomy vendor takes a location. The flag is therefore the assertion rather than the address. A directory that declares another version is a wrong directory rather than a wrong number. The flag accepts a version or a range of them, through the one range reader the engine has.
The candidate resolves under the local overlays, and every phase then runs twice over one tree. That is what makes a difference attributable to the schema. Two publishes of one package differ in a version string, in a digest and in a file timestamp. A report that read any of those would fire on every release, and would then carry no information at all. No dimension reads a published byte.
Two values are held constant across the two runs, because neither is a consequence of a taxonomy. The first is the identity of the run. The corpus descriptor states the package, the version and the taxonomy digest that wrote it, and a probe result states the digest. A comparison that let those move would report the version number as a change that the version number caused. The second is the path the taxonomy was read from, which reaches a finding about the taxonomy. That path is the artifact directory on one side and the lock on the other. Everything a taxonomy decides about a projection still moves: the exclusions, the entry points, the exports and every declared output.
A candidate that does not resolve reports five dimensions as unmeasured, and never as preserved. A refusal that names an overlay address is the addressability reading. There is no census, no run, no plan and no graph under a taxonomy that did not resolve. To report the other five as compatible would be the strongest available claim made out of a failure. The run then exits non-zero, because it could not measure rather than because it measured a break. A measured break exits zero, because an upgrade that needs a migration is the ordinary case (below).
The publisher measures compatibility against its own reference corpora and reference overlays, and attaches the result to the release as a claim. The consumer's run verifies that claim against documents that the publisher never saw. A claim that holds upstream but fails locally is the interesting case, not an anomaly. It means that the local corpus exercises something that the reference corpora do not.
headwater taxonomy migrate <artifact> --apply applies the mechanical steps. A step is mechanical when it names one new value. That is a property of the target list rather than a claim the publisher makes (the payload). The verb rewrites the value in the front matter of every document the step covers. It emits the rest as a task list with the affected documents attached, ready for a human or a coding agent. The distinction is the whole point. To move files is mechanical. To rewrite a document to fit the section contract of a new kind is not. To pretend that the second is automatable produces plausible, wrong documents at scale.
A run writes every file or none of them. Each rewrite is composed in memory first, and the result is read again and compared against what it patched. For a document the comparison covers the body byte for byte, and every scalar of the front matter under the key path that carries it. For an overlay it covers the operation set that the resolver reads back. Every operation keeps its position, its kind and its value, and every address this run did not move stays. Then the run opens every target before it writes a byte. A file that no process may write stops the run, and the tree is what it was. It reads each file back off the tree after the write, and it restores what it wrote when a write fails.
The documents and the overlay are one write set. An overlay re-addressed beside a document that still holds the old value is a corpus in neither state. So an overlay that no process may write leaves every document of the run untouched, and the refusal names the file. A crash between two writes is not covered, and nothing inside one program covers that without a journal.
The lock joins the same write set for two of the fields the next section names, and stays open for the rest. adoption.from and adoption.to are written by this run, on the terms HW-DR-0046 states. The owner, the expiry and the task list are not. The next section gives the reason that half stays open. Beside the three, taxonomy diff prints each add collision with the new base as a judgment task that shows both declarations (spec 2).
A kind that a homogeneous shelf carries has no byte to rewrite. Placement carries the kind there, so the document declares it nowhere and the remedy is to move the file. The run reports the shelf that carries the kind, rather than nothing about the document. A step that reached no writable byte and printed no line is a step a reader reads as applied.
Between majors, the corpus is legitimately between valid states
A migration with judgment-bearing tasks creates a period in which the corpus fully satisfies neither the old schema nor the new one. That is an ordinary major upgrade, not an anomaly. The upgrade is atomic for the taxonomy. The lock points at 4.0.0 or it does not, and the no-partial-load rule of spec 2 governs the schema alone. The upgrade is not atomic for the corpus. A spec written as if it were would make every real upgrade a lie.
Thus the migration state is recorded in the lock: from-version, to-version, an owner, an expiry, and the open task list. The from-version is the one field that names a prior valid state, and it is optional for the reason that the next section gives. While tasks remain open, checks run against the new schema. A finding is reported as migration-pending when its (document, rule) pair is one that the migration payload expects to fail. The payload declared what moved and what must be re-stated. Thus it knows which rules it broke for which documents, and each open task records that pair set.
A label by document alone would blanket every finding on a named document for the whole migration. Defects introduced yesterday would then read as expected breakage. The pair grain keeps yesterday's regression loud while the declared debt stays patient. migration-pending findings are counted, visible in coverage, never blocking, and never suppressed individually.
taxonomy migrate --apply writes from and to, and the rest of the seam stays where HW-OBL-0082 left it. That record posed three questions about the adoption block. HW-DR-0046 rules the one about this field. from is a semver and the release digest .headwater/taxonomy.yml still pins at the moment this run reads it. The two are kept as plain fields and never as one hash of the two. migrate writes both through the same all-or-nothing set as the document and overlay rewrites, and only where a digest is pinned. A corpus that takes its package from source has nothing here to verify, and the run says so and writes every other file regardless. The owner, the expiry and the task list stay untouched. headwater infer --owner <name> --write is still their one writer. HW-OBL-0082's other two questions are still open. Whether a payload surviving a rewrite is a rule this spec should state is one. Whether resolve --check may pass while the authored half is stale is the other.
taxonomy resolve --check passes while the authored half is stale, and that is a measurement rather than a ruling. The lock of this repository is the instance: its one task holds no finding, and the check exits 0 over it. resolve reads the package sources and never the corpus, so no run of it sees whether a pair still raises a finding. headwater check is the run that sees it, and it reports the open pairs, the closed pairs and the findings held. resolve --check reports which half moved: a source whose bytes changed, or an adoption block outside the form the renderer writes. The header of that file invites a person into the block, so the second case is a form and not a stale taxonomy.
When the last task closes, the state ends. The expiry is the anti-parking device, on the same terms as the expiry of a waiver. A migration state past its expiry is a finding against the owner. adoption.task.expired is the rule, and it names the task and the owner rather than a document. It is renewable only by an explicit move of the date — a decision with a paper trail, not a timeout that nobody notices. Waivers are per-rule, and suppressions are per-file. Neither fits a corpus that is half-way across, and that is why the state is its own mechanism, not a pile of either.
First contact: adoption is a migration from no taxonomy
An organization that adopts Headwater points it at a corpus that nobody wrote to any schema. That corpus was never valid, so it appears to fall outside the state above, which names a version that it came from. It does not. Only the from-version refers to a prior state. The (document, rule) grain, the owner, the expiry, the task list, and the counted-visible-never-blocking posture are all defined against the new schema (Q12).
So the from-version is absent, and nothing else changes. Before headwater init a corpus is governed by nothing, and every document in it is trivially valid. The findings that the proposed taxonomy raises over the existing tree are therefore the migration payload of that taxonomy's first version. A publisher computes a payload from the diff between two majors. On first contact, infer computes it from the diff between nothing and one. That payload is the adoption payload, and it is a migration state like any other.
The adopter thus gets a green build on the first run. Every document that does not yet fit carries a name, an expiry, and a line in the coverage report. That is what a grandfathering file gives, plus the three properties that such files omit.
No threshold ever converts an accounting into a silence. One shipped tool excludes offending files one at a time, then disables the rule once the list passes a limit. That is correct for a mature corpus and wrong here. First contact is the one moment at which every rule exceeds any such limit (HW-EVAL-adjacent-work §S.5). A payload therefore holds (document, rule) pairs however many there are, and spec 4's coverage obligations stay total. The cost is a large payload in the lock on a large corpus, and the lock is committed and reviewed.
Every run reports the count that remains. The expiry is a date, and a date arrives too late to tell anybody that a payload is not shrinking. So the run reports the number of open pairs beside coverage. A payload that does not move is then visible from the second run rather than from the expiry.
Adoption never runs on a flag. No invocation of the engine decides which findings count, and spec 6 declares no flag that scopes a run. A mode that gated on newly touched documents would make two runs over one tree disagree. What scopes the work instead is the cache, which derives what moved from content hashes and reaches the same verdict either way.
What the adoption store records, and what it refuses to
The store is .headwater/adoption.jsonl. A run of headwater taxonomy audit --record appends one line to it, and no line is ever rewritten. A run without the flag writes nothing at all. That verb gates nothing and exits 0, so a run of it inside a gate leaves the tree as it found it.
Why a store, and not a second reading of an older tree. An adoption reading is recoverable. The lock and the corpus are both committed, and headwater check --now <date> is deterministic over them. So the argument for the capture-cost store does not carry here, because that store exists for a value that no later reader can recover. The argument that carries is spec 12's: a change reaches this engine as a named set of inputs, and never as a second tree. No crate of this engine walks git history, so the tree of a past day is an input that no run holds. The only way this engine holds a series is one reading per invocation, from a caller who decided to take one.
What a reading holds. One line per invocation, and never one per task. It states the taxonomy digest and the date of the injected clock. Beside them go the count of tasks that the engine could not read, and one entry per task. A task entry states the identifier, the expiry, the state, the open pairs, the closed pairs and the findings held. The expiry sits in the entry so that a later reader answers "did this payload reach zero in time" from the store alone.
A run over a corpus that declares no payload is still a reading, and its task list is empty. A store that skipped that state would make "nobody recorded anything" and "the payload is gone" one file.
A task past its expiry reads as its whole pair set open. The check layer holds nothing for such a task, so it reports every pair that the task named and closes none of them. The reading states that, beside the state the check layer decided. A payload that lapsed with pairs open is a different end from a payload that reached zero. That is the distinction the words "before the expiry" name.
What a reading refuses to hold, and the reason for each.
| not recorded | why |
|---|---|
| the pairs, and the document each one names | The pairs are in the committed lock and the findings are in the check report. A copy here is a second copy that a lock edit falsifies, and the count is the thing that decays |
| the owner | The lock names the owner and the check report prints it beside every task, so a reader joins on the task identifier. A per-owner series is a performance measure, which spec 3 rules out for the store beside this one |
| a key of the block that nothing reads | Such a key holds nothing and refuses nothing, so it moves no count here. The check report is where one is named |
| wall-clock time, and any duration | Neither is reproducible, and both would change the file on a run that measured the same thing |
| the engine version, and the host | Neither is a fact about the payload |
Where it lands, and who reads it. Under .headwater/, which is outside the corpus root. No census row covers it, no language regime binds it, and no rule reads it. That is the boundary the capture-cost store sits on, and it is there for the same reason. taxonomy audit is the only reader. The file is committed plain text, so every reader of the repository recomputes each figure from the lines.
Two digests are two measurements, and the report never trends across them. The payload is a set of (document, rule) pairs. A rule that the taxonomy stopped running closes a pair with no change in what anybody wrote. So the report names every digest that the readings span, and it states that the figures beside them are not a trend. A series that averaged over a schema change would report a migration as an authoring trend.
What the report states, and what it cannot. It states the payload of this run. It states the fraction of tasks that stood at zero on or before their expiry, and the first and the last date the store holds. The denominator of that fraction is every task identifier in the store, and never the tasks the lock declares today. A task that closed and left the lock is a payload that reached zero. Over one reading the report states a value and no trend. The elapsed time between two readings is what the series adds, and no engine supplies it (HW-OBL-0008).
Conformance
Vendoring content is not adoption. A consumer can hold a perfect copy of the taxonomy and wire none of it. Conformance is a separate, evaluated question:
headwater conformance [--level <name>] [--now <date>]
evaluates the repository against rules that the taxonomy package ships. The rules cover checks wired in CI, gates required on the default branch, projections regenerated, hooks installed, and pin current. It reports gaps with remediation. The rules ship with the package. Thus a pin advance brings newly-added requirements into force automatically. Improve the method, and the next upgrade of every consumer surfaces the new gap. That loop is what turns a published method into an adopted one.
The verb reports and it gates nothing by itself. --level <name> is the one thing that moves its exit status. The section on levels below says what a level is before it says what that flag does.
The package names a rule, and the engine holds the reading
A conformance rule is two halves in two places. The package declares the name, the text an adopter reads, and the remediation. The engine holds the code that decides the rule against a tree. Neither half is any use alone, and the split is the same one that requires_engine already makes one level down.
A rule this engine cannot read ends the run. The precedent is exact. A publisher declares requires_engine, headwater_resolve::package::sources reads it, and an engine outside the declared range is refused before one source loads. A rule name that this engine holds no reading for is refused on the same grounds, and the message names the rule and the package. A publisher who adds a rule that needs a new reading raises the engine floor of the package. That mechanism exists already.
The alternative fails quietly, which is worse. An engine that skipped a rule it could not read would report a level over the wrong rule set. The publisher and the consumer would then disagree about what that level covers. The consumer would hold a green report about a requirement that nothing evaluated, and no line of it would say so.
Some rules no tree decides. A permission granted in the admin console of a platform is the standing example, and so is a hook that each clone installs for itself. Such a rule declares an attestation in place of a reading. The report names it, states what would decide it, and counts it as neither met nor missing. No level names such a rule until an attestation record exists, and 13 — Open obligations holds that wait. This is what "not silently dropped" means in a report that a person reads.
The pin is two numbers
"Pin current" was one number while a package was a version. A published artifact is now a digest over every file in it, and .headwater/taxonomy.yml carries taxonomy.digest beside taxonomy.version. A rule that read the version alone would pass a repository whose pinned digest names an artifact that nobody publishes any more.
So the reading compares both and reports each half. A repository whose package directory carries no release record pins nothing, because no published artifact stands behind that directory. The reading calls that a gap with a remediation, rather than a state it looks away from.
The remediation names five steps, and Waivers below says why one of them moves the existing package directory aside. A consumer cannot take the first of those steps from inside this tool, which 13 — Open obligations records as HW-OBL-0085. So the remediation states which step nobody can do, rather than a route that ends where the reader started.
No rule names the core, and that is not an omission. The section above says that conformance checks the core rather than the whole taxonomy. taxonomy validate already decides core satisfaction, and taxonomy resolve writes a lock only when the taxonomy validates. A lock is therefore a validated taxonomy, so the rule that holds the lock to the sources holds the core through it. A second reading of the core here would be a second answer to a question one verb already decides.
That guarantee is bounded to the rule set that validated the lock, and the lock states which one it was. A reader admits a lock on three conditions. The format is one this engine knows. The digest matches the taxonomy beside it, and the rules field matches the resolver rule set this engine runs. taxonomy validate's rules can gain or lose ground between one resolve and a later check. A lock current against its sources, and validated under a different rule set, is not evidence that today's rules held over it. The check is a marker comparison, and never a second run of the rules. A reader resolves nothing. It refuses a lock whose rules disagrees with the rule set it carries, the same way it refuses a stale digest. The message names taxonomy resolve as the remedy.
What a level means, and what stops it from becoming a score
A level is a named subset of the conformance rule set, and the package declares it. The maturity ladder is the ordering over those subsets. A level states what the adopter wired up. It is not a measurement of how good a corpus is, and this specification does not dress it as one. A publisher asserts the ordering. The engine measures which rules pass, and it asserts nothing else.
Three properties keep the number honest.
Nothing declares a level, and a key that tried to would end the run. The report derives the level from the rules that pass, so there is no field for an adopter to write a larger number into. waivers is the only key the conformance block of the consumer declaration takes, and any other one is refused by name. A level key read and dropped would leave an adopter holding a claim that nothing evaluated. That is the same defect as an engine which skipped a rule it could not read, from the other side. An adopter who wants a different rule set forks the package. A fork moves the package name and the digest that the report prints beside the level. A level with no package identity beside it means nothing, so the report never prints one alone.
A level with no rule is refused. The reader rejects a package that declares an empty level, because a rung that names no rule is a rung every repository already stands on. A rung arrives with the rules that earn it or it does not arrive.
A waiver moves the exit status and never the level. The next section states what that costs and what it buys.
Waivers
A consumer may deviate deliberately. A waiver names the rule, the reason, the owner, and an expiry. Waivers appear in the coverage report of the consumer, and they are visible to the publisher in aggregate. Deviation is fine. Invisible deviation is not.
A waiver lives in the files of the consumer, and the reason is structural. headwater taxonomy vendor replaces a vendored package directory whole, and it refuses to overwrite a directory that carries no release record. A shipped rule is therefore never edited locally, and a waiver written beside the rule it waives would not survive the next upgrade. The consumer declaration holds waivers instead, in a conformance block beside the pin. That file is authored, committed and read in a diff, which is where a deviation belongs.
conformance:
waivers:
- rule: pin.current
reason: accepted_deviation
owner: j.baxter
until: 2027-02-28
note: an adopter who has not yet vendored a published artifact
The expiry is required, on the terms the local escape hatch already takes. Spec 4 makes the expiry of a suppression mandatory because the expiry of a waiver was mandatory first. An expired waiver is reported as expired, and the rule under it is then evaluated as though no waiver stood there. A waiver thus fails toward the rule rather than toward the deviation, and the day it expires is the day the gate goes red.
A waiver against no rule ends the run. A waiver that names a rule the package does not declare is a deviation that nobody reviews away. The report that would list it has nothing to list it under. The refusal is the same shape as the one above, from the other direction.
A waived gap is still a gap. The report states the level that the passing rules reach, and a waived rule does not pass. --level <name> exits non-zero on a gap that no waiver covers, so a waiver buys a green gate and never a higher rung. That separates an adopter who accepted a deviation from an adopter who closed it, and it is the whole reason a level cannot be bought.
A waiver reaches a conformance rule, and spec 4 counts one against a check finding. One mechanism carries both populations, because the four fields and the mandatory expiry are the same in each. What differs is which rule a waiver may name. headwater conformance reads the waivers that name a conformance rule. The coverage account of headwater check reads the waivers that name a check rule. That second reader is why the paragraph below needs its exclusion. It does not exist yet, so every waiver in this repository today names a conformance rule, and the coverage line says so.
One rule class is outside the mechanism. A withholding rule is not waivable. A waiver buys time against an error that a later run corrects, and no later run undoes a disclosure.
Self-consumption is the vendored-bytes shape
A repository can both publish a package and consume it. This repository does, for headwater/standard, and the two roles meet each other at the pin.
Two shapes reach a consumer, and this one takes the shape already named above. Consuming states it: the caller fetches the artifact, vendor checks it against the pin, and the lock that names the result is committed. That is vendored bytes under version control. The other shape a consumer could take is a fetch that continuous integration performs on every change. It commits nothing, and it checks the artifact fresh each time. No check opens a socket (spec 0). So the second shape needs a fetch inside the checking loop, and the one crate with a client stays outside that loop (HW-DR-0075). The first shape needs none of that code. The artifact already sits in this repository's own history, because vendor wrote it there once.
Publishing and consuming share one lookup, and a publisher that also consumes meets that lookup from both sides. taxonomy publish finds its source under .headwater/packages/ by the name a manifest declares. taxonomy resolve finds its source the same way. A repository whose authored source and vendored target are one directory meets vendor's own refusal. vendor refuses to install over a directory that carries no release record. That directory is the source, and vendor never wrote it. Two directories under .headwater/packages/ that both declare one name do not solve this either. The lookup refuses rather than choose between them, naming every directory that declares the name (#369). The self-consumption problem itself is met here by the shape of the design rather than by an accident of it.
So the authored source moves out of .headwater/packages/, and taxonomy publish gains a way to read it there. This repository's own source sits at taxonomy-source/headwater-standard/, outside .headwater/packages/ entirely. taxonomy publish --from <dir> reads the manifest at a directory the caller names. It bypasses the lookup by name. Nobody edits .headwater/packages/headwater-standard/ after that. Only taxonomy publish --from and taxonomy vendor write there. Both run by hand, whenever the source changes. So the directory always carries the release record that vendor's own guard depends on.
The maintenance loop is four steps, and the pin is the second of them. A source change publishes an artifact under a new digest. vendor reads the pin it finds, and it refuses an artifact the pin does not name. So the author writes the digest that publish printed into .headwater/taxonomy.yml before the vendor step. The order is publish, then the pin, then vendor, then resolve. The header of taxonomy-source/headwater-standard/package.yml states the same four steps, and a case in engine/crates/cli/tests/publish.rs runs them out of that header.
taxonomy publish --from <dir> --check is what holds the loop (#1139). A person runs the four steps by hand, so a source can change and nobody republishes it. No check reads the source, because every check reads the lock and the vendored copy. The check publishes the source into a private directory outside the tree, and it compares that artifact with the vendored copy member by member. It exits 1 and names each file that moved when the two do not agree. This repository runs it in continuous integration, and an adopter who maintains and consumes a package can run it the same way.
Arriving at a corpus cold
Everything above describes a repository that already knows its publisher. A machine that holds only a location knows none of it. It needs to learn which corpora live there, what taxonomy governs each one, and where to start (Q14).
Two questions arrive together, and they close by different routes. How a machine learns that a corpus exists, when it holds no pointer at all, is registration. No file inside a corpus answers it, and no convention in the field pretends otherwise. Every one of them presumes a client that already resolved a name. What closes here is the other question, resolution: a machine holds a location, and it needs to learn what governs it.
Registration is an act of publication into a channel whose reader is already obliged (Q16). That definition is what explains the failure of every file-based attempt at it, including the one measured convention that tried (HW-EVAL-adjacent-work §O.3). Two obliged channels exist already and Headwater builds neither. A taxonomy package goes to a registry that a resolver must read to install it. A package that carries the publisher's corpus location thus registers that corpus with every consumer. And a rendered page carries the link relation below, so a reader that fetches the page reaches the served copy. Registration is thus the publisher's own act, and the descriptor is the whole of what Headwater supplies for it.
The corpus descriptor is a projection, and it is the one whose path the engine fixes. headwater generate writes it and generate --check holds it to regeneration, like any other projection. What it does not take from the taxonomy is its own location, and the reason is the whole point of it. A reader who has to consult the taxonomy to find the descriptor already has what the descriptor would have told them. So the descriptor sits at .headwater/corpus.json, relative to the repository root, and it is engine-defined and non-optional. That is the standing that the register projection already has, for a different reason.
Everything else about it is ordinary. It is generated from the roots, so it cannot go stale against them. That property is what lets one document carry per-root identity at all. The conventions that this resembles keep their index files bare and put identity on each collection. They do so because a hand-maintained index describes roots that somebody else edits.
The descriptor carries five things for each corpus root in the repository. The root path. The taxonomy identity and version. The lock hash. The entry points. And each declared export profile, with its output location, its tombstone grain and whether it is committed. It carries nothing that those artifacts already state about themselves. An export declares its own coverage (spec 12), and a second copy of that statement would disagree with the first at the next release.
Four of the five come from a file and one is derived. The engine reads the root, the identity, the lock hash and each declared export profile. It derives the entry points.
An entry point is derived, because no declaration in the language holds one. The entry point of a shelf is the document that the derived reading order puts first (spec 2). Spec 2 already names "reading order in generated indexes" as one consumer of that derivation. So a descriptor, a shelf index and a route agree about where a reader starts. Where no relation governs a shelf, the reading order is the path order.
An entry point carries a path and an identifier, and never a summary. The gate compares the bytes of this file. A summary is prose in front matter, so a descriptor that held one would fail the gate after an ordinary wording edit. A gate that fires on prose is a gate that everybody bypasses.
An export row states its name, its target, its output, its grain and whether it is committed, and never its filter. Spec 6 gives a profile an audience, a target, a filter and a tombstone grain. This file is served, and a filter clause names facet values. That sits one step closer to the content than the structure a descriptor may disclose. So a reader learns that a profile is filtered and at what grain, which is what a filtered view owes anybody. The clause itself reaches the reader who receives the export.
The row states committed only as false. headwater export builds a declared export that states committed: false at publish time. A reader who opens its output path in the tree finds nothing there. The row says so. The engine writes no committed: true, so the descriptor of a corpus that declares no such export keeps its bytes.
An earlier release of the descriptor carried no exports member at all. The projections reader of the day kept a kind and an output path, so no taxonomy could declare a profile. An empty list would then have said that a corpus exports nothing, rather than that nothing could declare an export. The reader takes the whole declaration now, and an empty list means what it says.
The descriptor also states each declared exclusion, with its reason. A root on its own overstates the corpus. This repository declares docs and excludes one directory under it. A cold reader that saw only the root would treat package content as governed content. No artifact that such a reader reaches states the exclusion, so the descriptor does.
JSON carries no comment, so the generated-file marker is a top-level member. The marker is the rule that protects a file that a person wrote. A path that the engine fixes is not a reason to drop it. An adopter may write a descriptor by hand before the verb reaches them. The member also answers the absence rule below, and one member for two rules is one fact in one place.
Three rules make it usable rather than decorative.
- A version carries a stated client behavior. A major version above what the reader understands is a hard failure with a message. A minor mismatch is a warning, and the reader continues. A version field with no rule attached is a string.
- Absence does not read as presence. The descriptor declares its own media type and a required shape. A reader that receives a success response which does not parse to that shape treats the descriptor as absent, not as malformed. The two have different remedies. A served host that answers every path with a default page is the ordinary case rather than the exotic one.
- The canonical location is inside the repository, and a served copy is reached by a pointer. A reserved path at the root of an origin is a poor fit here, for three reasons. A repository holds one or more corpora. A documentation site is often one part of a host that serves other things. And the party who writes the descriptor rarely controls the root. A rendered page therefore carries a link relation to the served copy, and the copy may sit anywhere that the site can put it.
The descriptor is a served artifact, so a filter reaches it first. It names roots, entry points and profiles, which is organizational structure. An export profile filters it exactly as it filters anything else, and a filtered descriptor announces that it is filtered.
Federation
Larger organizations use layers: a generic method, a divisional taxonomy that extends it, and a repository overlay that extends that. Two rules keep the stack coherent:
-
References run upward. A repository may reference its own tier or a higher one, never a sibling or a lower one. A downward reference makes the upper tier depend on something that it does not control, and the abstraction inverts. The legal reference set is derived from what a repository actually consumes, so it needs no hand-maintained registry.
-
Overlays compose in one direction. Each tier may override, add, or remove against the tier above it. A tier never reaches past its parent. Conflicts are resolution errors, not precedence puzzles. Overlay application must be confluent (spec 2). Thus a three-tier stack has no resolution order that anyone must remember.
Across taxonomies, not under them
A layered stack only helps organizations that share a root. Two divisions that adopted different taxonomies independently — after an acquisition, or simply because they arrived separately — have no common ancestor to build an overlay against. A merge of the two is a political project, not a technical one.
They do not need to merge. They need declared correspondences: SKOS-style mapping relations between their concept schemes (spec 2). exactMatch where two kinds are interchangeable, closeMatch where they are interchangeable for retrieval but not inference, broadMatch / narrowMatch where one is wider.
With mappings declared, an aggregator answers "every decision in the organization" across taxonomies that share no vocabulary. The next section says what an aggregator is. Neither division gives up its own. Neither taxonomy changes. A third artifact records how they correspond, and the tier that aggregates owns it — normatively, not conveniently. Pairwise mappings between peers grow quadratically, and they go stale on every publisher release (spec 2). That is the standard answer to this problem in knowledge organization, and there is no reason to invent a worse one.
The tier above a corpus harvests it
A tier that answers questions across many corpora needs their content. Two architectures were available, and only one survives contact with the constraints that this specification already set (Q9).
The aggregator is a solution corpus plus one anchor kind. The solution layer is an ordinary corpus. It authors the facts that live between repositories, and it consumes exports for everything else. What was unstated is how it reaches the corpora below, and nothing new is needed for that. An anchor kind is declared, and exactly one resolver owns it (spec 2). That resolver reads pinned corpus exports, in the way that the code_path resolver reads a source tree.
There is no merged graph. Merging is anchor resolution, and anchor resolution leaves nothing behind when a run ends. The solution corpus holds its own documents and its own declared edges. It resolves anchors against the exports that it pinned, and it rebuilds that resolution on every run. A merged graph would be canonical for nothing, would carry no reviewer, and would cost one rebuild to reproduce. Such an artifact does not need to exist.
The tier harvests, and it never fans out. Each source corpus carries a pin: an identity, a content hash, and a location. A scheduled job fetches each export out of band and commits it, and the resolver then reads the committed copy. The tier never queries a live endpoint. Three arguments agree, and this specification already made all three.
- Spec 0 forbids a network dependency at check time, and a fan-out query is one.
- Spec 6 budgets 100 ms for a route query. A fan-out across estates does not fit inside that, and a call that does not fit is a call that developers remove.
- A fan-out that meets an unreachable source either fails whole, or returns a smaller answer with no notice. The second outcome is the silent pass that spec 4 exists to forbid.
So a pinned export that the tier cannot read is a finding that names the pin. It is never a narrower answer, delivered quietly. The metadata-harvesting aggregators of the digital-library world reached this architecture under the same pressure, and the evaluation records what they found.
A tier pins an export profile, and the publishing corpus decided what is in it. A filter acts where the export runs and never where a tier reads. A tier that reads holds bytes that already crossed the boundary (Q17). So a tier never filters what it harvested, and it has nothing to filter: what arrived is what the publisher meant it to have. An anchor whose target the publisher withheld resolves to withheld rather than to a dangling reference (spec 2). The tier reports it at the profile's declared grain. Under counted, the resolver finds a withheld anchor by its digest in a tombstone. Under sealed, the export lists nothing, so a withheld anchor stays unresolved and the reason names the grain (HW-DR-0100).
A pin makes revocation late, and the specification says how late. A document that a publisher withholds today stays in the tier's committed copy until the next harvest. That is the price of harvest over fan-out, and it is not removable inside this architecture. So an export carries its generation time, and the harvest schedule is declared. The revocation lag is then a number that an operator can read rather than a surprise. The authorization systems that solve this problem in the other direction carry a freshness token on every answer. A design with no such token owes the reader the cadence instead (HW-EVAL-adjacent-work §O).
A solution-layer node is a declared anchor. A tier that models the estate is tempted into nodes for services, interfaces and capabilities. A node that asserts a service's properties has left the corpus and started to model the world (HW-EVAL-adjacent-work §A.1). Two arguments refuse it. Nothing in the design carries an obligation to keep such a node true, and a wrong node reads as structural rather than editorial. And a filter has nothing to attach to on a node that carries properties. A document has facets that a predicate reads, and an anchor is carried whole or withheld whole. So a solution-layer node carries an identifier, a name and an owner, and every substantive claim stays inside a document. This is what code_path already does. A concrete need that the anchor form cannot meet reopens it, argued as the change to the model that it would be.
Upstream awareness
A scheduled check compares the pinned version against the latest release of the publisher. It raises a change proposal, with the diff report and the migration assessment attached. It does not raise a notification that nobody acts on. The default is a draft change request that an agent can complete. A pin that only a human can advance is a pin that goes stale.
A proposal channel carries a budget, and the reason is measured. Studies of automated dependency proposals report about a third merged for undifferentiated version bumps. For security fixes the figure is about two thirds, against roughly four fifths for proposals that a person wrote. The mechanism is the same in all three. What moves the number is how selective the proposer is. So an unbounded channel converts into notification fatigue, and the usual remedy is a cap on open proposals. Headwater declares that cap beside the schedule, so an operator reads it rather than discovers it.
Whoever opens a proposal needs more than the permission to open one. On the platforms in common use, the permission to create a proposal does not include the permission to create the branch that it points at. The permission that does create a branch also permits a merge. So a proposer that authors its own branch is not confined by its permissions alone. Two mechanisms confine it, and an operator states which one is in force. Either a branch rule requires review and grants the proposer no exemption, or the proposer owns a separate repository and proposes from there. To leave this unstated is to claim a separation that the credential does not supply (Q7).
One pattern, three instances. A taxonomy pin, a requirements snapshot pin (Q19), and a source-export pin all work the same way. Each one fetches out of band, commits the result, checks against the committed copy, and compares on a schedule. Each one raises a change proposal and never a mutation. To state the pattern once is what keeps the third instance from arriving as a new mechanism.
A pin states the channel that carried it, and an import with no channel is refused. A digest authenticates the pin and never the publisher, which §What a package digest proves states for a package and HW-OBL-0115 records. The consequence is sharper inbound than outbound. An adopter who binds a package reads a lock and a diff. An edge that an import wrote reaches a graph that every later check agrees with. So an imported edge carries the weight of the channel the snapshot arrived on rather than the weight of the digest. The declaration therefore names both, in the words of the person who wrote the digest down, and neither is ever written by a verb. A channel stated beside the payload would certify itself, which is the move the pin exists to refuse.
A snapshot pin reports drift on each affected edge, and not only on the pin. A snapshot carries the upstream identity and revision of every item in it, so an advance says which items changed. Every edge into a changed item is then a finding until a person re-verifies it. Requirements practice reached the same mechanism and calls such an edge suspect. A proposal against the whole snapshot names a file, and a finding on an edge names the document whose author can act. That is spec 4's report-at-the-origin rule, applied to a second kind of upstream.
The rule that performs it is relation.target.suspect, and it is read from a run. It instantiates over every relation the taxonomy declares with created_by: import, and over every relation whose to names an anchor kind. It compares the verified_revision on the edge against the revision the resolver states now. For a snapshot, that revision is the one the snapshot pins. For the source-tree resolver, it is one digest over the path and the bytes of each regular file that the anchor matched. So a governs edge goes suspect when the code it governs changes, and a change to a modification time alone moves nothing. A literal path that names a directory has no digest, because the reach under a directory is the pattern's to state. So an edge onto a directory can never go suspect, and the rule reports it at Info on every run. The remedy it names is the same path with /** after it. It offers no fix, because that remedy changes what the edge reaches. The resolver never opens a named pipe, a socket or a device, because a read of a named pipe with no writer does not end. Such an entry adds nothing to the digest. So an edge that matches only such entries has no digest, and it can never go suspect. The rule reports that edge at Info on every run too. The remedy it names is the regular files that the document governs, or the removal of the entry. It names no /**, because /** after such a path names nothing. An edge with no recorded revision passes. The one exception is a document that the change under check re-verified. The change adds the document, or it moves the document's last_verified. There the rule reports the edge at Info with a fix that records the digest. headwater check --fix --change writes that fix only on such a document, and a run with no change offers it on no document. A digest over a set of entries says that the set changed, and it does not say how many entries changed. The count that the rule reports is the count of regular files that the digest covers. It reports the document that declares the edge, at the entry that carries it. The posture is advisory, because the remedy is a person who re-reads the upstream item. The revision reaches the rule off the binding the anchor resolver returned. That binding is also in the cache key of every instance about the edge. So an advance divides the key, and no run serves the verdict written before it.
A gate carries no drift verdict, and HW-OBL-0118 is why. The finding above is a check-layer output, read from a run over the tree in front of it. The artifact that headwater check --read-set writes holds one line for each document. An upstream item is on no line, so a gate that compares listed hashes decides nothing about one. That record states both open shapes and rules neither in. Until one of them is taken, a caller reads the drift from a run and never from a gate. A digest of working-tree bytes does not change this, because a path under the source tree is on no line of that artifact either.