Configuration
Status: current (2026-09-15)
All project-specific surface lives in one file, {schema.config_file_name},
at the consuming repo’s root (override with --config). Every key is
optional; an absent file yields defaults (doc root docs/, standard class
vocabulary, count-drift and behavioral-claims skipped because they need
project input).
The toolkit pins its own ruff version in pyproject.toml — this snippet is
sliced live from the source file, so a drift in either place fails the build:
[tool.ruff]
line-length = 100
src = ["src", "tests"]
# Ruff's default selection is E4/E7/E9 + F — syntax errors and pyflakes, little
# more. For a toolkit whose subject is engineering discipline, the rules below
# are the ones that catch a class of defect rather than a style preference:
# bugbear (B) for real bug shapes, import sorting (I) for review-noise-free
# diffs, pyupgrade (UP) so the floor in requires-python is actually used,
# flake8-return (RET) and simplify (SIM) for control flow that reads wrong,
# comprehensions (C4), and pathlib (PTH) since the codebase is Path-based
# throughout and os.path calls are the odd ones out.
[tool.ruff.lint]
select = ["E", "W", "F", "I", "B", "UP", "C4", "SIM", "RET", "PTH"]
[tool.ruff.lint.per-file-ignores]
# Test fixtures embed workflow/config YAML as literals; wrapping them to 100
# columns would obscure the shape under test, which is the point of the fixture.
"tests/*" = ["E501"]Fields
Section titled “Fields”The 42 fields of {schema.config_file_name} are
projected directly from the Config dataclass in
src/jk_standards/config.py:
| Field | Type | Default |
|---|---|---|
action_pin_extensions | list[str] | [".yml",".yaml"] |
action_pin_workflow_dir | str | ".github/workflows" |
boundaries | list[BoundaryRule] | [] |
claim_sources | list[ClaimSource] | [] |
count_triggers | list[str] | [] |
deps_only_manifests | list[str] | [] |
doc_completeness_exempt_classes | list[str] | ["archived"] |
doc_coverage_doc_scopes | list[str] | [] |
doc_coverage_module_min_percent | int | None | None |
doc_coverage_source_roots | list[SourceRoot] | [] |
doc_roots | list[DocRoot] | [{"extensions":[".md"],"path":"docs"}] |
drift_map | str | ".github/docs-drift-map.yml" |
exempt_dirs | list[str] | [] |
file_line_extensions | list[str] | ["c","cc","cpp","h","hpp","py","js","mjs","ts","tsx","astro","md","mdx","sh","yml","yaml"] |
file_line_source_roots | list[SourceRoot] | [] |
generated | list[GeneratedDoc] | [] |
import_cycle_packages | list[str] | [] |
ledger_roots | list[str] | ["docs/plans"] |
ledger_validations | str | ".jk/validations.yml" |
provenance_anchor_pattern | str | "(ref|fr)-[A-Za-z0-9-]+" |
provenance_bib_file | str | "" |
provenance_doc_roots | list[DocRoot] | [] |
provenance_phrase | str | "not original (research|theory)" |
release_pin_changelog | str | "CHANGELOG.md" |
release_pin_exclude | list[str] | [] |
release_pin_extensions | list[str] | [".md",".mdx",".yml",".yaml"] |
release_pin_repo | str | "" |
release_pin_repo_url | str | "" |
release_pin_untagged_versions | list[str] | [] |
snippet_doc_roots | list[DocRoot] | [] |
snippet_markers | list[SnippetMarkerSyntax] | [] |
snippet_source_roots | list[SourceRoot] | [] |
status_date_tolerance_days | int | 0 |
status_forbidden_extra | list[ForbiddenPhrase] | [] |
taxonomy_classes | list[str] | ["generated","gated","archived"] |
taxonomy_extra_files | list[str] | [] |
workflow_concurrency_dir | str | ".github/workflows" |
workflow_concurrency_extensions | list[str] | [".yml",".yaml"] |
workflow_concurrency_global_locks | list[str] | [] |
workflow_concurrency_ref_tokens | list[str] | ["github.ref","github.ref_name","github.head_ref","github.event.number","github.event.pull_request.number","github.run_id","github.sha"] |
workflow_perm_dir | str | ".github/workflows" |
workflow_perm_extensions | list[str] | [".yml",".yaml"] |
Most fields nest under a named YAML section rather than sitting at the top
level. The action-pinning check reads its two fields — action_pin_workflow_dir
and action_pin_extensions — from an action_pinning: section
(workflow_dir and extensions keys); both default so the section is
optional. See the checks reference for
the rule those fields tune.
The status_date_tolerance_days field is read from a status_prose: section
(date_tolerance_days key, a non-negative int, default 0) and tunes the
accuracy arm of the status-prose check. The accuracy arm is diff-scoped like
doc-drift: for each gated doc changed against the base ref it compares the doc’s
Status: anchor date against the doc’s own last commit in range, flagging an
anchor that lags by more than the tolerance. 0 flags any commit strictly newer
than the anchor; raising it grants a grace window before a stale date fails the
build. A doc whose only change since the anchor is the Status: line itself is
never flagged, and with no base ref the arm skips cleanly. A value that is not a
non-negative int (including a bool) raises a config error surfaced as exit 2.
See the checks reference for the rule this
field tunes.
The doc_completeness_exempt_classes field is read from a doc_completeness:
section (exempt_classes key, a list of front-matter class names, default
["archived"]) and tunes the doc-completeness check. A doc whose front-matter
class is in this list is skipped rather than required to be a mapped doc:
target or a cannot_drift entry — a deliberately frozen record should not have
to be registered. The exemption keys off the doc’s own front-matter class read
at run time, not cfg.generated, so a generated-config doc classed gated
stays governed. Setting the list to a different class exempts only that class;
the success summary notes how many docs were exempted. See the
checks reference for the rule this field
tunes.
The boundaries field is a list of forbidden-reference rules read from a
boundaries: section (a rules: list). Each rule names a from directory,
a forbid regex a line under it must not match, and optional name,
extensions, and hint keys; from and forbid are required. See the
checks reference for the rule these fields
tune.
The doc_coverage_source_roots and doc_coverage_doc_scopes fields are read
from a doc_coverage: section (source_roots and doc_scopes keys) and tune
the doc-coverage check, which catches Python code that no doc and no docstring
describes at all. source_roots are the trees whose modules the AST enumerator
walks (each entry defaults to .py files); doc_scopes are the doc
directories scanned for the whole-word symbol “mention” OR-signal. Both default
to empty, so the section is optional. See the
checks reference for the rule these fields
tune.
The doc_coverage_module_min_percent field is read from the same
doc_coverage: section (module_min_percent key, an int in [0, 100], unset
by default). It sets a soft advisory floor: a module whose live documented-unit
ratio falls below that percentage emits a ::warning (surfaced inline on the
PR) and is tallied in the check’s summary line, but it is strictly advisory — it
never fails the build. It composes with, and is additive to, both the binary
bare-module gate and the per-module baseline ratchet, whose committed floor map
lives at baselines/doc-coverage.json and is recorded only through the
doc-coverage --update-baseline CLI flag (see the CLI section below).
The four provenance_* fields are read from a research_provenance:
section (bib_file, anchor_pattern, phrase, and doc_roots keys).
bib_file names the bibliography whose id="..." spans define the stable
citation anchors — configuring it is what opts the research-provenance
check in; with no bibliography the check is skipped. Everything else
defaults. See the
checks reference for the rule
these fields tune.
The import_cycle_packages field is read from an import_cycle: section
(a packages: list) and tunes the import-cycle check, which flags
module-level import cycles inside a package. Each entry is a Python package
directory (relative to the root) whose module-level import graph is scanned;
an absent or empty list yields no packages, so the check skips (passes with 0
packages), mirroring boundaries’ skip-when-unconfigured contract. A malformed
section raises a config error surfaced as exit 2 rather than a check failure.
See the checks reference for the rule this
field tunes.
The workflow_perm_dir and workflow_perm_extensions fields are read from a
workflow_permissions: section and tune the workflow-permissions check,
which compares a reusable-workflow caller’s token grant against the scopes its
callee declares. They mirror action_pinning’s fields and are kept separate so
a repository can scope the two independently; both default to
.github/workflows and .yml/.yaml.
The workflow_concurrency_* fields are read from a workflow_concurrency:
section and tune the workflow-concurrency check. global_locks names the
groups that are meant to serialise the whole repository, so a deliberate
global mutex is declared rather than indistinguishable from a forgotten
github.ref; an absent list declares none. ref_tokens overrides what counts
as ref-scoping, defaulting to github.ref, github.ref_name,
github.head_ref, the pull-request number contexts, github.run_id, and
github.sha. A non-list global_locks, or a non-string entry within it,
raises a config error surfaced as exit 2 rather than silently declaring a lock
nobody wrote. See the
checks reference for the rules these
fields tune.
The release_pin_* fields are read from a release_pins: section and tune the
release-pins check, which asserts that every released version is tagged and
that every pin naming this repository resolves. repo (an owner/name slug)
opts the check in — without it there is nothing to recognise a pin to this
project by — and repo_url defaults to the GitHub URL derived from it.
changelog names the file whose ## [X.Y.Z] headings are checked,
extensions the files scanned for pins, and exclude the path prefixes never
scanned (historical migration notes must keep the pins those projects actually
used). untagged_versions records releases that shipped a changelog section
but never got a tag, keeping the check a ratchet on future releases; the count
is reported on every run so the debt stays visible. A non-list
untagged_versions, or a non-string entry, raises a config error surfaced as
exit 2 rather than exempting a version string no heading ever produces. See the
checks reference for the rules these fields
tune.
The ledger_* fields are read from a ledger: section and tune the ledger
check, which enforces the delivery-ledger format. roots lists the trees
searched for ledger.md files (default docs/plans); a repo with no ledger
under them reports itself skipped rather than failing. validations names the
file declaring the validation tokens a slice may cite — a token: command
mapping, default .jk/validations.yml. That indirection is what makes a ledger
portable: the ledger says which class of assurance a slice owes, and the repo
says how it is obtained, so the same token means one command in a native
project and another in a site. When the validations file is absent the token
arm skips and says so, rather than reporting every token as undeclared. See the
checks reference for the rules these fields tune.
Example — this repo’s own config
Section titled “Example — this repo’s own config”jk-standards dogfoods itself, so its own jk-standards.yaml is a live
example of every wired feature. Sliced from the source:
version: 1
doc_roots:
- path: docs
extensions: [".md"]
- path: site/src/content/docs
extensions: [".mdx"]
# The governed conventions layer: normative C++ standards a consuming repo
# adopts (cpp-language-standard, msvc-portability, warning-flags). Registered
# as a doc_root so its `class: gated` docs are actually swept by doc-taxonomy,
# status-prose, and count-drift — without this entry the gating is cosmetic.
- path: conventions
extensions: [".md"]
taxonomy:
# `plan` is the delivery-programme class: ledger-adjacent plans, evidence,
# and decision records under docs/plans/. They are governed by the `ledger`
# check (structure, DoD, evidence) rather than the drift map, so the class
# is exempted from doc-completeness below. Ledgers themselves stay `gated`
# per the ledger standard and carry an exact cannot_drift entry each.
classes: [generated, gated, archived, plan]
# ARCHITECTURE.md lives at the repo root, outside the doc_roots, so it is not
# swept by iter_docs. List it here so the doc-taxonomy check still governs its
# lifecycle class — the repo's own architecture exemplar must carry class:gated
# like any other gated doc.
extra_files:
- ARCHITECTURE.md
file_line_refs:
source_roots:
- path: src
extensions: [".py"]
# Inventory facts about the toolkit itself (how many checks, hooks, skills)
# must not be restated as numerals in the docs — they change as checks land.
# region:count-triggers
count_drift:
triggers:
- 'checks?\s+(?:in\s+total|total)'
- 'hooks?\s+(?:in\s+total|total)'
- 'skills?\s+(?:in\s+total|total)'
- 'checks?\s+ship(?:s|ped)?'
- 'hooks?\s+ship(?:s|ped)?'
- 'skills?\s+ship(?:s|ped)?'
- 'fields?\s+(?:in|of)\s+(?:the\s+)?config'
- '(?:checks?|skills?|hooks?|fields?)\s+are\s+available'
- '(?:checks?|skills?|hooks?|fields?)\s+are\s+registered'
# endregion:count-triggers
drift_map: .github/docs-drift-map.yml
doc_completeness:
# `archived` docs are frozen; `plan` docs (see taxonomy above) are the
# delivery programme's own working records, governed by the `ledger` check.
exempt_classes: [archived, plan]
behavioral_claims:
sources:
- type: pytest
path: tests
# Site JSON fixtures projected from Python source. `generated-freshness`
# runs each command, diffs against the tracked file, and restores the
# snapshot — so a source change without a regenerated fixture fails CI.
#
# coverage.json is intentionally NOT gated here: coverage numbers are
# environment-dependent (Python version, platform branches) and would drift
# between local dev and CI even with no source change. The emitter still
# runs at site-build time (site/package.json prebuild) so the site always
# shows current coverage; it just isn't diff-gated.
# region:generated-fixtures
generated:
- doc: site/src/generated/checks.json
command: jk-standards emit checks
- doc: site/src/generated/config-schema.json
command: jk-standards emit config-schema
- doc: site/src/generated/skills.json
command: jk-standards emit skills
- doc: site/src/generated/doc-coverage.json
command: jk-standards emit doc-coverage
# endregion:generated-fixtures
# The .mdx reference pages slice source via <CodeSnippet file=… region=… />;
# snippet-regions verifies every such reference resolves to a real
# region:<name> marker in the named file. This repo references regions only
# through CodeSnippet (which names its own file=), so no source_roots are
# declared — prose scanning stays off and every CodeSnippet target is checked.
snippet_regions:
doc_roots:
- path: docs
extensions: [".md"]
- path: site/src/content/docs
extensions: [".mdx"]
# The two directed boundaries ARCHITECTURE.md lists as invariants. Dependencies
# flow one way: the CLI and emitters import the check registry, so a check must
# never import back into either — that would invert the dependency and create a
# cycle. Each rule greps every .py under checks/ for the module-path reference;
# a genuinely-necessary crossing is waived in place with `# boundary-ok: <why>`.
# region:boundaries-config
boundaries:
rules:
- name: checks-no-cli
from: src/jk_standards/checks
extensions: [".py"]
forbid: 'jk_standards\.cli\b|from jk_standards import [^\n]*\bcli\b'
hint: "a check must not import the CLI — the CLI depends on checks, not the reverse"
- name: checks-no-emit
from: src/jk_standards/checks
extensions: [".py"]
forbid: 'jk_standards\.emit\b|from jk_standards import [^\n]*\bemit\b'
hint: "a check must not import the emitter — emit depends on checks, not the reverse"
# endregion:boundaries-config
# doc-coverage inverts doc-completeness: the ast enumerator walks these Python
# source roots listing public documentable units, and scans doc_scopes for the
# word-boundary "mention" OR-signal. A module fails only if EVERY unit in it is
# undocumented (no docstring, no drift-map glob match, no doc mention); a
# top-of-file `# doc-coverage-ok: <reason>` marker waives a module.
# region:doc-coverage-config
doc_coverage:
source_roots:
- path: src/jk_standards
extensions: [".py"]
doc_scopes: [docs, site/src/content/docs, conventions]
# endregion:doc-coverage-config
# import-cycle builds the module-level import graph for each listed package,
# finds every strongly-connected component of >1 module (a runtime import
# cycle), and reports it at file:line. Skip-when-unconfigured like boundaries:
# with no packages listed the check is a no-op. The toolkit gates its own
# source tree; its one real cycle (the checks re-export hub) is waived in place
# with `# import-cycle-ok:` at src/jk_standards/checks/__init__.py, so a NEW
# cycle is what turns this red.
# region:import-cycle-config
import_cycle:
packages:
- src/jk_standards
# endregion:import-cycle-config
# workflow-concurrency: a concurrency group must scope itself by ref, or be
# named here as a lock that is deliberately repo-wide. `pages` is the only one:
# publish-site.yml serialises real GitHub Pages deployments, which genuinely
# contend for a single resource. deploy-site.yml is absent because its group
# already carries `github.ref` for the build-only smoke path — the scoping that
# stops concurrent pull requests from cancelling each other's jobs.
# region:workflow-concurrency-config
workflow_concurrency:
global_locks:
- pages
# endregion:workflow-concurrency-config
# release-pins: every released version is tagged, and every pin naming this
# repo resolves to a real tag. `untagged_versions` records the three releases
# that shipped a changelog section but never got a tag — recovering those tags
# retroactively would date them wrong, so they are declared instead and the
# check ratchets on every release after them. The MIGRATION notes are excluded
# because their pins record what those projects actually adopted at the time;
# rewriting them would falsify the record.
# region:release-pins-config
release_pins:
repo: JimAKennedy/jk-standards
untagged_versions: ["0.2.0", "0.4.0", "0.7.0"]
exclude:
- MIGRATION-poly.md
- MIGRATION-nfr-review.md
# endregion:release-pins-config
# LLM-judged skill evaluation (evals/harness; `make eval`). Thresholds start
# generous with the ratchet time-boxed: revisit after two releases' score
# distributions (decision recorded in docs/plans/skill-evals/M002-decisions.md).
# region:skill-evals-config
skill_evals:
agent_model: claude-sonnet-5
judge_model: claude-sonnet-5
runs: 3
default_threshold: 0.6
# 8000: claude-sonnet-5 emits a thinking block before its text block, and
# on design-shaped tasks the thinking alone can exceed 4000 tokens — a
# cap it exhausts yields the runner's no-output sentinel and a zero score.
max_output_tokens: 8000
max_cases: 40
# endregion:skill-evals-configjk-standards <check-name> [--root DIR] [--config FILE] [--base REF]jk-standards all # every configured static check (+ doc-drift # when --base or GITHUB_BASE_REF is available)jk-standards list # list check namesjk-standards emit <name> # regenerate one drift-proof fixture under # site/src/generated/; <name> is one of # checks | config-schema | skills | coverage | # doc-coverage | alljk-standards emit <name> --check # exit 1 if the on-disk fixture differs from # what would be emitted now (CI drift gate)jk-standards doc-coverage --update-baseline # record/ratchet the per-module floor map at # baselines/doc-coverage.json (never via emit, so # a floor can never silently self-heal)jk-standards doc-coverage --update-baseline --allow-regression # with --update-baseline: permit a write that # LOWERS an existing floor (refused otherwise)jk-standards install-skills [vX.Y.Z|latest] [--dest DIR] [--force|--check|--update-lock] # vendor skills from skills-lock.json # (default dest .agents/skills); a version # argument moves the lock to that release — # verified upstream before any mutation — and # reinstalls both asset kinds atomically # ('latest' resolves via the newest published # GitHub Release, not the newest tag)jk-standards install-commands [vX.Y.Z|latest] [--dest DIR] [--force|--check] # vendor workflow commands from the same lock file # (default dest .claude/commands/jk, so a command # arrives namespaced as /jk:<name>)--update-baseline and --allow-regression are check flags, not emit verbs:
they belong to jk-standards doc-coverage, and --allow-regression is valid
only alongside --update-baseline.
Exit codes: 0 clean, 1 violations, 2 usage/config error. Checks whose config section is empty report themselves as skipped rather than failing — adoption is incremental by design.
Consuming
Section titled “Consuming”Pre-commit (pin to a release tag):
- repo: https://github.com/JimAKennedy/jk-standards rev: v0.13.0 hooks: - id: doc-taxonomy - id: status-prose - id: file-line-refsCI (the reusable workflow supplies checkout depth and base-ref wiring):
jobs:
checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ inputs.python-version }}
- name: Install jk-standards
run: pip install "git+https://github.com/JimAKennedy/jk-standards@${{ inputs.toolkit-ref }}"
- name: Run checks
run: jk-standards all --config "${{ inputs.config }}"