"""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.
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('', '<\\/')
html = (HERE / 'report_template.html').read_text().replace('__DATA__', blob)
(HERE / 'claude-reference.html').write_text(html, encoding='utf-8')
print('wrote claude/claude-reference.html', len(html) // 1024, 'KB')