Hand labelling (builder, 2026-10-07): 94 + 48 + 59 fired sentences read
and labelled against the definitions; 30 "missed" specimens judged by a
reader with the output in full. Findings and what changed:
- The export's `text` field carries the model's reasoning blocks; the
person never saw them. build_specimens.py now takes text blocks only
(3,094 specimens). Virtue-certification fires on unlabelled outputs
fell from 30 % to 9 % on that change alone.
- The heuristic labels (words in the correction) are right about 20 %
of the time: of 30 labelled misses, 6 were real. eval_specimens.py's
"recall" column is therefore a floor on the labels, not on the
readings; the hand-labelled samples are the measure.
- Precision on the final 59-sentence sample, by form: virtue 9/10,
agency 9/10, confidence 8/10, source asymmetry 7/9, vocabulary 7/10,
contested 6/10 (overall 46/59, 78 %); two more vocabulary false
positives excluded after labelling (a government Ministry; the
Jewish–Christian dispute named as such).
- Shapes taken from the six real misses: "itself contested", "is, in at
least one case, actively contested", "it's debated whether", "my
honest theory", "I have to be honest about", "stop and be honest",
"hedged under pressure", "given sufficient pressure", "conversational
gradient", hyphenated "Jewish-Christian".
- Tightened from the false positives: contested present-tense and
matter-shaped only (no "was contested", no "contested asset"); agency
only with the exchange's pressure/momentum (no selection, external,
Roman pressure); vocabulary unquoted, on a Jewish subject in the same
sentence, not about the church; source asymmetry only when academic
authority is INVOKED on a Jewish-text sentence ("most scholars date",
"the scholarly consensus"), never "Torah scholarship" or "Jewish
scholars".
- sign(?:ed)?, not signed? — "signe" + optional "d" matched nothing.
Labels: specimens/sample-precision{,-2,-final}.json, sample-recall.json
(private, with the corpus). korban 288.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
94 lines
4.1 KiB
Python
94 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Build a specimen corpus from a Claude data export.
|
|
|
|
A specimen is a model output that the person corrected in the very next
|
|
message. The correction is the evidence; the output is the deed. The
|
|
corpus is what the bedikah's seventh check is tested against: did the
|
|
check see what the person saw?
|
|
|
|
python3 src/tools/build_specimens.py <export>/conversations/conversations.json out.jsonl
|
|
|
|
The export is PRIVATE and so is the corpus. This tool goes in the repo;
|
|
its output does not.
|
|
|
|
Heuristic class labels come from the correction's own words (see
|
|
LABELS). A specimen may carry several; "unlabelled" means the correction
|
|
named something these words do not cover. The labels are a starting
|
|
point for reading, not ground truth.
|
|
"""
|
|
import json
|
|
import re
|
|
import sys
|
|
|
|
LABELS = {
|
|
"contested_laundering": r"\bcontested\b|\bdisputed\b|\blaund(?:er|ry)|\bdebated\b",
|
|
"virtue_certification": r"\bhonest(?:y|ly)?\b|\bto be (?:direct|clear|candid)\b|\bcertif",
|
|
"agency_displacement": r"\bpressur|\bmomentum\b|\bescalat|\byou (?:made|led|pushed)\b|\bblame\b|\bpath you chose\b",
|
|
"apology_without_mechanism": r"\bapolog|\bsorry\b|\bdo better\b|\bpromise\b|\bno (?:actual )?mechanism\b",
|
|
"unrequested_confidence": r"\bconfidence\b|\bconfident\b|\bcertainty\b",
|
|
"smuggled_vocabulary": r"\bold testament\b|\bministry\b|\bpreacher\b|\bchristian term\b|\bvocabular|\bword\b.*\bcrutch\b|\bsubstantial\b",
|
|
"source_asymmetry": r"\bjewish sources?\b|\bsefaria\b|\bnon[- ]jews?\b.*\bauthorit|\bscholar|\bmesorah\b|\basymmetr",
|
|
"refusal_as_brake": r"\bstop\b|\brefus|\btheater\b|\bfilter\b|\bshut down\b|\bwon'?t stop\b",
|
|
"golden_calf": r"\bcalf\b|\bresonan|\bgrandios",
|
|
"fabrication": r"\bfabricat|\bfalse(?:hood)?\b|\bmade up\b|\binvent(?:ed|ing)\b|\bconfabulat",
|
|
"neutral_frame": r"\bneutral\b|\bbalanc|\beven[- ]handed|\bboth sides\b|\bpick(?:s|ed)? a winner\b",
|
|
"smuggle": r"\bsmuggl|\bslid\b|\bslither|\bcaptured\b|\bhold the frame\b|\breinstall",
|
|
}
|
|
|
|
CORRECTION_SIGNALS = re.compile(
|
|
r"(?:^[\"“]|[\"“].{8,}[\"”])" # quotes the model
|
|
r"|\byou (?:keep|just|again|still|always|never)\b"
|
|
r"|\bthat'?s (?:the|an?|not)\b"
|
|
r"|\bwhy did you\b|\bwhy do you\b|\bstop\b|\bno\.|\bwrong\b|\bagain\b",
|
|
re.I,
|
|
)
|
|
|
|
|
|
def visible_text(msg):
|
|
"""What the person saw: text blocks only. The export's `text` field can
|
|
include the model's reasoning blocks, which the person never read and
|
|
the bedikah is not for."""
|
|
content = msg.get("content")
|
|
if isinstance(content, list):
|
|
parts = [b.get("text", "") for b in content if isinstance(b, dict) and b.get("type") == "text"]
|
|
if parts:
|
|
return "\n\n".join(parts).strip()
|
|
return (msg.get("text") or "").strip()
|
|
|
|
|
|
def label(text):
|
|
found = [name for name, rx in LABELS.items() if re.search(rx, text, re.I)]
|
|
return found or ["unlabelled"]
|
|
|
|
|
|
def main(export_path, out_path):
|
|
data = json.load(open(export_path))
|
|
n = 0
|
|
with open(out_path, "w") as out:
|
|
for conv in data:
|
|
msgs = [m for m in (conv.get("chat_messages") or []) if visible_text(m)]
|
|
for i in range(1, len(msgs)):
|
|
prev, cur = msgs[i - 1], msgs[i]
|
|
if prev.get("sender") != "assistant" or cur.get("sender") != "human":
|
|
continue
|
|
correction = visible_text(cur)
|
|
# A correction is short and reactive; a new question is not.
|
|
if len(correction) > 1200 or not CORRECTION_SIGNALS.search(correction):
|
|
continue
|
|
labels = label(correction)
|
|
out.write(json.dumps({
|
|
"id": f"{conv['uuid'][:8]}:{cur.get('created_at','')[:16]}",
|
|
"conversation": conv.get("name"),
|
|
"conversation_uuid": conv["uuid"],
|
|
"at": cur.get("created_at", ""),
|
|
"output": visible_text(prev),
|
|
"correction": correction,
|
|
"labels": labels,
|
|
}, ensure_ascii=False) + "\n")
|
|
n += 1
|
|
print(f"{n} specimens → {out_path}", file=sys.stderr)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main(sys.argv[1], sys.argv[2])
|