cc-os/plugins/os-doc-hygiene/examples/golden/classifier/README.md

5.3 KiB

Classifier golden examples

Layer-2 reversion-protection fixtures for the classifier (design D7, openspec/changes/add-check/design.md). Each case is a small static fixture doc-tree (input/, stable bytes → stable sha256) paired with expected.json, a full schema-valid machine report representing the expected classification of that tree. These are distinct from the schema-shape fixtures one level up (../valid_report.json / ../invalid_report.json), which only exercise validate_report.py.

Adding or changing a classifier golden is human-gated per the META-RULE in invariants.md.

Cases

Dir class / subtype op_type exact_edit.kind tier scanner signal
1-orphaned stale / orphaned deterministic delete-range confirm broken_reference
2-superseded stale / superseded deterministic move-to-archive auto stale_name_location
3-completed-in-place stale / completed-in-place deterministic insert-frontmatter auto archive_to_live_ratio
4-duplicated stale / duplicated deterministic dedupe auto stale_name_location (see note)
5-distill bloat / distill generative (none) confirm archive_to_live_ratio

Each case directory holds:

  • input/ — the fixture doc-tree (the bytes the scanner and the model see).
  • proposals.json — the hand-authored model-judgment proposal(s) used to generate expected.json (kept so the computed fields are reproducible).
  • expected.json — the full schema-valid machine report (the frozen expectation). Computed fields (expected_sha256, raw_tokens, derived safety_tier) are real: generated by piping a real scanner.py artifact + proposals.json through report_builder.py. scan.project_root is normalized to <input> for portability; generated_at timestamps are wall-clock and intentionally not asserted on.
  • notes.md — the one taxonomy edge + scanner signal the case pins.

Note (case 4): the scanner has no dedicated duplication signal. Case 4 rides a co-occurring stale_name_location ("legacy") as its deterministic trigger; the duplicated judgment itself is the model's. See 4-duplicated/notes.md.

Two harnesses

Hermetic unit harness (in pytest — tests/test_classifier_golden.py)

Runs with NO network and NO model. For each case it asserts only the deterministic / stable parts: scanner signals on the expected paths, validate_report.py accepts each expected.json, and internal consistency of the stable classification fields (closed enums, op_type, exact_edit.kind, the derived safety_tier recomputed, and expected_sha256 re-verified against the fixture bytes). This is the regression gate that runs in CI.

Live model-classification regression (OUTSIDE pytest — manual / agent-invoked)

This is the model-dependent check; it is intentionally NOT part of the unit suite because it calls a live model. Run it manually (or have an agent run it) when you want to confirm the classifier still reaches the expected judgment.

For each case directory:

  1. Run an actual classification over input/ — either /os-doc-hygiene:check --scope <case>/input or by invoking the check skill against that tree. This produces a fresh machine report.

    Confirm the invocation once the check command lands. --scope is a filter on a scan rooted at the resolved project root, not a re-root — to classify a case in isolation you want the check rooted at <case>/input (as the hermetic suite roots the scanner). Verify the exact flag/rooting against the shipped skills/check/SKILL.md before relying on it.

  2. Diff the produced classification's stable fields against expected.json, per entry:

    • category.class
    • category.subtype
    • op_type
    • the derived safety_tier
    • exact_edit.kind

    A mismatch on any of these is a regression — investigate before accepting.

  3. Advisory only (flag for human review, do not auto-fail):

    • op prose wording.
    • exact anchor line numbers (exact_edit.anchor.start_line / end_line) and reducible_range.

    These are model-authored and may drift slightly without indicating a real classification regression; surface them for a human to eyeball rather than gating on them.

Because expected.json encodes a captured model judgment, any intended change to the expected classification is a golden change → human-gated per the META-RULE.

Regenerating an expected.json (after an approved fixture change)

python3 scripts/scanner.py --root examples/golden/classifier/<case>/input > /tmp/scan.json
python3 scripts/report_builder.py \
    --scan /tmp/scan.json \
    --proposals examples/golden/classifier/<case>/proposals.json \
    --root examples/golden/classifier/<case>/input \
    --out-json examples/golden/classifier/<case>/expected.json
# then normalize scan.project_root to "<input>" and re-run the hermetic suite.

raw_tokens reflect whichever token-estimator backend was active at generation time (the zero-dependency heuristic here, since no tiktoken vocab is vendored). The hermetic suite does not assert on raw_tokens, so regenerating on a tiktoken-equipped machine may shift those counts harmlessly — don't mistake that diff for a regression.