Skip to main content

docs-drift

Detect drift between a documentation property and its source code, classify each finding (Confirmed / Tracked / Upcoming / Intentional / Needs-eng- confirmation), and draft held DOCSP tickets for confirmed drift only. Runs against any property given a manifest (docs source, code repo(s)). Use when a docs engineer wants an accuracy/drift check of a property against its source. Trigger phrases: "check docs drift", "accuracy report", "docs vs source audit", "find documentation drift", "drift check".

Source facts

Repository
mongodb/docs
Last source activity
September 17, 2026 at 22:55
Detected SKILL.md language
English
Stars
777
Forks
1,699

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
10 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
docs-drift
internal
true
description
Detect drift between a documentation property and its source code, classify each finding (Confirmed / Tracked / Upcoming / Intentional / Needs-eng- confirmation), and draft held DOCSP tickets for confirmed drift only. Runs against any property given a manifest (docs source, code repo(s)). Use when a docs engineer wants an accuracy/drift check of a property against its source. Trigger phrases: "check docs drift", "accuracy report", "docs vs source audit", "find documentation drift", "drift check".
# docs-drift Compares what the docs *say* against what the source *does*, then triages each mismatch so reviewers spend time only on real, actionable drift. Property behavior is driven entirely by a **manifest** (`references/<property>.yaml`, contract in `references/SCHEMA.md`) — there is no property-specific logic in the skill itself. **This skill never files Jira tickets and never edits docs.** It produces a report and *held* draft tickets for human review. ## Scope This skill covers products whose public surface is declared in code: CLI flags, config options, HTTP API request/response shapes, enums, and states. UI-driven products (those whose primary public interface is affordances in a web or desktop UI) are out of scope for this version. ## Prerequisites - **Snooty parser** at `~/.cache/docs-mongodb-internal/local-build-check/.venv/` (shared with the `local-build-check` skill). If missing, install per that skill. All Python below uses that venv's interpreter — call it `$PY`: `PY=~/.cache/docs-mongodb-internal/local-build-check/.venv/bin/python` <!-- TODO: update this prereq when the team migrates off the Snooty parser --> - **`/jira` skill** — for the optional final "file the held tickets" step and for triage ticket lookups. Never call jira-cli directly; delegate to `/jira`. - **`gh` CLI** authenticated against the source repo's org (for triage signals: linked engineering tickets, merged-but-unreleased PRs, release state). ## Inputs One argument: the property name. If `references/<property>.yaml` exists, run the pipeline below. If it does **not** exist, run **Discovery mode** first. **Draft coverage review.** To check how well an in-progress docs branch covers a new feature's source — before an engineering external review — point `docs.path` in the manifest at the draft branch's snooty source root and run normally. The report's coverage and accuracy scores show engineers exactly what is and isn't documented, and Needs-eng-confirmation findings highlight surface whose documentation intent is unclear. No manifest changes are needed beyond the `docs.path` update; revert it after the review. ## Discovery mode (first run on a new docset) Every docset has its own conventions — where the public surface lives, what its "do not document this" idiom looks like (a `// internal` comment, a hidden-options file, a naming convention, or nothing at all), which engineering project tracks it. A fresh docset has none of this recorded, so do not guess silently and do not hard-stop. Instead, **scaffold a draft manifest and confirm it with the user**: 1. **Run `/source-explorer` first** if the writer is unfamiliar with the codebase. Building a plain-language understanding of the source before writing `surface_hints` produces a more accurate manifest and avoids calibration errors on the first run. 2. Inspect `content/<property>/` and the source repo(s). Propose `source.surface_hints` from the repo layout; scan source comments for recurring "don't-document" idioms and propose them as `triage.intent_markers` candidates; detect the hidden-surface mechanism; infer `triage.eng_project` from linked tickets; set `component`. 3. Write the draft to `references/<property>.yaml` with `calibrated: false`. 4. Present it to the user and ask them to confirm/correct the surface hints and intent markers before proceeding. Do not invent details you cannot see — flag gaps instead. The first run is a **draft for review**, not an authoritative report: its whole purpose is to surface what the docset's conventions are so the human can confirm them. Their corrections (e.g. "those `// deprecated` items are intentional") get folded back into the manifest's `intent_markers` / `hidden_surface`, and `calibrated` is flipped to `true`. The manifest is a living per-docset calibration artifact, sharpened each time a human triages. ## Pipeline Run the five stages in order. Stages 1 (extract docs) and 3 (diff) are scripted and deterministic; Stages 2 (extract source), 4 (triage), and 5 (report + draft tickets) are agent work guided by the manifest. ### Stage 1 — Extract documented surface (scripted) ```bash $PY scripts/extract_docs.py --docs-path <manifest.docs.path> --out /tmp/docs-surface.json ``` This builds the docs with Snooty and walks the **rendered AST with includes resolved**, emitting documented names, types, defaults, enumerated values (with a `hedged` hint and `context`), symbol mentions, and per-file provenance. Because includes are resolved, content defined in an include is never reported as missing. If `manifest.docs.option_tables` is `true` (the property documents options as `list-table` rows whose first column is the option name, common in driver docsets), add `--option-tables` so those names enter the documented surface; otherwise they are skipped as table scaffolding and read as undocumented. If `manifest.docs.surface_pages` is set, add `--pages <globs>` to restrict extraction to those pages only. Use this to scope a run to a sub-section of a docset — for example, the REST API reference pages within a larger property like Ops Manager: ```bash $PY scripts/extract_docs.py --docs-path <manifest.docs.path> \ --pages "*/reference/api/*" "*/reference/api-resources/*" \ --out /tmp/docs-surface.json ``` Pages not matched by any glob are excluded from extraction and from the diff. Source surface items that are only documented on excluded pages will appear as candidates — apply `surface_pages` only when the corresponding `source.surface_hints` are also scoped to the same subsystem. ### Stage 2 — Extract source surface (agent) > To understand the source in plain language before running this stage, use `/source-explorer`. Stage 2 is a structured extraction task — it does not explain; it enumerates. Check out the source at `manifest.source.repos[].ref` (or default branch for a live run). Then, guided **only** by `manifest.source.surface_hints`, read the source and produce a structured list of the real public surface. For each item record `symbol`, `file`, `line`, and value. **Enumerate exhaustively — do not sample.** For each surface location a hint points to, list *every* member, not just the ones that look notable: - **Every** CLI flag / config option defined at that site (walk the whole flag registration block / option struct, not a subset). A common miss is a lone public flag amid many. - **Every** public API request/response field (walk all struct fields, incl. nested objects and enum-typed fields and their allowed values). - **Every** enum/state value. For each item also capture, where present in source: - **Defaults — including conditional ones.** If a value has more than one default constant (e.g. a base default and a different default under some condition), record *all* of them and the condition. Do not record only the first. - **Behavioral constraints.** Usage restrictions *enforced* in source — "only valid on restart", "errors if X", min/max bounds, mutually-exclusive-with — even when expressed as a validation error or a usage string rather than a type. These are part of the public contract and are frequently the drift. Record only enforced restrictions. **Do not** record: - Implementation or serialization details (struct tags, `#[serde(...)]`, "not serialized") - Plain descriptive prose ("sent to the server during handshake") - Type-system null guards and range validators — e.g. C# `ArgumentNullException` / `ArgumentOutOfRangeException` guards, `Ensure.IsGreaterThanZero()` / `Ensure.IsNotNull()` helpers, Java `Objects.requireNonNull()`, Go nil checks. These enforce language-level type contracts, not user-visible behavioral restrictions. A user cannot pass a null value to a typed API — this is not drift. Stage 3 drops some non-enforcing strings, but excluding type-system enforcement at Stage 2 avoids generating candidates that will never be actionable. - **Language deprecation markers.** Some languages use attributes or annotations rather than comments to mark deprecated surface: C# `[Obsolete("...")]`, Java `@Deprecated`, Python `@deprecated`. Capture the attribute/annotation text as the item's `intent_marker` field just as you would a comment substring. Do not rely solely on the manifest's `triage.intent_markers` list for these — the attribute text is on the item itself. Also read `manifest.source.hidden_surface` locations and record which surface is intentionally hidden, and any `manifest.triage.intent_markers` substring found on an item's source comment — these feed triage, they are not drift. Do not hardcode anything property-specific; the hints tell you where to look. **Emit a structured `source-surface.json`** (schema in `scripts/compare_surfaces.py`) so Stage 3 can compare deterministically. Each item: `name`, `kind`, `type`, `defaults[]` (each `{value, condition}` — record *all*, conditionals included), `allowed_values[]`, `constraints[]`, `hidden`, `intent_marker`, `symbol`/`file`/`line`. Write it to `/tmp/source-surface.json`. Your job here is faithful extraction into this schema — not judgment. ### Stage 3 — Diff (scripted — deterministic) ```bash $PY scripts/compare_surfaces.py --docs /tmp/docs-surface.json --source /tmp/source-surface.json --out /tmp/candidates.json ``` This is deterministic on purpose. The script matches every source item against the documented surface and emits a candidate for **every** mismatch — across presence, type, conditional defaults, constraints, and enum values — with no curation or early stop, so recall does not vary run to run. It applies the guards mechanically: content present in the docs surface (any include provenance) is never "missing", and hedged/illustrative value lists are never "missing values". Each candidate carries `triage_signals` (`hidden`, `intent_marker`) for the next stage. Do not hand-diff in this stage — the determinism is the point. If a real mismatch class is being missed, fix `compare_surfaces.py` (or the Stage 2 extraction feeding it), not by adding ad-hoc agent judgment here. ### Stage 4 — Triage (agent, over the complete candidate set) Read `/tmp/candidates.json` and classify **every** candidate into exactly one label. Because Stage 3 is exhaustive, the candidate set is complete — triage the whole list, do not re-discover or drop items. **Start from each candidate's `suggested_label`** — the script assigns it deterministically from the candidate kind and triage signals (`undocumented_surface` → Needs-eng-confirmation; `documented_not_in_source` / `type_mismatch` / `conditional_default_undocumented` / `constraint_undocumented` / `enum_value_missing` → Confirmed; any `intent_marker` or `hidden` → Intentional). Keep `suggested_label` unless one of these overrides applies: - **Rename → Confirmed.** An `undocumented_surface` candidate whose source name is a near-variant of a name the docs document but that does not exist in source (e.g. source `metricsLogPath` while docs document `metricsLoggingFilepath`) is a rename — the docs assert a flag that does not exist. Upgrade from Needs-eng-confirmation to **Confirmed**. - **Tracked / Upcoming** — only when an external signal applies: an open DOCSP ticket by `manifest.component` (Tracked), or surface tied to an unreleased version (Upcoming), established from source release state / an open fix version in any `manifest.triage.eng_project` project (accepts a single key or a list), with the released anchor derived at runtime (highest `vN.N` dir; `manifest.docs.baseline_version` overrides; do not trust a single `snooty.toml` `latest-version`). Name the ticket/version. - **External API reference (`manifest.docs.external_api_ref` is true).** When the property's reference surface is split between the prose docs site and an external generated API reference (rustdoc/`docs.rs`, Javadoc, etc.), public surface that is merely absent from `content/` is **not** drift — it is likely covered by the API reference the skill does not build. Keep such `undocumented_surface` candidates at **Needs-eng-confirmation**; never upgrade them to Confirmed on absence alone. (Renames and wrong types still apply.) The full decision order (first match wins) lives in `references/SCHEMA.md`. Record the deciding signal for each candidate, and note any override of `suggested_label`. **Uncalibrated docset (`manifest.calibrated` is false or absent):** the docset's intent markers and conventions are not yet human-confirmed, so the triage signals are unreliable. Be conservative — bias `Confirmed` candidates that rest only on absence-of-a-marker toward **Needs-eng-confirmation**, and present the report as a draft for human review rather than drafting tickets aggressively. Clear, signature-level mismatches (renames, wrong types) may still be Confirmed; judgment calls about intent should defer. Treat the human's corrections as calibration input for the manifest. After classifying every candidate, write the results to `/tmp/classified.json` — same structure as `candidates.json` but with a `label` field added to each item. This file is the input to the scoring script in Stage 5a. ### Stage 5a — Report (agent) Before filling the report, run the scoring script: ```bash $PY scripts/score.py --classified /tmp/classified.json --out /tmp/scores.json ``` Read `coverage_pct` and `accuracy_pct` from `/tmp/scores.json` and fill the matching placeholders in the report. Then fill the rest of `assets/report.md` and write it to `/tmp/<property>-accuracy-report.md`. Confirmed findings appear with full Docs-say / Source-say / action; Upcoming and Intentional are listed separately and clearly marked "no action"; the findings table lists every finding with its classification and any linked ticket/version. Set `{{resolved_version}}` to the released-version anchor triage derived at runtime (Stage 4) — not a manifest field; if it can't be determined, use the source `ref`. ### Stage 5b — Held draft tickets (agent) For each **Confirmed** finding only, fill `assets/draft-ticket.md` and write it to `/tmp/<property>-draft-tickets/<n>.md`. Map severity to priority (High→Critical-P2, Medium→Major-P3, Low→Minor-P4). Tag with `manifest.component` and the `bug` label. **Do not file them.** Present the report and the list of held drafts to the user. Only if the user explicitly approves, file selected drafts via the `/jira` skill. ## Example (mongosync) Input: `mongosync`. Representative findings and how each is classified: - **Confirmed** — *metricsLogPath flag name.* Docs document `--metricsLoggingFilepath`; source registers the flag as `metricsLogPath` (`flags.go`) and the documented name exists nowhere in source. `documented_not_in_source` + rename → Confirmed → drafts a held ticket. - **Confirmed** — *loadLevel conditional default.* Docs state "Default: 3"; source defines `DefaultLoadLevel=3` and `DefaultLoadLevelWithPreExistingData=2` and the option's prose never mentions the condition. `conditional_default_undocumented` → Confirmed. - **Needs-eng-confirmation** — *enableCappedCollectionHandling.* A non-hidden public flag absent from the docs, no intent marker → undocumented public surface, intent unclear → defer to engineering, no ticket. - **Intentional** — *INITIALIZING state.* Documented states omit it; source marks it `// external-only` → matches an intent marker → no ticket. - *(not surfaced)* — *createIndexesBatchSize "missing type".* The type is defined in an include, which the Snooty AST resolves, so it never appears as missing — the include guard in action. Only the two Confirmed findings become held draft tickets; the report lists all of them grouped by classification. ## Reading the five classifications - **Confirmed** — real drift; act on it (the held drafts). - **Tracked** — real drift, already ticketed; no new ticket. - **Upcoming** — accurate vs source but tied to an unreleased version; revisit after that version ships. - **Intentional** — hidden/internal/legacy/illustrative by design; leave as is. - **Needs-eng-confirmation** — can't be settled from source alone; ask the SME. ## Validation and classification reference `references/expected-mongosync.json` is the adjudicated answer key for mongosync — the one property with human-verified ground truth — pinned to a 2026-03-23 source/docs snapshot. It serves two purposes: 1. **Classification exemplar.** It is a worked example of all five labels with per-finding `docs_say` / `source_say` / `rationale` / `detection_notes`. Use it to calibrate triage on properties that have no answer key of their own — the reasoning (include resolution, hedged-language suppression, source intent markers, defer-when-unmarked) is property-agnostic. 2. **Regression baseline.** To check the skill still behaves, run the pipeline against the pinned mongosync fixture and compare findings to the answer key: the three Confirmed items appear as Confirmed, the two known false positives (`hotDocIDs`, `createIndexesBatchSize`) do **not** appear as Confirmed, and the remaining seven classify as Intentional / Needs-eng-confirmation as recorded. Compare directly against `expected.json`; no separate scorer is committed. Other pilot properties are validated qualitatively, not scored: a coherent classified report plus either a correctly-formed draft ticket or a clear "no confirmed drift" result.
View on GitHub