# Documentation Audit — post-v1.38.0

Audit window: post-v1.36.2 triage (`docs/audit-triage-v1.33.0.md` was the most recent — no `audit-triage-v1.36.2-final.md` exists, so prior fixes were verified by reading current source). Recent merges examined: #1495 (proc-macro registry), #1497 + #1499 (skip-first-pass embed → schema v27), #1505 (`cqs index --model`), #1506 (`cqs ref reindex`), #1486 release notes, plus 8 audit close-out PRs that landed post-v1.38.0.

#### DOC-V1.38-1: README + lib.rs + Cargo.toml claim `--improve-docs` writes to source; default has been patch-mode since v1.30.1
- **Difficulty:** easy
- **Location:** `README.md:659` ("`cqs index --llm-summaries --improve-docs  # Generate + write doc comments to source files`"), `README.md:673` ("`--improve-docs` generates and writes doc comments back to source files"), `src/lib.rs:19` (Features bullet: "`--improve-docs` generates and writes doc comments to source files via LLM"), `SECURITY.md:48` (Surfaces table: "LLM output is **written back to source files in place**. A poisoned chunk can produce a doc comment that lands in the user's repo on commit"). Verified by `src/doc_writer/rewriter.rs:224, 607, 1204` — default writes patches under `.cqs/proposed-docs/<rel>.patch` and requires `--apply` to actually mutate source. SECURITY.md *itself* (line 55) correctly documents the v1.30.1 review-gate; lines 48 and the README/lib.rs claims contradict it.
- **Description:** This is a "lying doc" cluster — three top-level surfaces (README, crate-level rustdoc, SECURITY.md surfaces table) tell users / agents that a flag *will* mutate source files, when in fact the flag writes a unified-diff patch by default. SECURITY.md is the worst offender because the threat-model row claims a write that the mitigation row (#7 below it) explicitly disclaims. An agent reading SECURITY.md as authoritative would either over-fear the flag (and avoid using it) or under-fear `--apply`.
- **Suggested fix:** README.md:659 → `# Write proposed doc comments as patches under .cqs/proposed-docs/ (review with git apply); pass --apply to write directly`. README.md:673 → "`--improve-docs` writes proposed doc comments as patches under `.cqs/proposed-docs/` for review (pass `--apply` to write back to source files in place). Both cached by content_hash." SECURITY.md:48 row → change "Yes — commits the LLM's output into git" to "Patch-only by default (review-gate); `--apply` writes in place — see Mitigation #3 below". src/lib.rs:19 → "`--improve-docs` writes proposed doc comments as `.cqs/proposed-docs/*.patch` for review; `--apply` writes them in place".

#### DOC-V1.38-2: CONTRIBUTING.md still describes `src/cli/registry.rs` and `for_each_command!` — both deleted in #1495
- **Difficulty:** easy
- **Location:** `CONTRIBUTING.md:156-157` (architecture overview lists `registry.rs - for_each_command! table; single source of truth for dispatch...` and `dispatch.rs - ...per-command arms generated from registry.rs`), `CONTRIBUTING.md:367` (Adding a New CLI Command checklist step 4: "**Registry row** — add a `(bind, wild, name, batch_support, body)` row to `group_a` or `group_b` in `src/cli/registry.rs`. The `for_each_command!` macro generates dispatch + variant_name + batch_support from this single row; a missing row is a compile error."). Verified `ls src/cli/registry.rs` — file does not exist. `src/cli/definitions.rs:341` now reads `#[derive(Subcommand, cqs_macros::CqsCommands)]`; `src/cli/dispatch.rs:13-16` and `:209-213` describe the proc-macro replacement.
- **Description:** Lying doc — guides a contributor (human or agent) through a checklist that points to a file that doesn't exist. Anyone following the doc to add a new command will fail step 4 immediately, and the architecture overview misrepresents the dispatch mechanism. Triaged in v1.33.0 as "single-registration command registry"; the implementation got replaced by `#[derive(CqsCommands)]` proc-macro (`crates/cqs-macros/`) but the documentation never followed.
- **Suggested fix:** Replace the two arch-overview lines with `definitions.rs - Clap argument definitions, Commands enum, and #[derive(cqs_macros::CqsCommands)] which generates dispatch + variant_name + batch_support` and drop the `registry.rs` row entirely. Replace step 4 with: "**Derive surfaces** — `Commands` enum variants get dispatch + `variant_name()` + `batch_support()` from `#[derive(cqs_macros::CqsCommands)]` automatically. Per-variant attribute knobs: `#[cqs(handler = ...)]`, `#[cqs(batch_support = "yes")]`, etc. See existing variants in `src/cli/definitions.rs` and the macro in `crates/cqs-macros/`. A missing handler is a compile error from the derive expansion."

#### DOC-V1.38-3: CONTRIBUTING.md says "Schema v26" twice; current schema is v27 (#1497)
- **Difficulty:** easy
- **Location:** `CONTRIBUTING.md:212` ("`store/        - SQLite storage layer (Schema v26, WAL mode)`") and `CONTRIBUTING.md:226` ("`migrations.rs - Schema migration framework (v10-v26, including ... v26 composite (source_type, origin) index on chunks)`"). Verified: `src/store/helpers/mod.rs:151` reads `pub const CURRENT_SCHEMA_VERSION: i32 = 27;`. `src/schema.sql:1` reads `cq index schema v27`. `src/store/migrations.rs:398` registers `(26, 27, |c| Box::pin(migrate_v26_to_v27(c)))`. The v26→v27 migration (#1452 / #1497) added `chunks.needs_embedding INTEGER NOT NULL DEFAULT 0` plus a partial index, to support skip-first-pass embedding under `--llm-summaries`.
- **Description:** Stale schema citation — same flavour of the line-citation drift fixed in v1.37.0 (P1-12, where SECURITY.md cited v25 after v26 landed). New audit cycle, same drift. Anyone reading CONTRIBUTING.md to plan a migration will undercount versions and miss v27's needs_embedding column.
- **Suggested fix:** Line 212 → `store/        - SQLite storage layer (Schema v27, WAL mode)`. Line 226 → `migrations.rs - Schema migration framework (v10-v27, including v19 FK cascade, v20 trigger, v21 splade tokens, v22 chunks.umap_x/y, v23 reconcile fingerprint, v24 vendored-code trust, v25 notes.kind, v26 composite (source_type, origin) index on chunks, v27 chunks.needs_embedding for skip-first-pass embed under --llm-summaries (#1452))`.

#### DOC-V1.38-4: SECURITY.md cites `src/lib.rs:813` for `enumerate_files`; actual line is 771
- **Difficulty:** easy
- **Location:** `SECURITY.md:223` — "Symlinks are **skipped** entirely — `enumerate_files` / `enumerate_files_iter` (`src/lib.rs:813`, `WalkBuilder::follow_links(false)`)...". Verified by `grep -n "pub fn enumerate_files" src/lib.rs` → line 771. (v1.37.0 P1-12 already fixed this drift once — moving from `:601` to `:813`. The line has drifted again since.)
- **Description:** Stale line-number citation — third drift cycle on the same anchor (601 → 813 in v1.37.0; now 813 → 771 in v1.38.0+). This is exactly the kind of staleness the team rule on "lying docs as P1" cares about: an auditor cross-referencing SECURITY.md to verify the symlink-skip claim will read line 813, find an unrelated function, and either chase the wrong code or assume the doc is unreliable.
- **Suggested fix:** Replace the bare line citation with a stable anchor: `enumerate_files / enumerate_files_iter in src/lib.rs (search for "WalkBuilder::follow_links(false)")` so the doc no longer drifts every release. If a literal line number is preferred, update to `src/lib.rs:771`.

#### DOC-V1.38-5: CHANGELOG `[Unreleased]` is empty; ~14 user-visible PRs landed since v1.38.0
- **Difficulty:** easy
- **Location:** `CHANGELOG.md:8-10` — `[Unreleased]` block is a single header line followed immediately by `## [1.38.0] - 2026-05-06`. Commits between v1.38.0 cut (`4bd12645`, 2026-05-06) and HEAD include #1487 (build_compact_data tests), #1488 (daemon GC + train-data tests), #1489 (prepare_for_embedding tests), #1490 (tasklist UTF-16 BOM fix), #1491 (splade .bak rollback), #1492 (cagra .bak rollback), #1493 (drop IndexBackendError variants), #1495 (proc-macro registry), #1497 (skip-first-pass embed → schema v27), #1499 (embedding_base NULL post-skip fix), #1500 (proc-macro DX), #1501 (drop IndexBackendError wrapper), #1502 (id_map Box<str>), #1503 (reranker batch by max_length), #1504 (cmd_model_swap bail), #1505 (cqs index --model parity), #1506 (cqs ref reindex flag parity).
- **Description:** Same shape as the v1.33.0 P1-11 finding ("CHANGELOG [Unreleased] missing 10+ post-v1.33.0 PRs that landed"). Each release the [Unreleased] block goes empty at cut and stays empty for the entire next dev cycle. The bigger deal here is that #1497 bumped the schema to v27 — that's a behaviour-visible change that should land in [Unreleased] the same day the migration commit lands, not at the next release. Schema bumps in particular need to be visible to users planning upgrades, not buried in commit subjects.
- **Suggested fix:** Add an `[Unreleased]` body listing the post-v1.38.0 PRs grouped by Added / Changed / Fixed. At minimum: an "Added" line for `cqs index --model` parity (#1505), `cqs ref reindex --llm-summaries`/`--improve-docs`/`--hyde-queries` flag parity (#1506), `[derive(CqsCommands)]` proc-macro replacing `for_each_command!` (#1495); a "Changed" line for skip-first-pass embed under `--llm-summaries` (#1497, schema v27); a "Fixed" line for cagra/splade `.bak` rollback (#1491, #1492). Adopt a CI gate or pre-commit hook that fails when a non-`docs:`/`chore:` commit lands without a CHANGELOG entry.

#### DOC-V1.38-6: README "Performance" section pinned to BGE-large + v1.27.0 file; default has been EmbeddingGemma since v1.35.0
- **Difficulty:** medium
- **Location:** `README.md:950` — "Measured 2026-04-16 on the cqs codebase itself (562 files, 15,516 chunks) with CUDA GPU (NVIDIA RTX A6000, 48 GB) on WSL2 Ubuntu. Embedder: BGE-large (1024-dim). SPLADE: ensembledistil (110M, off-the-shelf). Raw measurements: [`evals/performance-v1.27.0.json`](evals/performance-v1.27.0.json)." Default preset has been `embeddinggemma-300m` since v1.35.0 (verified `src/embedder/models.rs:477` `default = true` annotation). Latency profile differs materially: 768-dim vs 1024-dim, 2K vs 512 max-seq, different ONNX session init time.
- **Description:** Stale benchmark caption — the table reports daemon/CLI numbers a user expects to reproduce on a current install, but the install ships a different default embedder than the one measured. README currently says "Embedding latency (GPU vs CPU)" gives a per-batch 50-doc CUDA cost of 0.3 ms/doc *for BGE-large*; on EmbeddingGemma-300m at 768-dim those numbers will be different. Not lying about the corpus, but lying about what the numbers describe.
- **Suggested fix:** Either (a) re-run the perf bench on the v1.38.0 default and update the table + replace `evals/performance-v1.27.0.json` with `evals/performance-v1.38.0.json`, or (b) edit the caption to "Measured 2026-04-16 on cqs v1.27.0 with the v1.27.0 default embedder (BGE-large). Numbers shift on the v1.35+ EmbeddingGemma default — re-bench planned for v1.39." Option (b) is the cheap-fix; (a) is the right-thing-to-do.

#### DOC-V1.38-7: SECURITY.md lists schema as `v22, v23, v24, v25, v26` for vendored-code chunks; v27 not enumerated
- **Difficulty:** easy
- **Location:** `SECURITY.md:59` — "(#1221, schema v24)" reference is correct, but the broader v23-v25-v26 enumeration in CONTRIBUTING.md and the lack of any v27 mention in either file means an auditor reading SECURITY.md / CONTRIBUTING.md alongside the schema header in `src/schema.sql:1` (`cq index schema v27 ... v22+v23+v24+v25+v26+v27 columns annotated inline`) sees a one-version mismatch. SECURITY.md doesn't mention v27 at all. Same root cause as DOC-V1.38-3 but on a different surface.
- **Description:** Not lying so much as silently incomplete — v27 added `chunks.needs_embedding`, which controls whether a chunk goes into the first-pass embed pool under `--llm-summaries`. That's behaviour-visible to anyone tracing a "why isn't this chunk in HNSW yet?" path. Worth one paragraph in either SECURITY.md (under "Index Storage") or PRIVACY.md (under "What Gets Stored") so post-skip chunks aren't a mystery.
- **Suggested fix:** Add a sentence to `SECURITY.md` "Index Storage" section (line 277-280): "Schema v27 (#1497) adds `chunks.needs_embedding`; chunks created during a `--llm-summaries` reindex skip the initial cold embed and get embedded once the LLM-enriched description is available, then cleared by `enrichment_pass`." Update `PRIVACY.md:15` similarly.

#### DOC-V1.38-8: Cargo.toml `Description` declares "50.9 / 76.2 / 88.6" but README leaves the bge-large/v9-200k/nomic rows pre-retune
- **Difficulty:** easy
- **Location:** `Cargo.toml:9` description: "50.9% R@1 / 76.2% R@5 / 88.6% R@20 on v3.v2 dual-judge code-search (218 queries, EmbeddingGemma-300m default with per-category SPLADE α retuned for the new dense backbone)". `README.md:704` admits "Other rows are pre-retune (apples-to-apples 2026-05-02 on cqs v1.35.0, all 5 slots reindexed `--force --llm-summaries`); their numbers will shift up under the new alphas, but a fresh sweep across all five slots is queued." Status of the queued sweep: no follow-up commit since 2026-05-03 retune; non-gemma rows in the table are still pre-retune. v1.33.0 audit P1-5/P1-6 flagged the equivalent issue and parked it as ticket #1369 (which I cannot verify is still open without GitHub access).
- **Description:** Not lying about gemma's numbers — those are real. But the README's per-preset table presents the pre-retune BGE/v9/coderank rows as if they're directly comparable to the gemma row, with a one-paragraph caveat agents are likely to skip. Prior audit triage flagged this as P1; no fix has landed.
- **Suggested fix:** Either (a) run the queued sweep across the 4 non-gemma slots and refresh rows 709-712 of `README.md`, or (b) hide the pre-retune rows behind a `<details>` block titled "Other presets (pre-retune)" so the gemma row is the unambiguous default-config number.

#### DOC-V1.38-9: README "How It Works" Step 1 says "19 other chunk types"; actual count differs and was last counted in v1.27 era
- **Difficulty:** easy
- **Location:** `README.md:670` — "Tree-sitter extracts functions, classes, structs, enums, traits, interfaces, constants, tests, endpoints, modules, and 19 other chunk types across 54 languages". Verified by counting `define_chunk_types!` rows in `src/language/mod.rs:683-744` → ~30 distinct rows (Function, Method, Class, Struct, Enum, Trait, Interface, Constant, Section, Property, Delegate, Event, Module, Macro, Object, TypeAlias, Extension, Constructor, Impl, ConfigKey, Test, Variable, Endpoint, Service, StoredProc, Extern, Namespace, Middleware, Modifier, ...). The "10 enumerated + 19 other = 29" arithmetic was right when written; current total is higher.
- **Description:** Mild drift — the kind of claim contributors are supposed to update under CONTRIBUTING.md "Adding a New Chunk Type → 5. Update docs". Not load-bearing for correctness, but agents quoting the count back at users will be off by one or two. Worth fixing in the same pass as DOC-V1.38-3 since both are "count the rows" stalenesses.
- **Suggested fix:** Replace the literal count with a generated-from-source pointer: "...constants, tests, endpoints, modules, and 20+ other chunk types across 54 languages (see `src/language/mod.rs::define_chunk_types!` for the full list)". Avoids future drift and saves the contributor checklist step.

#### DOC-V1.38-10: PROJECT-MEMORY drift in `MEMORY.md` — schema "v22", test counts, version, fixture R@K all stale
- **Difficulty:** easy
- **Location:** `~/.claude/projects/-mnt-c-Projects-cqs/memory/MEMORY.md` (loaded as user auto-memory for every session): "Version: 1.29.1", "Schema: v22", "Tests: ~1717 lib tests post-#1105", "Metrics (refreshed v3.v2, BGE-large, 2026-04-25): test R@5 63.3%, dev R@5 74.3%". Actual: version 1.38.0 (Cargo.toml:1), schema 27 (`src/store/helpers/mod.rs:151`), default model embeddinggemma-300m (not bge-large), README's measured number agg R@5 76.2% on gemma. The memory blob explicitly warns "Do NOT cite the 63.3%/74.3% as current state" but then cites v1.29.1, v22, etc. as if current.
- **Description:** Not a repo-facing doc per se, but it ships into every Claude session for this project — meaning every audit, every plan, every fresh-eyes review starts from a 9-version-stale schema number, a 9-version-stale binary version, and a model that hasn't been the default for 3 minor versions. Higher-leverage to fix than any single README typo because it biases every downstream decision the assistant makes. Out of scope for a code audit if you read "Documentation" strictly as repo-only files; calling it out here because the audit prompt explicitly mentioned "Documentation" without scoping to the repo, and the memory file is the single highest-traffic stale doc in the system.
- **Suggested fix:** User runs through MEMORY.md and replaces: `Version: 1.29.1` → `Version: 1.38.0`, `Schema: v22` → `Schema: v27`, default embedder line `BGE-large default (1024-dim)` → `EmbeddingGemma-300m default (768-dim, since v1.35.0)`, refresh test count via `cargo test --features cuda-index 2>&1 | grep "^test result:"`, drop the canonical-pre-refresh metric paragraph (it's two regression cycles old now). Adopt a `groom-memory` skill (sibling of `groom-notes`) that re-runs every minor release.

---

## Summary

10 findings, 4 of which are "lying docs" by the team's P1 rule (DOC-V1.38-1 `--improve-docs` write-back claim across README/lib.rs/SECURITY.md; DOC-V1.38-2 CONTRIBUTING.md describing a deleted file and macro; DOC-V1.38-3 schema v26 vs actual v27 in CONTRIBUTING.md; DOC-V1.38-4 stale `lib.rs:813` line citation in SECURITY.md). Two are completeness gaps that promise nothing false but leave a release of churn invisible (DOC-V1.38-5 empty `[Unreleased]`; DOC-V1.38-7 v27 schema not surfaced in SECURITY/PRIVACY). Three are stale-but-not-quite-lying (DOC-V1.38-6 perf table pinned to v1.27.0/BGE-large; DOC-V1.38-8 unrefreshed non-gemma R@K rows; DOC-V1.38-9 chunk-type count). One is project-memory drift (DOC-V1.38-10).

The cluster signal: `--improve-docs` semantics changed in v1.30.1 and three docs disagree about it three minor versions later. The repeat offender is line-number citations and version numbers — same drift pattern hit `lib.rs:601 → 813 → 771` and `Schema v25 → v26 → v27` across two consecutive audits. Mechanical fix: switch to anchor-based citations (function-name search rather than literal line) and add a CHANGELOG-presence pre-commit hook so the [Unreleased] block can't go empty for an entire dev cycle again.
