Skip to content

docs(src): reconcile every docstring with the code it describes - #1173

Merged
schubydoo merged 2 commits into
mainfrom
docs/docstring-drift-audit
Jul 31, 2026
Merged

docs(src): reconcile every docstring with the code it describes#1173
schubydoo merged 2 commits into
mainfrom
docs/docstring-drift-audit

Conversation

@schubydoo

Copy link
Copy Markdown
Owner

Reconciles every docstring in src/ with the code it actually describes. Documentation only — no behaviour change.

Proven docstring-only, not asserted

Every .py file in the diff was parsed, had its docstrings stripped, and compared as an AST against its base version:

.py files touched: 53   AST-identical: 53   code changed: 0   non-.py files: 0

Re-run it yourself — the script is in scratch/docstring-audit-findings.md under Structural check.

What this contains

  • 1455 docstrings audited by a 23-agent sweep, then independently re-verified by hand before anything was written.
  • 241 findings applied: 152 DRIFT (prose contradicted the code), 48 DUPE, 27 FLUFF.
  • Builds on f84b9641, which added the 192 docstrings interrogate had flagged as missing.

⚠️ Docstrings here deliberately describe BROKEN behaviour

This is the part worth understanding before reviewing. Where the code has a real defect, the docstring now documents the defect accurately rather than restating an intent the code does not honour — that was the explicit call: document as-is, triage separately. Prose describing an unhonoured guarantee is exactly what this audit existed to eliminate.

The consequence, recorded in scratch/docstring-audit-findings.md: the moment a defect is fixed, its docstring becomes wrong again — in the more dangerous direction, understating a guarantee that now holds. The audit file carries a reverse index mapping each code fix to the docstrings that must move with it.

16 code findings came out of this audit and are tracked separately; all have now been triaged with decisions recorded. They land in follow-up PRs, not here.

Gates

  • interrogate src/100.0%
  • just check — exit 0 · 4672 passed · coverage 99.99%
  • Rebased on current main, 0 behind

no-changelog: no user-facing behaviour changes.

schubydoo and others added 2 commits July 30, 2026 17:57
Brings `interrogate src/` from 87.5% to 100.0%.

Every gap was outside ruff's reach: pydocstyle's D1xx rules do not descend
into function bodies and skip `_name` members, so all 192 nodes were nested
functions (route handlers inside create_app, closures, callbacks) or
module-level private helpers. No top-level public function, class, or module
docstring was missing.

Purely additive: 211 insertions, 0 deletions. No existing docstring, comment,
or line of code is modified. The 84 sites that already carried an explanatory
`#` comment keep it, with a one-line summary added above.

Verified: interrogate 100%, ruff check + format, pyright 0 errors,
4661 tests passing, coverage 99.99%, docs lint + mkdocs --strict.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Audited all 1455 docstrings in src/ against their implementations and fixed
the ones that misdescribed the code. Also removed comments that merely
restated their own docstring and condensed padded comment blocks.

Docstrings only — no executable line changes. Verified structurally rather
than by eye: for all 52 touched files, parsing HEAD and the working copy,
stripping every module/class/function docstring, and comparing the ASTs
gives an exact match.

What changed:

- 152 docstrings that made claims the code does not honour: exceptions and
  HTTP status codes never raised, locking/ordering/thread-affinity that
  isn't real, parameters the body ignores, caller behaviour attributed to
  the callee, and "still ahead"/"not wired up yet" notes for work that has
  since shipped.
- 48 comments that restated their own docstring, deleted.
- 27 padded comment blocks condensed. Issue refs, safety rationale,
  ordering warnings, and cross-references were preserved throughout;
  net -167 comment lines.

Several docstrings overstated a safety property — claiming a gate "fails
closed", a value is redacted, or a symlink target is "never followed/read"
where the code does otherwise. Those now describe actual behaviour,
including the limitation, so the prose can be trusted as written.

14 findings were code defects rather than prose drift. Per maintainer
direction those are documented as-is here and written up separately for
triage rather than fixed in a docs change.

interrogate: still 100.0%. just check: green (4661 passed, coverage 99.99%,
pyright 0 errors).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@clauster-ci

