concordat artefact rule run evaluates one canon lint rule package against a
local checkout and reports structured findings. rust-makefile-baseline audits
every governed Rust Cargo surface and the root Makefile for canonical build,
test, and lint targets (FP-003), plus a binding Whitaker lint gate
(QG-001):
concordat artefact rule run rust-makefile-baseline --repo /path/to/checkout
From rule version 0.3.2, QG-001 reads three common recipe shapes as the shell runs them, so correct Makefiles no longer draw false findings:
- A
command -vprobe followed by|| exit N, or by|| { ...; exit N; }whose only exit is a final non-zero one, fails hard when the tool is missing. It is no longer reported as a soft skip. A zero exit, an earlier exit in the block, or a probe guarding&&still is. whichcounts as an existence guard only where it is a command word. Quoted prose such asprintf '... which writes ...'does not count.- Before the gate, a Make variable whose every definition is a run of
NAME=valueassignments, such as$(GATE_RUSTFLAGS), is read as the environment prefix it expands to, including for nested surfaces. An unquoted value holding a shell operator, such asX=1||true, is not.
A Makefile that 0.3.1 reported as noncompliant or indeterminate only because of one of these shapes can now be compliant. Nothing needs migrating.
rust-toolchain-policy audits the Rust toolchain a repository builds with
(RT-022 to RT-027; see Migrating to 0.2 for adopting it):
concordat artefact rule run rust-toolchain-policy --repo /path/to/checkout
It requires that:
rust-toolchain.tomlpins the channel exactly, as a release such as1.93.1or a dated nightly such asnightly-2026-09-20, neverstableornightly(RT-022);- a pinned nightly is at most
window_daysold, 56 by default (RT-023); .github/dependabot.ymlhas arust-toolchainentry on the pin's directory with a daily or weekly interval (RT-024);- every publishable library crate declares
rust-version(RT-025, a warning); - every declared MSRV is exercised by CI: a workflow installs that toolchain,
a job or step runs cargo-msrv or is named for the MSRV, a step selects its
toolchain from
rust-version, or the pin is that version (RT-026); - no workflow names a dated nightly other than the pinned one (RT-027).
A repository whose nightly cannot move records that in
docs/developers-guide.md under a heading containing "Toolchain policy",
naming the pinned channel in the section. That exempts RT-023, RT-024 and
RT-025 while the section names the current pin. A repository with no Cargo
manifest is outside the rule. Workspace members are judged where .concordat
declares them as Rust surfaces.
dependabot-update-shape audits .github/dependabot.yml against the estate's
update shape (DB-005):
concordat artefact rule run dependabot-update-shape --repo /path/to/checkout
It requires that every updates entry:
- runs daily (
schedule.interval: daily); - ends with one catch-all group,
patterns: ["*"]andupdate-types: [minor, patch], with no other key but the defaultapplies-to: version-updates, so a major update arrives in its own pull request unless an earlier narrow group, such as a lockstep family, takes it; - places only narrow groups before the catch-all: each needs a
patternslist in which no pattern is a bare wildcard, such as a lockstep family likerstest-bdd*; - sets
versioning-strategy, on acargoentry, only toautoorlockfile-only.
When the repository has an action manifest under .github/actions, its
github-actions entries must also cover / and every directory holding one,
for example with directories: ["/", "/.github/actions/*"]. * matches one
path segment and ** any number, as in Dependabot. A repository without a
Dependabot configuration passes, and a configuration that cannot be decoded is
reported as indeterminate.
dependency-family-contracts reports whether the dependency relationships the
estate has recorded are protected by the Dependabot configuration (report-only):
concordat artefact rule run dependency-family-contracts --repo /path/to/checkout
A relationship applies only where the repository declares every member:
- a family or type-identity pair (for example
pyo3withpyo3-build-config, orpyo3with theserde-pyobjectbridge) needs one narrow group taking both ahead of the catch-all (DF-001), and not shadowed by an earlier group (DF-003). Dependabot matches the published package name, so a bridge renamed inCargo.toml(pyo3-serde = { package = "serde-pyobject" }) is named in the group byserde-pyobject, not by the alias, and anignorerule would freeze the pair rather than move it; - independently versioned packages (for example maturin with PyO3) need an upper
bound or an
ignorerule on a member (DF-002); an exact pin is not one; - a missing configuration, an unreadable manifest or lock, a Python project
outside the root, or a Cargo manifest outside the governed Rust surfaces is
reported as indeterminate (DF-006). A repository that has not declared
language.rust.surfacesis read at its root only. Each nestedCargo.toml(hidden,target,vendor,node_modulesand similar trees excepted) is then read loosely: one that declares a package a recorded relationship names, or that cannot be read or decoded, is named and makes the result indeterminate until the repository declares its surfaces. A nested manifest that names no related package draws nothing. A directory that cannot be listed during that scan is reported the same way.
To keep an older generation deliberately, record an exception in .concordat:
dependencies:
family_exceptions:
- relationship: pyo3-build-config-family
rationale: the extension stays on the 0.28 generation
review_trigger: review when PyO3 0.30 ships
main-owned-codescene-coverage audits the coverage topology (CV-005):
concordat artefact rule run main-owned-codescene-coverage --repo /path/to/checkout
It requires that:
- pull-request coverage uses the main-derived local ratchet, sets
publish-artefact: 'false', and neither invokes CodeScene nor receivesCS_ACCESS_TOKEN; - exactly one workflow triggered by a push to
main(optionally alongsideworkflow_dispatch) writes that baseline and uploads, from a step guarded ongithub.ref == 'refs/heads/main'as well as the credential, under aconcurrencyblock that queues rather than cancels. The comparison must be a whole&&conjunct, and a condition carrying an unquoted||counts as unguarded, because the disjunction makes every other conjunct optional. The credential guard is the output of an earlier step in the same job, which runs exactlyecho "available=${{ secrets.CS_ACCESS_TOKEN != '' }}" >> "$GITHUB_OUTPUT"with noif:and noenv; the upload testssteps.<id>.outputs.available == 'true'and passes${{ secrets.CS_ACCESS_TOKEN }}directly to itsaccess-tokeninput. The uploader'smodedefaults toupload, so a step that omits the input satisfies this;checkandinstalldo not; - the job holding the upload step declares
environment: codescene, and no other job, and no pull-request workflow, declares it; - a workflow that uploads to CodeScene names
CS_ACCESS_TOKENonly in that check step's command and in the upload'saccess-tokeninput: never in anenvblock at any level, anotherrunbody, another action's input or a condition; - no workflow passes
installer-checksum, referencesCODESCENE_CLI_SHA256, or refreshes the CodeScene installer digest; - every platform that ratchets coverage on pull requests also ratchets on the
trunk push, because the baseline is keyed by
runner.osand only a push tomainwrites it.
Malformed YAML is reported as indeterminate rather than assumed to comply. A job that delegates to a reusable workflow is reported by name, while the workflow's other jobs are still evaluated. A coverage job's runner platform is read from its literal labels; a label written as an expression is classified only when every literal it could select agrees, and is otherwise indeterminate. A repository that generates no coverage anywhere is not a subject of this rule.
Adopting the rule in a repository removes a quality gate from its pull-request
lane, so an adoption takes a full review rather than a mechanical merge on
green. Two things follow an adoption and are expected, not defects: the
CodeScene Code Coverage (main) check never completes on a pull request,
because it waits for an upload no pull-request lane sends, and it is required
nowhere; and a CodeScene project whose gates configuration is absent fails
check mode on every pull request, which is a property of that project's
configuration rather than of the repository's workflows.
CV-005 0.2.0 changes what a compliant publisher looks like. A repository that adopted an earlier version moves as follows:
- Add a step before the upload, in the same job, with an
idand noif:orenv, whose only command isecho "available=${{ secrets.CS_ACCESS_TOKEN != '' }}" >> "$GITHUB_OUTPUT". - Guard the upload on
steps.<id>.outputs.available == 'true'beside the main-ref comparison. A condition namingCS_ACCESS_TOKENitself no longer counts as a guard. - Pass
${{ secrets.CS_ACCESS_TOKEN }}directly to the upload action'saccess-tokeninput, and remove everyenvbinding of the token at workflow, job and step level. - Replace any direct
cs-coverage uploadcommand with theupload-codescene-coverageaction, which binds the token from its input.
CV-005 0.3.0 adds the environment that holds the token. A repository on 0.2.0 moves as follows:
- Declare
environment: codesceneon the upload job, and on no other job; a pull-request workflow declares it on none. GitHub reads environment names without regard to case, soCodeScenenames the same environment. - In the repository settings, create that environment with a custom
deployment branch policy admitting the branch
mainalone, and moveCS_ACCESS_TOKENfrom the repository secrets into it. The Concordat Auditor reports each step of that move (CV-006).
With the environment declared, a workflow_dispatch of the publisher from any
branch but main is refused for the whole job, matrix legs that do not upload
included, rather than skipped at the upload step. A branch proof of a coverage
change runs through the pull-request lane's coverage run instead.
Options:
--repo PATH— the checkout to audit (defaults to the current directory).--format {table,json}— output format (defaults totable).
Verdicts are three-valued. compliant means the finding set is empty;
noncompliant means the policy proved a violation; indeterminate means the
policy could not prove compliance and fails closed (for example, the Makefile
includes other files, or the parse had to recover from syntax errors).
Exit codes: 0 compliant; 1 at least one finding, including indeterminate
verdicts; 2 operational failure (for example, the pinned makeutil or
conftest executable is missing), reported on standard error.
An unreadable file is not an absent one. If the root Cargo.toml or the root
Makefile exists but cannot be inspected, because of permissions or any other
filesystem failure, the command exits 2 and names the path rather than
reporting the checkout as having no Rust surface or no Makefile. Only a genuine
absence produces the no-applicability result.
A rule package the command cannot choose a policy input for is one of those
operational failures, not a finding. Every shipped package either has a
built-in input or declares one as sensor.input in its rule.yaml; a package
with neither is refused, with the registered packages and the declarable kinds
named in the message. There is no fallback, because a policy handed an envelope
of the wrong shape does not fail — it answers confidently about facts it was
never asked about, and the command emits that answer as a verdict rather than
an error.
For Rust below the repository root, declare each Cargo.toml in .concordat
under language.rust.surfaces. The declaration is authoritative; an empty list
states that no Rust surface is governed, while an absent declaration retains
the root-Cargo.toml compatibility fallback. For example:
language:
rust:
surfaces:
- path: rust/Cargo.toml
The root Makefile remains the only Makefile audited. It must define the
canonical targets and qualify Whitaker for every declared surface: use a direct
cd rust && $(WHITAKER) command or a direct literal
$(WHITAKER) --manifest-path rust/Cargo.toml argument. When a checkout governs
both Cargo.toml and rust/Cargo.toml, the root surface still needs a gate
that runs in the root context. The rule proves only prerequisites and complete,
literal same-file $(MAKE) target chains; conditionals, dynamic recursion,
includes, and ambiguous shell forms are indeterminate.
The command requires two external tools on PATH: conftest and the pinned
makeutil (see
platform-standards/canon/lint-rules/rust-makefile-baseline/README.md for the
pin and regeneration workflow).
Auditing Whitaker provisioning
The whitaker-provisioning package audits how a checkout provisions the
Whitaker lint suite (QG-002):
concordat artefact rule run whitaker-provisioning --repo /path/to/checkout
The estate provisions Whitaker one way: the shared-actions install-whitaker
action, pinned to a revision listed in the rule's
compliant_install_whitaker_refs parameter. The action installs an exact
whitaker-installer version, never pins the lint suite, and always passes
--no-source-fallback; its own contract proves those rules, so this package
does not re-check installer flags. It reports:
- an install of
whitaker-installer,cargo-dylintordylint-linkby Cargo or another action, a download of one, or a directwhitaker-installerrun, in a workflow, a composite action, a Makefile or a script; - an
install-whitakerpin that is not listed, a fork of the action, or a local copy of it.
The list holds only first-parent commits on shared-actions main: a commit on
a side branch that was merged in never qualifies, even when its tree matches. A
first-parent commit qualifies when it is an approved root, or descends from one
and leaves the install-whitaker directory content-identical to it; a commit
that changes the directory is refused until a reviewer adds it as an approved
root (install_whitaker_roots in the rule's parameters). After shared-actions
gains commits, regenerate the list from a full (not shallow) clone and verify
it:
uv run python scripts/whitaker_revisions.py sync ../shared-actions
uv run python scripts/whitaker_revisions.py check ../shared-actions
uv run python scripts/whitaker_revisions.py list ../shared-actions
sync rewrites the rule manifest's list, check exits 1 and names each
missing or non-derivable revision when the manifest's membership has drifted
(it does not compare order, so a reordered manifest passes), and list prints
what the clone derives, oldest first. Each accepts --tip <ref> (default
origin/main). A status of 2 means the clone or manifest could not be read.
Whitaker itself is exempt as the producer, and a developer-environment script
that CI never runs may be exempted by name, repository and reason in the rule's
parameters. Exemptions match the repository slug read from the origin remote.
See platform-standards/canon/lint-rules/whitaker-provisioning/README.md for
what the policy recognizes and what it declines to judge.
Auditing the Rust build defaults
The rust-build-defaults package audits the estate's build standard:
concordat artefact rule run rust-build-defaults --repo /path/to/checkout
It reads the files Cargo and rustup auto-discover, and the Makefile and
workflows whose builds replace them, so it needs conftest and makeutil on
PATH. Four clauses read the configuration:
- the parallel
rustcfrontend is carried by everyrustflagssource, whererust-toolchain.tomlpins a nightly channel; - the
moldlinker is named in a source that applies on Linux, and in no source that reaches beyond Linux; - the sources are repeated rather than merged, because Cargo replaces one with
another instead of combining them. Backend flags in a target-specific source
remain local to that target, whereas a backend named by
[build].rustflagsis a build-wide default and must survive each target source that replaces it; - the Cranelift codegen backend is the development-profile default, or the repository records an exception.
The backend clause accepts two states. Configure the backend where the repository's own suite passes under it:
[unstable]
codegen-backend = true
[profile.dev]
codegen-backend = "cranelift"
Where the suite fails under it, record the exception instead: a section in
docs/developers-guide.md whose heading names the backend, naming the failing
tests and the toolchain the measurement was taken on. The audit checks that the
section names the channel rust-toolchain.toml pins, so bumping the pin past
the measurement reports that the recorded state no longer covers the pinned
toolchain. Clearing that is a line in the section saying what the pin is now
and whether a fresh measurement was taken; the audit does not claim to know
whether one is owed. A repository with neither the backend nor an exception is
noncompliant.
Note that a package override beneath the profile is not the profile's default:
Cargo applies [profile.dev.package."serde"] codegen-backend to that package
alone.
Both the document list and the heading keyword are rule parameters, so a repository that records the exception elsewhere can be accommodated without changing the policy. So are the two flags, the backend name, and the platform list that makes the linker clause applicable.
Three more clauses read the builds that replace those defaults:
- BD-007 — where Cranelift is the development default, every
cargo llvm-covrun, in a Makefile recipe or a workflow step, selects LLVM, because coverage instrumentation is LLVM-only. So does a build without--releasein a release- or tag-triggered workflow. Prefix the command withCARGO_PROFILE_DEV_CODEGEN_BACKEND=llvm, export it, or pass--profilefor a profile that selectsllvm. Where the test profile also selects Cranelift, setCARGO_PROFILE_TEST_CODEGEN_BACKEND=llvmtoo, because the dev-profile override leaves tests on Cranelift and coverage instrumentation then fails. A shared-actionsgenerate-coveragestep needs nothing: it selects LLVM itself. For a coverage command selecting the release profile, a root-manifest release backend of LLVM proves that profile uses LLVM; an explicit Cranelift backend is noncompliant. A package-level Cranelift override, or a relevant manifest fact that is unavailable or malformed, leaves the LLVM selectionindeterminate. A package-level LLVM override does not prove the profile's default backend. BD-007 checks package overrides for the selected profile in both rootCargo.tomland.cargo/config.toml; profile-level LLVM alone cannot prove coverage if a relevant package override is unresolved or selects Cranelift. - BD-008 — a Makefile recipe reachable from
lint,test,typecheckorbuildthat assignsRUSTFLAGSrestates-Zthreads=8(on a nightly toolchain only) and themoldlinker flag (where the build targets Linux). An assignedRUSTFLAGSreplaces everyrustflagssource, soRUSTFLAGS="-D warnings"alone turns the standard off for that gate. netsuke'sGATE_RUSTFLAGSis the reference shape.
A directly invoked, installer-managed whitaker binary is the exception to
retaining those development flags: its toolchain is separate from the
repository's Cargo build. The audit requires one statically known RUSTFLAGS
boundary for that route and rejects development frontend, Cranelift, or
mold flags there, including encoded flags and explicit development/test
profile overrides. Missing or unresolved values, multiple possible
assignments, or a separate Cranelift profile backend are indeterminate.
Only a recognized direct whitaker invocation receives this treatment; an
unknown wrapper or executable substitution does not. This checks explicit
Make and Cargo configuration facts. It does not inspect the caller's ambient
environment or prove the suite's runtime backend. Ordinary Cargo commands and
test runners remain subject to the fast flag requirement.
- BD-009 — a workflow step that runs cargo build, check, clippy,
doc, nextest or test directly with RUSTFLAGS set carries the same
flags: -Zthreads=8 unless the step runs a non-nightly toolchain, and the
mold linker flag only when the job runs on a literal Linux runner
(ubuntu-*, ubicloud-* or a linux label). shared-actions setup-rust
sets it to -D warnings by default for every later step; pass
rustflags: '' to leave the configuration in charge, or a value carrying the
fast flags.
A Makefile makeutil cannot fully parse makes BD-007 and BD-008
indeterminate rather than passed; the other clauses still report.
A workflow file that does not decode (invalid YAML, not UTF-8 text, or not a
mapping) is reported as indeterminate under BD-009, naming the file and that
category; the file is never skipped silently.
Upgrading to rule 0.2.0: a repository that passed 0.1.1 can now fail or report
indeterminate, because BD-007 to BD-009 read the Makefile and the workflows,
which 0.1.1 never opened.
- Install the pinned
makeutilonPATHbesideconftest; the command now needs it for any checkout that has a rootMakefile, even one whose configuration alone would have passed. - Where Cranelift is the development default, add the LLVM selection to every
cargo llvm-covrecipe or step and every non---releasebuild in a release- or tag-triggered workflow (BD-007), and the test-profile override where the test profile also selects Cranelift. - Restate
-Zthreads=8(nightly toolchains only) and themoldlinker flag (Linux builds only; a literal Linux runner for BD-009) in every gate recipe that assignsRUSTFLAGS, or in the workflow step that sets it, or let the Cargo configuration decide by leavingRUSTFLAGSunassigned (BD-008 and BD-009). For a shared-actionssetup-ruststep, passrustflags: ''or a value that carries the flags. - A Makefile
makeutilrefuses, or a gate that delegates through-C,-for a computed target, isindeterminate, not compliant. Write the delegation literally, as$(MAKE) target, to have it followed. - Clauses BD-001 to BD-006 read the same files as before, so their verdicts do not change.
Auditing the spelling gate
The spelling-config-baseline package audits a checkout's spelling gate
against typos-config-builder's 0.1.x design, using leynos/cuprum as the
reference wiring:
concordat artefact rule run spelling-config-baseline --repo /path/to/checkout
It applies when the checkout has any part of a spelling setup (a spelling
target, typos.toml, typos.local.toml, vendored machinery, or a workflow
that mentions Typos), and reports:
- PD-007: a recipe reachable from
spellingrunstypos-config-builder gate, throughuvxoruv tool runwith--fromnaming the builder repository at a release tag at or abovev0.1.3, and its exit status reaches Make. A commit or branch pin,--check, the render-only default command, and a direct Typos run are each reported. - PD-008: no
TYPOS_VERSION,PATHSPEC_VERSIONor builder commit variable, and nospelling-helper-test,spelling-phrase-checkorspelling-configtarget. - PD-009: no vendored generator,
typos_rollout*script, phrase-check script, or their tests. - PD-010: no workflow runs Typos directly or drift-checks
typos.toml; CI runsmake spelling. - PD-011:
.gitignoreignores.typos-oxendict-base.jsonand.typos-oxendict-base.toml, with the last matching pattern deciding as in Git, so a later!pattern unignores the cache. - PD-012:
typos.local.tomlexists and declaresschema = 1. - PD-013:
AGENTS.mdcarries typos-config-builder's spelling block, copied verbatim from the pinned release'sdocs/agents-md-spelling.mdbetween itstypos-config-builder:agents-mdmarkers, and gives no othermake spellingortypos.tomlguidance outside it.
typos.toml is regenerated on every run and is never checked for drift. The
builder release is pinned, but the shared dictionary it reads is live, so an
edit to it reaches every repository on its next run.
Auditing the YAML lint gate
The yaml-lint-baseline package audits how a checkout lints its YAML and
GitHub Actions workflows:
concordat artefact rule run yaml-lint-baseline --repo /path/to/checkout
It applies when the checkout has any YAML file, and reports:
- YL-001: a recipe reachable from
lintrunsyamllint --strictover agit ls-fileslist that names*.ymland*.yaml, and its exit status reaches Make. Write it asgit ls-files -z '*.yml' '*.yaml' | xargs -0 -r $(YAMLLINT) --strict --.-d,--config-dataand--no-warningsare reported. - YL-002: a recipe reachable from
lintruns actionlint with a binding exit status, and does not switch off its shellcheck integration with an empty-shellcheck=. - YL-003:
.yamllint-estate.ymlis the canonical estate base, copied verbatim fromcanon/lint/yaml/..yamllint.ymlextends it and sets onlyextendsandignore; a localignorereplaces the base's, so a repository restates any base pattern it still needs..yamllintand.yamllint.yamlare reported, because yamllint reads them before.yamllint.yml. - YL-004:
.github/actionlint.yamllistsubicloud-*underself-hosted-runner.labels. - YL-005: every CI job that runs
make lintfirst usesleynos/shared-actions/.github/actions/install-yaml-lint, pinned to a full commit SHA, and no step installs yamllint, actionlint or shellcheck by hand or lints through a third-party action. A job that calls a reusable workflow makes the absence of a lint job indeterminate.
yamllint must run from the repository root, because it resolves extends:
against the current directory.
Auditing the uv gate
The uv-gate-baseline package audits how a checkout reaches uv, against the
vendored uv_gate.py helper (see the shared-actions users' guide for the
helper itself):
concordat artefact rule run uv-gate-baseline --repo /path/to/checkout
It applies when the checkout uses uv (a recipe, workflow or action that runs
it, a uv.lock, or the helper) and reports:
- UV-001:
scripts/uv_gate.pyexists and matches a canonical digest. - UV-002: nothing assigns
UV_CACHE_DIRorUV_TOOL_DIR, in the Makefile, workflows, or composite actions. - UV-003: no recipe runs
uv run,uv sync,uv tool runoruvxexcept through$(UV_GATE), andUV_GATEispython3 scripts/uv_gate.py. - UV-004:
uv.lockexists besidepyproject.toml. - UV-005: no recipe refreshes, upgrades, locks, purges the cache or retries
around uv; a
locktarget may lock and upgrade. - UV-006: every tool spec is
name==VERSION,name@VERSION,git+URL@<full commit SHA>orname[extra] @ git+URL@<full commit SHA>, optionally followed by a fragment such as#subdirectory=x(written\#in a Make variable's value, which Make turns into#). A short SHA, a branch, a suffix after the SHA, or a\#in a recipe line is not a pin. A variable in a workflow step is resolved from theenv:of the workflow, job and step (innermost wins); one it cannot resolve to a literal is indeterminate. typos-config-builder is also accepted at a release tag when it is run directly withuvx, because the spelling baseline requires that form and the helper refuses a Git spec that is not a commit. - UV-007: Git dependencies in
pyproject.tomlare pinned to a full commit.
An include, a recovered Makefile parse, an unresolvable variable, or a file
that cannot be decoded makes the affected check indeterminate rather than
passing it, even when the checkout shows no other sign of using uv.
Auditing documentation presence (Python and Rust)
The documentation-presence package audits the interrogate gate that keeps
every Python definition documented:
concordat artefact rule run documentation-presence --repo /path/to/checkout
It applies when the checkout owns Python files (outside fixtures, vendored
and cached trees) and reports:
- DG-001: a command that
make lintreaches (through prerequisites or$(MAKE) target) runs interrogate at a threshold of at least 100, with no relaxing flag (--ignore-nested-functionsand the like, or the same switches in[tool.interrogate]), no exclusion beyond test data and vendored trees, and every owned Python file, including.github/**scripts, under one run's paths. - DG-002: the run is exact. It prints every definition (
-vv, orverbose = 2in the configuration) and its recipe fails on anyMISSEDrow, because interrogate rounds its percentage to one decimal and so passes a lone gap in a large repository.
For a checkout that owns Rust library crates the same package also reports:
- DG-003: every library crate denies
missing_docs, through its own[lints.rust], through a workspace[workspace.lints.rust]table that the crate inherits with[lints] workspace = true, or through a crate-root#.warnandallowfail; a crate-root#![expect(missing_docs, reason = "...")]is accepted for generated items, and anexpectwithout a reason fails. Any module-level or item-levelalloworexpectofmissing_docsin the library's sources also needs a reason. - DG-004: a clippy run is reachable from
make lint, or a recorded clippy-gate exception names a wrapper script that is. Record the exception in adocs/developers-guide.mdsection whose heading contains "Clippy gate" and whose body names the script in backticks; the script must exist, run clippy through cargo, and be run by a command in a recipemake lintreaches.
The audit reads a command the way the shell runs it. An environment prefix is
not part of the command, so
UV_CACHE_DIR=.uv-cache uv tool run ... interrogate and
env -i X=1 uv run interrogate ... are runs of interrogate, and
X=1 $(MAKE) interrogate reaches the interrogate target. A Make prefix (@,
-, +) is ignored. A quoted "X=1" is a command name, not an assignment,
and a word that merely names the tool (true interrogate ..., or the package in
uv run --with interrogate) is no run. The vendored helper counts as a
launcher:
python3 scripts/uv_gate.py tool --from interrogate==1.7.0 -- interrogate … is
a run. make -C dir target runs another Makefile, so the walk cannot follow it
and reports the reachability as unresolved.
A variable the audit cannot resolve to one value, a configuration file that is
missing or not TOML, or a lint target it cannot follow makes the affected
check indeterminate rather than passing it. The package README carries the
reference Makefile wiring.
Upgrading to rule 0.3.1
Version 0.3.1 is a patch correction to recognition of the existing canonical helper contract. Once this revision is available and adopted, re-run the audit; stored reports do not change automatically. Previously unrecognized valid helper invocations can then be measured, without automatically making their repositories compliant.
Use python or python3 with the repository-relative scripts/uv_gate.py
path; ./scripts/uv_gate.py is also supported. For the pinned tool route,
retain the existing interrogate pin and command separator:
python3 scripts/uv_gate.py tool --from interrogate==1.7.0 -- interrogate -vv -f 100 .
The prepared-environment route is also recognized:
python3 scripts/uv_gate.py run -- interrogate -vv -f 100 .
For run, keep exactly one -- before the command. Before that separator,
both modes reject flags that override the helper's network, cache, locking, or
freshness policy, including --offline, refresh, upgrade, and reinstall flags
and their forbidden short aliases. The helper controls those settings itself.
Invalid helper paths, unpinned tool specifications and refused argument forms
receive no gate credit.
Keep the existing DG-001 and DG-002 requirements: make lint must reach the
gate, cover every owned Python file at a threshold of 100, print definitions at
verbose level 2 and fail on any MISSED row. Unsupported Make expansion that
prevents proving execution or its arguments remains indeterminate, as do
unreadable configuration and unprovable target reachability. Recognition of a
helper invocation does not clear these findings or relax the coverage checks.
Upgrading to rule 0.3.0
Version 0.3.0 adds the clippy-gate exception: a repository whose make lint
reaches clippy only through a wrapper script can record why, and name the
script, to satisfy DG-004. Nothing changes for a repository without the record.
Upgrading to rule 0.2.0
Version 0.2.0 of documentation-presence adds the Rust rules. Repositories
that own no Rust library crate see no change. A repository that owns one is now
audited for DG-003 and DG-004, including a Rust-only repository that owns no
Python, which 0.1.0 treated as out of scope. To remediate:
- Set
missing_docs = "deny"in[workspace.lints.rust](or in each crate's[lints.rust]) and add[lints] workspace = trueto every member that should inherit it. Document the public items the lint then reports. - Give every module-level or item-level
allow(missing_docs)orexpect(missing_docs)areason, or remove it. Only generated or macro-produced items justify one. - Make sure
make lintreaches acargo clippycommand written in the recipe (a wrapper script that runs clippy out of sight reads as noncompliant). Indeterminate findings name the manifest, source file or recipe line the audit could not read.
Auditing Markdown formatting wiring
The markdown-formatting-baseline package audits how a checkout formats and
lints its Markdown, using leynos/netsuke as the reference wiring:
concordat artefact rule run markdown-formatting-baseline --repo /path/to/checkout
It applies wherever a Markdown file exists outside dependency, build, and tool cache directories, and reports:
- FP-003 — the root
Makefileexists and definesfmtandcheck-fmt. - PD-002 — a recipe reachable from
check-fmtrunsmdtablefix --check --git --include-untracked --wrap --renumber --breaks --ellipsis --fences, and its exit status reaches Make. The rewrite flags are required as well as the selection flags: a check without--wrappasses files the formatter would still change. - PD-003 — a recipe reachable from
fmtrunsmdtablefix --in-placewith the same flags directly, not through themdformat-allwrapper. - PD-004 — a recipe reachable from
fmtrunsmarkdownlint-cli2 --fixdirectly, not through themdformat-allwrapper. - PD-005 —
.markdownlint-cli2.jsoncexists and carries the baselineconfigentries verbatim, setsgitignoretotrue(so--fixleaves Git-ignored Markdown alone), and lists every baselineignoresglob. Further rules and globs may be added; the canonical file to copy isplatform-standards/canon/lint/markdown/.markdownlint-cli2.jsonc. - PD-006 — CI lints Markdown through
DavidAnson/markdownlint-cli2-actionpinned to a full commit SHA withglobs: '**/*.md'. A workflow step that runsmarkdownlint-cli2from a shell, or drivesmake markdownlint, is noncompliant. A step that only installs the linter is noncompliant unless a compliant action step lints Markdown in the same workflow, in which case it is provisioning for something else (a test suite that runs the linter as a subprocess, say). An action step guarded by a literally false condition, or sitting in a job guarded by one, never runs and does not satisfy the check. The pin must name a commit, not an annotated tag object: the rule asks the GitHub API what each pin names, reports a tag object as noncompliant with the commit to pin instead, and reports PD-006 asindeterminatewhen it cannot reach the API. Each distinct pin costs one or two API calls, and a pin repeated within one run is looked up once and the answer reused. Unauthenticated, GitHub allows 60 calls an hour, so a sweep over many repositories spends it quickly and every later pin reports indeterminate. The rule therefore authenticates withGITHUB_TOKENor the concordat credentials when either is set, and otherwise withgh auth tokenfor the API's own host when the GitHub CLI is logged in there. A pin that could not be checked because the limit was spent says so, and names the remedy (wait for the reset, or use a token with quota left), rather than reading like an unknown pin. Pass--github-api-urlto use another API root, such as a GitHub Enterprise Server. An unreadable credentials file stops the run with exit code 2.
Definite answers, a commit or a tag object, are also kept on disk, because
what a SHA names never changes. A later run, in any repository, asks GitHub
nothing about a pin it has already seen. A refusal, a spent rate limit or an
unreachable API is never kept, so the next run asks again. The cache lives in
$XDG_CACHE_HOME/concordat/action-pins, or ~/.cache/concordat/action-pins
when that is unset. CONCORDAT_PIN_CACHE_DIR or --pin-cache-dir names
another directory (the flag wins), and --no-pin-cache neither reads nor
writes it. An entry that cannot be decoded is deleted and asked again, so
removing the directory is always safe. Answers are kept per API root, so a
--github-api-url for a GitHub Enterprise Server never receives an answer
cached from github.com.
The Makefile checks expand Make variables that are assigned exactly once and
unconditionally, so $(MDTABLEFIX) --check $(MDTABLEFIX_SELECT) is audited
through its values. The tool must be the command word of its segment; a mention
inside echo, an assignment, or a comment does not count. A variable the rule
cannot resolve, a conditional rule or include on the path, or a workflow it
cannot decode is reported as indeterminate.
The canonical recipes are:
MDTABLEFIX ?= mdtablefix
MDLINT ?= markdownlint-cli2
MDTABLEFIX_SELECT = --git --include-untracked
MDTABLEFIX_RULES = --wrap --renumber --breaks --ellipsis --fences
fmt:
$(MDTABLEFIX) --in-place $(MDTABLEFIX_SELECT) $(MDTABLEFIX_RULES)
@unset FORCE_COLOR; $(MDLINT) --fix "**/*.md"
check-fmt:
$(MDTABLEFIX) --check $(MDTABLEFIX_SELECT) $(MDTABLEFIX_RULES)
and the canonical CI step is:
- name: Lint Markdown
uses: DavidAnson/markdownlint-cli2-action@21c1be1b93ad9ed58fa840aacc3f279cde2a72ff # v24.2.0
with:
globs: '**/*.md'
Adopting the baseline in a repository already using mdformat-all
The wrapper is noncompliant under PD-003 and PD-004, so a repository moving to the baseline changes three things and gains one prerequisite:
- Replace the
mdformat-allcall infmtwith the two direct invocations above, and givecheck-fmtits ownmdtablefix --checkline.check-fmtpreviously did no Markdown checking at all in some repositories. - Install
mdtablefix0.6.0 or later. Earlier releases have neither--checknor--git, so both recipes fail immediately against one. Pin the same version in continuous integration. - Replace any
npm install -g markdownlint-cli2step, and any workflow step running the linter ormake markdownlint, with the pinned action step above. Copy.markdownlint-cli2.jsoncfromplatform-standards/canon/lint/markdown/if the repository has none.
Expect the first make fmt after the change to rewrite more files than usual.
--git --include-untracked reaches every Markdown file Git tracks plus the
untracked files it does not ignore, including hidden directories such as
.rules/ that a wrapper's directory walk skipped. Commit that reformatting on
its own so the wiring change stays readable.
Upgrading to rule 0.2.0: PD-002 and PD-003 now require the rewrite flags
--wrap --renumber --breaks --ellipsis --fences as well as the selection flags
on every mdtablefix invocation. A repository whose check-fmt passed only
--git --include-untracked was compliant under 0.1.0 and is noncompliant under
0.2.0, because that check applies no rewrite and passes files fmt would still
change. To comply, pass $(MDTABLEFIX_RULES) in both targets as in the recipes
above, then run make fmt and commit the reformatting on its own. The default
list is the rule parameter mdtablefix_rule_flags, and an override replaces it
for that package; mdtablefix_select_flags works the same way.
Upgrading to rule 0.3.0: PD-005 now requires .markdownlint-cli2.jsonc to set
the boolean "gitignore": true. A configuration without the key, or with
false, was compliant under 0.2.0 and is noncompliant under 0.3.0, because
markdownlint-cli2 --fix then rewrites Git-ignored Markdown that mdtablefix
leaves alone. To comply, add "gitignore": true beside config and ignores,
or copy the canonical
platform-standards/canon/lint/markdown/.markdownlint-cli2.jsonc, then run
make fmt and commit any reformatting on its own.
Run the audit to confirm the result:
concordat artefact rule run markdown-formatting-baseline --repo .
Sweeping the Rust estate
scripts/parabellum_sweep.py audits every repository listed in
docs/parabellum/estate.yaml and appends one record per repository to the
append-only campaign ledger docs/parabellum/ledger.jsonl:
uv run python -m scripts.parabellum_sweep [--only a,b] [--limit N] [--force]
uv run python -m scripts.parabellum_sweep report
A repository already ledgered at its current head commit is skipped unless
--force is given, so an interrupted sweep resumes by re-running the same
command. The report subcommand regenerates
docs/parabellum/baseline-report.md from the ledger.