mirror of
https://github.com/theclaymethod/unslop.git
synced 2026-09-19 01:45:09 +08:00
d6f632117f
Applies a reviewed cleanup list without changing any script's CLI flags, JSON
keys, exit codes, or stderr messages (all pinned by eval rows).
Shared modules (dedup):
- scripts/_lang.py: ENGLISH_FUNCTION_WORDS/english_function_share/is_probably_english,
previously byte-identical copies in banned_phrase_scan.py and structure_scan.py.
- structure_scan.py now imports split_sentences from readability_metrics instead
of keeping its own copy.
- silhouette_scan.py's stopword set is a verified pure superset of
structure_scan's; SILHOUETTE_STOPWORDS = structure's set | the extras.
- harvest_classify.py imports recency_value/DATE_FLOOR from harvest_samples.
- evals/_check_support.py: ROOT, run(cmd, timeout=60), load_evals() -- the
timeout=60 safety net that only check_contrib.py had now covers check_pairs,
check_seeded_docs, check_mimic, check_contrib, check_voice, and
check_pattern_coverage too.
Small cleanups: contribute.py's row_fn drops its unused category param;
run_model_parity.py's resolve_models param renamed responses->payload;
harvest_classify.py's heuristic() attaches suspect_ai/dictated so
rank_enriched needs no reconstruction; check_pattern_coverage.py's paired flag
formulas become a plain "if neither: both = True"; check_contrib.py drops the
__import__("scripts.contribute", ...) spelling for a normal import;
run_mimic_refine.py computes docs_a/matrix_a once and passes it to both
make_live_source and write_outputs; calibrate_pairs.py factors its four
near-identical contraction-replacement closures into one _contraction_repl
helper used by both directions.
Altitude items: GENRE_SUPPRESSIONS lookup tables replace the inline
`genre != "..."` conditionals in structure_scan.py and silhouette_scan.py;
check_gates_doc.py additively verifies every *.py token in a gate command
exists under ROOT (behavioral-tune and rubric-judge are exempt -- neither
command has a .py token).
Efficiency (Phase 2), each verified against the same eval rows / diffed
outputs before landing:
- run_model_parity.py replaces its subprocess-per-scanner-call helpers with
in-process imports of banned_phrase_scan/structure_scan/validate_preservation
(mirrors run_mimic_refine's import pattern). PARITY slice: 14.2s -> 0.33s.
- voice_score.py's lcs_len (O(n*m) DP) is replaced by
has_common_substring_over(), an O(n+m) rolling-hash check for "any shared
substring longer than the 120-char threshold" (hash matches are verified
against the source text, so no false positives). Nothing pins the exact
longest_common_substring value (checked); it now reports the matched
threshold window length on a hit, 0 otherwise -- documented in the
docstring. The violation boolean is unchanged.
- gi_score() precomputes per-key distances once per candidate/impostor
instead of recomputing distances() from scratch every trial; trials do a
subset-weighted sum over the precomputed values. Arithmetically exact
(same RNG draw order, same float sums) -- verified the VOICE-08
determinism value and the full check_voice --separation/--gi/--gaming
output are byte-identical before/after.
- check_voice.py and check_pairs.py convert their subprocess-per-cell/row
scanner calls to in-process imports (voice_score/voice_profile,
banned_phrase_scan/structure_scan), mirroring the CLI's own decline/exit
logic so output stays byte-compatible.
Deferred (out of scope for a contract-safe pass): a protects-grain redesign,
giving silhouette_scan.py its own English-decline gate, and decomposing
run_mimic_refine.py's build_report().
python3 evals/run_adversarial.py: 434 PASS / 1 XFAIL / 0 FAIL, unchanged
throughout; wall time 54.8s -> 25.3s.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6CYksdLbXbTAxcAQjvHz5
224 lines
7.6 KiB
Python
224 lines
7.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Classify harvested candidates into situation/register coverage cells."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
sys.path.insert(0, str(HERE))
|
|
|
|
from harvest_samples import DATE_FLOOR, recency_value # noqa: E402
|
|
|
|
|
|
CELLS = [
|
|
"numbers_data",
|
|
"question_addressed",
|
|
"anecdote_markers",
|
|
"disagreement",
|
|
"openings_closings",
|
|
]
|
|
|
|
|
|
def load_candidates(path: Path) -> list[dict[str, Any]]:
|
|
data = json.loads(path.read_text())
|
|
if isinstance(data, dict):
|
|
return data.get("candidates", [])
|
|
if isinstance(data, list):
|
|
return data
|
|
raise ValueError("candidates file must contain a list or {candidates: [...]}")
|
|
|
|
|
|
def cells_for(text: str) -> list[str]:
|
|
lowered = text.lower()
|
|
cells = []
|
|
if re.search(r"\b\d+(?:[,.]\d+)?%?\b", lowered):
|
|
cells.append("numbers_data")
|
|
if "?" in text or re.search(r"\b(?:why|how|what|when|where|which)\b", lowered):
|
|
cells.append("question_addressed")
|
|
if re.search(r"\b(?:last quarter|yesterday|once|during|when we|i noticed|i remember)\b", lowered):
|
|
cells.append("anecdote_markers")
|
|
if re.search(r"\b(?:disagree|however|instead|not convinced|push back)\b", lowered):
|
|
cells.append("disagreement")
|
|
if re.search(r"\b(?:hi|thanks|best|regards|closing|opening|first off)\b", lowered):
|
|
cells.append("openings_closings")
|
|
return cells
|
|
|
|
|
|
def quality_for(candidate: dict[str, Any], cells: list[str]) -> int:
|
|
words = int(candidate.get("words") or len(re.findall(r"\w+", candidate.get("text", ""))))
|
|
score = 3
|
|
if 40 <= words <= 220:
|
|
score += 1
|
|
if cells:
|
|
score += 1
|
|
if candidate.get("suspect_ai"):
|
|
score -= 2
|
|
if candidate.get("dictated"):
|
|
score -= 1
|
|
return max(1, min(5, score))
|
|
|
|
|
|
def candidate_id(candidate: dict[str, Any], index: int) -> Any:
|
|
return candidate.get("id", index)
|
|
|
|
|
|
def source_position(candidate: dict[str, Any]) -> str:
|
|
source = candidate.get("source", {})
|
|
return str(source.get("line", source.get("offset", "")))
|
|
|
|
|
|
def coverage_from(candidates: list[dict[str, Any]]) -> dict[str, int]:
|
|
coverage = {cell: 0 for cell in CELLS}
|
|
for candidate in candidates:
|
|
for cell in candidate.get("cells", []):
|
|
coverage[cell] = coverage.get(cell, 0) + 1
|
|
return coverage
|
|
|
|
|
|
def rank_enriched(candidates: list[dict[str, Any]]) -> list[int]:
|
|
seen_empty = set()
|
|
rank_rows = []
|
|
for idx, candidate in enumerate(candidates):
|
|
cells = candidate.get("cells", [])
|
|
fills_empty = any(cell not in seen_empty for cell in cells)
|
|
seen_empty.update(cells)
|
|
rank_rows.append((idx, fills_empty))
|
|
ranked = sorted(
|
|
rank_rows,
|
|
key=lambda row: (
|
|
not row[1],
|
|
-int(candidates[row[0]].get("quality") or 0),
|
|
-recency_value(candidates[row[0]]),
|
|
bool(candidates[row[0]].get("suspect_ai")),
|
|
bool(candidates[row[0]].get("dictated")),
|
|
str(candidates[row[0]].get("source", {}).get("path", "")),
|
|
source_position(candidates[row[0]]),
|
|
row[0],
|
|
),
|
|
)
|
|
return [candidate_id(candidates[idx], idx) for idx, _ in ranked]
|
|
|
|
|
|
def heuristic(candidates: list[dict[str, Any]]) -> dict[str, Any]:
|
|
enriched = []
|
|
seen_empty = set()
|
|
for idx, candidate in enumerate(candidates):
|
|
cells = cells_for(candidate.get("text", ""))
|
|
quality = quality_for(candidate, cells)
|
|
fills_empty = any(cell not in seen_empty for cell in cells)
|
|
seen_empty.update(cells)
|
|
enriched.append({
|
|
"index": idx,
|
|
"id": candidate.get("id", idx),
|
|
"cells": cells,
|
|
"quality": quality,
|
|
"why": "lexical heuristic matched " + (", ".join(cells) if cells else "no named cell"),
|
|
"fills_empty_coverage_cell": fills_empty,
|
|
"source": candidate.get("source", {}),
|
|
"suspect_ai": candidate.get("suspect_ai"),
|
|
"dictated": candidate.get("dictated"),
|
|
})
|
|
return {
|
|
"coverage_matrix": coverage_from(enriched),
|
|
"candidates": enriched,
|
|
"ranking": rank_enriched(enriched),
|
|
}
|
|
|
|
|
|
def write_agent_tasks(candidates: list[dict[str, Any]], out_dir: Path) -> dict[str, Any]:
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
chunks = []
|
|
prompt = (
|
|
"Classify each candidate into WP10b situation/register cells. "
|
|
"Return JSONL rows with candidate_index, cells, quality 1-5, and one-line why. "
|
|
"Cells include numbers_data, question_addressed, anecdote_markers, "
|
|
"disagreement, openings_closings, plus any clearly justified additional cell."
|
|
)
|
|
for start in range(0, len(candidates), 10):
|
|
chunk = candidates[start:start + 10]
|
|
path = out_dir / f"harvest-classify-{start // 10 + 1:03d}.json"
|
|
payload = {
|
|
"contract": "tier-1-pack-detector",
|
|
"prompt": prompt,
|
|
"candidates": [
|
|
{"candidate_index": start + i, "text": c.get("text", ""), "source": c.get("source", {})}
|
|
for i, c in enumerate(chunk)
|
|
],
|
|
}
|
|
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
|
|
chunks.append(str(path))
|
|
return {"task_files": chunks, "chunk_size": 10}
|
|
|
|
|
|
def result_candidate_key(row: dict[str, Any]) -> Any:
|
|
for key in ("candidate_id", "id", "candidate_index", "index"):
|
|
if key in row:
|
|
return row[key]
|
|
raise ValueError("result row missing candidate id")
|
|
|
|
|
|
def merge_results(candidates_path: Path, results_path: Path) -> dict[str, Any]:
|
|
candidates = load_candidates(candidates_path)
|
|
rows_by_id: dict[Any, dict[str, Any]] = {}
|
|
for line in results_path.read_text().splitlines():
|
|
if line.strip():
|
|
row = json.loads(line)
|
|
rows_by_id[result_candidate_key(row)] = row
|
|
|
|
merged = []
|
|
for idx, candidate in enumerate(candidates):
|
|
cid = candidate_id(candidate, idx)
|
|
row = rows_by_id.get(cid)
|
|
if row is None and idx in rows_by_id:
|
|
row = rows_by_id[idx]
|
|
cells = list(row.get("cells", [])) if row else []
|
|
quality = int(row.get("quality")) if row and row.get("quality") is not None else quality_for(candidate, cells)
|
|
why = str(row.get("why", "no classifier result")) if row else "no classifier result"
|
|
merged.append({
|
|
**candidate,
|
|
"id": cid,
|
|
"cells": cells,
|
|
"quality": max(1, min(5, quality)),
|
|
"why": why,
|
|
})
|
|
|
|
return {
|
|
"coverage_matrix": coverage_from(merged),
|
|
"candidates": merged,
|
|
"ranking": rank_enriched(merged),
|
|
}
|
|
|
|
|
|
def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--candidates", required=True)
|
|
parser.add_argument("--mode", choices=["heuristic", "agent"], default="heuristic")
|
|
parser.add_argument("--out-dir", default="harvest-agent-tasks")
|
|
parser.add_argument("--merge")
|
|
return parser.parse_args(argv)
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
args = parse_args(argv)
|
|
if args.merge:
|
|
print(json.dumps(merge_results(Path(args.candidates), Path(args.merge)), indent=2, sort_keys=True))
|
|
return 0
|
|
candidates = load_candidates(Path(args.candidates))
|
|
if args.mode == "heuristic":
|
|
print(json.dumps(heuristic(candidates), indent=2, sort_keys=True))
|
|
else:
|
|
print(json.dumps(write_agent_tasks(candidates, Path(args.out_dir)), indent=2, sort_keys=True))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
raise SystemExit(main(sys.argv[1:]))
|