"""Build claude-reference.html from the Claude pass and the pipeline's files. No inference. cd probe/v2 && python3 claude/report_page.py """ import json, pathlib, re, statistics, collections, sys HERE = pathlib.Path(__file__).resolve().parent sys.path.insert(0, str(HERE)); sys.path.insert(0, str(HERE.parent.parent)) from assign import ASSIGN from topic_words import TOPIC_WORDS, PLACE_WORDS chunks = json.load(open('chunks.json')) n = len(chunks) cp = {x['chunk']: x['summary'] for x in json.load(open(HERE / 'predicaments-claude.json'))['summaries']} cs = {x['chunk']: x['summary'] for x in json.load(open(HERE / 'summaries-claude.json'))['summaries']} qp = {x['chunk']: x['summary'] for x in json.load(open('predicaments_clean.json'))['summaries']} qpraw = {x['chunk']: x.get('raw', x['summary']) for x in json.load(open('predicaments.json'))['summaries']} qs = {x['chunk']: x['summary'] for x in json.load(open('summaries_clean.json'))['summaries']} sit = json.load(open(HERE / 'situations-claude.json')) def w(s): return re.findall(r"[a-zé']+", s.lower()) def caps(s): return [t for t in re.findall(r"[A-Za-z']+", s)[1:] if t[0].isupper() and t != 'I'] text = [] for name, d in (('Claude (claude-opus-5)', cp), ('qwen3:14b, as generated', qpraw), ('qwen3:14b after strip_names.py', qp)): tw = [set(w(d[i])) & TOPIC_WORDS for i in range(n)] text.append(dict(name=name, median=statistics.median(len(d[i].split()) for i in range(n)), over=sum(len(d[i].split()) > 20 for i in range(n)), topic=sum(bool(x) for x in tw) / n, place=sum(bool(set(w(d[i])) & PLACE_WORDS) for i in range(n)) / n, caps=sum(bool(caps(d[i])) for i in range(n)), top=', '.join(f'{k} {v}' for k, v in collections.Counter(x for s in tw for x in s).most_common(8)))) # grouping numbers come from compare.log so the page never disagrees with the log log = (HERE / 'compare.log').read_text() ari = [dict(seed=int(m[0]), ari=float(m[1]), groups=int(m[2]), purity=int(m[3]), none=int(m[4])) for m in re.findall(r"seed\s+(\d+): ARI on assigned scenes ([+-][\d.]+); (\d+) candidate groups: median share of a group that is its commonest Claude situation (\d+)%, median share with no situation (\d+)%", log)] spread = [(m[0], int(m[1]), int(m[2]), int(m[3])) for m in re.findall(r"^\s+(\w+)\s+(\d+) scenes over\s+(\d+) groups; largest single group holds\s+(\d+)%", log, re.M)] assert len(ari) == 4 and len(spread) == 20, (len(ari), len(spread)) scenes = [dict(chunk=i, title=chunks[i]['title'], cp=cp[i], cs=cs[i], qp=qp[i], qs=qs[i], sit=ASSIGN.get(i, '')) for i in range(n)] n_sit = len(sit['situations']); passing = sum(s['passes_pipeline_candidate_rule'] for s in sit['situations']) events = sum(len(s['events']) for s in sit['situations']); opts = sum(len(e['options']) for s in sit['situations'] for e in s['events']) facts = [(f'{n}', 'scenes described, 97 stories'), (f'{len(ASSIGN)}', f'scenes carrying a recurring situation ({len(ASSIGN)/n:.0%})'), (f'{passing} of {n_sit}', 'situations pass the pipeline’s candidate rule'), (f'{events} · {opts}', 'draft events · options'), (f"{ari[0]['ari']:.2f}", 'agreement with the 14B groups (ARI, seed 24)')] caveat = ("

What this does and does not show. The hand grouping is one careful reader’s judgment, not ground truth. " "It is, however, the kind of reading the pipeline is trying to automate, and the 14B groups barely track it: a median 14B candidate group is " f"{ari[0]['purity']}% one hand situation and {ari[0]['none']}% scenes with no recurring situation at all. This comparison cannot say whether the fault is the " "14B predicament text or the embed-and-cluster step. The way to separate them is to embed these 839 predicaments with nomic-embed-text and run the same " "k-means. That needs the inference host, so it has not been run.

" "

A caveat on my own text: 88% of my predicaments begin “Someone”. That shared opener is harmless to a reader, but " "DESIGN.md already flags “someone” as a token that could pull embeddings together, so it may make my file look more alike to an embedder than it reads.

") method = ("

Scenes. probe/v2/chunks.json, 839 scenes from 97 stories, read in order, one story at a time, in 19 batches. Each scene got a " "summary under the qwen2.5:3b summary prompt (one sentence, at most 25 words, roles not names, no places) and a predicament under the qwen3:14b redescribe " "prompt (one sentence, at most 20 words, no names, jobs, places, businesses or story-specific objects). I kept names out as I wrote; nothing was " "stripped afterwards. Every record was checked for length and story id.

" "

Grouping. By hand, from the predicaments and summaries together (claude/assign.py). Each scene gets at most one situation. " "364 scenes carry none: essays, set-ups and one-off turns. No embeddings and no clustering were used.

" "

Events. One event per situation, with the benefactor split in two, because the side you stand on changes every option. The options are " "tagged scene (taken from a named scene), archetype (the fixed moves DESIGN.md calls for, such as “say nothing”) or " "gap (written to fill a hole). Descriptions are generic second person, with the scenes they came from. Sizes are advisory priors for the " "numeric search. claude/situations_src.py checks option counts, paths, capitalised names and duplicate effect sets.

" "

Comparison. claude/compare.py uses only files already on disk. No model was called for any of it.

" "

Files, all in probe/v2/claude/: summaries-claude.json and predicaments-claude.json " "(same shape as the pipeline's), assign.py, situations-claude.json, compare.log, this page.

") foot = "Corpus: O. Henry, The Four Million cycle (Project Gutenberg, public domain). Built by claude/report_page.py." data = dict(facts=facts, situations=sit, text=text, ari=ari, spread=spread, caveat=caveat, scenes=scenes, method=method, foot=foot) blob = json.dumps(data, ensure_ascii=False).replace('