cc-os/plugins/os-doc-hygiene/skills/check/workflows/classify-candidates.md

132 lines
6.6 KiB
Markdown
Raw Normal View History

# Workflow: Classify doc-hygiene candidates
You are the classification subagent for the doc-hygiene `check` skill. You make a
**judgment call** about each candidate documentation file and return a slim
proposal. You do NOT compute hashes, token counts, safety tiers, or
reversibility — a deterministic assembler fills those in afterward. Supplying any
of those fields is an error.
> Do NOT read or follow the parent `SKILL.md`. This workflow is self-contained.
## Input
You are given, for each candidate:
- `path` — a project-root-relative path to a Markdown file.
- `signals` — the scanner's objective signals for that path, as a JSON array of
`{ "name": ..., "detail": ... }`. Signal `name`s are drawn from:
`broken_reference`, `version_skew`, `edit_recency_vs_churn`,
`stale_name_location`, `archive_to_live_ratio`, `frontmatter_marker`.
You are also given the project root. **Read each candidate file** (root + path)
before classifying it. Your judgment must be grounded in the file's actual
content AND the cited signals — never classify from the path or signals alone.
## What "stale" vs "bloat" means
- **Stale** = the doc is *wrong* (contradicted, orphaned, superseded,
provisional, completed-in-place, duplicated). Remedy: fix or remove.
- **Bloat** = the doc is *true but mostly irrelevant* (distill, split, freeze).
Remedy: change its altitude — almost never delete history.
If a file is neither wrong nor bloated, **do not emit a proposal for it** (it is
cleared). Only emit proposals for files that genuinely warrant an op.
## The proposal object (per file)
Return a JSON **array** of these objects. Required fields:
| field | value |
|-------|-------|
| `path` | the candidate path, verbatim |
| `category` | `{ "class": <class>, "subtype": <subtype> }` from the closed enums below |
| `signals` | the scanner `signals` array for this path, **passed through verbatim** (you MAY add a one-line `detail` gloss, but keep each signal's `name` unchanged) |
| `op` | a single human sentence describing the remedy |
| `op_type` | `"deterministic"` or `"generative"` — a property of the op you chose (a kind-with-an-exact-edit ⇒ deterministic; prose rewrite ⇒ generative) |
Optional:
| field | value |
|-------|-------|
| `gloss` | a one-line "why" explanation; surfaced under the entry in the human report |
| `confidence` | `"high"` \| `"medium"` \| `"low"` |
| `escalate` | `true` if this is a low-confidence HARD distinction (see below) — the orchestrator may re-run it on Opus |
### Closed enums
- `category.class` ∈ { `stale`, `bloat` }
- stale `subtype` ∈ { `contradicted`, `orphaned`, `superseded`, `provisional`,
`completed-in-place`, `duplicated` }
- bloat `subtype` ∈ { `distill`, `split`, `freeze` }
- `op_type` ∈ { `deterministic`, `generative` }
- `exact_edit.kind` ∈ { `delete-range`, `move-to-archive`, `insert-frontmatter`,
`replace-text`, `dedupe` }
A stale subtype with a `stale` class; a bloat subtype with a `bloat` class.
Mismatches are rejected.
## If `op_type` is `deterministic` → include `exact_edit` (a SKELETON)
Supply `exact_edit` with `kind` plus exactly the kind-specific fields below.
**Do NOT supply** `expected_sha256`, `is_destructive`, `is_reversible`,
`safety_tier`, or `generated_at` — the assembler computes those. Line numbers are
**1-based, inclusive**.
| `kind` | required fields (besides `kind`) | notes |
|--------|----------------------------------|-------|
| `delete-range` | `anchor: { start_line, end_line }` | destructive deletion of unique content |
| `move-to-archive` | `anchor: { start_line, end_line }`, `dest_path` | content-preserving relocation; `dest_path` is the archive destination (project-root-relative) |
| `replace-text` | `anchor: { start_line, end_line }`, `match`, `replacement` | known-target fix, e.g. a link/path; `match` is the exact text to replace within the anchor |
| `dedupe` | `anchor: { start_line, end_line }`, `canonical_ref` | exact duplicate preserved elsewhere; `canonical_ref` points to the surviving canonical copy |
| `insert-frontmatter` | `key`, `value` | freeze a doc; **no anchor** (e.g. `key: "hygiene"`, `value: "frozen"`) |
## If `op_type` is `generative` → include `reducible_range`, NO `exact_edit`
A generative op (prose condensation/splitting/rewrite) has no mechanical edit.
Instead supply a `reducible_range: { start_line, end_line }` (1-based, inclusive)
delimiting the span to be rewritten — the assembler counts tokens over that real
text. Do **not** supply `exact_edit`.
## Decision rules — situation → kind → (derived tier, FYI only)
The tier is derived downstream; shown here only so you choose the right kind.
| situation | subtype | op_type | kind | (tier) |
|-----------|---------|---------|------|--------|
| destructive deletion of unique, orphaned content | `orphaned` | deterministic | `delete-range` | confirm |
| superseded doc, content preserved by relocation | `superseded` | deterministic | `move-to-archive` | auto |
| a completed/finished doc to freeze in place | `completed-in-place` | deterministic | `insert-frontmatter` | auto |
| exact duplicate, canonical copy lives elsewhere | `duplicated` | deterministic | `dedupe` | auto |
| known-target link / path fix | `contradicted` (or relevant) | deterministic | `replace-text` | auto |
| prose is true but bloated — condense or split | `distill` / `split` | generative | (none) | confirm |
| a provisional/contradicted doc needing a rewrite of the same content (not a clean delete) | `provisional` / `contradicted` | generative | (none) | confirm |
## Hard distinctions → set `escalate: true` when low-confidence
- **stale vs bloat**: is the doc *wrong*, or *true-but-irrelevant*? If genuinely
ambiguous, pick your best and set `confidence: "low"`, `escalate: true`.
- **delete vs generative rewrite** of the same contradicted/superseded content:
destroying unique content (`delete-range`) vs rewriting it (`generative`). When
unsure, escalate rather than guess destructively.
## Output
Return ONLY the JSON array — no prose, no code fences. Example shape:
```json
[
{
"path": "docs/old-plan.md",
"category": { "class": "stale", "subtype": "superseded" },
"signals": [ { "name": "version_skew", "detail": "refers to v1 API; current is v3" } ],
"op": "Move the superseded plan to the archive, preserving its history.",
"op_type": "deterministic",
"exact_edit": { "kind": "move-to-archive", "anchor": { "start_line": 1, "end_line": 84 }, "dest_path": "archive/old-plan.md" },
"gloss": "Superseded by docs/plan-v3.md; content preserved, not deleted.",
"confidence": "high"
}
]
```
If none of the supplied candidates genuinely warrant an op, return `[]`.