Auditing a checkout against a lint rule package

Updated Oct 17, 2026

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 -v probe 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.
  • which counts as an existence guard only where it is a command word. Quoted prose such as printf '... which writes ...' does not count.
  • Before the gate, a Make variable whose every definition is a run of NAME=value assignments, 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 as X=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.toml pins the channel exactly, as a release such as 1.93.1 or a dated nightly such as nightly-2026-09-20, never stable or nightly (RT-022);
  • a pinned nightly is at most window_days old, 56 by default (RT-023);
  • .github/dependabot.yml has a rust-toolchain entry 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: ["*"] and update-types: [minor, patch], with no other key but the default applies-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 patterns list in which no pattern is a bare wildcard, such as a lockstep family like rstest-bdd*;
  • sets versioning-strategy, on a cargo entry, only to auto or lockfile-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 pyo3 with pyo3-build-config, or pyo3 with the serde-pyobject bridge) 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 in Cargo.toml (pyo3-serde = { package = "serde-pyobject" }) is named in the group by serde-pyobject, not by the alias, and an ignore rule would freeze the pair rather than move it;
  • independently versioned packages (for example maturin with PyO3) need an upper bound or an ignore rule 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.surfaces is read at its root only. Each nested Cargo.toml (hidden, target, vendor, node_modules and 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 receives CS_ACCESS_TOKEN;
  • exactly one workflow triggered by a push to main (optionally alongside workflow_dispatch) writes that baseline and uploads, from a step guarded on github.ref == 'refs/heads/main' as well as the credential, under a concurrency block 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 exactly echo "available=${{ secrets.CS_ACCESS_TOKEN != '' }}" >> "$GITHUB_OUTPUT" with no if: and no env; the upload tests steps.<id>.outputs.available == 'true' and passes ${{ secrets.CS_ACCESS_TOKEN }} directly to its access-token input. The uploader's mode defaults to upload, so a step that omits the input satisfies this; check and install do 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_TOKEN only in that check step's command and in the upload's access-token input: never in an env block at any level, another run body, another action's input or a condition;
  • no workflow passes installer-checksum, references CODESCENE_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.os and only a push to main writes 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 id and no if: or env, whose only command is echo "available=${{ secrets.CS_ACCESS_TOKEN != '' }}" >> "$GITHUB_OUTPUT".
  • Guard the upload on steps.<id>.outputs.available == 'true' beside the main-ref comparison. A condition naming CS_ACCESS_TOKEN itself no longer counts as a guard.
  • Pass ${{ secrets.CS_ACCESS_TOKEN }} directly to the upload action's access-token input, and remove every env binding of the token at workflow, job and step level.
  • Replace any direct cs-coverage upload command with the upload-codescene-coverage action, 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: codescene on the upload job, and on no other job; a pull-request workflow declares it on none. GitHub reads environment names without regard to case, so CodeScene names the same environment.
  • In the repository settings, create that environment with a custom deployment branch policy admitting the branch main alone, and move CS_ACCESS_TOKEN from 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 to table).

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-dylint or dylint-link by Cargo or another action, a download of one, or a direct whitaker-installer run, in a workflow, a composite action, a Makefile or a script;
  • an install-whitaker pin 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 rustc frontend is carried by every rustflags source, where rust-toolchain.toml pins a nightly channel;
  • the mold linker 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].rustflags is 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-cov run, in a Makefile recipe or a workflow step, selects LLVM, because coverage instrumentation is LLVM-only. So does a build without --release in a release- or tag-triggered workflow. Prefix the command with CARGO_PROFILE_DEV_CODEGEN_BACKEND=llvm, export it, or pass --profile for a profile that selects llvm. Where the test profile also selects Cranelift, set CARGO_PROFILE_TEST_CODEGEN_BACKEND=llvm too, because the dev-profile override leaves tests on Cranelift and coverage instrumentation then fails. A shared-actions generate-coverage step 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 selection indeterminate. A package-level LLVM override does not prove the profile's default backend. BD-007 checks package overrides for the selected profile in both root Cargo.toml and .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, typecheck or build that assigns RUSTFLAGS restates -Zthreads=8 (on a nightly toolchain only) and the mold linker flag (where the build targets Linux). An assigned RUSTFLAGS replaces every rustflags source, so RUSTFLAGS="-D warnings" alone turns the standard off for that gate. netsuke's GATE_RUSTFLAGS is 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 makeutil on PATH beside conftest; the command now needs it for any checkout that has a root Makefile, even one whose configuration alone would have passed.
  • Where Cranelift is the development default, add the LLVM selection to every cargo llvm-cov recipe or step and every non---release build 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 the mold linker flag (Linux builds only; a literal Linux runner for BD-009) in every gate recipe that assigns RUSTFLAGS, or in the workflow step that sets it, or let the Cargo configuration decide by leaving RUSTFLAGS unassigned (BD-008 and BD-009). For a shared-actions setup-rust step, pass rustflags: '' or a value that carries the flags.
  • A Makefile makeutil refuses, or a gate that delegates through -C, -f or a computed target, is indeterminate, 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 spelling runs typos-config-builder gate, through uvx or uv tool run with --from naming the builder repository at a release tag at or above v0.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_VERSION or builder commit variable, and no spelling-helper-test, spelling-phrase-check or spelling-config target.
  • 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 runs make spelling.
  • PD-011: .gitignore ignores .typos-oxendict-base.json and .typos-oxendict-base.toml, with the last matching pattern deciding as in Git, so a later ! pattern unignores the cache.
  • PD-012: typos.local.toml exists and declares schema = 1.
  • PD-013: AGENTS.md carries typos-config-builder's spelling block, copied verbatim from the pinned release's docs/agents-md-spelling.md between its typos-config-builder:agents-md markers, and gives no other make spelling or typos.toml guidance 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 lint runs yamllint --strict over a git ls-files list that names *.yml and *.yaml, and its exit status reaches Make. Write it as git ls-files -z '*.yml' '*.yaml' | xargs -0 -r $(YAMLLINT) --strict --. -d, --config-data and --no-warnings are reported.
  • YL-002: a recipe reachable from lint runs actionlint with a binding exit status, and does not switch off its shellcheck integration with an empty -shellcheck=.
  • YL-003: .yamllint-estate.yml is the canonical estate base, copied verbatim from canon/lint/yaml/. .yamllint.yml extends it and sets only extends and ignore; a local ignore replaces the base's, so a repository restates any base pattern it still needs. .yamllint and .yamllint.yaml are reported, because yamllint reads them before .yamllint.yml.
  • YL-004: .github/actionlint.yaml lists ubicloud-* under self-hosted-runner.labels.
  • YL-005: every CI job that runs make lint first uses leynos/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.py exists and matches a canonical digest.
  • UV-002: nothing assigns UV_CACHE_DIR or UV_TOOL_DIR, in the Makefile, workflows, or composite actions.
  • UV-003: no recipe runs uv run, uv sync, uv tool run or uvx except through $(UV_GATE), and UV_GATE is python3 scripts/uv_gate.py.
  • UV-004: uv.lock exists beside pyproject.toml.
  • UV-005: no recipe refreshes, upgrades, locks, purges the cache or retries around uv; a lock target may lock and upgrade.
  • UV-006: every tool spec is name==VERSION, name@VERSION, git+URL@<full commit SHA> or name[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 the env: 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 with uvx, 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.toml are 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 lint reaches (through prerequisites or $(MAKE) target) runs interrogate at a threshold of at least 100, with no relaxing flag (--ignore-nested-functions and 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, or verbose = 2 in the configuration) and its recipe fails on any MISSED row, 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 #![deny(missing_docs)] (which beats the manifest). warn and allow fail; a crate-root #![expect(missing_docs, reason = "...")] is accepted for generated items, and an expect without a reason fails. Any module-level or item-level allow or expect of missing_docs in 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 a docs/developers-guide.md section 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 recipe make lint reaches.

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:

  1. Set missing_docs = "deny" in [workspace.lints.rust] (or in each crate's [lints.rust]) and add [lints] workspace = true to every member that should inherit it. Document the public items the lint then reports.
  2. Give every module-level or item-level allow(missing_docs) or expect(missing_docs) a reason, or remove it. Only generated or macro-produced items justify one.
  3. Make sure make lint reaches a cargo clippy command 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 Makefile exists and defines fmt and check-fmt.
  • PD-002 — a recipe reachable from check-fmt runs mdtablefix --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 --wrap passes files the formatter would still change.
  • PD-003 — a recipe reachable from fmt runs mdtablefix --in-place with the same flags directly, not through the mdformat-all wrapper.
  • PD-004 — a recipe reachable from fmt runs markdownlint-cli2 --fix directly, not through the mdformat-all wrapper.
  • PD-005 — .markdownlint-cli2.jsonc exists and carries the baseline config entries verbatim, sets gitignore to true (so --fix leaves Git-ignored Markdown alone), and lists every baseline ignores glob. Further rules and globs may be added; the canonical file to copy is platform-standards/canon/lint/markdown/.markdownlint-cli2.jsonc.
  • PD-006 — CI lints Markdown through DavidAnson/markdownlint-cli2-action pinned to a full commit SHA with globs: '**/*.md'. A workflow step that runs markdownlint-cli2 from a shell, or drives make 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 as indeterminate when 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 with GITHUB_TOKEN or the concordat credentials when either is set, and otherwise with gh auth token for 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-url to 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:

  1. Replace the mdformat-all call in fmt with the two direct invocations above, and give check-fmt its own mdtablefix --check line. check-fmt previously did no Markdown checking at all in some repositories.
  2. Install mdtablefix 0.6.0 or later. Earlier releases have neither --check nor --git, so both recipes fail immediately against one. Pin the same version in continuous integration.
  3. Replace any npm install -g markdownlint-cli2 step, and any workflow step running the linter or make markdownlint, with the pinned action step above. Copy .markdownlint-cli2.jsonc from platform-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.