Files
bikoret/src/tools/build_specimens.py
T
Scott KarpandClaude Fable 5.1 dd1ffaa1b3 Check 7 tuned against hand-labelled specimens; the corpus takes visible text only
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>
2026-10-07 00:22:21 +00:00

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])