Aller au contenu

Drift & enforcement

Ce contenu n’est pas encore disponible dans votre langue.

For org admins.

Beyond checking docs against your standard (see Standards & attestation), Hekkos tracks drift — when the code and the docs disagree — and gives you the levers to act on it. This guide covers drift findings and their severity model, how drift surfaces on open PRs, the optional merge gate, documentation-authority tiers, the standing drift PR, and where a repo’s drift surfaces on the Repositories page.

Three lenses: drift, principle, advisory

Hekkos surfaces three kinds of finding, unified as violations on the Violations tab of Overview (there’s no standalone Violations page — the old /violations bookmark redirects into that tab). Each answers a different question:

LensQuestionExample
DriftDoes the code match what’s documented?a route the docs describe no longer exists
PrincipleDoes the code obey your declared rules?an /orgs/{id} route is missing its tenant-auth middleware
AdvisoryIs the code good practice, even when the docs agree?the code uses AES-128 — and so do the docs, but it’s legacy

Category. Every violation carries a subject categorysecurity, tenancy, architecture, dependencies, … — inherited from the rule it belongs to. That’s what lets you ask “is password encryption respected throughout the app?” and filter to the security category.

Principles are rules you author (Standards → Policies): a scope (which code they apply to) plus a condition, with a polarityrequired (the code must satisfy it) or discouraged (flag any code that matches). Curated best-practice principles (weak crypto/hashes/curves, legacy signatures, weak TLS versions or cipher suites, disabled TLS or SSH host-key verification, JWT “none” algorithm, low bcrypt cost, world-writable files, permissive CORS, deprecated dependencies) ship enabled and advisory by default:

Where a rule comes from. A principle is authored one of three ways: turned on from the catalogue (a curated preset), hand-written in the CEL editor, or extracted from a standards doc. Extraction reads a source you opt in as a standards source (a repo’s docs or a Confluence space), turns each normative statement (“must use…”, “do not use…”) into an advisory rule — binding a catalogue preset where one fits, else a generated check — and links it back to the document. An extracted rule carries a Source chip on its card; like every rule it starts advisory, and only becomes enforcing if you flip its Required toggle. A statement that maps to no precise check is skipped (not silently turned into a vague rule). Extraction, the standards documents behind it, and the Org → Initiative → Repo scope tree all live on the dedicated Policies page.

AI-judged rules (advisory). Some rules can’t reduce to a precise check — “error messages must not leak internals”, “services own their persistence”. For those, an admin can opt the org into the judged lane (Standards → Policies, off by default): during a scan, an AI judges the org’s semantic principles against the code and raises advisory findings. Every verdict is grounded (a finding citing code not shown to the model is discarded) and the lane never blocks a merge — only a deterministic rule can. Turning it on sends code context to the model; self-hosted deployments route it to a local model.

Scope: org, initiative, or project. A rule applies org-wide by default. Group repos into an initiative (a department’s platform, say) and scope a rule to it in the editor’s Applies to selector — it then applies only to that group. A repo is judged against the union of its org-wide rules plus every initiative it belongs to; deleting an initiative reverts its rules to org-wide. they comment, never block, and you can disable any of them or ignore an individual finding as not-applicable (it won’t come back until the code changes). When you want one enforced, flip its Required toggle — a deterministic principle can then gate merges through the hekkos/principles gate (fuzzy rules stay advisory). Principle and advisory findings are kept out of the Trust/Accuracy score and the drift posture — those remain a pure code-vs-docs measure.

On the Violations tab, each finding’s code location is a deep-link to the offending line on the default branch, and a principle finding links its rule to the matching card on the Policies page. A Required badge marks the findings that can actually stop a merge (the hekkos/principles gate), distinct from severity. When a code area concentrates the violations — at least 40% of at least 10 — a one-line nudge names that folder so you can clear the most at once; it’s read-only (the fix is the code, so there’s no ignore-folder action here, unlike the repo’s governance view).

Real-state drift — when the code and the docs disagree

Beyond section attestation, Hekkos tracks whether your docs match the actual code. It extracts the real state (routes, env vars, functions, types, CLI flags) and diffs it against what the docs claim, producing drift findings with a severity model:

  • critical — a doc points users at a route / env var / flag that no longer exists (they’ll hit failures).
  • warning — a documented symbol is gone, or a documented signature no longer matches the code.
  • info — real state that nothing documents.

The drift count is actionable drift only. The Drift severity on Overview → Repositories, the repo’s findings, and the drift posture on Insights all count findings where the docs disagree with the code (critical + warning). The info class — code nothing documents — is a coverage signal, not drift: it’s reflected in your doc-coverage score, not in the drift number. (Otherwise every undocumented internal helper would read as “critical drift,” which is what used to make the count balloon into the thousands.)

The “Drift caught” headline on Home

The Home dashboard carries a Drift caught headline: how much deterministic documentation drift the gate has surfaced across your repos to date, with the cleared subset called out beside it. It’s an at-a-glance value/ROI figure for a QBR — not a Trust number, and never a billed quantity (your plan is priced by repo, not by findings; see Plans & usage).

It stays honest about what it can prove:

  • Caught counts the verified-tier, code-domain drift findings the rollup already reports — each finding once (one row per fingerprint, so a re-detection on a later scan never inflates it), across the repos in your current scope filter.
  • Cleared is the resolved subset. A finding can clear three ways — a doc fix, a human dismissal, or the drift simply no longer being detected (the code moved or was deleted) — and the record keeps no reason, so the label reads “fixed, dismissed, or no longer detected” rather than claiming a fix or a “prevention” it can’t stand behind.

The headline fails quiet: while the figure is still loading, or when nothing has been caught yet, it shows nothing — a blank is never dressed up as a reassuring “0 caught.”

What a scan measured over — the coverage manifest

Trust is only honest presented alongside the slice it was measured over, so each baseline scan records a coverage manifest. The real-state metrics response carries it as coverage_manifest (repo-scoped), plus a measured_note that qualifies the headline — “structured (high-confidence) extraction covered X% of code files” — followed by any degraded-coverage warnings: cgo off (a language extracted at the low-confidence regex tier, so no structured signature drift for it), a SCIP indexer that didn’t run (no call graph for that language), or a scan truncated at the fetch cap (Trust measured over the first N files, not the whole repo). The same warnings appear on the MCP get_real_state tool as a Coverage: note. Generated files (.pb.go, anything marked @generated or Code generated … DO NOT EDIT) and build/cache directories are excluded from the denominator so codegen never reads as undocumented surface. Hekkos discloses these limits; it does not silently widen the scan.

Tuning what the scanner treats as docs and what it skips

By default the drift scanner recognizes a fixed set of documentation extensions (.md, .rst, .adoc, .org, .html, …) and skips a fixed set of build/vendor directories (node_modules/, vendor/, dist/, testdata/, …). An admin can adjust both per org under Settings → scan config (API: GET/PUT /api/v1/orgs/{id}/scan-config): add or remove skip directories, and stop treating a default doc extension as documentation. A no-config org is unchanged.

Two honesty guarantees bound this:

  • It never moves your bill. These knobs feed the drift scanner only — they do not change what counts as a governed document (the plan meter), so a repo can’t be made cheaper by hiding docs from the scanner. The billed unit stays the repo (see Plans & usage).
  • It’s always disclosed. When your registry differs from the defaults, the scan’s coverage manifest records the exact delta and adds a note — “measured with a customized doc/skip registry” — so a Trust number is always read against the surface it actually measured, never mistaken for “measured over everything.”

Adding a new doc extension is limited to formats Hekkos can parse — an extension its extractor doesn’t understand is rejected rather than silently fetched-but-inert.

(Which documentation types an org is governed on — README, SECURITY, ADRs, … — is a separate, deliberately non-self-serve control: because it defines the governed denominator, only a Hekkos operator can adjust it for an org, and every change is written to the operator audit log.)

In-code comments are a documentation source too

Documentation doesn’t have to live in your knowledge base. Hekkos now also reads each function’s or type’s own leading doc comment — a Go godoc block, a JSDoc/TSDoc comment, a Python docstring, a Javadoc, a rustdoc, or a Ruby # comment — as a real documentation source. A symbol documented only by the comment right above it still counts as covered (see Coverage). (Only Go captures comments in a CGO_ENABLED=0 build; the other languages need the cgo server build.)

When a symbol’s own comment provably disagrees with its code, Hekkos raises a stale_comment finding — the comment appears to be out of date. It fires only on the two cases it can check deterministically: the comment restates a function signature that no longer matches, or it names a function/env var that no longer exists. It is always informational and low-confidence: it never blocks a merge, is never written into a PR or the standing drift PR, and never moves your Trust number — it’s a quiet heads-up that a comment may have rotted, nothing more.

(A note on languages: the typed-signature check is effectively Go-only — Hekkos captures a full typed signature for Go, but for other languages the captured shape is an untyped call form, so only the dead-reference case applies there.)

Optional: semantic comment-rot (judged lane). The two checks above are purely structural — they can’t catch a comment whose meaning drifted while the signature stayed the same (a comment that says “returns the cached value” when the code now recomputes on every call). If your org has opted into the judged lane, a scan also runs an AI pass that reads each documented symbol’s comment against its code and raises the same low-confidence stale_comment finding when the comment’s described behavior contradicts the code. It shares every guardrail of the deterministic signal — always informational, never blocks, never moves Trust — and inherits the judged lane’s posture: off unless you opt in, grounded (a verdict it can’t quote from the code is discarded), token-metered against your plan budget, and fail-soft (if the model is unreachable, slow, or over budget the scan simply skips the pass and finishes normally — it never fails a scan). Self-hosted deployments route it to a local model, same as the other judged rules.

Confluence is a measured source

Real-state drift isn’t limited to docs that live in your repository. A connected Confluence space is a measured source too: Hekkos runs each fully-open page through the same extract-and-diff pipeline as your in-repo docs, so a wiki page that claims a function signature, route, or env var the code no longer has raises a drift finding attributed to the repository it describes. Two rules keep this honest:

  • Code-resolvable only. A wiki claim becomes a finding only when it maps to something real in the code — a live symbol whose signature disagrees, or a symbol the repo genuinely had and removed. A page that name-drops a route which never existed in the repo isn’t drift; it’s dropped to the unverified tail, not attributed to a repo it was never about.
  • A separate sub-metric, never the code headline. A wiki page is not your repository’s real surface, so wiki findings are held on their own honesty count (how many wiki claims disagree with the code) and are excluded from the code Trust, coverage, and posture numbers at every site that computes them. They are also excluded from the standing hekkos/drift PR and the merge gates — a wiki edit is not a code change, so there’s no PR to open and no build to block. Fixes happen in Confluence; Hekkos re-checks on the next sync. In a repo’s Findings view a wiki finding carries a Wiki source chip and an Open page in Confluence link instead of a GitHub deep-link, and the repo’s code drift count stays code-only with wiki drift shown as a discrete “+N from Confluence” indicator.

Wrapper-read env vars and flags aren’t false positives

An env var or flag your code reads through a config-loader wrapper — e.g. get("DATABASE_URL") rather than a literal os.Getenv("DATABASE_URL") — used to get flagged as a critical “documented but no longer in the code” finding, even though the variable is very much still in use. It no longer does. Hekkos now treats the bare name appearing anywhere in the code text as evidence the code still uses it, so a documented-but-wrapper-read variable is quietly matched instead of reported. Build-time env vars read the Vite/Astro/SvelteKit way — import.meta.env.PUBLIC_APP_URL — are recognized as a direct read too, alongside process.env.X and os.getenv, so a documented var read only through import.meta.env is matched, not flagged. Only a documented env var or flag whose name appears nowhere in the code stays a loud critical — that’s the genuinely-removed case you actually want to hear about.

What no longer counts as drift

A drift finding is only useful if it names something real. Hekkos now discards a range of look-alikes before they can raise a finding, so the loud slots go to genuine mismatches:

  • Placeholders. Backticked stand-ins in a template or example — REPLACE_ME, CHANGE_ME, YOUR_TOKEN, EXAMPLE_KEY, TODO, and the like — are never treated as env-var contracts.
  • Ordinary words and SQL. Common prose verbs (get, set, run, use) and SQL keywords (EXISTS, COUNT, IN, ON, DISTINCT, …) quoted in a doc are no longer mistaken for function or symbol names that “went missing”.
  • Filenames. A token that is shaped like a file (budget_rescan.go) is no longer read as a code symbol that vanished.
  • Library calls. A symbol that belongs to a dependency you import (chi.URLParam) is matched against that dependency, not flagged as a missing symbol of your own.
  • Platform config. An UPPER_SNAKE name mentioned right beside GitHub-platform wording — a repository secret, an Actions variable, an environment secret — is recognized as CI/platform configuration, not an application env var your code must still read.

Env vars in workflows and ops files count as evidence

An env var doesn’t have to appear in your application code to count as “still used”. Hekkos now reads your ops surface as corroborating evidence — GitHub Actions workflow YAML, shell scripts, Dockerfiles, Makefiles, .env files, and Terraform / OpenTofu (.tf, .tf.json, and .tofu). A variable a doc mentions that lives only in .github/workflows/deploy.yml (say GCP_PROJECT_ID) is matched there instead of being reported as “documented but gone”.

Infrastructure is checked for drift too: a doc that names a Terraform resource, variable, output, or module address is matched against the actual .tf / .tf.json / .tofu files. The address can be written the way you’d reference it in HCL — bare (aws_s3_bucket.assets), module-qualified (module.vpc.aws_subnet.private), or for a for_each/count instance (aws_instance.web[0]) — and all resolve to the same resource, so a doc pointing at a resource that was renamed or removed is caught however it’s addressed.

The unverified tier

Not every doc reference can be checked with confidence. A bare, unfalsifiable anchor — a plain lowercase word a doc happens to backtick, with nothing in the code to confirm or deny it — used to be able to render as a loud “Critical”. It no longer can. Hekkos sorts findings into two tiers:

  • Verified findings are the ones it can stand behind. These lead the PR, count in the headline, can be acknowledged, and can block (where blocking is enabled).
  • Unverified findings are low-confidence guesses. They are collapsed into a single closed “Unverified” section, excluded from every headline count, never written into your docs, never given an acknowledge handle, and they never block a merge — because Hekkos genuinely can’t tell whether they’re drift. They’re shown only so nothing is hidden from you.

If a later extractor improvement lets Hekkos confirm one of these, it graduates into the verified tier on the next scan and starts counting normally.

On an open PR

When a pull request introduces new drift (its code change leaves a doc stale), Hekkos posts one comment summarizing what drifted. Findings are grouped by the doc file each stale claim lives in — the unit you’d actually open to fix them — under an honest headline count (e.g. 4 doc claims out of date · across 2 docs), not sorted into a wall of severity buckets.

  • The comment is self-updating: it is edited in place on every push (never restacked into a wall of duplicate reviews) and is deleted once the drift is resolved.
  • Each doc’s findings collapse into an expandable group, worst-affected doc first. When a single push turns up more than a screenful, the surplus is folded into one “+N more claims across M more docs” line that points at the repo’s findings on the Repositories page — so the comment stays skimmable no matter how much drifted.
  • Lower-confidence guesses never lead: they are held in a separate collapsed Unverified section and left out of the headline count (see The unverified tier below).
  • By default this is comment-only — Hekkos never blocks your merge.
  • An org can opt into blocking (pr_open_blocking_enabled in the drift config), in which case a canonical-tier finding at or above pr_open_block_severity requests changes (reference/spec sources never block — see authority below).

A suggested doc fix, written for you

Separately from the drift summary, when a PR’s code change leaves an existing doc stale, Hekkos may post a second comment — “Documentation Drift Detected” — containing the doc update it recommends, drafted from your diff. This is the same suggestion engine behind suggestion PRs, applied to the change you’re already making so the doc fix can ride along in review.

  • Trivial PRs never get one. Doc-only, test-only, and dependency-bump changes are filtered out before any analysis, as are diffs with no visible doc impact — most PRs see no comment at all.
  • One comment, refreshed in place. On each push Hekkos edits that same comment with its latest draft rather than stacking a new one, so the PR carries exactly one suggestion comment and it always reflects the current diff.
  • Dismiss it the same way as any suggestion, by commenting on the PR: /hekkos override <reason>. If the merge gate (below) is enabled, dismissal also clears the hekkos/doc-drift status so the PR can merge.

When the stale line is in your diff, Accept is one click. If the doc claim that went stale sits on a line your PR actually edited, Hekkos posts a native GitHub suggested change on that line — Accept is the Commit suggestion button, no command to type. This only works for the minority case where the stale line is in the PR’s diff; the common case (the stale doc is elsewhere) is handled off-diff by the standing drift PR’s committed fix (below) and the dashboard, not an inline suggestion. A suggestion is only offered for a high-confidence fix with a single, unambiguous locus — Hekkos never rewrites a line it can’t pinpoint.

The merge gate (optional)

If a repo’s .hekkos.yml (see per-repo overrides) sets enforcement.merge_gate: true, Hekkos posts a hekkos/doc-drift commit status on PRs that introduce documentation drift. Combined with a GitHub branch-protection rule requiring that check, this blocks a merge until the drift is addressed or the suggestion is dismissed.

The doc-standards merge gate

Separately from the drift gate above, you can make a doc type block merges when it’s set to Required on the Standards page. This is the enforcement side of attestation, and it posts its own status context, hekkos/doc-standards — independent of hekkos/doc-drift, so the two gates never clear or block each other.

What it blocks, and when. On each PR open/update, for every doc type you set to Required, Hekkos checks the doc at the PR’s head and fails the hekkos/doc-standards check only when a required doc or section is missing (Missing / Sections missing). It is deliberately narrow and safe:

  • Change-scoped — it only evaluates a Required doc type whose file the PR actually touches, so an unrelated PR is never blocked, and an external contributor’s PR can’t fan the check out across every doc type.
  • Fails open — an off-standard/stale doc, a doc type with no authored standard, an unresolved path, a fresh repo not yet scanned, or any transient read error all pass. Editing a standard never retroactively blocks in-flight PRs.
  • Org policy wins — a repo’s .hekkos.yml may tighten enforcement but can never relax an org Required doc type.

Because it computes at the PR head, pushing an empty commit or reopening the PR can’t clear a genuine gap — only adding the missing documentation (or waiving the doc type) does.

Turn it on for a repo. Add hekkos/doc-standards to the repo’s required status checks in branch protection — but, as with the drift gate, only after you’ve set at least one doc type to Required and seen Hekkos post the check once.

Dismissing the gate

The hekkos/doc-standards check is a real merge blocker, so clearing it is authorized. Comment:

/hekkos override standards <reason>

Only a commenter with write/admin access to the repo (or a Hekkos org admin) can clear it — a read/triage collaborator’s dismiss is refused. The standards token routes the dismiss to this context only; a plain /hekkos override <reason> (or /hekkos override drift …) clears the softer drift nudge and never the standards gate.

The policy gate

The Principle lens (Standards → Policies) checks whether your code obeys a declared rule — “all database access goes through the service layer”, “no weak or outdated encryption”, “routes under /orgs/{id} are tenant-guarded”. Every principle finding is advisory by default: it shows on the PR and the Violations tab, but does not block. When you want one enforced, flip its Required toggle, and it gates merges through a third, independent status context — hekkos/principles (named for the principle rules it evaluates, since it checks code, not documents; separate from hekkos/doc-drift and hekkos/doc-standards).

What it blocks, and when. On each PR open/update, Hekkos evaluates your enabled principles at the PR’s head and fails hekkos/principles only when the PR introduces a violation of a rule you set to Required. It is deliberately narrow and safe — the same posture as the other two gates:

  • Introduced-only — a violation already present on the base branch is never gated (you can’t be blocked by pre-existing debt, and touching that file doesn’t trap the PR); only a newly-introduced violation blocks.
  • Baseline-gated — when you first flip a rule to Required, Hekkos re-scans your repos to record the rule’s existing violations as the baseline. Until that baseline lands the rule fails open (it never blocks), so turning on enforcement can’t red-X every open PR in one shot over debt that predates the rule. Once the baseline exists, only violations introduced after it can block.
  • Deterministic-only — only a rule Hekkos can prove (a value or graph check — Go AST, the import graph) may be set Required. Fuzzy/LLM-judged rules stay advisory forever, and a low-confidence (e.g. regex-tier) detection lands in the unverified tail and never gates.
  • Fails open — no enabled blocking rules, no facts, an unsupported language, a fresh repo, or any transient scan error all pass. A scan error can never self-inflict a merge block.

The blocked PR’s Hekkos comment carries a Policy violations section naming each introduced violation, its severity, and a link to the offending line.

Turn it on for a repo. Set at least one principle to Required on the Policies page, let Hekkos post the check once, then add hekkos/principles to the repo’s required status checks. As with the other gates, fork PRs are not gated (Hekkos posts the repo’s own statuses), so scope the required check to repos whose PRs come from branches.

The agent-authored-PR gate (hekkos/agent-drift)

The three gates above target anyone’s PR. The agent gate is the one forcing function Hekkos can mandate on the one place drift is now created fastest: a PR opened by a coding agent (Copilot, Devin, Cursor, Claude Code, …). When enabled, it is a hard, non-dismissible block — unlike the drift nudge, it cannot be cleared with /hekkos override; it clears only when the drift is actually resolved.

Enable it. Set agent_merge_gate: true in your org’s drift config, let Hekkos post the check once, then add hekkos/agent-drift to the repo’s required status checks. Default is off — nothing appears until you opt in.

What it blocks, and when. On each PR open/update, once opted in, Hekkos posts hekkos/agent-drift on every PR (so a required check never wedges a human’s PR) and fails it only when both: the PR author is a known coding agent (a GitHub App whose login matches an agent — dependency/CI bots like Dependabot are excluded), and the PR introduces high-confidence drift. A human author always passes — Hekkos never blocks a person by authorship. Like the other gates it is introduced-only, high-confidence-only, and fails open on any scan error.

A green gate links to the signed attestation. When the gate passes and the repo carries a durable signed Trust Receipt — a repo whose latest baseline has a real falsifiable accuracy lane (a consistent npm lockfile today) — the check’s Details link points at that receipt in the dashboard, so an agent-authored merge lands with a dated, third-party-verifiable provenance record rather than just a green check. A repo with nothing honestly signable (harden-before-sign) keeps the plain guide link — the gate never links to a receipt that does not exist. A failing gate always keeps the guide link, since the author needs to know how to unblock.

The four gates, side by side

ContextChecksBlocks when
hekkos/doc-driftdocs vs. code (drift)a documented route/env/symbol no longer exists (org opt-in)
hekkos/doc-standardsdocument structurea required doc or section is missing (per-doc-type Required)
hekkos/principlescode vs. a declared rulea PR introduces a violation of a Required principle
hekkos/agent-driftdrift on an agent-authored PRa coding-agent PR introduces high-confidence drift (org opt-in; non-dismissible)

Documentation authority — stop old plans reading as drift

Hekkos tracks documentation wherever your repo keeps it — the root README, the docs/ tree, a package-level site/README.md or web/README.md, .github/ notes — not just the root and docs/. (Anything under vendor/, node_modules/, dist/, build/, or testdata/ is excluded.) That means a real contract in a package README — say a marketing site’s env-var table — is checked for drift like any other doc, but it also means internal notes and review reports now fall in scope.

Not every Markdown file is a current-state contract. A repo often keeps historical plans, specs, and design notes (e.g. under docs/), and by default a route or env var mentioned in a superseded plan — or in an internal scratch note now in scope — would be flagged as drift because the code moved on. Source-tier authority fixes that without deleting anything: tier those paths historical or ignored and they stop raising drift.

Under Settings → Documentation Authority, an admin adds ordered (path glob → tier) rules that apply org-wide — to every repo — first match wins, and anything unmatched stays canonical, so the feature is fully automatic until you tune it:

TierOn a code–doc mismatchCounts as coverageIn chat answersIngested
canonicalLoud drift findings — can block, where blocking is enabledYes — the document of recordYesYes
referenceWarnings only — never blocksYesYesYes
historicalRaises no driftNoNo — excluded, so a cut feature in an old plan won’t resurface in chatYes — kept for lineage
ignoredRaises no drift — never checkedNoNoNo — not ingested, never fetched

For example, docs/rfcs/** → historical quiets an entire back-catalogue of implemented RFCs in one rule. A dry-run button previews the effect against a repo (“N findings, was M”) before you save. Rules live in the org’s drift config (authority_rules) and take effect immediately — no re-scan needed.

This table governs org-wide rules only. To quiet a folder on a single repo rather than everywhere, open that repo on the Repositories page — the concentration banner in its governance view and /hekkos ignore both write a repo-scoped ignore (below), which does not appear in this table. Documentation Authority shows a read-only count of any repo-scoped ignores and points you to Ignored folders in the repo’s governance view to manage them; the editable list here is org-wide rules only.

Planning trees are historical by default. Documents under plans/, proposals/, or rfcs/at any depth, whether a repo-root plans/ or a nested docs/plans/ — describe intended state, so a mismatch there is expected rather than drift, and Hekkos tiers those trees historical automatically, with no rule needed. (A leading **/ in one of your own authority globs matches the same way: **/plans/** → historical covers both the root and nested plans/ trees, while still respecting segment boundaries — it won’t catch a myplans/.) Your own rules always win (first match), so if a plans/ folder is actually a current-state contract for you, add an explicit … → canonical rule to override. specs/, design/, and adr/ are not defaulted — a spec or ADR can be a live contract, and API specs are matched against code as structured route sources regardless.

Route paths reconcile across mount prefixes automatically, in both directions, so a prefix mismatch never flags an endpoint that’s really documented:

  • Doc writes the full path. A doc’s full orgs-scoped path (the .../widgets route under /api/v1/orgs/{id}) matches the code’s handler-local registration for /widgets (any framework’s prefixing), so you won’t see a route flagged as “documented but missing” just because the handler registers only the relative segment.
  • Doc writes the relative path. The reverse also holds: when your docs describe the endpoint by the relative path the handler actually writes (/widgets), the full mounted route Hekkos reconstructs from the prefix (.../widgets under /api/v1/orgs/{id}) is credited as documented too — it won’t show up in Document these first, and the endpoint won’t produce a duplicate finding for its handler-local and full forms. This works across every supported framework (chi, gorilla/mux, gin/echo/fiber, Express, FastAPI, Flask, Spring, JAX-RS, Micronaut, NestJS, ASP.NET).

Relative-path credit is deliberately conservative: it applies only when the mounted path is unambiguous (one route reconstructs to that relative form) and the relative doc carries actionable confidence. If two different prefixes produce the same relative path, or a genuine top-level route collides with a nested one on the same source line, Hekkos fails safe and flags both rather than silently crediting the wrong endpoint. (These illustrative paths deliberately omit the HTTP method: the extractor only treats a METHOD /path pair as a route claim, so a guide explaining route matching never accidentally flags its own examples.)

What the detector does NOT flag

The extractor is built to read contracts, not incidental text, so several kinds of mention never become drift:

  • Fenced examples. A route in a diagram fence (```mermaid) or a func signature in a non-Go fence is illustrative, not a contract — only prose, inline code, untagged fences, and route/Go-bearing fences (```http, ```bash, ```go) are read as claims.
  • Removed / planned mentions. A symbol on the same line as an unambiguous removal phrase (“was removed”, “no longer exists”), under a Known Stubs / Planned / Roadmap heading, or under a changelog Removed / Deprecated section (Keep-a-Changelog style), is demoted to the PR’s unverified tail rather than raised as a confident finding. A Changed section is not demoted — a modified-but-live contract still drifts loudly.
  • Example & snippet docs. A code reference in a doc under an examples/ or snippets/ path is illustrative usage, not a live contract, so it is demoted to the unverified tail. Only the doc anchors are demoted — example source code is still inventoried, so a sample never becomes a false “undocumented”.
  • Non-code idioms that share a shape. CSS custom properties (a double-dash design token), CSS or crypto call forms (rgba(…), HKDF(…)), and GitHub webhook events written as a dotted provider.event pair are recognised as collisions and dropped or demoted. (These examples are written without code formatting on purpose — spelled as literal backticked tokens, they would be extracted as the very claims this section says are ignored.)
  • Its own notes. The <!-- hekkos-drift --> note Hekkos injects (below) is stripped before the next scan re-reads the doc, so a note never re-generates the finding it describes.

The standing hekkos/drift PR

Hekkos maintains one standing pull request per repo, on the hekkos/drift branch and targeting your repo’s default branch — whatever it’s named (main, master, develop, …), not an assumed main/master — that collects the current drift and annotates the affected doc sections with a note. For a changed signature the note shows the current signature (“signature changed to …”); for a removed route/symbol/flag there is no current value to show, so the note states plainly that the reference no longer exists in the code. Its title says what and how many (e.g. docs: 12 doc claims out of date (4 docs)), and its body groups the findings by doc, leads with a short executive summary, separates confident findings from an unverified (lower-confidence) tail, and — when the drift clusters in one folder — nudges you toward the fastest cleanup. Each finding line carries a deep link (↗) to the exact doc line the stale claim sits on, and a signature-change finding also links the code location it now disagrees with, so you can jump straight to what to fix. It is human-safe: if you push your own commits to that branch, Hekkos will not overwrite them — it comments instead.

When the PR first appears

The first drift PR for a repo is introduced gently, so a fresh install doesn’t open with a repo-wide rewrite:

  • It follows your rollout pace. If your org is on the default pilot-first rollout (see Rollout pace), the very first drift PR for a repo is held back until that repo’s baseline scan is a couple of days old, mirroring how suggestion PRs are paced. It opens automatically once that window passes — you don’t have to do anything, and /hekkos regenerate bypasses the wait if you want it now.
  • Volume is capped on first contact. When a repo turns up a large backlog of drift, the first PR writes in-doc notes for only the handful of most-affected docs and tells you how to author the rest — the body still lists the full count, but the diff stays reviewable.
  • Concentration is surfaced, not annotated. If most of the drift is concentrated in one folder, the first PR annotates docs outside that folder and points you at ignoring the concentrated folder first (below) — rather than writing notes into the very folder it’s suggesting you quiet.

A real fix, committed for you

For the deterministic slice of drift — a documented signature that no longer matches the code, where Hekkos knows the exact correct value and can find the stale text unambiguously — the standing PR doesn’t just leave a note asking you to fix it. It rewrites the stale text to the correct value right in the PR’s diff, so Accept is merging the PR. The committed change replaces the note it would otherwise have written; you review a concrete one-line fix, not a “please update this” reminder.

This is held to a strictly higher bar than a note: it only fires on a high-confidence finding with a concrete replacement value and a single, unambiguous occurrence of the stale text. Anything Hekkos can’t rewrite safely — a deletion or rename with no replacement value, a claim buried in prose, an ambiguous match — stays a note for you to resolve by hand or acknowledge. Everything else that can’t be committed in-diff on your PR (the off-diff majority) lands here, on the standing PR, rather than as an inline suggestion.

One interaction model — Accept, Dismiss, Ignore

Everything Hekkos surfaces — a drift finding, a doc suggestion, a principle or advisory — takes the same small set of verbs, and each is recorded once (on a shared outcome log) so Hekkos learns what you accept. The verbs mean the same thing on every surface: the PR conversation, the standing drift PR, the repo’s governance view, the Violations tab, and the Suggestions panel on Overview.

  • Accept — “yes, keep/do this.” For a drift finding: /hekkos ack (below), the Acknowledge button in the repo’s governance view, or a checkbox on the standing PR (see below). For a doc-fix suggestion: merge it — and for the deterministic slice Hekkos has already written the fix into the diff for you (see A real fix, committed for you below), so Accept is a one-click merge.
  • Override — “clear the gate,” with a reason: /hekkos override <reason> on a PR clears the merge gate for that PR (add standards or drift to target one of the two gates). A bare /hekkos override replies with usage instead of doing nothing — a reason is required so the override is auditable. (/hekkos dismiss still works as an alias, but “override” is the verb for clearing a gate; “dismiss” is reserved for dismissing a notification.)
  • Snooze — “not now / not applicable here,” with a reason: /hekkos snooze <reason> on a doc-suggestion comment clears the nudge for that PR and records the disposition as ignored (a softer signal than a dismiss — it tells the learning loop “not this time,” not “this is wrong”). Same authorization as dismiss; a bare /hekkos snooze replies with usage. (Distinct from /hekkos ignore <folder-glob> below, which is the repo-wide folder ignore, not a per-PR disposition.)
  • Ignore — “no / not here”: /hekkos ignore <folder-glob> (admin, repo-wide), the folder ignore in the repo’s governance view, the per-finding “not applicable” on the Violations surface, or Ignore on the Suggestions panel — which is also how you reject a suggestion you don’t want (the old “Dismiss” button folded into Ignore; on the panel there are now just two verbs, Accept and Ignore).

Open fix PR — the primary action. In the dashboard, a code drift finding leads with Open fix PR, which jumps straight to this repo’s standing hekkos/drift PR — where the fix (a surgical correction, or a note where an automated fix isn’t safe) actually lands. Acknowledge is the secondary “accept the current state as-is” action, not the default: fixing drift, not dismissing it, is the intended path. A wiki (Confluence) finding has no PR — its fix happens in Confluence — so it leads with Open page in Confluence instead.

Snooze — “not now.” A drift finding can also be snoozed for a set number of days (default 7): it drops off the active surfaces exactly like an Accept, but Hekkos automatically re-surfaces it when the timer expires if the drift is still real. It’s the honest middle ground between accepting a finding you haven’t decided on and ignoring one you don’t want to forget.

On the dashboard. The Suggestions panel on Overview carries the same Accept / Ignore buttons for queued doc suggestions, so you can triage from the dashboard without opening each PR. Every one of these is written to the same outcome log as the PR-side verbs.

In the repo’s governance view. The same feed, filtered to that one repository, appears as a Fixes in flight section beside its findings — an open fix links straight to its pull request, a queued draft carries the same Accept / Ignore actions. So the loop (drift detected → fix drafted → PR to merge) closes in the pane already showing the drift, not only on the dashboard.

One-click checkbox accept. The standing drift PR renders a checkbox next to each note-residue finding — tick it to accept that finding, no command to type. Only org members can accept this way (Hekkos authorizes the person who ticked it).

Driving it from the PR conversation

  • /hekkos regenerate — rebuild the PR from the current real state (this does override your edits, since you asked). Org members only.
  • /hekkos ack <handle> — accept a specific drift finding so it stops surfacing until the underlying code changes again. Each finding line prints a short 12-character handle (not a long UUID) that you copy straight into the command. /hekkos accept <handle> is an accepted synonym. Org members only.
  • /hekkos ack <doc-path>bulk-accept every verified finding in one doc at once (e.g. /hekkos ack docs/config.md or /hekkos ack README.md), instead of acking them one line at a time. Only the verified findings are acked; any unverified guesses in that doc are left untouched (they never blocked or counted anyway), and Hekkos tells you how many of each it handled. Org members only.
  • /hekkos ignore <folder-glob> — tier a whole doc folder historical so it stops raising drift, straight from the PR (e.g. /hekkos ignore docs/rfcs/**). The ignore is scoped to this repo only — the one the PR lives in — so other repos in your org are unaffected, even if they have a folder at the same path. Org admins only — a non-admin who tries it is refused with a reply. On success Hekkos appends the rule, quiets that repo’s matching findings, and replies with exactly what it ignored, the fact that those docs also drop out of Hekkos chat answers for this repo (see the RAG trade-off below), and how to undo it — under Ignored folders in this repo’s governance view (open the repo on the Repositories page). This is the same repo-scoped ignore as the concentration banner, driven from GitHub.

Old full-UUID /hekkos ack commands from previously-opened PRs keep working, so nothing you’ve bookmarked breaks. (/hekkos override is not a drift command — it belongs on suggestion comments; used here, Hekkos replies pointing you to ack/accept instead.)

Acting on a finding refreshes the PR

/hekkos ack and /hekkos ignore now rebuild the standing PR in the same job. Accept or ignore a finding and the PR’s body, title, and count update to match immediately — no more acking a finding and watching it linger in a PR that never changed. (An ignore that clears hundreds of findings updates the PR in one pass.) The human-safe guard still holds: if you own the branch, Hekkos refreshes nothing and comments instead.

When everything clears

Once the last finding behind the PR is resolved, acknowledged, or ignored, Hekkos closes the loop instead of leaving a stale PR open forever:

  • If your docs still carry drift notes from a previously merged rebuild, Hekkos flips the PR to a note-removal PR (retitled docs: remove N stale drift notes — all clear) and leaves it open — merging it strips those now-orphaned notes out of your docs.
  • If there’s nothing left to clean up, Hekkos closes the PR with an all-clear comment. If drift regresses later, a fresh PR opens.
  • Notes that were merged into your default branch and later acknowledged or ignored are treated as orphaned and removed on the next rebuild, so a merged note never outlives the finding it described.

Hekkos won’t heckle its own PRs

The drift check is skipped on Hekkos’ own branches (hekkos/drift and hekkos/doc-improvements). Refreshing the standing PR — even a force-push — will never make Hekkos post a drift comment on its own pull request. (The guard is an exact branch-name match on Hekkos-pushed branches in your own repo, so naming a branch hekkos/… yourself, or opening one from a fork, doesn’t dodge analysis.)

A repo’s drift findings

A repo’s findings on the Repositories page (open a repo from the list — its governance view, visible to every member) are where a repo’s drift lives — they render whether or not you’ve adopted a doc standard.

Open Documentation Drift is deliberately distinct from Real-State Trust: it’s a count of doc/code mismatches, not a Trust number, so a big raw number here does not by itself drag your Trust down.

Pick a repo from the list and its governance view lists its findings worst-first, each with its effective severity and its source (built-in default, an org severity rule, or a per-finding override), the code file:line, and the doc location — with acknowledge and override-severity controls on every finding. Acknowledging a finding stops it surfacing until the underlying code changes again. The list shows honest empty, error, loading, and not-yet-scanned states — an error is never dressed up as “no drift”.

The repo’s detail header also carries a live Real-State Trust badge — a small SVG scored the same way as the org-wide Trust number and banded green / amber / red. By default it’s authenticated and in-app: it renders for a signed-in member viewing the repo, and the same URL returns nothing to anyone without a session.

Sharing the badge in a public README. If you want the badge on a public README, an admin can flip Public badge to on in the repo’s detail header. This is off by default and opt-in per repo — Hekkos never exposes a repo publicly on your behalf. Once on, the header shows a copy-ready markdown snippet (![docs trust](…/badge/{org}/{repo}/trust.svg)) that loads from an unauthenticated URL, so it works in an open repo. That public badge exposes only the numeric Trust percent and its band color — never file paths, doc or finding text, entity content, or even the fact that the repo exists: a repo that isn’t published (or any unknown id) returns an identical neutral docs trust: private badge, so the public URL can’t be used to probe your private repos. Flip it back to off at any time and the public URL immediately reverts to that neutral badge.

Priority to document

Above a repo’s findings, a Priority to document panel ranks its undocumented code by call-graph centrality — how many other entities and routes depend on each one — so you document the highest-impact gaps first instead of triaging a flat list. Each row names the entity and its file:line — a deep link to that code on GitHub where the file is known — how many callers point at it, and a route badge when an HTTP route targets it directly. A clean or not-yet-scanned repo shows a short teaching empty state rather than looking broken.

An entity that already carries an in-code doc comment is hidden by default — the panel exists to surface what has no documentation yet, so a commented symbol doesn’t belong at the top of it. A “Show N documented in code” toggle beneath the panel title switches to a documented-in-code view (those rows, badged “documented in code” with a short preview of the comment); toggling it off returns to the undocumented surface. If that comment looks out of date — from either the deterministic check or the optional semantic pass above — the row shows a “comment may be stale” badge.

Documented endpoints don’t clutter the list. A route handler whose endpoint is already documented externally (its route is written up in your API reference, so the route itself isn’t drifting) is ranked below every truly-undocumented entity and badged “documented via API route” — the handler function still has no code-level doc of its own, so it stays listed (a documented endpoint can still have an unexplained handler), just not at the top. This is a ranking cue only: such a handler is still counted as undocumented in your Coverage score, because the symbol itself carries no documentation.

The same call graph is exposed to agents as two MCP tools — risk_ranking (this ranking) and impact_of (the blast radius of a single entity) — so you can ask the same questions from your IDE (see Ask Hekkos & MCP).

The concentration banner — ignore a whole folder at once

When a single doc folder dominates a repo’s findings (the classic case: hundreds of stale references all coming from docs/rfcs/** or an internal planning directory), the repo’s governance view shows a concentration banner naming that folder directly above the findings it explains. An admin gets a one-click Ignore this folder button; a member sees the nudge but is told to ask an admin.

Ignoring the folder tiers it historical as a repo-scoped ignore — scoped to the selected repo, not org-wide — so it stops raising drift on this repo while leaving every other repo untouched. It is the same repo-scoped action as /hekkos ignore typed on that repo’s PR. Clicking Ignore this folder opens a confirmation dialog — nothing is saved on that single click — carrying a dry-run preview of the exact effect on that repo (“this repo would show NM drift findings”) and the plainly-stated trade-off you must accept:

Ignored folders (per repo)

Once a repo has at least one repo-scoped ignore — created by the concentration banner above or by /hekkos ignore on one of its PRs — the repo’s governance view lists them under Ignored folders for the selected repo, each with an Un-ignore control (admins only). This is the single place to review and undo a repo’s ignores; they deliberately do not appear in Settings → Documentation Authority, which is org-wide rules only. The list shows only the selected repo’s ignores, so switching repos in the picker changes what you see.

Un-ignoring a folder removes the rule, but its findings don’t reappear instantly — they return on the next scan of that repo. The row disappears right away, so don’t read the still-quiet findings as “un-ignore didn’t work”; they’ll come back once Hekkos next scans the repo.

Tuning severity defaults

Under Settings → Drift Severity Rules, an admin can override the built-in default severity for a drift kind — the key is a drift kind (doc_only) or a kind:entity_kind pair (doc_only:env_var). For example, set doc_only:env_var to info if that class is noisy for your org. This sits alongside the per-finding override in the repo’s governance view (which retunes a single finding) and the source-tier authority rules (which quiet whole doc paths).

Advanced drift-config toggles (opt-in)

Two org-level drift-config toggles are off by default — turn them on only deliberately, because each changes how confidently Hekkos raises or holds back a class of finding.

  • strict_anchoringpromote absent-grammar mismatches. When a doc names something from an API/config grammar the repo extracts zero entities of — a GraphQL field, a gRPC RPC, a k8s/config object — Hekkos by default keeps that claim in the low-confidence unverified tail: a repo that merely mentions such a grammar without actually having it shouldn’t raise confident false drift. Turning strict_anchoring on promotes those absent-grammar mismatches to actionable drift. Enable it only once you know that grammar is fully extracted for your repos, or the false-positive guard it removes will start firing.

  • learn_from_dismissalsdemote drift kinds you keep rejecting. When on, a drift kind that a repo has demonstrably rejected — measured over your own accept/dismiss history and Wilson-gated so a couple of dismissals aren’t enough — is demoted below the PR threshold into the report tail. It is never dropped and never auto-dismissed; you still see it, just not as loud actionable drift. Off by default, because quieting a class of finding from past behavior is a suppression-class change you should opt into knowingly.