/find-rule-surface-drift
This skill is host-language-neutral: it audits the agent-rules and documentation surface, not application source syntax. Its Python scripts are the installed implementation, not a claim that the host project is Python.
You are the orchestrator for a SUSPECT skill that audits the agent-rules surface against ADR 0005's tiered-storage convention.
Scope
- Default root:
.claude/. - Default
CLAUDE.mdpath:<root>/CLAUDE.md. - Default docs dir:
<root>/docs/(top-level.mdfiles only — nested package docs likesubsystems/<name>.mdare out of scope for this skill). - Default ONBOARDING.md path:
<project-root>/ONBOARDING.md(the human entry point; pass--onboarding-mdto override or point at a missing path to skip the onboarding bands). - Output:
reports/find-rule-surface-drift/<scan-id>/. - No code or doc edits; detection only.
How success is judged
- The run creates a fresh scan dir under
reports/find-rule-surface-drift/<scan-id>/withdetections.jsonl,report.md, andfindings.json. - Each command's exit code is honored; stop on non-zero and report the failing command instead of rendering stale detections.
- Handoff identifiers are valid: every
findings.jsonrecord uses one of thepatternnames in Findings and carriesfile/linenoevidence. - No silent drops: the JSONL record count matches
findings.summary.findings_totaland thefindingsarray length. - Detector edits are not trusted until the Replay fixtures commands pass
for
fixtures/exercise-all/andfixtures/exercise-onboarding/.
Pipeline
set -euo pipefail
SCAN_ID="scan-$(date -u +%Y%m%d-%H%M%S)"
REPORT_DIR="reports/find-rule-surface-drift/$SCAN_ID"
mkdir -p "$REPORT_DIR"
.venv/bin/python .claude/skills/find-rule-surface-drift/scripts/detect.py \
--output "$REPORT_DIR/detections.jsonl"
.venv/bin/python .claude/skills/find-rule-surface-drift/scripts/report.py \
--detections "$REPORT_DIR/detections.jsonl" \
--output-md "$REPORT_DIR/report.md" \
--output-json "$REPORT_DIR/findings.json" \
--target ".claude/"
Findings
oversized_root:.claude/CLAUDE.mdexceeds the soft size budget (default 30,000 chars, per ADR 0005). Demote content to load-on-demand docs; don't subdivide at root.oversized_doc: a.claude/docs/*.mdexceeds the per-doc soft budget (default 50,000 chars). Decide whether it's one topic or two — legitimate inventories may warrant a higher threshold or an explicit exemption.dormant_doc: a file exists under.claude/docs/but has no row in CLAUDE.md's Supplementary Documentation table. Either add a trigger row or remove the doc — load-on-demand docs need an explicit "Read when…" entry to be reachable.missing_doc: a row in the Supplementary Documentation table references a file that does not exist on disk. Either fix the row or create the doc.unreferenced_doc: a doc is registered in the table but appears in no other artifact under.claude/. May be legitimately reachable only via its trigger; the recommendation reads "confirm or remove."missing_link: ONBOARDING.md links to a path that does not exist on disk. Treat as a real defect — broken links in the human entry point block onboarding directly. Either fix the link target or remove the link.dormant_in_onboarding: a doc registered in CLAUDE.md's Supplementary Documentation table is not linked from ONBOARDING.md. Info-level only. Most agent-targeted docs (oracle, model-tiering, deployment, sub-agents, etc.) legitimately stay unmentioned in human onboarding. Use this band when triaging newly-registered docs to decide whether the human path should reference them — never as a blocking signal.
Calibration
--max-root-chars and --max-doc-chars are soft budgets. ADR 0005
sets the root soft budget at ~30K — high enough that legitimate
project context fits, low enough to push back when always-loaded
content drifts past the practical signal-density ceiling. The per-doc
ceiling defaults to ~50K; legitimate inventory docs (subsystem maps,
full catalogues) can exceed this and should either raise the threshold
project-wide or carry a documented exemption.
Replay fixtures
Run fixture output outside each fixture root so report artifacts do not
pollute the next unreferenced_doc check:
set -euo pipefail
FIXTURE_ROOT=".claude/skills/find-rule-surface-drift/fixtures/exercise-all"
FIXTURE_OUT="reports/find-rule-surface-drift/_fixture-runs/exercise-all"
mkdir -p "$FIXTURE_OUT"
.venv/bin/python .claude/skills/find-rule-surface-drift/scripts/detect.py \
--root "$FIXTURE_ROOT" \
--project-root "$FIXTURE_ROOT" \
--onboarding-md "$FIXTURE_ROOT/NO_ONBOARDING.md" \
--max-root-chars 200 \
--max-doc-chars 200 \
--output "$FIXTURE_OUT/detections.jsonl"
.venv/bin/python .claude/skills/find-rule-surface-drift/scripts/report.py \
--detections "$FIXTURE_OUT/detections.jsonl" \
--output-md "$FIXTURE_OUT/report.md" \
--output-json "$FIXTURE_OUT/findings.json" \
--target "$FIXTURE_ROOT"
FIXTURE_ROOT=".claude/skills/find-rule-surface-drift/fixtures/exercise-onboarding"
FIXTURE_OUT="reports/find-rule-surface-drift/_fixture-runs/exercise-onboarding"
mkdir -p "$FIXTURE_OUT"
.venv/bin/python .claude/skills/find-rule-surface-drift/scripts/detect.py \
--root "$FIXTURE_ROOT" \
--project-root "$FIXTURE_ROOT" \
--onboarding-md "$FIXTURE_ROOT/ONBOARDING.md" \
--output "$FIXTURE_OUT/detections.jsonl"
.venv/bin/python .claude/skills/find-rule-surface-drift/scripts/report.py \
--detections "$FIXTURE_OUT/detections.jsonl" \
--output-md "$FIXTURE_OUT/report.md" \
--output-json "$FIXTURE_OUT/findings.json" \
--target "$FIXTURE_ROOT"
Expected buckets: exercise-all emits one each of oversized_root,
oversized_doc, dormant_doc, missing_doc, and unreferenced_doc;
exercise-onboarding emits one each of missing_link and
dormant_in_onboarding.
When things go sideways
| Case | Signal | Response |
|---|---|---|
| Target absent | --root, --claude-md, --docs-subdir, or --onboarding-md points at a missing surface. |
Let the detector's zero-finding output stand if it exits 0; name the absent target in the report. |
| Zero findings | detections.jsonl is empty and report.md says Findings: 0. |
Treat as a clean scan only after confirming the intended root and onboarding path were used. |
| Script non-zero exit | Any command exits non-zero. | Stop the pipeline, paste the command and stderr, and do not run report.py against stale detections. |
Next Skills
/fix-workflowfor executing a single migration (e.g. demote one oversized section, register one dormant doc, prune one unreferenced doc)./decideif a finding reveals a tradeoff the ADR doesn't yet cover (e.g. "we keep acceptingoversized_docforarchitecture.md— raise the per-doc budget formally")./prevent-regressionif a particular drift recurs often enough to justify a pre-commit check (the plannedoversized_roothard guardrail in ADR 0005's Verification section).
Notes for the orchestrator
- This is a Stage 1 skeleton. Cross-tool sync drift between
.claude/CLAUDE.mdand.augment/rules/imported/*is not yet detected — it requires content-level comparison heavier than a filename check. Add as a follow-up band when an actual drift incident motivates the cost. find_unreferencedexcludesworktrees/and__pycache__/from its grep scan. Mirrored CLAUDE.md files inside agent worktrees would otherwise mask every doc as "referenced."- Replay fixtures live under
fixtures/exercise-all/andfixtures/exercise-onboarding/; keep their output outside the fixture roots. - The
unreferenced_docband usesgrep -lacross.claude/. Docs that are reached only via the supplementary table's trigger (and never named elsewhere) will fire here legitimately; treat the finding as "confirm reachability," not "delete on sight."