Checks
Status: current (2026-09-20)
Each check is exposed as a CLI subcommand (jk-standards <name>), and the
doc-facing ones also ship as pre-commit hooks. All checks emit
GitHub-Actions ::error annotations and exit non-zero on violations.
This page is generated from site/src/generated/checks.json, which is
itself emitted from the Python registry — the list below is what actually
ships in v0.19.0: 19
entries, 18 of which
run without a git base ref and are wired as pre-commit hooks. The
remaining 1 run only
in CI where a base ref is available.
action-pinning— every GitHub Actions `uses:` is pinned to a commit SHA. sourcebehavioral-claims— prose claims cite tests that actually exist. sourceboundaries— architectural layering rules are grep-enforced. sourcecount-drift— inventory facts live in one place, not restated as numerals. sourcedoc-completeness— every doc under a doc_root is mapped or declared. sourcedoc-coverage— every code module must be documented *somewhere*. sourcedoc-drift— code that a doc describes cannot move without the doc. sourcedoc-taxonomy— every doc must declare a lifecycle class. sourcefile-line-refs— enduring docs and source comments cite symbols, not lines. sourcegenerated-freshness— generated docs must diff clean against their generator. sourceimport-cycle— no module-level import cycle within a package. sourceledger— enforce the delivery-ledger format. sourcerelease-pins— released versions are tagged, and every pin to this repo resolves. sourceresearch-provenance— summarised research never reads as original research. sourceskill-lint— every skill directory is internally consistent. sourcesnippet-regions— doc references to code regions resolve to real markers. sourcestatus-prose— ban stale progress-tracking prose in gated docs. sourceworkflow-concurrency— a concurrency group is either ref-scoped or a declared lock. sourceworkflow-permissions— a reusable-workflow caller grants what its callee needs. source
doc-taxonomy
Section titled “doc-taxonomy”Every doc under the configured roots must carry YAML front-matter
class: <value> from the configured vocabulary (default generated,
gated, archived). A doc with no front-matter fails
[verified: test_checks::test_missing_class_flagged], as does an unknown
class value [verified: test_checks::test_invalid_class_flagged].
The class is the contract the other checks key off: gated docs are linted and drift-mapped, archived docs are exempt, generated docs are freshness-checked.
status-prose
Section titled “status-prose”Applies only to class: gated docs; archived docs are exempt
[verified: test_checks::test_archived_docs_exempt_from_status_prose].
Rule 1 — a Status: line must carry a (YYYY-MM-DD) anchor. Undated
status fails [verified: test_checks::test_undated_status_flagged]; a dated
one passes [verified: test_checks::test_dated_status_passes].
Rule 2 — progress-tracking phrases (the not-yet-implemented family, phase
tracking, TODO-count claims) are rejected outright
[verified: test_checks::test_forbidden_phrase_flagged]. That state belongs
in the changelog, issues, or generated dashboards. Extra project-specific
phrases can be added via status_prose.forbidden_extra.
Rule 3 — the accuracy arm. Rule 1 proves a Status: anchor is present;
this arm proves it is not stale. Diff-scoped like doc-drift, it runs only
when a git base ref resolves (--base, or GITHUB_BASE_REF in CI). For each
gated doc in the change range it compares the anchor date against the doc’s
last-touched commit date in that range and flags the doc when the commit is
newer than the anchor beyond a tolerance window
[verified: test_checks::test_status_prose_accuracy_flags_stale_anchor_in_range].
A doc whose only change since the anchor is the Status: line itself is never
flagged — re-stamping the date is not a substantive edit, and treating it as
one would make the re-stamp immediately re-trip the check
[verified: test_checks::test_status_prose_accuracy_ignores_status_only_edit].
The window is status_prose.date_tolerance_days (default 0 — flag any commit
strictly newer than the anchor); a wider window forgives edits within N days
[verified: test_checks::test_status_prose_accuracy_within_tolerance_not_flagged].
When no base ref resolves — a local run with no --base and no
GITHUB_BASE_REF — the accuracy arm skips cleanly and never fails, while the
presence arm still runs
[verified: test_checks::test_status_prose_accuracy_skips_without_base_ref_presence_green].
A summary line records whether the arm ran (naming the base ref and
changed-file count) or skipped, so CI-vs-local behaviour is legible.
The worktree arm. The range arm compares an anchor against the doc’s last commit, so a pre-commit run has nothing to compare and stays silent — the violation used to surface only in CI, one commit too late. The worktree arm closes that gap: a gated doc with an uncommitted substantive edit is compared against today, with no base ref required [verified: test_checks::test_worktree_arm_flags_dirty_substantive_edit_without_base]. A Status-only working-tree edit is never substantive [verified: test_checks::test_worktree_arm_ignores_status_only_dirty_edit], a clean tree stays silent [verified: test_checks::test_worktree_arm_silent_on_clean_tree], an anchor already at today passes [verified: test_checks::test_worktree_arm_passes_todays_anchor], and the tolerance window applies unchanged [verified: test_checks::test_worktree_arm_respects_tolerance_window]. Untracked docs stay outside governance (they are invisible to CI checkouts too) [verified: test_checks::test_worktree_arm_ignores_untracked_docs].
Relatedly, a doc-drift failure whose mapped doc carries a dated anchor now says so up front — fixing the drift by editing the doc is itself an edit that stales the anchor, and the warning turns that two-round-trip chain into one [verified: test_doc_drift::test_failure_notes_status_anchor_chain] [verified: test_doc_drift::test_failure_has_no_anchor_note_for_unanchored_doc].
file-line-refs
Section titled “file-line-refs”Enduring docs and source comments must cite symbols or region names, not
line numbers — references like engine.cpp:42
rot on the next edit and are flagged
[verified: test_checks::test_file_line_ref_in_doc_flagged].
Docs are scanned on every line. Configured source roots are scanned only inside comments — the same reference in a string literal is code, not commentary, and is ignored [verified: test_checks::test_source_comment_scanned_code_ignored].
Escape hatch: a [file-line-ok] marker on the line
[verified: test_checks::test_file_line_ok_marker_exempts], for
consciously-pinned references such as commit-SHA permalinks. Dated review
records belong in exempt_dirs — their line refs are frozen by the
review’s date anchor.
count-drift
Section titled “count-drift”A numeral (Arabic or spelled out) adjacent to a configured inventory
trigger phrase is flagged
[verified: test_checks::test_hardcoded_count_flagged] — the fact belongs in
a generated source of truth, interpolated as {'{counts.x}'}, which is
never flagged [verified: test_checks::test_counts_template_passes].
Triggers are project-supplied and should be inventory-scoped phrases, not bare nouns, so compositional prose never matches. With no triggers configured the check is skipped.
Exemptions: archived docs; fenced code blocks
[verified: test_checks::test_code_fence_exempt]; markdown table rows; and
counts-ok markers on the line, the preceding line, or whole-file before
the first heading [verified: test_checks::test_counts_ok_marker_exempts].
behavioral-claims
Section titled “behavioral-claims”Opt-in markers make prose claims machine-checkable:
- A
verified:marker must cite an entry in the scraped test index — an unresolved citation fails [verified: test_checks::test_unresolved_citation_flagged], a resolving one passes [verified: test_checks::test_resolved_citation_passes]. - The ⚠-unverified marker is allowed but counted and warned — the honest-state metric [verified: test_checks::test_unverified_marker_is_warning_not_error].
Citation formats by index source: Suite.TestName for gtest
[verified: test_checks::test_gtest_index_resolves], filestem/slug for js
test files, filestem::test_name for pytest.
generated-freshness
Section titled “generated-freshness”For each configured (doc, command) pair the check snapshots the tracked content, runs the generator, diffs, and restores the snapshot — a stale doc is flagged and the working tree is left untouched [verified: test_checks::test_stale_generated_doc_flagged_and_restored]; a fresh doc passes [verified: test_checks::test_fresh_generated_doc_passes].
doc-drift
Section titled “doc-drift”Diff-scoped: needs a git base ref (--base, or GITHUB_BASE_REF in CI).
Reads the drift map; if the change touches a mapping’s sources without
touching its doc, the check fails
[verified: test_doc_drift::test_source_change_without_doc_flagged].
Touching the doc in the same range satisfies the mapping
[verified: test_doc_drift::test_source_change_with_doc_passes], unmapped
changes pass untouched
[verified: test_doc_drift::test_unmapped_change_passes], and a
Docs-Not-Affected: <reason> commit trailer bypasses with the
justification recorded in history
[verified: test_doc_drift::test_trailer_bypasses].
The trailer is a range-level assertion: one trailer satisfies every triggered mapping in the PR.
Manifests listed in doc_drift.deps_only_manifests are excluded from
triggering mappings when every changed line is a dependency pin — a
"name": "value" entry whose value is version shaped, meaning an optional
range operator followed by a digit-led version
[verified: test_doc_drift::test_deps_only_bump_does_not_trigger] — because a
Dependabot bump is not a taxonomy change. Position in the file is deliberately
not consulted: a diff is a fragment, and a real bump’s hunk usually opens
after the "dependencies": { line, so any rule requiring that line in
context rejects the very case the exemption exists to allow
[verified: test_doc_drift::test_deps_only_accepts_mid_block_hunk]. Two blocks
bumped in one diff, with neither opening line in context, are accepted for the
same reason
[verified: test_doc_drift::test_deps_only_accepts_both_blocks_without_either_opening_line],
and diff metadata lines are not mistaken for changes
[verified: test_doc_drift::test_deps_only_ignores_diff_metadata_lines].
Judging the value’s shape rather than the line’s position is also what keeps
the exemption narrow. A scripts-block edit still triggers
[verified: test_doc_drift::test_scripts_block_change_still_triggers], including
a single-token value such as "test": "vitest" that is entry-shaped but not
version shaped
[verified: test_doc_drift::test_deps_only_rejects_single_token_script_value];
so does a package rename
[verified: test_doc_drift::test_deps_only_rejects_package_rename], a multi-token
script value
[verified: test_doc_drift::test_deps_only_rejects_scripts_edit], and a bump
mixed with a script edit in the same diff
[verified: test_doc_drift::test_deps_only_rejects_mixed_bump_and_scripts_edit].
An empty diff is not an exemption
[verified: test_doc_drift::test_deps_only_rejects_empty_diff], and manifests not
listed get none at all
[verified: test_doc_drift::test_unlisted_manifest_still_triggers_on_deps_bump].
A dependency pinned to something not version shaped — a git URL, an npm:
alias, a file: path — is not recognised, so its bump still triggers the
mapping. That is the same conservative direction the check already fails in,
and it is preferred to widening the accept rule until a scripts edit slips
through.
The map also carries a cannot_drift registry: docs that legitimately have
no touch-correlation source, each recording a required, non-empty reason so
a deliberate exemption is distinguishable from an accidental omission (the
distinction doc-completeness keys off). A valid entry parses
[verified: test_doc_drift::test_cannot_drift_valid_entry_parses]; an entry
missing its reason
[verified: test_doc_drift::test_cannot_drift_missing_reason_rejected], with a
blank reason
[verified: test_doc_drift::test_cannot_drift_empty_reason_rejected], or missing
its doc key
[verified: test_doc_drift::test_cannot_drift_missing_doc_rejected] is a config
error surfaced as exit 2
[verified: test_doc_drift::test_cannot_drift_invalid_entry_cli_exit_2]. The
worked example is site/src/content/docs/reference/skills.mdx: it renders its
catalog by importing site/src/generated/skills.json at build time — nothing
on the page is hand-maintained — so a skill add or rename updates the
generated JSON (freshness-gated by generated-freshness and the emit --check)
and the page reflects it with no source edit to mirror. Mapping a
touch-correlation rule at a build-time-generated page would only manufacture a
false positive on every skill change, so it is declared cannot_drift instead.
doc-completeness
Section titled “doc-completeness”Every doc iter_docs enumerates under a configured doc_root must be
accounted for in the drift map — either as a mapping’s doc: target or as a
cannot_drift entry. A page that is neither is an accidental omission, and
this check names it: it emits ::error file=<doc>,line=1:: for each
unregistered doc and fails
[verified: test_doc_completeness::test_unregistered_doc_fails_naming_it], while
a doc that is mapped
[verified: test_doc_completeness::test_mapped_only_passes] or declared
un-driftable
[verified: test_doc_completeness::test_cannot_drift_only_passes] passes. The
remediation names both escape routes — add a mappings entry or a cannot_drift
entry — and deliberately does not name the docs already accounted for. On
success it prints doc-completeness: all N doc(s) mapped or declared
[verified: test_doc_completeness::test_success_emits_summary].
Beyond that forward pass the check also runs a reverse existence pass: every
registered doc — whether a mapping’s doc: target or a cannot_drift entry —
must still exist on disk. A registry entry naming a path that no longer exists
is an orphan, and the check names it and its registry, because the two registries
go stale in distinct ways. A stale cannot_drift entry silently pre-exempts any
future doc later created at that path from the completeness gate
[verified: test_doc_completeness::test_cannot_drift_orphan_is_reported]; a stale
mapping becomes an unsatisfiable gate — the doc can never be produced, so its only
escape is a Docs-Not-Affected trailer on every affecting commit
[verified: test_doc_completeness::test_mappings_orphan_is_reported_with_unsatisfiable_gate_framing].
The pass tests plain filesystem existence, not iter_docs membership, so an entry
legitimately naming a doc outside the configured doc_roots is tolerated
[verified: test_doc_completeness::test_entry_naming_file_outside_doc_roots_is_tolerated],
and it runs unconditionally — a fact about the filesystem, independent of the git
fail-open branch
[verified: test_doc_completeness::test_orphan_reported_when_git_tracking_fails_open].
The success summary gains a count of how many registry entries were
existence-checked, so a green run still proves the reverse pass ran
[verified: test_doc_completeness::test_all_entries_existing_reports_zero_orphans_and_names_checked_count].
A doc whose front-matter class is in doc_completeness.exempt_classes
(default archived) is skipped before the mapped/declared test — an archived
page is deliberately frozen, so requiring it to be mapped or declared is noise,
and it passes even when it is neither
[verified: test_doc_completeness::test_unmapped_archived_doc_passes]. The
exemption keys off the doc’s own front-matter class, not cfg.generated: a
gated orphan in the same tree still fails
[verified: test_doc_completeness::test_unmapped_gated_doc_still_fails_alongside_archived],
and pointing exempt_classes at a different class exempts only that one
[verified: test_doc_completeness::test_custom_exempt_classes_exempts_named_class].
The success summary notes how many docs were exempted by class
[verified: test_doc_completeness::test_summary_notes_exempt_count].
Unlike doc-drift it needs no git base ref: the working tree and the map are
its only inputs, so it runs unconditionally as a static check under
jk-standards all and as a pre-commit hook. It honors the same doc_roots,
extensions, and exempt_dirs as every other doc check — a file outside the
configured extensions is never enumerated
[verified: test_doc_completeness::test_multiple_doc_roots_and_extensions] and
an exempt_dirs path is skipped
[verified: test_doc_completeness::test_exempt_dirs_excluded].
It reuses doc-drift’s cannot_drift parser, so a malformed registry — an entry
missing its required reason — surfaces as the same config error: (exit 2)
rather than a traceback
[verified: test_doc_completeness::test_malformed_cannot_drift_cli_exit_2], and a
mappings entry missing its doc key fails the same way
[verified: test_doc_completeness::test_mapping_missing_doc_key_cli_exit_2]. A
missing drift map is itself a failure
[verified: test_doc_completeness::test_missing_drift_map_fails].
doc-coverage
Section titled “doc-coverage”The doc-drift family catches a doc that lies about code; this check catches the opposite gap — code that no doc, and no docstring, describes at all. It walks the configured Python source roots and enumerates each module’s public documentable units (the module itself, its top-level public classes and functions, and those classes’ public methods), then asks of every unit whether ANY of three independent OR-signals holds:
- docstring — the unit carries a non-empty docstring.
- drift — the unit’s file matches a
sources:glob in the drift map, so a change to it is already touch-correlated to a doc. - mention — the unit’s bare symbol name appears as a whole word in one of the configured doc scopes.
A unit is documented iff at least one signal fires — the disjunction, not the
conjunction [verified: test_doc_coverage::test_docunit_documented_is_disjunction].
A docstring on the module alone keeps the module green
[verified: test_doc_coverage::test_module_docstring_alone_keeps_module_green], a
sources: glob match documents it via the drift signal
[verified: test_doc_coverage::test_drift_map_glob_documents_module], and a
whole-word symbol mention in a doc scope documents it via the mention signal
[verified: test_doc_coverage::test_symbol_mention_in_doc_scope_documents_module].
The mention is a whole-word match, not a substring — a symbol embedded in a
longer token does not count
[verified: test_doc_coverage::test_mention_is_whole_word_not_substring].
The gate is deliberately lenient: it fails at module granularity. A module is
flagged only when EVERY one of its public units is undocumented by all three
signals — a genuinely bare file that nothing, anywhere, describes
[verified: test_doc_coverage::test_fully_bare_module_fails]. One
::error file=<module>,line=1:: is emitted per fully-undocumented module so it
surfaces inline on PRs
[verified: test_doc_coverage::test_bare_module_emits_error_with_path_and_line],
and the summary line reports the unit count, module count, undocumented count,
and live-waiver count
[verified: test_doc_coverage::test_clean_run_summary_reports_unit_and_module_counts].
Baseline ratchet. Beyond the binary bare-module gate, the check enforces a
per-module floor: it recomputes each module’s live documented-unit ratio and
compares it against the committed floor recorded at baselines/doc-coverage.json,
hard-failing any module that slipped below its recorded ratio and naming the
module with its before/after ratio
[verified: test_doc_coverage::test_ratchet_regression_below_floor_fails_naming_module_and_ratio].
The ratchet composes with — it does not replace — the binary gate
[verified: test_doc_coverage::test_ratchet_composes_with_binary_gate]: holding at
the floor passes
[verified: test_doc_coverage::test_ratchet_holding_at_floor_passes], improving
above it passes
[verified: test_doc_coverage::test_ratchet_improvement_above_floor_passes], and a
module not yet in the baseline is first-seen-passes
[verified: test_doc_coverage::test_ratchet_new_module_not_in_baseline_passes]. With
no baseline committed yet the ratchet is inert and the summary line says so
[verified: test_doc_coverage::test_ratchet_no_baseline_passes_and_reports_first_run].
The floor map is recorded — and ratcheted up — only through the explicit
doc-coverage --update-baseline CLI flag (never emit all, so a floor can never
silently self-heal); a write that would lower an existing floor is refused
unless --allow-regression is also passed
[verified: test_doc_coverage::test_update_baseline_lowering_refused_without_allow_regression],
and re-recording an unchanged tree reproduces the file byte-for-byte
[verified: test_doc_coverage::test_update_baseline_is_byte_idempotent].
Advisory floor. An optional doc_coverage.module_min_percent sets a soft
per-module target: a module whose live documented-unit ratio is below that
percentage emits a ::warning (surfaced inline on the PR) and is tallied in the
summary line, but the advisory never fails the build — it is strictly additive to
the binary gate and the ratchet
[verified: test_doc_coverage::test_advisory_below_floor_warns_but_exit_stays_zero].
A module exactly at the floor is not flagged
[verified: test_doc_coverage::test_advisory_at_floor_not_flagged], and when the
field is unset the summary line is byte-identical to before, with no advisory
clause and no warnings
[verified: test_doc_coverage::test_advisory_unset_adds_no_clause_and_no_warning].
Escape hatch: a # doc-coverage-ok: <reason> marker in the file’s leading comment
block (before the first code) waives the whole module in place
[verified: test_doc_coverage::test_escape_hatch_waives_module]; a shebang above
the marker is fine
[verified: test_doc_coverage::test_escape_hatch_after_shebang_still_waives], but a
marker buried in code or a docstring does not waive
[verified: test_doc_coverage::test_marker_buried_in_code_does_not_waive]. Live
waivers are counted in the summary line so rising escape-hatch usage stays visible
in CI logs.
Scope: Python and C++. The default enumerator is an ast walk over the
configured source roots; sources with a C++ suffix (.cpp, .cc, .cxx,
.c++, .hpp, .hh, .hxx, .h++, .h, .c) are instead parsed with
tree-sitter-cpp and enumerated by a public-declaration heuristic:
- function — a named function declaration or definition at namespace scope
(recursing into
namespace/extern "C"bodies so the true name is used). - class / struct — a named
classorstructspecifier; anonymous ones are skipped since no doc could name them. - method — a public member function, resolved with C++ default-access
rules: a
classstarts private and astructstarts public, and eachpublic:/private:/protected:label flips visibility for the members that follow, so only currently-public methods are enumerated.
The has_docstring signal fires when a Doxygen doc comment — one opening with
///, //!, /**, or /*! — sits on the line immediately above the
declaration; a plain // or /* */ comment does not count, mirroring how a
Python docstring (not any comment) is the signal. The drift and mention
OR-signals, the disjunction rule, and the module-granular gate all carry over
unchanged — a C++ unit is documented iff it has a doc comment, its file matches a
drift-map sources: glob, or its bare name is mentioned in a doc scope.
Known limits: the native grammar ships only in the optional jk-standards[cpp]
extra. When a C++ source root is configured but tree-sitter-cpp is not installed,
the check degrades gracefully rather than failing — the C++ files contribute zero
units and a single summary line reports how many were skipped and points at
jk-standards[cpp], so a grammar-less repo keeps working on the PyYAML-only
zero-dependency default. A C++ file that fails to parse into a translation unit
likewise contributes zero units instead of raising. With no source roots
configured the check skips cleanly.
action-pinning
Section titled “action-pinning”Every GitHub Actions uses: reference under .github/workflows/**/*.yml
(and *.yaml) must be pinned to a full 40-char commit SHA. A floating ref
(actions/checkout@v6, @main, a bare tag) lets the upstream owner — or
anyone who compromises their account — change what runs in your CI without a
diff on your side, so it is flagged with file:line
[verified: test_checks::test_action_pinning_floating_ref_flagged]; a
40-hex-SHA pin passes
[verified: test_checks::test_action_pinning_sha_pinned_passes]. A docker://
image with no digest is unpinned and flagged too
[verified: test_checks::test_action_pinning_docker_image_flagged], and every
unpinned ref adds to the returned error count
[verified: test_checks::test_action_pinning_multiple_unpinned_counted].
Local uses: ./… (or ../…) action and reusable-workflow refs are accepted
— they live in your own tree and move with it
[verified: test_checks::test_action_pinning_local_ref_accepted].
Escape hatch: a # action-pin-ok: <reason> marker on the offending line
[verified: test_checks::test_action_pinning_marker_same_line_exempts] or the
line immediately above it
[verified: test_checks::test_action_pinning_marker_line_above_exempts]
suppresses the finding, for the rare ref that genuinely cannot be SHA-pinned.
With no .github/workflows directory the check skips cleanly
[verified: test_checks::test_action_pinning_missing_workflows_dir_skipped].
The workflow directory and scanned extensions are configurable via the
action_pinning config section. The shipped templates/dependabot.yml is
the companion that keeps pinned SHAs current by surfacing each upstream
update as a reviewable Dependabot PR — pinning without update automation
rots.
snippet-regions
Section titled “snippet-regions”Docs point readers at slices of source two ways: an MDX
<CodeSnippet file=… region=… /> component that renders the named region,
and prose region:<name> mentions. Each reference must resolve to a
region:<name> marker that actually exists in the declared source tree —
otherwise the snippet silently rots when the region is renamed or deleted,
with no diff on the doc side to warn anyone.
A <CodeSnippet> names its own file=; that file must exist
[verified: test_checks::test_snippet_regions_codesnippet_missing_file_flagged]
and must define the region
[verified: test_checks::test_snippet_regions_codesnippet_mdx_resolves]. A
region with no matching marker is flagged with path:line
[verified: test_checks::test_snippet_regions_dangling_codesnippet_flagged_with_path_line].
Prose mentions carry no file, so they resolve against the union of markers
across the configured snippet_regions.source_roots — poly-style
# region: in shell and // region: in C++ both feed that union
[verified: test_checks::test_snippet_regions_prose_resolves_shell_and_cpp],
and a mention matching no marker is flagged with path:line
[verified: test_checks::test_snippet_regions_dangling_prose_flagged_with_path_line].
With no source roots configured there is nothing to resolve against, so
prose scanning is skipped
[verified: test_checks::test_snippet_regions_no_source_roots_skips_prose];
CodeSnippet references, which name their own file, are still validated.
Marker syntax mirrors site/src/components/CodeSnippet.astro, the three
forms the repo already renders — // region:, # region:, and
<!-- region: --> — and is per-file-type overridable via
snippet_regions.markers, so a repo can map its own comment style (a SQL
-- region:, say) onto a file extension
[verified: test_checks::test_snippet_regions_per_file_type_marker_syntax].
Escape hatch: a # snippet-region-ok: <reason> marker on the reference line
[verified: test_checks::test_snippet_regions_escape_hatch_same_line] or the
line immediately above it
[verified: test_checks::test_snippet_regions_escape_hatch_line_above]
suppresses the finding — the same two-line window used by action-pinning,
count-drift, and file-line-refs.
boundaries
Section titled “boundaries”A boundary is a directed constraint between components — one directory
MUST NOT reference another (the CLI may call the check registry, but a
check must not reach back into the CLI). It is the most common architectural
invariant and the easiest to break by accident, so this check turns a stated
boundary into a grep-level gate: each configured rule names a from directory,
an optional file-extensions filter, and a forbid regex, and any line under
that directory matching the regex is flagged with file:line
[verified: test_checks::test_boundaries_forbidden_reference_flagged_with_file_line].
A clean tree passes
[verified: test_checks::test_boundaries_clean_tree_passes], and every matching
line adds to the returned violation count
[verified: test_checks::test_boundaries_multiple_violations_counted]. This is a
line-level textual gate, not an import graph — it catches the reference forms
you name and nothing subtler.
The extensions filter narrows the scan to the relevant source files, so a
prose mention of the forbidden path in a neighbouring .md note is not a
violation
[verified: test_checks::test_boundaries_extensions_filter_ignores_other_files].
A rule whose from directory is absent skips cleanly rather than crashing
[verified: test_checks::test_boundaries_missing_from_dir_skips_rule], and a
malformed forbid regex is surfaced as a violation so a broken rule fails
loudly instead of silently passing
[verified: test_checks::test_boundaries_invalid_forbid_regex_is_a_violation].
With no boundaries rules configured the check is a clean no-op
[verified: test_checks::test_boundaries_no_rules_configured_skips].
Escape hatch: a # boundary-ok: <reason> marker on the offending line
[verified: test_checks::test_boundaries_ok_marker_same_line_suppresses] or the
line immediately above it
[verified: test_checks::test_boundaries_ok_marker_line_above_suppresses]
suppresses the finding, honoring any language-appropriate comment opener
(#, //, /*, <!--, --, ;). Unlike the other escape hatches, live
suppressions are counted and reported in the check’s summary line
[verified: test_checks::test_boundaries_suppression_count_reported_in_summary],
so rising escape-hatch usage is visible in CI logs rather than silent. Rules
are declared in the boundaries config section.
research-provenance
Section titled “research-provenance”Documentation that summarises published scholarship must make its
provenance mechanically visible, so summarised prior work can never be
mistaken for original research. The check is opted in by configuring
research_provenance.bib_file; with no bibliography configured it is
skipped [verified: test_checks::test_provenance_unconfigured_skips], and a
configured path that doesn’t exist is flagged rather than silently passing
[verified: test_checks::test_provenance_missing_bib_file_flagged].
Citation resolution applies to every non-archived doc: a citation link
(#ref-* / #fr-*, or whatever anchor_pattern matches) must resolve to
an id="..." defined in the bibliography file — a dangling anchor is
flagged with path:line
[verified: test_checks::test_provenance_dangling_citation_flagged_with_path_line],
a resolving one passes
[verified: test_checks::test_provenance_resolved_citation_passes], and a
bibliography id defined twice is flagged at its redefinition
[verified: test_checks::test_provenance_duplicate_bib_ids_flagged_with_file_line]
— entries are stable anchors, never renumbered. The anchor pattern is
configurable
[verified: test_checks::test_provenance_custom_anchor_pattern], and a
malformed pattern is surfaced as a violation so a broken config fails
loudly
[verified: test_checks::test_provenance_invalid_anchor_pattern_is_a_violation].
Pages opted in via provenance: research front-matter must additionally
carry a provenance sentence matching the configured phrase regex
(default not original (research|theory))
[verified: test_checks::test_provenance_research_page_missing_sentence_flagged]
and an **Attribution:** note assigning claims to the three provenance
classes — sourced claim, practical distillation, project-specific value
[verified: test_checks::test_provenance_research_page_missing_attribution_flagged].
A page carrying both passes
[verified: test_checks::test_provenance_conformant_research_page_passes];
pages without the front-matter marker get citation checking only
[verified: test_checks::test_provenance_unmarked_page_needs_no_markers],
and archived docs are exempt throughout
[verified: test_checks::test_provenance_archived_docs_exempt]. The prose
discipline behind the markers is the research-provenance skill.
Escape hatch: a # provenance-ok: <reason> marker on the citing line
[verified: test_checks::test_provenance_ok_marker_same_line_exempts] or the
line immediately above it
[verified: test_checks::test_provenance_ok_marker_line_above_exempts]
suppresses citation-resolution findings, for links that legitimately point
outside the project’s bibliography — the same two-line window used by
action-pinning, count-drift, and snippet-regions. The page-level
requirements have no marker hatch: a page that shouldn’t carry them
shouldn’t declare provenance: research.
import-cycle
Section titled “import-cycle”A circular module-level import between two files of the same package is a
latent crash — one import-order change away from an ImportError — and it
couples the files so tightly that neither can be read, or moved, alone. This
check builds the runtime import graph from each package’s own module-level
imports, finds every strongly-connected component of more than one module (a
cycle), and reports it. It is opted in by listing the packages to scan under
import_cycle.packages; with none configured it emits a skip summary and
passes
[verified: test_import_cycle::test_run_skips_and_returns_zero_when_no_packages_configured].
A detected cycle is emitted as one ::error at the file:line of a real
in-cycle import, naming the full member chain, and every unsuppressed cycle
adds to the returned count
[verified: test_import_cycle::test_run_emits_error_at_file_line_and_returns_cycle_count]
— a transitive A -> B -> C -> A loop is caught as one cycle spanning all
three modules
[verified: test_import_cycle::test_analyze_detects_transitive_three_module_chain].
An acyclic graph passes
[verified: test_import_cycle::test_analyze_acyclic_graph_returns_no_cycle],
and a module that imports itself is not a cycle between distinct modules
[verified: test_import_cycle::test_analyze_self_importing_module_is_not_a_cycle].
Only imports that actually run at import time form an edge. An import nested
in a function or method body is deferred until the unit is called, so it can
never close an import-time loop
[verified: test_import_cycle::test_analyze_zero_when_the_cycle_import_is_function_local];
an import guarded by if TYPE_CHECKING: never executes at runtime at all
[verified: test_import_cycle::test_analyze_type_checking_guarded_import_forms_no_cycle];
but a try: import x except ImportError: fallback is a real runtime edge and
is counted
[verified: test_import_cycle::test_try_except_import_is_a_real_runtime_edge].
Breaking a cycle is exactly that move: push one of its imports inside the
function that uses it. Output is deterministic across repeated runs
[verified: test_import_cycle::test_run_output_is_deterministic_across_repeated_runs],
and a file that does not parse is surfaced in a summary line rather than
dropped silently
[verified: test_import_cycle::test_run_surfaces_parse_failures_in_a_summary_line].
Every run ends with a summary line —
import-cycle: N package(s), N cycle(s), N suppression(s) via import-cycle-ok, N unparseable file(s)
— so cycle, waiver, and parse-failure counts stay visible in CI logs even on
an otherwise-green run.
Escape hatch: an # import-cycle-ok: <reason> marker on an in-cycle import
line
[verified: test_import_cycle::test_run_waives_cycle_via_inline_import_cycle_ok_marker]
or the line immediately above it
[verified: test_import_cycle::test_run_honors_import_cycle_ok_marker_on_line_above]
waives the whole cycle, honoring any language-appropriate comment opener
(#, //, /*, <!--, --, ;). Like boundaries, live suppressions
are counted and reported in the summary line, so rising waiver usage stays
visible rather than silent
[verified: test_cli::test_import_cycle_escape_hatch_suppresses_and_counts]. An
out-of-shape import_cycle config value fails loudly as a config error
(exit 2) rather than a traceback
[verified: test_cli::test_import_cycle_out_of_shape_config_exits_2], and a
non-string entry in packages is rejected the same way
[verified: test_cli::test_import_cycle_non_string_package_entry_exits_2].
Packages to scan are declared in the import_cycle config section.
workflow-permissions
Section titled “workflow-permissions”A workflow_call producer can never exceed the token scope its caller was
granted. When a caller grants less than a callee’s permissions: block asks
for, the run does not fail inside a job — it fails to compose, before any job
starts, as a startup_failure carrying no annotation. Nothing points at the
cause.
This check reads that relationship statically. Every job calling a local
reusable workflow (uses: ./…) has its effective grant compared against the
union of the scopes the callee declares; a scope the callee asks for and the
caller does not confer is flagged at the uses: line
[verified: test_workflow_composition::test_permissions_caller_missing_scope_flagged],
while a caller granting a superset passes
[verified: test_workflow_composition::test_permissions_caller_superset_passes].
The union spans the callee’s job-level blocks as well as its top-level one,
since the ceiling applies to the whole called workflow
[verified: test_workflow_composition::test_permissions_callee_job_level_scope_counted].
A permissions: block on the calling job takes precedence over the caller
file’s own
[verified: test_workflow_composition::test_permissions_calling_job_block_overrides_workflow_level].
The write-all shorthand satisfies every request
[verified: test_workflow_composition::test_permissions_write_all_caller_satisfies_everything];
read-all does not satisfy one for write
[verified: test_workflow_composition::test_permissions_read_all_caller_fails_a_write_requirement];
and an explicit permissions: {} grants nothing, which is a denial rather than
an absent declaration
[verified: test_workflow_composition::test_permissions_empty_caller_block_grants_nothing].
Several missing scopes on one call are reported as a single reviewable finding
[verified: test_workflow_composition::test_permissions_one_finding_per_edge_not_per_scope].
Two silences keep the check honest rather than noisy. A caller declaring no
permissions: block at all is skipped, because its effective grant comes from
a repository-level default this check cannot see and any finding would be a
guess
[verified: test_workflow_composition::test_permissions_undeclared_caller_grant_skipped];
a callee declaring none anywhere requests nothing to satisfy
[verified: test_workflow_composition::test_permissions_callee_declaring_nothing_passes].
Only local ./… callees are resolved — a pinned owner/repo@sha reference
lives outside the tree and cannot be read from disk
[verified: test_workflow_composition::test_permissions_remote_callee_ignored] —
and a ./… ref to a file that is not there is left for GitHub to report
[verified: test_workflow_composition::test_permissions_missing_callee_file_not_flagged].
A workflow that does not parse is skipped rather than crashing the check
[verified: test_workflow_composition::test_permissions_unparseable_workflow_skipped],
and with no workflows directory the check skips cleanly
[verified: test_workflow_composition::test_permissions_missing_workflows_dir_skipped].
Escape hatch: a # workflow-permissions-ok: <reason> marker on the uses:
line
[verified: test_workflow_composition::test_permissions_marker_same_line_exempts]
or the line immediately above it
[verified: test_workflow_composition::test_permissions_marker_line_above_exempts]
suppresses the finding. The scanned directory and extensions are configurable
via the workflow_permissions config section
[verified: test_workflow_composition::test_workflow_dirs_configurable].
workflow-concurrency
Section titled “workflow-concurrency”A concurrency: group is a mutex whose name is a string. Two runs sharing a
name queue, and with cancel-in-progress: false GitHub cancels the older
pending entry once a third contender arrives. That is correct when the name
stands for something genuinely shared — a Pages deployment, a staging
environment — and a silent repository-wide serialiser when it does not.
The failure mode does not look like a config bug. A group omitting github.ref
funnels every branch and pull request into one lock, so unrelated pull requests
cancel each other’s jobs; the symptom is a cancelled job and a failing
aggregate gate on a pull request containing no defect, which reads as CI flake.
It also appears only under concurrent load, so a quiet repository looks fine
until it is not.
This check makes the distinction explicit: a group must either carry a
ref-scoping expression
[verified: test_workflow_composition::test_concurrency_ref_scoped_group_passes]
or name a lock declared under workflow_concurrency.global_locks
[verified: test_workflow_composition::test_concurrency_declared_global_lock_passes].
Anything else is flagged at the group: line
[verified: test_workflow_composition::test_concurrency_unscoped_group_flagged],
and declaring one lock does not bless every other unscoped group
[verified: test_workflow_composition::test_concurrency_undeclared_lock_still_flagged].
The group that serialised this repository’s own pull requests —
pages-deploy-${{ inputs.deploy }}, carrying no ref token — is exactly what
the check rejects
[verified: test_workflow_composition::test_concurrency_catches_the_deploy_site_regression].
Job-level concurrency: blocks are covered alongside workflow-level ones
[verified: test_workflow_composition::test_concurrency_job_level_group_flagged],
as is the concurrency: <name> string shorthand
[verified: test_workflow_composition::test_concurrency_shorthand_string_form_flagged].
A block with no group: key has nothing to scope and is left alone
[verified: test_workflow_composition::test_concurrency_block_without_group_ignored],
and an unparseable workflow is skipped rather than crashing the check
[verified: test_workflow_composition::test_concurrency_unparseable_workflow_skipped].
With no workflows directory the check skips cleanly
[verified: test_workflow_composition::test_concurrency_missing_workflows_dir_skipped].
A group built from an expression that contains a ref token passes even when
one branch of that expression is a literal — a ternary yielding a global lock
for a real deploy and a ref-scoped name for a build-only smoke is the intended
shape
[verified: test_workflow_composition::test_concurrency_expression_containing_ref_passes].
This is the check’s known limit: it reads the group as text and cannot tell
which branch of an expression applies, so a genuinely global branch hidden
inside an otherwise ref-scoped expression is accepted. What counts as
ref-scoping is configurable via workflow_concurrency.ref_tokens
[verified: test_workflow_composition::test_concurrency_custom_ref_tokens_respected],
and the scanned directory via workflow_concurrency.workflow_dir
[verified: test_workflow_composition::test_workflow_dirs_configurable].
Escape hatch: a # concurrency-scope-ok: <reason> marker on the group: line
[verified: test_workflow_composition::test_concurrency_marker_same_line_exempts]
or the line immediately above it
[verified: test_workflow_composition::test_concurrency_marker_line_above_exempts]
suppresses the finding.
release-pins
Section titled “release-pins”Adoption instructions pin by tag, which makes the tag the load-bearing
artefact: rev: v1.2.0 in a consumer’s .pre-commit-config.yaml, or
uses: OWNER/REPO/.github/workflows/x.yml@v1.2.0 in their CI, works only if
that tag exists. When a release ships a changelog entry but the final
git tag && git push is skipped, nothing notices — the tree is green, the
changelog reads correctly, and the documented instructions become dangling refs
that fail in the consumer’s CI rather than this repo’s.
This check closes that loop with two rules. First, every ## [X.Y.Z] heading
in the changelog must have a matching vX.Y.Z tag
[verified: test_release_pins::test_released_version_without_tag_flagged],
satisfied when the tag is present
[verified: test_release_pins::test_released_version_with_tag_passes]. An
[Unreleased] heading never requires one — holding unshipped work is that
section’s purpose
[verified: test_release_pins::test_unreleased_heading_never_requires_a_tag].
The newest release section is also exempt: it is the release in flight
[verified: test_release_pins::test_newest_release_section_may_await_its_tag].
A release commit dates its changelog section before the tag is pushed — the tag
is cut from the merged result — so requiring one there would fail the release
pull request on a required check, leaving it unmergeable and the tag uncuttable.
The check would block the process it exists to protect. That costs one release
of detection latency and no more: skip the tag and the next release pushes the
section down, where it is judged like any other
[verified: test_release_pins::test_skipped_tag_is_caught_once_the_next_release_lands].
An [Unreleased] heading is not a release section and shields nothing beneath it
[verified: test_release_pins::test_unreleased_heading_does_not_consume_the_in_flight_exemption].
Versions released before this check existed are recorded under
untagged_versions, so it ratchets on future releases instead of relitigating
history [verified: test_release_pins::test_declared_untagged_version_exempted];
declaring one version exempts only that one
[verified: test_release_pins::test_declaring_one_version_does_not_exempt_another],
and both the declared-exemption and awaiting-tag counts are reported in the
summary line so neither state fades into silence.
Second, every pin naming this repository must resolve to a real tag. A uses:
reference to a missing tag is flagged
[verified: test_release_pins::test_uses_pin_to_missing_tag_flagged] and one to
an existing tag passes
[verified: test_release_pins::test_uses_pin_to_existing_tag_passes]. The
pip install "git+…@ref" form is checked too
[verified: test_release_pins::test_pip_git_install_form_checked], as are the
commented consume-from-a-pinned-tag examples in a reusable workflow’s own
header — a comment is still guidance
[verified: test_release_pins::test_commented_pin_in_a_workflow_header_checked].
A rev: names no repository itself, so it counts only when the nearest
preceding repo: line names this one
[verified: test_release_pins::test_rev_under_this_repo_checked]. A third
party’s rev: in the same file is left alone
[verified: test_release_pins::test_rev_under_a_third_party_repo_ignored], and
scanning resumes correctly at the next block naming this repository
[verified: test_release_pins::test_rev_switches_back_to_this_repo_after_a_third_party_block].
A uses: naming another owner is likewise ignored
[verified: test_release_pins::test_uses_naming_another_owner_ignored]. Only
release-shaped refs are judged: a commit SHA
[verified: test_release_pins::test_sha_pin_is_not_a_release_pin] or a branch
name [verified: test_release_pins::test_branch_pin_is_not_a_release_pin] is
somebody else’s rule to enforce. Vendored trees such as node_modules are
never scanned [verified: test_release_pins::test_node_modules_not_scanned].
Historical records belong in exclude
[verified: test_release_pins::test_excluded_path_not_scanned] — a migration
note describing what a project actually adopted at the time must keep its
original pin, and without the exclusion it would be flagged like any other
[verified: test_release_pins::test_excluded_path_still_flagged_when_not_excluded].
Three skip contracts keep the check from inventing findings. With no
release_pins.repo configured there is nothing to recognise a pin to this
project by [verified: test_release_pins::test_unconfigured_repo_skips]. Outside
a git checkout the tag list is unreadable
[verified: test_release_pins::test_non_git_directory_skips]. And a repository
reporting no tags at all is skipped rather than treated as one where every pin
dangles [verified: test_release_pins::test_repo_without_tags_skips]: a shallow
CI checkout — actions/checkout fetches no tags unless fetch-depth: 0 — and
a project before its first release are indistinguishable here, and reporting
every pin as broken on the strength of an incomplete checkout would be worse
than staying quiet. The dogfood CI job already sets fetch-depth: 0.
Escape hatch: a # release-pin-ok: <reason> marker on the offending line
[verified: test_release_pins::test_pin_marker_same_line_exempts] or the line
immediately above it
[verified: test_release_pins::test_pin_marker_line_above_exempts] suppresses
the finding, honouring any language-appropriate comment opener (#, //,
/*, <!--, --, ;) — the HTML form matters most, since pins live in
markdown [verified: test_release_pins::test_changelog_marker_above_heading_exempts].
Repository identity and scanning scope are configured in the release_pins
section; an out-of-shape untagged_versions fails as a config error (exit 2)
[verified: test_release_pins::test_untagged_versions_non_list_raises], as does
a non-string entry
[verified: test_release_pins::test_untagged_versions_non_string_entry_raises].
ledger
Section titled “ledger”A delivery ledger is the whole state of a programme in one Markdown file: milestones, the slices they decompose into, the rows each slice closes, each slice’s definition of done, and the validation tokens that must pass before it may claim to be done. The ledger standard defines the format; this check is what makes it a format rather than a convention.
The trade the ledger makes is that a file cannot enforce its own invariants — so every guarantee a schema would have given is recovered here as a gate.
Structure. A milestone heading declares an M-prefixed three-digit ID, and
a slice heading declares that milestone’s ID plus a two-digit slice number. A
ledger with no milestones is flagged
[verified: test_ledger::test_ledger_with_no_milestones_flagged], as is a slice
written before any milestone
[verified: test_ledger::test_slice_before_any_milestone_flagged] or one naming
a milestone other than the section it sits in
[verified: test_ledger::test_slice_naming_a_different_milestone_flagged] — a
slice belongs to the section it is written in, so the two can never disagree.
Duplicate slice IDs are flagged
[verified: test_ledger::test_duplicate_slice_id_flagged], and a milestone
missing one of its required keys is named
[verified: test_ledger::test_milestone_missing_required_key_flagged].
Slice extent. A slice runs to the next slice heading or the next
milestone-level (##) heading, whichever comes first. Sections below the last
slice — ## Sequencing, ## Related issues — are siblings of the milestones,
so their tables are not read as anybody’s rows
[verified: test_ledger::test_milestone_level_section_after_the_last_slice_is_not_absorbed].
Headings deeper than ## leave the slice open, so a slice may sub-divide its
own body without orphaning the rows below
[verified: test_ledger::test_sub_heading_inside_a_slice_keeps_the_slice_open].
Evidence commit SHAs. A SHA an evidence file names must resolve to a real
commit
[verified: test_ledger::test_evidence_naming_an_unresolvable_commit_flagged];
one that does passes
[verified: test_ledger::test_evidence_naming_a_real_commit_passes]. Evidence is
what separates an asserted completion from a demonstrated one, so an
unresolvable SHA is worse than none — unlike a TBD it is indistinguishable
from a real one. The failure is structural rather than careless: evidence
written as work lands ships inside the commit it would name, so the value
cannot be known, and a field that cannot be filled truthfully gets filled
falsely. Omitting it is the correct answer there, and passes
[verified: test_ledger::test_evidence_without_a_commit_line_passes]; the
Slice: trailer is the join. Backfilled evidence, whose commit already exists,
still names it. Where git cannot answer — outside a repository, or a shallow
clone that does not reach the commit — the SHA is skipped rather than reported
[verified: test_ledger::test_evidence_shas_unchecked_outside_a_git_repo], the
same posture release-pins takes toward a checkout with no tags fetched.
Vocabulary. Slice and row statuses come from the declared set; anything
else is flagged
[verified: test_ledger::test_invalid_slice_status_flagged]
[verified: test_ledger::test_invalid_row_status_flagged]. accepted is a
first-class status, not an error — a deliberate no-change recorded so a later
pass does not rediscover it as an omission
[verified: test_ledger::test_accepted_is_a_valid_slice_status].
Dependencies. A Depends entry must name a slice the same ledger declares
[verified: test_ledger::test_depends_on_unknown_slice_flagged]; a declared ID
resolves rather than being reported as unknown
[verified: test_ledger::test_depends_on_declared_slice_passes].
Definition of done. Every slice carries a non-empty checklist
[verified: test_ledger::test_slice_without_definition_of_done_flagged] — a
slice without one cannot be finished, only abandoned. A done slice must have
every box checked [verified: test_ledger::test_done_slice_with_unchecked_item_flagged]
and every row closed or accepted; one whose boxes and rows agree passes
[verified: test_ledger::test_done_slice_fully_closed_passes].
Plan and evidence. A slice past open implements a plan, so it must name
one [verified: test_ledger::test_slice_past_open_without_plan_flagged], and a
named plan must exist
[verified: test_ledger::test_named_plan_that_does_not_exist_flagged]. Evidence
is owed later than a plan: a done slice without its evidence file is flagged
[verified: test_ledger::test_done_slice_without_evidence_file_flagged] while an
open one is not [verified: test_ledger::test_open_slice_without_evidence_file_passes],
because evidence records what was run and nothing has been. Both paths must
resolve inside the ledger’s own directory
[verified: test_ledger::test_path_outside_the_ledger_directory_flagged], so a
programme stays one movable tree.
Validation tokens. A slice names tokens, never commands; the consuming repo maps them to commands in its validations file. A token that file does not declare is a failure rather than a silently skipped gate [verified: test_ledger::test_undeclared_validation_token_flagged], as is an empty set [verified: test_ledger::test_empty_validation_set_flagged]. When the repo has no validations file the arm skips and says so [verified: test_ledger::test_token_arm_skips_without_a_validations_file] — it cannot tell a typo from an unconfigured project, so it declines to judge rather than reporting every token as wrong.
Rows. A slice’s table must carry the ID, Item, Verification and
Status columns
[verified: test_ledger::test_row_table_missing_required_column_flagged]; extra
columns (severity, disposition, source section) are carried through untouched.
A row with no verification is flagged
[verified: test_ledger::test_row_without_verification_flagged] — the cell names
the test, check, or artifact a reviewer audits. Rows are optional: an
infrastructure slice with no table passes
[verified: test_ledger::test_slice_without_a_row_table_passes].
Placeholders. TBD, TODO, FIXME, XXX and friends never survive into
a committed ledger [verified: test_ledger::test_placeholder_flagged]. State the
real value, or record the row as open with what is still unknown.
Skip contracts. A repo with no ledger under the configured roots reports itself as having nothing to check [verified: test_ledger::test_no_ledger_is_not_a_violation], and a ledger outside those roots is not governed [verified: test_ledger::test_ledger_outside_configured_roots_is_ignored].
Escape hatch: a <!-- ledger-ok: <reason> --> comment suppresses the finding
on the line it sits on
[verified: test_ledger::test_escape_hatch_suppresses_the_line_it_sits_on]. The
reason is load-bearing — an empty hatch suppresses nothing
[verified: test_ledger::test_escape_hatch_without_a_reason_does_not_suppress],
because a suppression without a written reason is the silent exemption this
discipline exists to prevent. Roots and the validations file are configured in
the ledger section.
Source resolution. A ledger-level Source: whose value is a single
whitespace-free token is a repo-relative path and must resolve to a file or
directory
[verified: test_ledger::test_source_path_dangling_flagged]
[verified: test_ledger::test_source_path_directory_passes]; a prose value —
a description, an external archive, an input document deleted after
assessment — is not a path and is never checked
[verified: test_ledger::test_source_prose_skipped], and the standard hatch
suppresses a flagged line
[verified: test_ledger::test_source_dangling_hatch_suppresses].
skill-lint
Section titled “skill-lint”A skill is a skills/<name>/SKILL.md instruction file an agent runtime
loads, optionally with bundled assets beside it. The pieces a runtime
actually depends on — the front-matter that names and triggers the skill,
and the scripts its body tells the reader to run — can each rot with no
test failing: the generated inventory and the installer key on the
front-matter name, selection depends on the description’s trigger
phrasing, and a renamed or unshipped asset leaves the skill instructing
readers to run a file that is not there.
Four rules close those gaps. Front-matter must parse and its name must
match the directory name
[verified: test_checks::test_skill_lint_name_dir_mismatch_flagged]
[verified: test_checks::test_skill_lint_missing_frontmatter_flagged],
satisfied by a conforming skill
[verified: test_checks::test_skill_lint_clean_skill_passes]. The
description must carry the “Use when” trigger phrasing every skill in
this repo uses to make an agent select it
[verified: test_checks::test_skill_lint_missing_trigger_flagged]. A
backtick-referenced same-directory script (foo.sh / foo.py, no path
separator) must exist beside the skill
[verified: test_checks::test_skill_lint_dangling_asset_flagged]
[verified: test_checks::test_skill_lint_existing_asset_passes]; a
reference carrying a path separator is out of scope
[verified: test_checks::test_skill_lint_pathed_reference_ignored]. And a
sibling *.sh must carry the owner-executable bit
[verified: test_checks::test_skill_lint_nonexecutable_sh_flagged], while
*.py assets are exempt because skill bodies invoke them through the
interpreter
[verified: test_checks::test_skill_lint_py_asset_needs_no_exec_bit].
A repository without a skills/ directory — every downstream consumer —
is skipped, not failed
[verified: test_checks::test_skill_lint_no_skills_dir_skips].
Escape hatch: a skill-lint-ok: <reason> marker on the offending line or
the line immediately above suppresses exactly that finding
[verified: test_checks::test_skill_lint_marker_exempts].