Adopt in a repo
This walkthrough takes a fresh repository from zero to full jk-standards
gating. If you already have pre-commit, jk-standards.yaml, and a
drift map, jump straight to the Quickstart instead.
Step 1 — Pick your doc roots
Section titled “Step 1 — Pick your doc roots”Decide where your docs live. Common shapes:
docs/for a plain markdown reposite/src/content/docs/for an Astro/Starlight site- Both, if you have some raw markdown and a rendered site
Every file under a doc root will need class: frontmatter. If you have
long-frozen historical docs you don’t want to classify, list their
containing directory under exempt_dirs.
Step 2 — Write the config
Section titled “Step 2 — Write the config”Create jk-standards.yaml at the repo root. The toolkit dogfoods
itself, so its own config is the reference example:
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-configField-by-field detail is in the Configuration reference.
Step 3 — Classify your docs
Section titled “Step 3 — Classify your docs”Frontmatter for every file under the doc roots:
---class: gatedtitle: My doc---
# ...The three canonical classes:
- gated — living, current-state doc. Linted by
status-prose,file-line-refs,count-drift, andbehavioral-claims. - archived — frozen dated record. Exempt from all linting; the content is a historical artifact.
- generated — machine-produced. Freshness-checked against its generator command, but not prose-linted.
Run jk-standards doc-taxonomy — it lists every doc missing a class or
carrying an unknown one.
Step 4 — Wire pre-commit
Section titled “Step 4 — Wire pre-commit”Add jk-standards to your .pre-commit-config.yaml, pinned to a release
tag. Every shipped hook is defined once in .pre-commit-hooks.yaml —
this is the exact definition your pre-commit install will resolve
when you enable doc-taxonomy:
- id: doc-taxonomy
name: docs carry a lifecycle class
entry: jk-standards doc-taxonomy
language: python
types_or: [markdown, mdx]
pass_filenames: falseEnable the ones that fit your repo:
- repo: https://github.com/JimAKennedy/jk-standards rev: v0.13.0 hooks: - id: doc-taxonomy - id: status-prose - id: file-line-refs # opt in when you have inventory nouns to gate: # - id: count-drift # opt in when you have a test suite to cite: # - id: behavioral-claimsThen pre-commit install and pre-commit run --all-files to catch
existing violations.
Step 5 — Wire the reusable CI workflow
Section titled “Step 5 — Wire the reusable CI workflow”doc-drift needs a git base ref (the PR merge base) and full commit
history, so it lives in CI rather than pre-commit. Consume the shipped
reusable workflow:
name: docson: [pull_request, push]
jobs: discipline: uses: JimAKennedy/jk-standards/.github/workflows/doc-discipline.yml@v0.13.0 with: toolkit-ref: v0.1.0 # pin the same version you used above config: jk-standards.yaml # path in your repoThe workflow checks out the PR with fetch-depth: 0, installs jk-standards
at the pinned ref, and runs jk-standards all. First-run failures name
the exact drift; fix by either updating the doc or adding a
Docs-Not-Affected: <reason> trailer to a commit in the range.
A companion reusable workflow, pre-commit.yml, runs your pre-commit hooks
in CI from the same shape of caller. Reach for it when a repo wants a CI gate
on hooks it cannot run on pre-commit.ci — pass the optional local-config
input to also run a second, self-referential config:
name: pre-commiton: [pull_request, push]
jobs: pre-commit: uses: JimAKennedy/jk-standards/.github/workflows/pre-commit.yml@v0.13.0 with: config: .pre-commit-config.yaml # path in your repo local-config: .pre-commit-config.local.yaml # optional; empty = skipStep 6 (optional) — Enable inventory gating
Section titled “Step 6 (optional) — Enable inventory gating”If your prose says things like “N presets ship” or “N chapters in the
guide”, configure count_drift.triggers and interpolate the number from
a generated JSON:
count_drift: triggers: - 'factory\s+presets?' - 'chapters?\s+in\s+total'
generated: - doc: docs/counts.json command: node scripts/emit-counts.mjsThen reference it in prose as {counts.presets} rather than a numeral —
the count-drift check refuses numerals adjacent to trigger phrases,
but leaves the template literal alone.
Step 7 (optional) — Enable behavioral claims
Section titled “Step 7 (optional) — Enable behavioral claims”If you want prose to cite tests, configure a test-index source:
behavioral_claims: sources: - type: pytest # gtest | js | pytest path: testsThen in prose, add a verified: marker after each claim. The marker
wraps a key from the scraped test index in square brackets — pytest keys
look like test_module::test_name, gtest keys look like Suite.TestName.
Unresolved citations fail; the ⚠ unverified marker is allowed but
counted as an honest-state metric.
The Checks reference uses citations like these throughout — every claim about the toolkit’s behavior points at a real test.
- How to add a check — write a new check and wire it through the CLI, hooks, and emitters.
- Checks reference — every check with its rule and escape hatches.