Skip to content

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"]
Ruff pin and line-length live in pyproject.toml.

The 42 fields of {schema.config_file_name} are projected directly from the Config dataclass in src/jk_standards/config.py:

FieldTypeDefault
action_pin_extensionslist[str][".yml",".yaml"]
action_pin_workflow_dirstr".github/workflows"
boundarieslist[BoundaryRule][]
claim_sourceslist[ClaimSource][]
count_triggerslist[str][]
deps_only_manifestslist[str][]
doc_completeness_exempt_classeslist[str]["archived"]
doc_coverage_doc_scopeslist[str][]
doc_coverage_module_min_percentint | NoneNone
doc_coverage_source_rootslist[SourceRoot][]
doc_rootslist[DocRoot][{"extensions":[".md"],"path":"docs"}]
drift_mapstr".github/docs-drift-map.yml"
exempt_dirslist[str][]
file_line_extensionslist[str]["c","cc","cpp","h","hpp","py","js","mjs","ts","tsx","astro","md","mdx","sh","yml","yaml"]
file_line_source_rootslist[SourceRoot][]
generatedlist[GeneratedDoc][]
import_cycle_packageslist[str][]
ledger_rootslist[str]["docs/plans"]
ledger_validationsstr".jk/validations.yml"
provenance_anchor_patternstr"(ref|fr)-[A-Za-z0-9-]+"
provenance_bib_filestr""
provenance_doc_rootslist[DocRoot][]
provenance_phrasestr"not original (research|theory)"
release_pin_changelogstr"CHANGELOG.md"
release_pin_excludelist[str][]
release_pin_extensionslist[str][".md",".mdx",".yml",".yaml"]
release_pin_repostr""
release_pin_repo_urlstr""
release_pin_untagged_versionslist[str][]
snippet_doc_rootslist[DocRoot][]
snippet_markerslist[SnippetMarkerSyntax][]
snippet_source_rootslist[SourceRoot][]
status_date_tolerance_daysint0
status_forbidden_extralist[ForbiddenPhrase][]
taxonomy_classeslist[str]["generated","gated","archived"]
taxonomy_extra_fileslist[str][]
workflow_concurrency_dirstr".github/workflows"
workflow_concurrency_extensionslist[str][".yml",".yaml"]
workflow_concurrency_global_lockslist[str][]
workflow_concurrency_ref_tokenslist[str]["github.ref","github.ref_name","github.head_ref","github.event.number","github.event.pull_request.number","github.run_id","github.sha"]
workflow_perm_dirstr".github/workflows"
workflow_perm_extensionslist[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.

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-config
jk-standards.yaml — the toolkit gates itself with itself.
jk-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 names
jk-standards emit <name> # regenerate one drift-proof fixture under
# site/src/generated/; <name> is one of
# checks | config-schema | skills | coverage |
# doc-coverage | all
jk-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.

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-refs

CI (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 }}"
The reusable doc-discipline workflow.