clauster-ci Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

⚠️ No changeset found. This PR changes src/ but adds no .changeset/*.md fragment.

clauster releases are changesets-only: a user-facing change needs a fragment, or it ships with
no changelog entry and no version bump. Add one — run knope document-change, or commit a
.changeset/<slug>.md with front-matter default: patch|minor|major|perf|security|build
and a one-line summary. Internal-only PRs (CI, refactor, tests) can add the
no-changelog label instead.

Advisory only — this check never blocks the merge.

@schubydoo schubydoo added the no-changelog Skip the changeset/changelog requirement (CI / refactor / tests / docs) label Jul 31, 2026
Repository owner deleted a comment from claude Bot Jul 31, 2026
@schubydoo
schubydoo marked this pull request as ready for review July 31, 2026 02:07
@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@schubydoo
schubydoo merged commit 530aaa6 into main Jul 31, 2026
50 checks passed
@schubydoo
schubydoo deleted the docs/docstring-drift-audit branch July 31, 2026 02:40
schubydoo added a commit that referenced this pull request Jul 31, 2026
Findings 0, 0b and 7 from `scratch/docstring-audit-findings.md`. Each fix
also deletes the limitation paragraph #1173 deliberately added to its docstring
and restates the guarantee that now holds — fixing the code alone would have
re-introduced drift in the more dangerous direction.

Finding 0 — setup wizard returned 500 instead of 403 on a non-ASCII token.
`secrets.compare_digest` raises TypeError on a non-ASCII str, so `?token=%C3%A9`
escaped as an unhandled 500. This gate fronts the FIRST-RUN wizard, which is
unauthenticated by construction, binds non-loopback in the Docker image, and
gates a config writer — an unhandled raise on attacker-controlled input on the
one surface with no auth in front of it. Never a bypass; it 500'd rather than
granting. `isascii()` is checked first, which leaks only whether the input was
ASCII, never any part of the token. Both the GET and the POST CSRF path are
covered.

Finding 0b — a secret one level deep came back unmasked. The hint reached scalar
leaves and list elements, but a nested dict re-derived it from its own keys, so
{"auth": {"value": "sk-live-..."}} leaked — not an exotic shape, it is how a
lot of MCP server configs are written, and this is the code-executing
config-write tier. A secret-shaped key now marks its whole subtree.

Blast radius traced, since the audit flagged it as unconfirmed: six `cw.`
call sites — MCP listings (x3), settings misc, subagent frontmatter, and
`config_write_mcp_cli._is_redacted`, which is the only one where the change
alters a decision rather than display output. `redact.py`'s same-named
line-oriented function is untouched.

Finding 7 — LIST read a plugin symlink's out-of-tree target. `is_file()` and
`read_bytes()` both follow symlinks and both ran before `_is_read_only_file`
tested `is_symlink()`, so the target's frontmatter description reached the
listing — a stated containment boundary that GET/PUT/DELETE honoured and LIST
did not. Classified before the read; the entry is still surfaced as
plugin-owned and non-editable, just without a description it should never have
read.

Sibling sweep (the audit suggested it): `config_write_skills` already
classifies first in both places and carries a comment naming the hazard;
`config_write_hooks` reads one known settings path rather than iterating
user-supplied entries, so neither is exposed. Subagents was the only gap.

just check green: 4678 passed, coverage 99.99%, interrogate src/ 100.0%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
schubydoo added a commit that referenced this pull request Jul 31, 2026
…1174)

Findings **finding 0**, **finding 0b** and **finding 7** from the
docstring audit — the three security-boundary defects. Follows #1173,
which documented them accurately; each fix here also deletes the
limitation paragraph that PR added and restates the guarantee that now
holds, per the audit's reverse index.

## Finding 0 — setup wizard returned **500 instead of 403** on a
non-ASCII token

`secrets.compare_digest` raises `TypeError` on a non-ASCII `str`, so
`?token=%C3%A9` escaped as an unhandled 500.

This gate fronts the **first-run wizard**: unauthenticated by
construction, binds non-loopback in the Docker image, and gates a config
writer. So it was an unhandled raise on attacker-controlled input on the
one surface with no auth in front of it. **Never a bypass** — it 500'd
rather than granting.

`isascii()` is checked first, which leaks only whether the input was
ASCII, never any part of the token. The expected token is
`secrets.token_urlsafe(32)`, ASCII by construction, so no legitimate
token can be rejected. Both the GET and the POST CSRF path are covered.

## Finding 0b — a secret one level deep came back **unmasked**

The hint reached scalar leaves and list elements, but a nested dict
re-derived it from its own keys:

```
flat under auth    -> {'auth': '********'}                        masked
NESTED under auth  -> {'auth': {'value': 'sk-live-…'}}            LEAKED
```

Not an exotic shape — it is how a lot of MCP server configs are written,
and this is the code-executing config-write tier. A secret-shaped key
now marks its whole subtree.

**Blast radius traced** (the audit flagged it as unconfirmed): six
`cw.redact_secrets` call sites — MCP listings ×3, settings misc,
subagent frontmatter, and `config_write_mcp_cli`, the only one where the
change alters a *decision* rather than display output, and in the
fail-closed direction. `redact.py`'s same-named line-oriented function
is untouched.

## 7 — LIST read a plugin symlink's **out-of-tree target**

The module docstring promises a symlink's target is *never
followed/read*. GET, PUT and DELETE honoured that; LIST did not,
surfacing the target's frontmatter `description`. Classification now
happens before anything stats the target.

**Sibling sweep** (the audit suggested it): `config_write_skills`
already classifies first in both places and carries a comment naming the
hazard; `config_write_hooks` reads one known settings path rather than
iterating user-supplied entries. Subagents was the only gap.

## Review round already applied

`clauster-backend-reviewer` found two real defects, both reproduced
before accepting:

- **The wider mask broke the MCP write surface** — a regression this
branch introduced. A user-chosen server *name* like `oauth-gw` matched
the secret-key regex and masked its own structural `type`, which
`_entry_transport` then rejects, 422ing every write in the scope. Fixed
with per-entry redaction so a name never enters the hint chain.
- **`_list_agents` still *followed* the symlink** — `is_file()` ran
before the guard and stats the target, so the new docstring's "never
followed" was an overclaim, and it silently dropped dangling symlinks
from LIST while GET still reported them present.

## Gates

`just check` exit 0 · **4680 passed** · coverage 99.98% · `interrogate
src/` **100.0%**

---------

Co-authored-by: Schuby <12485317+schubydoo@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
schubydoo added a commit that referenced this pull request Jul 31, 2026
`--max-turns 60` turned out to be the **binding limit on real pull
requests**, not a runaway guard. Two runs in the docstring-audit series
died with `"subtype": "error_max_turns"` partway through reading the
diff, at roughly 8 minutes each — #1173, and #1177 this week.

## Why this is worth fixing rather than retrying

The failure mode is worse than getting no review. The action still posts
its progress comment, so the PR gains a Claude comment carrying an
unticked checklist and an error line. At a glance it **looks reviewed**
while containing no findings at all — the same "looks reviewed when it
wasn't" hazard the `Fail if nothing was posted` step was added to catch,
arriving by a different route. Both times the maintainer had to spot it
and clean up by hand.

## Why 60 is too low

Reading a multi-file diff is turn-expensive — chunked `Read`s plus `gh
pr diff` — and this reviewer is additionally asked to fetch the rules
file from the default branch, anchor every finding as an inline comment,
and submit exactly one grouped review. 60 turns does not cover that on
anything but a small change.

## Why 150 and not more

150 keeps the cap **meaningful** instead of making it dead weight.
Measured ~8s per turn on the failing runs, so it binds at roughly 20
minutes — still inside the job's existing `timeout-minutes: 30`. The job
timeout remains the hard backstop, and the `Fail if nothing was posted`
step is untouched, so the older failure (a run reporting success while
publishing nothing) is still caught.

Going much higher would let the turn cap drift past the job timeout, at
which point it stops guarding anything.

## Checks

- `uvx zizmor .github/workflows/claude-review.yml` — no findings (3
pre-existing suppressions)
- `actionlint .github/workflows/claude-review.yml` — clean, with
`shellcheck` on PATH so the embedded `run:` blocks were linted too
- Parsed the workflow back to confirm the value lands as `--max-turns
150` and `timeout-minutes` is still 30

No changeset: CI-only, no user-facing effect — `no-changelog`.

---------

Co-authored-by: Schuby <12485317+schubydoo@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
schubydoo added a commit that referenced this pull request Jul 31, 2026
…1158 (#1190)

Documentation audit of every merged PR from #1158 onward, ahead of the
1.0.1 deploy.

## What I checked

| PRs | Verdict |
| --- | --- |
| #1160#1166, #1169, #1179, #1186, #1187 | Dependency bumps and CI — no
user-facing surface |
| #1158, #1167, #1168 | Documentation PRs already |
| #1173 | Docstrings only, proven code-free by AST comparison |
| #1177, #1185 | Docs updated in their own PRs — generated tables
confirmed still in sync |
| **#1174, #1183** | Changed `src/` with **no** doc update — audited in
detail |

## Two real gaps, both in `troubleshooting.md`

That page is where a user lands when something breaks, which is what
makes these worth fixing before a deploy.

**Config errors (#1183, finding 1).** A YAML *syntax* error used to
traceback out of every CLI verb, including `doctor`. It now exits `2`
with one line, and `doctor` gained a third `config` detail: `config is
not valid YAML: ...`.

The page described two failure shapes and said *"the `invalid config`
detail names the offending key"* — true for a schema error, but not for
a parse error, which names a line and column. It is now three shapes in
a table, each with what is wrong and where the message points, and the
doctor-row table lists the new string.

**Claustrum startup (#1183, finding 12).** `_resolve_binary` — claustrum
simply not installed, the commonest real failure — used to raise without
setting `_error`, so `/healthz` reported `running:false, error:null` and
the reason survived only in the server log. It now carries the reason.

The "cannot connect to claustrum" section sent readers to the daemon
log. It now starts with `/healthz`, which is both faster and where the
answer actually is.

## What I verified rather than assumed

Every documented string is grepped from the code, not paraphrased:

```
ops.py:91    f"no config found: {exc}"
ops.py:94    f"invalid config: {exc}"
ops.py:100   f"config is not valid YAML: {exc}"
__main__.py  f"clauster: config error: {exc}"   (x2)
claustrum_daemon.py:179   "error": self._error
```

## Three undocumented surfaces — two now explained, one deliberately not

Nothing had *drifted* for these, because none of them were documented.
But two produce a UI state a user reads as a fault:

- **A plugin-provided subagent** (#1174, finding 7) lists with no
description and refuses edit and delete. Both are deliberate — a plugin
owns the file, and Clauster never follows the symlink, so it genuinely
does not know what the subagent does. Undocumented, that is a blank row
you cannot fix. Now explained, plus a troubleshooting row.
- **An unreadable skill** (#1183, finding 13) still appears, carrying an
error instead of the missing information, and never hides the others.
One sentence.

Deliberately **not** documented, and worth recording why:

- **The listing JSON keys** (`frontmatter_error`, `files_error`,
`has_skill_md`). The OpenAPI schema already describes them, and prose
that repeats a schema is a second copy that drifts. The behaviour is
user-visible; the key names are not.
- **`install-service` rejecting a `"` in a path** (#1183, finding 9).
Windows forbids `"` in filenames, so a real Windows path never trips it
— it is reachable only by rendering a Windows unit from Linux, and the
error already says what is wrong.

## Also checked, no gap

- The secret-masking sentence in the config-editor guide stays true
under the widened mask (#1174, finding 0b).
- `max_bridges` (#1177) and the config-editor bounds (#1185) were
updated in their own PRs; `gen_config_reference.py --check` confirms the
generated tables are in sync.

## Checks

`just docs-lint` and the strict `mkdocs build` both clean.

`no-changelog`: documentation only, no behaviour change.

---------

Co-authored-by: Schuby <12485317+schubydoo@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-changelog Skip the changeset/changelog requirement (CI / refactor / tests / docs)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant