Standard-Enforcement Standard
This meta-standard governs how every other standard in the pitlab corpus is mechanically enforced — what enforcement a standard owes, how it declares it so the declaration is machine-verifiable, and how we prove that enforcement is both real and still running. It exists because enforcement across the corpus is inconsistent: a few standards ship a hard gate or a full deploy-gate + daily-reconciler + dead-man triad, while most are prose a human is trusted to remember, and there is no shared answer to “is this standard actually enforced, and how would we know if its enforcement silently died.” It is pitlab’s policy-as-code / compliance-as-code layer — a control plane for the controls themselves. The decision and rejected alternatives are recorded in ADR-0176.
The unit of enforcement is the clause, not the whole standard
Section titled “The unit of enforcement is the clause, not the whole standard”Enforcement is classified per obligation (clause), declared in one ## Enforcement section per standard. Standards are mixed: the Documentation Standard has an un-checkable “one dense fact per heading” clause sitting beside a trivially-checkable “required front-matter fields present” clause. Classifying the whole standard as one thing would either over-claim enforcement it cannot deliver or silently drop its checkable clause. The section is a fixed-schema table — one row per obligation:
| Column | Meaning |
|---|---|
Obligation | the normative clause, in a few words |
Class | machine / review / advisory (see next section) |
Layer | deploy / scheduled / deploy+scheduled / — |
Mechanism | the concrete enforcer (script path, pipeline, Zabbix trigger, hook, or named human checkpoint) |
Dead-man | the <check>.age key if the mechanism is scheduled, else n/a |
Three enforcement classes decide what a clause owes — machine, review, advisory
Section titled “Three enforcement classes decide what a clause owes — machine, review, advisory”Every clause is exactly one of three classes, and a review clause must have teeth:
machine— an automated mechanism can decide pass/fail. It owes at least a deploy-time check (see the two-layer rule).review— no script can decide it, but a named human checkpoint enforces it: a/wrapupcheck, a/code-reviewdimension, a PR-template line. Areviewclause that names no concrete checkpoint is demoted toadvisory— this is what stops “review” becoming a dumping ground for anything inconvenient to automate.advisory— aspirational, no enforcement. It carries a written justification for why it is not enforced (e.g. “retrieval-quality judgement, no reliable signal”).
The two-layer rule — a scheduled check is owed only if the clause can break without a code change
Section titled “The two-layer rule — a scheduled check is owed only if the clause can break without a code change”A machine clause owes a deploy-time check (the factory inspection) whenever its only violation path is a commit — which CI already re-inspects. It owes a scheduled check (the annual roadworthy) only if the thing can also break out-of-band: a manual change, a live runtime state, or time decay. Both layers are earned only when both vectors are real. This one criterion refutes “both always” and is the primary bound on proliferation — a doc’s front-matter cannot drift out-of-band, so it is deploy-time only; a plaintext port can be hand-added and its cert can silently expire, so it earns both.
The worth-it filter — a scheduled check is mandated only when drift is silent and matters
Section titled “The worth-it filter — a scheduled check is mandated only when drift is silent and matters”Eligibility (can-drift) is necessary but not sufficient: a scheduled check costs a daily pipeline run, a Zabbix item, a trigger, and its own watchdog. Among clauses that can drift out-of-band, a scheduled check + watchdog is mandated only when the drift would be silent — no existing alarm and no visibly-broken service would surface it — AND consequential. If some other signal already catches the drift, or it is trivial, the scheduled check is optional with a recorded reason. You build a watchdog only where a real problem would otherwise go unseen.
Every mandated scheduled check carries a <check>.age freshness dead-man
Section titled “Every mandated scheduled check carries a <check>.age freshness dead-man”A scheduled check that silently stops firing is worse than none — it is a false sense of safety. So every mandated scheduled check carries a <check>.age Zabbix freshness watchdog, built by reusing the existing machinery, never a bespoke per-check dead-man:
- the check stamps
/var/lib/pitlab/<check>.laston each successful run; - a
<check>.ageUserParameter(incron-heartbeats.conf, deployed bysync_control01.yml) exposes seconds-since-last-run; zabbix_cron_heartbeats_monitoring.py’sJOBStable registers the item + a stale-threshold trigger (cadence + grace).
This is the same pattern already carrying schedule.reconciler.age, monitoring.reconciler.age, and vulnscan.run.age.
Liveness derives from the system that RUNS the check, never from agent-local filesystem state
Section titled “Liveness derives from the system that RUNS the check, never from agent-local filesystem state”The bullets above describe a check that runs on one host and stamps that host’s /var/lib/pitlab. That shape is correct for a host cron or timer, and wrong for anything a pipeline runs — because the stamp lands on whichever agent won the job. The pitlab pool is runner01 and runner02 (ADR-0330); a pipeline that stamps locally leaves each host’s copy stale roughly half the time, and the dead-man flaps between them — firing not because the check stopped running, but because it ran somewhere else. The bug is not the agent count. The bug is deriving liveness from a side effect on one box instead of from the outcome the check actually has.
The rule, by what runs the check:
- A pipeline-run check derives liveness from the pipeline system.
ado_ci_reconciler.py(a control01 cron) polls the ADO API for each watched pipeline’s last succeeded run and stamps/var/lib/pitlab/<check>.lastfrom itsfinishTime(ADR-0329). This is host-agnostic by construction at any agent count, generalises to every pipeline rather than the two that happen to exist, and measures the outcome — did the run succeed — rather than a side effect. The pipeline itself stamps nothing. - A host cron/timer check stamps locally, exactly as the bullets above describe. It genuinely runs on one host, so the host’s filesystem is the authoritative record.
Both shapes are legitimate; naming the wrong one is the failure. The same reasoning covers a pipeline-written Prometheus textfile metric read by one host’s Alloy — a third instance of the identical assumption, recorded in ADR-0329.
A dead-man may be registered on any host, not only control01
Section titled “A dead-man may be registered on any host, not only control01”zabbix_cron_heartbeats_monitoring.py creates its items on the control01 Zabbix host, so for a long time a scheduled control anywhere else structurally could not declare a dead-man — and therefore could not be declared conforming to this standard at all. That is a limit of one registry, not a property of the pattern. Two registration shapes are now recognised, and the meta-gate resolves both:
| Where the check runs | Registration | Item key |
|---|---|---|
| control01 (or a pipeline, stamped by the API reconciler) | zabbix_cron_heartbeats_monitoring.py JOBS table | <check>.age |
| Any other fleet host | that host’s own configure playbook, plus a staleness trigger (ADR-0219) | vfs.file.time[/var/lib/pitlab/<check>.last,modify] |
standards_enforcement_gate.py indexes both, so a row may name either and still resolve. A standard whose scheduled mechanism runs off control01 declares the second shape and is conforming; it is not an exception and does not belong on the allowlist.
The dead-man tower terminates at an external snitch (healthchecks.io), outside our failure domain
Section titled “The dead-man tower terminates at an external snitch (healthchecks.io), outside our failure domain”Every <check>.age watchdog — including the two reconcilers that watch everything else — is evaluated by Zabbix. The recursion of “who watches the watcher” would therefore terminate at Zabbix, and if it did, nothing external would watch Zabbix: a total Zabbix/control01 outage would silently darken every dead-man, because the thing that would page you is the thing that died. That recursion is now closed by one external base case (Issue #1702, ADR-0210): control01 pings a healthchecks.io URL every 5 minutes, but only after proving Zabbix is genuinely responsive (apiinfo.version + an authenticated problem.get). If Zabbix/control01/LAN is down the ping is withheld, and healthchecks.io pages Arron out-of-band (email HME + Pushover, from their servers) after a 5 m period + 15 m grace. The pinger’s own liveness is watched in-domain by alerting.deadman.age; the snitch itself is the out-of-domain terminal watcher. Gating the ping on Zabbix-health is the mechanism — a naive unconditional ping would stay green while Zabbix was dead. Runbook: External Dead-Man’s-Snitch.
Go-live note: the external half (the healthchecks.io account + Email/Pushover integrations + the RULE-12 negative-test proof of out-of-band firing) is provisioned once by hand, because the account email must be an Apple HME proxy. Until then the control01 pinger runs inert-but-alive (
ping=url-unset) and Issue #1702 stays open for the go-live proof.
Enforcement is declared in a table resolved against the live repo, so a fake mechanism fails
Section titled “Enforcement is declared in a table resolved against the live repo, so a fake mechanism fails”The ## Enforcement table is the single source of truth — there is no separate registry to drift out of sync. The meta-gate parses it and resolves every named machine-class mechanism against the live repos: the gate script path must exist in one of the indexed roots (below), and a scheduled row’s <check>.age key must be present in the JOBS table. A standard that names a gate that does not exist fails the build — because an assertion satisfiable without the feature is not a test (PD12). This is the Config-Reference Validation Gate Standard “resolve against the live authority, never a hand-maintained mirror” pattern applied to enforcement declarations.
Four repos are indexed as mechanism roots; under-claiming enforcement is as much a defect as over-claiming it
Section titled “Four repos are indexed as mechanism roots; under-claiming enforcement is as much a defect as over-claiming it”standards_enforcement_gate.py indexes ansible/, docker-stacks/, pitlab-docs/ and hass-config/. A machine row may name a file in any of them; a mechanism in an un-indexed repo (tf-*, claude-config) still cannot be named in a machine row and must be described in prose or classed review.
hass-config joined the list at Issue #2219 (ADR-0344), and the reason generalises. Its pipeline runs hard-fail gates (lint_zha_triggers.py, lint_ha_automations.py) over the automations two standards govern — genuine machine enforcement — but because the resolver could not see the repo, the Home Assistant Standard’s raw-zha_event clause had to be declared review, asserting that a human checkpoint enforced something a script blocks the deploy on. That is the same species of defect as naming a gate that does not exist, in the opposite direction: both stop the table describing the estate, and the table is what the coverage number is computed from. A resolver limitation must be fixed in the resolver, never paid for by a permanently misclassified declaration.
The bound that keeps this honest is unchanged: a root is indexed only when its pipeline demonstrably runs blocking gates over files a standard governs. Indexing every repo on the agent would raise the chance a phantom mechanism resolves by coincidence against an unrelated same-named file — the precise false-pass the resolver exists to prevent — so each addition is a decision with an ADR, paired with a regression case that proves the row resolves with the root and fails without it. Adding a root also obliges every pipeline that runs the gate to sync that checkout first (pipelines/docs.yml and standards_enforcement_reconciliation.yml both do): an unsynced agent checkout would resolve a newly-added gate on one runner and fail it on the other.
Promote a row to machine only after its mechanism is on main — the docs and the gate live in different repos, so there is no atomic commit
Section titled “Promote a row to machine only after its mechanism is on main — the docs and the gate live in different repos, so there is no atomic commit”The clause above is unforgiving by design, and its cost lands on everyone, not on the author. A standard lives in pitlab-docs; the mechanism it names lives in ansible or docker-stacks. There is no single commit that can land both, so a review → machine promotion pushed before its gate exists does not fail the person who wrote it — it fails the publish pipeline for every concurrent session until the second repo catches up. RULE 9’s “docs are updated at the same time as the code they document” has no cross-repo form; this is that form.
This binds any machine row, newly authored as well as promoted. The rule was written for a review → machine promotion, and read narrowly it appears to exempt a brand-new row shipped alongside a brand-new mechanism — which is the more common case and carries the identical hazard, because the meta-gate resolves the row against the live repos either way and cannot tell the two apart. Recurred 2026-08-11 (Epic #2369): a new Hypercare section declared two machine rows naming hypercare_countdown.py and zabbix_hypercare_monitoring.py in the same push that created them, and the pre-push hook rejected it — correctly — because the ansible files were not yet on main. The docs push had to be split in two around the ansible push.
Push the mechanism repo first, confirm it is on main, then push the row. The same ordering applies in the other direction to any pipelines/docs.yml step that invokes a script from ansible/ — the pipeline syncs ~/ansible from origin/main at run time, so the script must already be there.
Observed live on 2026-08-06: three Vulnerability Management Standard rows were promoted to machine naming dt_sla_conformance_check.py and the dead-man dtrack.sla.conformance.age, neither of which had reached ansible/main. The meta-gate returned six violations and reddened pipeline 18 estate-wide, blocking an unrelated Epic’s acceptance test (Issue #2203). The promotion itself was correct work; only its order was wrong.
A NEW standard has the opposite cross-repo dependency, where no push order is safe — forthcoming: closes it
Section titled “A NEW standard has the opposite cross-repo dependency, where no push order is safe — forthcoming: closes it”The ordering rule above assumes the dependency points one way: the docs name the mechanism, so the mechanism goes first. Adding a brand-new standard points the other way at the same time, and the two constraints are irreconcilable without help. validate_standards_taxonomy.py requires every *-standard.md to be registered in ansible’s standards_taxonomy.yml (ADR-0252), so:
- docs-first opens a window where the file exists unregistered → the taxonomy gate reds as
UNREGISTERED; - registry-first opens a window where the row names an absent file → the same gate reds as a stale row.
Either way the red lands on whichever concurrent session pushes next, not on the author — the identical asymmetry, in the opposite direction. Observed live on 2026-08-07 (Issue #2219): pipeline 18 run 7665 went red on an unrelated session’s docs push during exactly this window.
The registry therefore accepts a forthcoming: list — a row pre-registered ahead of its file, carrying its tracking issue — which downgrades the absent-file finding to a warning and yields one order with no red window at all:
- push the registry row plus its
forthcoming:entry (gate warns: file not landed); - push the
pitlab-docsstandard file (gate green); - push the removal of the
forthcoming:entry (gate green, warning cleared).
It is deliberately narrow, because a softened check is a check someone will reach for: a forthcoming: name registered in no bucket still fails — the flag softens the absent-file check and never substitutes for registration — and once the file lands the now-pointless flag warns rather than fails, since failing there would re-open the very window it exists to close. All four behaviours are proven-red in test_validate_standards_taxonomy.py, paired against the same tree without the flag so the flag is demonstrably what changes the verdict.
A cross-repo GATE has a GUARANTEED red window, not a race — land it non-blocking, repair, then promote
Section titled “A cross-repo GATE has a GUARANTEED red window, not a race — land it non-blocking, repair, then promote”The two orderings above govern the declaration — a row promoted to machine, a standard file registered. This is the same asymmetry one level down, on the gate itself, and it is the general case of which those two are instances: a gate cannot land atomically with the corpus repair it demands, because its subject lives in another repo. Whichever repo goes first, the shared pipeline is red until the second push arrives, and the red falls on whoever pushes next.
Observed live on 2026-08-10 (Issue #2257, ADR-0378): a strengthened validate_adr_numbers.py landed on ansible main at 08:17 AEST. pipelines/docs.yml already invoked that script, so the new row-shape checks went live for the shared docs pipeline the instant they landed, while the 31-row corpus repair they demanded was still unpushed. Pipeline 18 was red for 19 minutes (run 8165 → cleared by run 8170 at 08:36). The blast radius happened to be one push — the author’s own claim_adr_number.sh claim, which lands on origin/main by design — but any concurrent session pushing docs in that window would have gone red through no fault of its own.
This is not a race. A race is a window you might get lucky in. An already-wired cross-repo gate has a guaranteed non-zero window whose length is exactly the gap between two pushes. And the two obvious fixes conflict, which is why the reasoning kept being re-derived: repair-first inverts the window to zero, but the meta-gate above fails when a machine row names a script not yet on main, which forces gate-first.
The resolution, and one order that has no red window at all:
- Mechanism repo — the new or strengthened check ships in warn mode: it reports every finding it would fail on, and exits 0. The script is now on
main, so amachinerow naming it resolves — the conflict above dissolves. - Subject repo — the corpus repair. The gate warns and finds nothing.
- Mechanism repo — flip the check to blocking. Green by construction.
The non-blocking switch MUST live in the same repo as the gate — a module constant, a --strict flag defaulting off, a per-check warn list. A continueOnError: true on the pipeline step lives in the subject repo and so reintroduces the exact cross-repo coupling being removed.
Where the gate’s subject lives in ONE repo, put the gate in that repo and the problem vanishes — mechanism and subject land in a single atomic commit with nothing to sequence. That is the gate-location rule applied correctly, not a workaround, and it is why shift_left_coverage_gate.py lives in pitlab-docs/hooks/ beside the pipeline and hook it reads. Reach for the three-push sequence only when the gate genuinely must read another repo’s tree.
A window that is opened anyway is DECLARED, not discovered. Where a check cannot be made non-blocking, the author states the window’s start, expected length and blast radius when it opens — not at wrap-up. An undeclared window is charged to whoever trips over it. Governed by ADR-0380.
The mechanism contract — what any enforcement mechanism must satisfy, regardless of type
Section titled “The mechanism contract — what any enforcement mechanism must satisfy, regardless of type”Enforcement is not only a Python gate — it is equally an Ansible idempotent drift-remediation pipeline, a Zabbix trigger, or a docs hook. Rather than privilege enumerated types (which leaves a new type ungoverned), a mechanism of any type is valid if and only if it satisfies one contract:
- Fails closed — on a violation it fails (non-zero / red / firing). It may fail open only on a transient authority-unreachable error, and it logs that.
- Proven red before trusted green — a committed negative-control test demonstrates it failing on the breakage it guards (the Build-Hook & Pipeline-Gate Test Standard).
- Alerts by class — an operational finding pages Pushover and reds its run; it never emails an alert (Notification Channel Policy).
- Carries a
<check>.agedead-man if scheduled. - Is ARA-bound if it mutates prod — a mechanism that remediates is also governed by the Autonomous Remediation Authority Standard.
- Is config-as-code in the correct repo — per the Pipeline-Gate Script Location Standard (ADR-0106).
This standard composes with the existing enforcement standards, it does not replace them
Section titled “This standard composes with the existing enforcement standards, it does not replace them”This is a governance layer above the concrete enforcement standards. It cites, it does not redefine: the Build-Hook standard owns “proven-red,” the Config-Reference standard owns the pre/post-deploy resolution split, the Monitoring-as-Code Standard is the reference implementation of the full deploy-gate + daily-reconciler + .age-dead-man triad, and ADR-0106 owns where scripts live. Where it overlaps the Autonomous Remediation Authority Standard’s principle that “the enforcer must itself be monitored,” precedence is split: detection, declaration, and freshness are governed here; mutation authority and blast-radius are governed by ARA. A self-healing enforcement check obeys both standards at once.
Retrofit — legacy standards are grandfathered on a deadlined allowlist
Section titled “Retrofit — legacy standards are grandfathered on a deadlined allowlist”The blocking meta-gate ships from day one, but reads a standards-enforcement-exceptions.yml allowlist of not-yet-migrated standards — each row carrying a tracking issue and a deadline, mirroring eit-exceptions.yml and the External Exposure Register. The rules:
- a new or changed standard is never exempt — it must carry a conforming
## Enforcementsection now; - a listed legacy standard is exempt from the structural check until its deadline;
- a standard past its deadline fails the build (deploy-time) and pages (the scheduled deadline-sweep).
The allowlist burns to zero via the retrofit issue under Epic #1713, its progress visible as a burndown on the Standards Enforcement dashboard. As of 2026-07-26 (Issue #1719) the allowlist has burned to zero — every standard in the corpus carries a conforming ## Enforcement section and the meta-gate is fully blocking with no grandfathering.
Domain consolidation inherits the grandfather obligation — it never escapes it
Section titled “Domain consolidation inherits the grandfather obligation — it never escapes it”When the domain-unit merge (ADR-0252) folds several source standards into one domain doc, the meta-gate sees one ## Enforcement table for the domain (the first table is the single source of truth it parses), so the retrofit obligation must be carried across the merge intact, never dropped. Two cases, both faithful to “the allowlist only ever shrinks”:
- Domain with ≥1 table-bearing member → strictly checked; every absorbed grandfathered member owes its clauses in the combined table. The domain doc already satisfies the structural check via its conforming member(s), so it is not allowlisted — but a grandfathered member folded in beside a conforming one does not thereby lose its retrofit. Each absorbed member’s clauses MUST be authored as rows in the domain doc’s single combined
## Enforcementtable, under the same Issue #1719 retrofit, now scoped to the domain doc rather than a standalone file. This is the case handled forcredential-aggregation(→ secrets-encryption),service-mothball(→ lifecycle-naming), andstateful-appliance-config-export(→ iac-deployment) (Issue #2008). - Domain whose members are ALL grandfathered → inherits ONE allowlist row for the domain doc. If every source folded into a domain was itself grandfathered (no conforming table yet), the merged doc inherits zero enforcement rows and cannot pass the structural check on its own. The absorbed source rows in
standards-enforcement-exceptions.ymlare removed (the files no longer exist) and replaced by ONE row for the domain doc, keeping the earliest member due-date and the same Issue #1719 retrofit. This is the inherited-grandfather case documented as a comment in the exceptions file. It is not the forbidden “add a NEW standard to the allowlist” case: no new obligation escapes — an existing, tracked one is carried under its domain home until authored.
A DECLARED control is not an ADHERED-TO control — adherence is measured separately
Section titled “A DECLARED control is not an ADHERED-TO control — adherence is measured separately”Everything above governs declaration: does a standard claim a control, and does that control exist in a live repo. That is necessary and it is not sufficient. The meta-gate’s metrics have read failures_total=0, allowlist_total=0, strictly_checked=21 continuously since the retrofit burned to zero — every standard conforms structurally, forever, by construction. A monthly assurance report built on those numbers alone would be vacuously green, which is the exact failure PD12 names: an assertion satisfiable without the feature is not a test.
Three gaps hide behind a green declaration gate, and all three are measured monthly by standards_adherence_gap.py (ADR-0351):
| Gap tier | The question | Why the meta-gate is blind to it |
|---|---|---|
| Declaration | Does the clause have enforcement at all? | advisory rows, and review rows whose checkpoint is a human, pass by design. Nothing trends whether that un-enforced share is growing. |
| Operation | Did the mechanism actually run, and pass? | The gate resolves that a script exists. A gate nothing invokes, or whose scheduled run silently stopped, resolves identically to a healthy one. |
| Conformance | Of the population governed, what share is genuinely checked? | A green gate over 40% of the estate is indistinguishable from a green gate over 100%. |
The conformance tier is the sharpest, because the estate’s guards are deliberately rollout-safe: the service test-plan gate SKIPs a service with no plan rather than failing it, eit-exceptions.yml carves out plaintext listeners, and the External Exposure Register records approved un-gated hostnames. Each is a correct engineering choice and an unmeasured adherence deficit. Every machine-readable exception register is therefore censused monthly and broken down by kind — a single summed number destroys the only actionable fact, which is which class of exception is growing.
An unevidenced control counts against the score — unknown is a finding, never a skip
Section titled “An unevidenced control counts against the score — unknown is a finding, never a skip”A machine clause whose operation cannot be evidenced is reported unknown and counted against the adherence SLI. Silently omitting the unmeasurable would inflate the score precisely where the estate is weakest, and APRA CPS 234 requires regular testing of controls — a control you cannot evidence is one you cannot claim. Making unknown cost something is what drives dead-men and coverage probes to accrete instead of stalling at “it’s wired, trust it”.
The first live baseline, 2026-08-08: 129 machine clauses, 39 evidenced-healthy — an adherence SLI of 30.2% — with 90 unknown, 0 stale, 0 uninvoked. The estate can prove its controls exist and are wired; it can evidence recent healthy operation for under a third. That number is expected to rise, and must never be raised by making the unmeasurable invisible.
Adherence is an SLI with an error budget, not a checklist
Section titled “Adherence is an SLI with an error budget, not a checklist”standards_adherence_sli = evidenced-healthy machine clauses ÷ all machine clauses, emitted as a Prometheus textfile and trended between runs. The error-budget spend is open-gap-days: a gap opened and closed inside the month costs little, one open for four months has consumed a great deal. This is the same shift already made from “backups exist” to a proven monthly restore drill, and from “we scan” to clocked remediation SLAs — it is what makes a monthly cadence meaningful rather than ceremonial.
A resolver limitation reported as an estate defect is the same lie as a gate that never fires
Section titled “A resolver limitation reported as an estate defect is the same lie as a gate that never fires”The adherence engine’s first live run reported 37 clauses as “invoked by nothing”. Every one was a bug in the resolver, not a gap in the estate: a basename-keyed self-reference guard discarding a genuine pipelines/configure_timezone.yml → playbooks/configure_timezone.yml link; un-normalised path-form mechanism tokens (pipelines/docker_host.yml never matching the basename-keyed index); and indexing only pipelines/ while hass-config uses the repo-root azure-pipelines.yml layout this corpus documents as valid.
This is the same reasoning that added hass-config to the indexed roots: under-claiming enforcement is as much a defect as over-claiming it, because both stop the table describing the estate. A report that cries wolf is worse than no report — it trains the reader to ignore it. So every finding class carries a committed negative control proven red before trusted green (Build-Hook & Pipeline-Gate Test Standard), and a resolver limitation is always fixed in the resolver, never paid for by a false finding.
Standards must ACCRETE, and the accretion is counted
Section titled “Standards must ACCRETE, and the accretion is counted”RULE 9 already obliges every session to surface a standards gap and propose the standard or amendment that closes it. Nothing counted whether that happened, so a period of zero standards growth was indistinguishable from a period of perfect coverage. A static control set is a decaying one — the CPS 234 “capability commensurate with evolving threat” lens — so accretion is now a measured output of delivery and operations, not a prose aspiration.
Three obligations make the loop real:
- Every standard cites at least one ADR. A standard with no governing decision is unilateral rule-making, not policy. Checked monthly and reported per standard.
- Every period’s accretion is counted — standards created or amended and ADRs added, derived from git history over
docs/standards/anddocs/adr/. A month with zero accretion and a growing gap register is itself the finding, reported as such. - Delivery, design and operations all feed the same register.
/wrapupfiles each surfaced gap;/grillmelayer 3’s standards-gap findings route there rather than dying in a design doc; recurring/investigateand/vulnscanfindings are gap candidates by definition.
Accretion volume is not accretion quality, so the monthly report names each change rather than only counting it — a number nobody can judge is a decoration.
Conformance checklist — adding or changing a standard
Section titled “Conformance checklist — adding or changing a standard”- The standard has a
## Enforcementsection: a table with one row per normative obligation. - Each row’s
Classismachine/review/advisory; everyreviewrow names a concrete human checkpoint; everyadvisoryrow carries a justification. - Each
machinerow’sLayerfollows the two-layer rule — deploy-time only unless the clause can break without a code change, in which case add scheduled (subject to the silent-and-matters filter). - Every scheduled mechanism names its
<check>.agedead-man in theDead-mancolumn and registers it in theJOBStable. - Every named mechanism exists in the live repo (the meta-gate resolves it) and satisfies the mechanism contract (fails closed, proven-red, alerts by class, config-as-code in the right repo).
- A
review→machinepromotion was pushed after its mechanism reached the other repo’smain, not before — there is no atomic cross-repo commit, and a docs-first push reds the publish pipeline for every concurrent session, not for its author. - A new or strengthened cross-repo gate landed non-blocking first, with the corpus repair second and the promotion to blocking third — the switch living in the gate’s own repo (cross-repo gate window). Where the gate’s subject lives in one repo, the gate lives there too and lands atomically with it. Any window opened anyway is declared when it opens.
- A machine
Mechanismcell names a concrete filename, not a glob — the resolver matches literal*.py/*.yml/*.yamlfile tokens by basename, so a wildcard likeconfigure_*_zabbix.ymlis read as a phantom_zabbix.ymland fails resolution. Name one real playbook/script and describe the family in prose. - A machine
Mechanismcell names only files that resolve — the resolver checks every.py/.yml/.yamltoken in the cell, including one added “for context”. Naming a real gate that lives outside the four indexed roots (ansible/,docker-stacks/,pitlab-docs/,hass-config/) fails the build even when the primary named gate resolves. Refer to such a gate in prose without its.py/.ymlextension, or class the obligationreviewand name the pipeline as the checkpoint. Do not apply that workaround to a gate in an indexed root — a real hard-fail script declaredreviewunder-states enforcement and is itself a violation. - A
machinerow’s resolvable mechanism is a.py/.yml/.yamlfile — a shell script (.sh) is not tokenized, so a row whose only mechanism is a.sh(e.g.secret_scan.sh) fails as “no resolvable mechanism”. Name the.ymlpipeline/playbook that invokes it (describe the.shin prose), or — if no in-tree pipeline actually runs it over the governed files — class the obligationreview. - A brand-new standard is not added to the allowlist — it conforms immediately. Only pre-existing standards are grandfathered, each with an issue and a deadline.
Enforcement
Section titled “Enforcement”This standard is its own first conformant example (the full-dogfood coherence proof — its own Rule 3 makes it two-layered):
| Obligation | Class | Layer | Mechanism | Dead-man |
|---|---|---|---|---|
Every standard has a well-formed ## Enforcement section | machine | deploy | standards_enforcement_gate.py (structural), pitlab-docs CI (ID 18) — live (#1715) | n/a |
| Every named mechanism resolves against the live repo | machine | deploy+scheduled | standards_enforcement_gate.py (resolution) — live (#1715); standards_enforcement_reconciliation.yml out-of-band re-check — live (#1716) | standards.enforce.age |
| No allowlist row is past its deadline | machine | deploy+scheduled | standards_enforcement_gate.py deadline check — live, deploy-time (#1715); standards_enforcement_reconciliation.yml deadline-sweep + paging — live (#1716) | standards.enforce.age |
| The meta-gate is proven red before trusted green | review | — | Build-Hook Standard committed regression test (/code-review) | n/a |
| A cross-repo gate lands non-blocking first, its corpus repair second, its promotion to blocking third — and any window opened anyway is declared when it opens | review | — | /code-review gate-authoring dimension (the switch must live in the gate’s own repo) plus /wrapup, which reviews whether the session opened an undeclared red window. No machine mechanism is possible or claimed: push order across two repos is not observable from any single commit, the same limitation that classes “the gate is authored BEFORE the fix” as review (ADR-0380) | n/a |
Adherence is measured monthly across all three gap tiers, with unknown counted against the SLI | machine | scheduled | standards_adherence_gap.py, monthly cron declared in schedule_config.yml — live (#2248) | standards.gapanalysis.age |
| Every standard cites at least one governing ADR | machine | scheduled | standards_adherence_gap.py governance check — reported per standard | standards.gapanalysis.age |
| Accretion is counted AND named per reporting period | machine | scheduled | standards_adherence_gap.py accretion census over docs/standards/ + docs/adr/ git history | standards.gapanalysis.age |
| A resolver limitation is fixed in the resolver, never reported as an estate defect | review | — | committed negative controls in test_standards_adherence_gap.py, reviewed at /code-review | n/a |
| Headings are self-contained and one-dense-fact | advisory | — | justification: retrieval-quality judgement, no reliable machine signal (Documentation Standard) | n/a |
Bootstrapping note (resolved 2026-07-16). All mechanisms named in the table above are now live and resolve: the deploy-time meta-gate (
standards_enforcement_gate.py, #1715), the scheduled reconciliationstandards_enforcement_reconciliation.yml+standards.enforce.agedead-man (#1716), and the Pillar-2 observability layer — thestandards-enforcementGrafana dashboard + syntheticprobe_successrollup cell (#1718, ADR-0180). With #1718 landed, this standard has been removed fromstandards-enforcement-exceptions.yml— it is no longer grandfathered and the meta-gate now resolution-checks its own## Enforcementtable strictly on every run, the full-dogfood end state. Only the retrofit of the remaining legacy standards (#1719) is still in flight.