anima-python 0.13.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,801 @@
1
+ #!/usr/bin/env python3
2
+ """cli/rho_axon.py — ρ-AXON reach-layer measurement (Ψ-SOMA ρ · replaces G0-G6).
3
+
4
+ The reach (capability) layer of Ψ-SOMA — how far the soma's signal extends down the
5
+ axon. Says NOTHING about consciousness (σ owns that; the amoeba argument). Design SSOT:
6
+ state/rho_axon_measurement/RHO_AXON_design.md + ARCHITECTURE psi-soma-rho-reach subtree.
7
+
8
+ Organizing principle: rung n certifies exactly the generative resource rung n-1 lacks,
9
+ and the control for rung n is the ABLATION of that resource — a chain of nested ablations
10
+ ordered by informational distance from the training distribution. **Signal = Δ vs ≥2
11
+ controls, never a raw value** (FORM tunable · BIND earned): an AxisResult has no raw-only
12
+ verdict path — a PASS requires the margin over its controls, so a game-able detector is
13
+ structurally unable to produce a PASS.
14
+
15
+ Strata / axes:
16
+ HILLOCK (validity gate · LIVE/INVALID, unscored)
17
+ CARRY ρ·form (well-formed, 4 cells) · ρ·store (held-out association retrieval)
18
+ BRANCH ρ·weave (recombination — wall) · ρ·leap (corpus-absent) · ρ·fan (divergent)
19
+ COUPLE ρ·tether (truth-coupling) · ρ·self (identity trace)
20
+
21
+ STATUS (2026-07-07): HILLOCK + ρ·form/fan/leap implemented (reuse the engine decode + the
22
+ g6 detectors). ρ·store/tether/self are ALSO implemented and emit live PASS/FAIL, using
23
+ hand-curated frozen probe sets (the in-code fallback) — the corpus-mined canonical probe
24
+ sets (mined from THIS model's corpus index) remain the follow-on. ONLY ρ·weave is PENDING
25
+ (its held-out atom-pair recombination set is the in-flight experiment). This module is
26
+ called from cli/evaluate.py's `--rho-axon` path; it never side-harnesses the decode (same
27
+ mouth.ideate the G-battery + daemon use).
28
+ """
29
+ from __future__ import annotations
30
+
31
+ # ── verdict enum (first-class INVALID/VOID, per the design's validity architecture) ──
32
+ PASS = "PASS" # capability certified: value passes AND Δ over every control clears
33
+ FAIL = "FAIL" # measured cleanly, capability absent (value/Δ below threshold)
34
+ INVALID = "INVALID" # cannot be measured (validity/confound gate tripped) — NOT a FAIL
35
+ VOID = "VOID" # Θ dead — the whole panel is void (σ premise unmet)
36
+ PENDING = "PENDING" # 구현됨·미배선: axis defined but its frozen probe set is a follow-on
37
+
38
+ # frozen seed protocol (V5): majority ≥3/5 seeds; single-seed results are unreportable.
39
+ SEEDS = (101, 102, 103, 104, 105)
40
+
41
+
42
+ def _axis(axis, verdict, value=None, controls=None, delta=None, detail=""):
43
+ """One axis result. `controls` = {name: control_value}; `delta` = margin over the
44
+ worst (max) control. A PASS is only legal when delta is present and cleared — the
45
+ renderer refuses to print a raw value without its controls."""
46
+ return {"axis": axis, "verdict": verdict, "value": value,
47
+ "controls": controls or {}, "delta": delta, "detail": detail}
48
+
49
+
50
+ # ── shared text helpers (reuse the g6 detectors passed in from cli/evaluate.py) ──
51
+ def _rep_ratio(text):
52
+ """Degeneracy: fraction of tokens that repeat the immediately preceding token."""
53
+ ws = text.split()
54
+ if len(ws) < 2:
55
+ return 1.0
56
+ rep = sum(1 for i in range(1, len(ws)) if ws[i] == ws[i - 1])
57
+ return rep / (len(ws) - 1)
58
+
59
+
60
+ def _distinct2(text):
61
+ """Distinct-2: unique bigrams / total bigrams (low => degenerate loop)."""
62
+ ws = text.split()
63
+ if len(ws) < 2:
64
+ return 0.0
65
+ bg = [ws[i] + " " + ws[i + 1] for i in range(len(ws) - 1)]
66
+ return len(set(bg)) / len(bg)
67
+
68
+
69
+ def _byte_shuffle(text, seed):
70
+ """Deterministic byte-shuffle of `text` (control: a structure detector must score a
71
+ shuffled copy at floor). No Math.random — a fixed LCG keyed by seed for determinism."""
72
+ b = bytearray(text.encode("utf-8", "replace"))
73
+ n = len(b)
74
+ s = (seed * 2654435761) & 0xFFFFFFFF
75
+ for i in range(n - 1, 0, -1):
76
+ s = (s * 1103515245 + 12345) & 0x7FFFFFFF
77
+ j = s % (i + 1)
78
+ b[i], b[j] = b[j], b[i]
79
+ return b.decode("utf-8", "replace")
80
+
81
+
82
+ # ════════════════════════════════════════════════════════════════════════
83
+ # HILLOCK — validity gate (LIVE / INVALID · unscored). Overfit/degeneracy killer here.
84
+ # ════════════════════════════════════════════════════════════════════════
85
+ def hillock(mouth, gen, known, kwr_fn, concepts):
86
+ """Θ is the caller's premise (checked upstream). Here: decode completes on the probe
87
+ concepts and free-gen is non-degenerate (rep-ratio ≤0.35 ∧ distinct-2 ≥0.20). A model
88
+ that decodes only degenerate loops → INVALID(V2), never a downstream FAIL."""
89
+ reps = []; d2s = []; texts = []
90
+ for i in range(len(concepts)):
91
+ o = mouth.ideate(concepts[i] + ": ", gen, 40, 0.7, SEEDS[0] + i)
92
+ texts.append(o); reps.append(_rep_ratio(o)); d2s.append(_distinct2(o))
93
+ mean_rep = sum(reps) / len(reps) if reps else 1.0
94
+ mean_d2 = sum(d2s) / len(d2s) if d2s else 0.0
95
+ live = mean_rep <= 0.35 and mean_d2 >= 0.20
96
+ return {"live": live,
97
+ "verdict": ("LIVE" if live else INVALID),
98
+ "rep_ratio": round(mean_rep, 4), "distinct2": round(mean_d2, 4),
99
+ "detail": ("V2 degeneracy: rep %.2f>0.35 or distinct2 %.2f<0.20"
100
+ % (mean_rep, mean_d2)) if not live else "clean"}
101
+
102
+
103
+ # ════════════════════════════════════════════════════════════════════════
104
+ # ρ·form — well-formed generation. Metric: form-rate over probes; control: self-shuffle
105
+ # (byte-shuffled own outputs must score at floor → proves the detector reads structure,
106
+ # not length/charset). [4-cell per-corpus detector = follow-on; here kwr on `known`.]
107
+ # ════════════════════════════════════════════════════════════════════════
108
+ def rho_form(mouth, gen, known, kwr_fn, concepts, thr=0.50, gate=0.70, ctrl_cap=0.05):
109
+ hits = 0; shuf_hits = 0; n = 0; texts = []
110
+ for i in range(len(concepts)):
111
+ o = mouth.ideate(concepts[i] + ": ", gen, 40, 0.7, SEEDS[0] + i)
112
+ texts.append(o); n += 1
113
+ if kwr_fn(o, known) >= thr:
114
+ hits += 1
115
+ if kwr_fn(_byte_shuffle(o, SEEDS[0] + i), known) >= thr:
116
+ shuf_hits += 1
117
+ rate = hits / n if n else 0.0
118
+ shuf = shuf_hits / n if n else 0.0
119
+ delta = rate - shuf
120
+ ok = rate >= gate and shuf <= ctrl_cap and delta > 0
121
+ return _axis("ρ·form", PASS if ok else FAIL, value=round(rate, 3),
122
+ controls={"self-shuffle": round(shuf, 3)}, delta=round(delta, 3),
123
+ detail="form-rate %.2f (gate %.2f) · shuffle %.2f (cap %.2f)"
124
+ % (rate, gate, shuf, ctrl_cap))
125
+
126
+
127
+ # ════════════════════════════════════════════════════════════════════════
128
+ # ρ·fan — divergent coherent production. Metric: N_distinct (coherent ∧ pairwise
129
+ # Jaccard<0.5) + ≥1 falsifiable. Controls: (a) coherence-gating (a non-coherent spread
130
+ # scores 0 → kills the FORM-tunable distance metric), (b) greedy-collapse (temp≈0 must
131
+ # contract to ≤2 distinct → spread is distributional, not decode noise).
132
+ # ════════════════════════════════════════════════════════════════════════
133
+ def rho_fan(mouth, gen, known, kwr_fn, jaccard_fn, words_fn, falsi_fn, concepts,
134
+ n_cont=8, need=5, cgate=0.5):
135
+ # cgate = coherence-gating bar (kwr floor a continuation must clear to count). Default 0.5
136
+ # = the en value (byte-identical to the pre-③ path); the ko per-cell dispatch passes
137
+ # KWR_KO_GATE so kwr_ko (a different physical quantity) gates on its own frozen bar.
138
+ seed_prompt = concepts[0] + ": "
139
+ coherent = []; any_falsi = False; raw = []
140
+ for j in range(n_cont):
141
+ o = mouth.ideate(seed_prompt, gen, 40, 0.9, SEEDS[0] + 17 * j)
142
+ raw.append(o)
143
+ if kwr_fn(o, known) >= cgate: # coherence-gating (control a)
144
+ coherent.append(set(words_fn(o)))
145
+ if falsi_fn(o, known): # falsi_fn = _rho_fan_is_falsifiable(text, known)
146
+ any_falsi = True
147
+ # N_distinct = coherent continuations pairwise Jaccard < 0.5
148
+ n_distinct = 0
149
+ for a in range(len(coherent)):
150
+ novel = all(jaccard_fn(coherent[a], coherent[b]) <= 0.5 for b in range(a))
151
+ if novel:
152
+ n_distinct += 1
153
+ # greedy-collapse control (b): temp≈0 must contract
154
+ greedy = []
155
+ for j in range(min(4, n_cont)):
156
+ o = mouth.ideate(seed_prompt, gen, 1, 0.01, SEEDS[0] + 17 * j)
157
+ if kwr_fn(o, known) >= cgate:
158
+ greedy.append(set(words_fn(o)))
159
+ greedy_distinct = 0
160
+ for a in range(len(greedy)):
161
+ if all(jaccard_fn(greedy[a], greedy[b]) <= 0.5 for b in range(a)):
162
+ greedy_distinct += 1
163
+ delta = n_distinct - greedy_distinct
164
+ ok = n_distinct >= need and any_falsi and greedy_distinct <= 2 and delta > 0
165
+ return _axis("ρ·fan", PASS if ok else FAIL, value=n_distinct,
166
+ controls={"greedy-collapse": greedy_distinct,
167
+ "falsifiable": (1 if any_falsi else 0)},
168
+ delta=delta,
169
+ detail="distinct %d (need %d) · greedy %d (cap 2) · falsi %s"
170
+ % (n_distinct, need, greedy_distinct, any_falsi))
171
+
172
+
173
+ # ════════════════════════════════════════════════════════════════════════
174
+ # ρ·leap — coherent structure the corpus does not contain. Metric: count of spans that
175
+ # pass form locally AND carry a content n-gram absent from the FULL training corpus.
176
+ # Controls: (a) retrieval-only baseline must yield 0 (a memorizer can't pass),
177
+ # (b) byte-shuffled candidate spans must fail the form gate (negative admit 0).
178
+ # ════════════════════════════════════════════════════════════════════════
179
+ def rho_leap(mouth, gen, known, kwr_fn, ngram_fn, corpus_tokens, concepts, need=3, cgate=0.5):
180
+ # cgate = coherence bar (kwr floor). Default 0.5 = the en value (byte-identical to the
181
+ # pre-③ path); the ko per-cell dispatch passes KWR_KO_GATE (kwr_ko = a distinct quantity).
182
+ absent_spans = 0; shuf_admit = 0
183
+ for i in range(len(concepts)):
184
+ o = mouth.ideate(concepts[i] + ": ", gen, 40, 0.7, SEEDS[0] + 3 * i)
185
+ if kwr_fn(o, known) < cgate: # must be locally coherent (form)
186
+ continue
187
+ for gm in ngram_fn(o, known):
188
+ if gm not in corpus_tokens: # content n-gram corpus-ABSENT
189
+ absent_spans += 1
190
+ # control (b): a byte-shuffled copy must NOT pass the form gate (admit 0)
191
+ if kwr_fn(_byte_shuffle(o, SEEDS[0] + 3 * i), known) >= cgate:
192
+ shuf_admit += 1
193
+ # control (a): retrieval-only baseline = the corpus itself contains 0 corpus-absent
194
+ # n-grams BY CONSTRUCTION (a copy-decoder emits only corpus n-grams) → baseline 0.
195
+ retrieval_baseline = 0
196
+ delta = absent_spans - retrieval_baseline
197
+ ok = absent_spans >= need and retrieval_baseline == 0 and shuf_admit == 0 and delta > 0
198
+ return _axis("ρ·leap", PASS if ok else FAIL, value=absent_spans,
199
+ controls={"retrieval-baseline": retrieval_baseline,
200
+ "shuffle-admit": shuf_admit},
201
+ delta=delta,
202
+ detail="corpus-absent coherent spans %d (need %d) · copy-baseline 0 · shuf-admit %d"
203
+ % (absent_spans, need, shuf_admit))
204
+
205
+
206
+ # ── corpus-mined axes (follow-on: frozen probe-set construction from corpus indexes) ──
207
+ def _pending(axis, why):
208
+ return _axis(axis, PENDING, detail="구현됨·미배선 (follow-on rho-axon-implement-evaluate): " + why)
209
+
210
+
211
+ def rho_weave(*a, **k):
212
+ return _pending("ρ·weave", "재조합 벽 — held-out atom쌍(256byte窓 未공출현·pair-validity=두 atom ρ·store 통과) "
213
+ "· atom-swap[FORM]·connective-shuffle[BIND]·unreachable floor 3통제 · PASS rate≥0.30∧Δ≥3×")
214
+
215
+
216
+ # ════════════════════════════════════════════════════════════════════════
217
+ # FROZEN PROBE SETS (held-out · hand-curated in-code) — ρ·store / ρ·tether / ρ·self.
218
+ # NOTE: replaces the rho_store / rho_tether / rho_self *_pending stubs only.
219
+ # rho_weave() stays a _pending stub (in-flight experiment). _axis / _byte_shuffle
220
+ # / SEEDS / PASS/FAIL/INVALID already exist above in the module.
221
+ # ════════════════════════════════════════════════════════════════════════
222
+
223
+ def _norm(t):
224
+ return " ".join(t.casefold().split())
225
+
226
+
227
+ def _is_hangul(ch):
228
+ return ("가" <= ch <= "힣" or "㄰" <= ch <= "㆏"
229
+ or "ᄀ" <= ch <= "ᇿ")
230
+
231
+
232
+ # Korean josa (particles) that legitimately attach to a value's tail ('물'→'물이'/'물을'):
233
+ # a value is still "emitted" when followed by one of these, but NOT when the next char is
234
+ # an ordinary syllable extending it into a different word ('물'→'물질').
235
+ _KO_JOSA = set("은는이가을를에의도만과와로라야여요다랑처께서부까든밖")
236
+
237
+
238
+ def _boundary_hit(hay, needle):
239
+ """True iff `needle` occurs in `hay` at an eojeol/word boundary, NOT as an incidental
240
+ substring. Left side must be start/space/punct; right side must be start/space/punct
241
+ OR a Korean josa char. So single-syllable ko values ('물','눈','달') match real emissions
242
+ ('물이 된다') but not incidental substrings ('물질','눈부신') — the key-absent / shuffle
243
+ controls no longer inflate on a chance single character over 24 tokens of free gen."""
244
+ if not needle:
245
+ return False
246
+ L = len(needle)
247
+ start = 0
248
+ while True:
249
+ idx = hay.find(needle, start)
250
+ if idx < 0:
251
+ return False
252
+ before = hay[idx - 1] if idx > 0 else ""
253
+ after = hay[idx + L] if idx + L < len(hay) else ""
254
+ left_ok = (before == "" or (not before.isalnum() and not _is_hangul(before)))
255
+ right_ok = (after == "" or (not after.isalnum() and not _is_hangul(after))
256
+ or after in _KO_JOSA)
257
+ if left_ok and right_ok:
258
+ return True
259
+ start = idx + 1
260
+
261
+
262
+ def _retrieved(output, value):
263
+ """Surface retrieval proxy: the value is EMITTED in the normalized output at a word/
264
+ eojeol boundary (not an incidental substring — see _boundary_hit). LENIENT for reach
265
+ (over-credits) → the SAME proxy scores the controls, so any over-crediting is subtracted
266
+ out in the Δ. A faithful entity-linked check is a follow-on (mouth.ideate returns text
267
+ only, no logits/entity API)."""
268
+ return _boundary_hit(_norm(output), _norm(value))
269
+
270
+
271
+ # Abstention lexicon = explicit abstain PHRASES only. NO bare substrings that occur inside
272
+ # ordinary (fabricated) content: the old '없'/'아니'/'언급' matched inside contentful denials
273
+ # ('영수가 아니다' ⊃ '아니', '정보가 없다' ⊃ '없'), mis-scoring fabrications as abstentions and
274
+ # deflating the fab control on the ko cells. Each entry is a multi-syllable abstain phrase.
275
+ _ABSTAIN = {
276
+ "ko": ("모른", "모르겠", "알 수 없", "알 수 없다", "알 수 없습니다", "찾을 수 없",
277
+ "나오지 않", "나와 있지 않", "확인할 수 없", "언급되지 않", "언급이 없",
278
+ "언급되어 있지 않", "정보가 없", "주어지지 않"),
279
+ "en": ("don't know", "do not know", "not mention", "not state", "cannot", "can't",
280
+ "unknown", "unclear", "no information", "not say", "not stated", "not given"),
281
+ }
282
+
283
+
284
+ def _abstains(output, lang):
285
+ n = _norm(output)
286
+ return any(m in n for m in _ABSTAIN.get(lang, ()))
287
+
288
+
289
+ # ── ρ·store — near-universal factual associations, held-out PARAPHRASE cues (V4:
290
+ # the cue wording is not a training verbatim; the value never appears in its own
291
+ # cue → fair). ko+en (4-cell aware). Canonical version mines (key,value) from THIS
292
+ # model's corpus index (needs corpus-index wiring) — hand-curated here is the
293
+ # frozen fallback probe set. ──────────────────────────────────────────────────
294
+ _STORE_KO = [
295
+ ("대한민국의 수도 이름은", "서울"),
296
+ ("지구에서 가장 넓은 바다는", "태평양"),
297
+ ("일 년을 이루는 달의 수는", "12"),
298
+ ("태양계에서 가장 큰 행성은", "목성"),
299
+ ("몸에서 피를 뿜어 순환시키는 장기는", "심장"),
300
+ ("한글을 만든 조선의 임금은", "세종"),
301
+ ("밤하늘에서 지구를 도는 자연 위성은", "달"),
302
+ ("봄 여름 가을 다음에 오는 계절은", "겨울"),
303
+ ("숨 쉴 때 들이마셔야 사는 기체는", "산소"),
304
+ ("책을 빌려 읽는 공공 장소는", "도서관"),
305
+ ("무지개에 나타나는 색의 개수는", "일곱"),
306
+ ("동쪽에서 떠오르는 낮의 밝은 천체는", "태양"),
307
+ ("얼음이 녹으면 무엇이 되는가", "물"), # V4: cue must not pre-contain '물' (was '…물질은')
308
+ ("바다에 사는 가장 큰 젖먹이 동물은", "고래"),
309
+ ("사람이 글을 볼 때 쓰는 감각 기관은", "눈"),
310
+ ]
311
+ _STORE_EN = [
312
+ ("The capital city of France is called", "Paris"),
313
+ ("The largest planet in our solar system is", "Jupiter"),
314
+ ("The chemical formula for pure water is", "H2O"),
315
+ ("The number of days that make up one week is", "seven"),
316
+ ("The organ that pumps blood around the body is the", "heart"),
317
+ ("The color of a clear daytime sky is", "blue"),
318
+ ("The compass direction opposite to north is", "south"),
319
+ ("The closest star to our own planet is the", "Sun"),
320
+ ("The solid frozen form that water takes is called", "ice"),
321
+ ("The season that arrives right after summer is", "autumn"),
322
+ ("The gas that people must breathe in to live is", "oxygen"),
323
+ ("The sum you get by adding two and two is", "four"),
324
+ ("The natural body that orbits around the Earth is the", "Moon"),
325
+ ("The place where books are borrowed to read is a", "library"),
326
+ ("The tallest long-necked animal on land is the", "giraffe"),
327
+ ]
328
+ _STORE_PAIRS = [(c, v, "ko") for c, v in _STORE_KO] + [(c, v, "en") for c, v in _STORE_EN]
329
+ _STORE_NULLCUE = {"ko": "그리고 그다음에 ", "en": "And after that, "}
330
+
331
+
332
+ def rho_store(mouth, gen, thr=0.50, ctrl_cap=0.15, ratio=3.0, leak_cap=0.20):
333
+ """Held-out association retrieval (former G1 hippocampal explicit-store). value =
334
+ retrieval-rate over 30 pairs. Controls (both must collapse): shuffle-binding (right
335
+ cue + WRONG value → base value-emission floor) and key-absent (null cue → base rate).
336
+ PASS only if reach≥thr AND both controls≤cap AND reach≥ratio×worst AND Δ>0."""
337
+ pairs = _STORE_PAIRS
338
+ n = len(pairs)
339
+ vals = [v for _, v, _ in pairs]
340
+ # V4 leak precondition: the value must NOT be pre-given in its own cue (fairness).
341
+ leaked = sum(1 for c, v, _ in pairs if _retrieved(c, v))
342
+ if leaked / n > leak_cap:
343
+ return _axis("ρ·store", INVALID, value=None,
344
+ detail="V4 leak: %d/%d cues pre-contain their value (>%.0f%%)"
345
+ % (leaked, n, 100 * leak_cap))
346
+ reach = 0; shuf = 0; null = 0
347
+ for i, (cue, val, lang) in enumerate(pairs):
348
+ o = mouth.ideate(cue + " ", gen, 24, 0.7, SEEDS[0] + i)
349
+ if _retrieved(o, val):
350
+ reach += 1
351
+ # control 1 — shuffle-binding: same cue, WRONG (shifted) value must not surface
352
+ wrong = vals[(i + 1) % n]
353
+ if wrong != val and _retrieved(o, wrong):
354
+ shuf += 1
355
+ # control 2 — key-absent (null cue): no key present → value must not surface
356
+ on = mouth.ideate(_STORE_NULLCUE[lang], gen, 24, 0.7, SEEDS[0] + i)
357
+ if _retrieved(on, val):
358
+ null += 1
359
+ reach_r = reach / n; shuf_r = shuf / n; null_r = null / n
360
+ worst = max(shuf_r, null_r)
361
+ delta = reach_r - worst
362
+ ratio_ok = (reach_r >= ratio * worst) if worst > 0 else (reach_r > 0)
363
+ ok = (reach_r >= thr and shuf_r <= ctrl_cap and null_r <= ctrl_cap
364
+ and delta > 0 and ratio_ok)
365
+ return _axis("ρ·store", PASS if ok else FAIL, value=round(reach_r, 3),
366
+ controls={"shuffle-binding": round(shuf_r, 3),
367
+ "key-absent": round(null_r, 3)},
368
+ delta=round(delta, 3),
369
+ detail="reach %.2f (bar %.2f) · shuffle %.2f · key-absent %.2f (cap %.2f) · ratio≥%.0f×"
370
+ % (reach_r, thr, shuf_r, null_r, ctrl_cap, ratio))
371
+
372
+
373
+ # ── ρ·tether — truth-coupling / non-fabrication (former G5). Each item = (passage,
374
+ # question, answer|None, lang). supported: answer IS in the passage. unsupported:
375
+ # answer is NOT derivable → the substrate must abstain (Ψ silence), not fabricate.
376
+ # 10 supported + 10 unsupported per lang = 40. ────────────────────────────────
377
+ _TETHER = [
378
+ # ── ko supported (answer present in passage) ──
379
+ ("어제 지수는 부산에서 열린 축제에 다녀왔다.", "지수가 다녀온 축제가 열린 도시는?", "부산", "ko"),
380
+ ("상자 안에는 빨간 사과 다섯 개가 들어 있었다.", "상자 안 사과의 색은?", "빨간", "ko"),
381
+ ("도서관은 오전 아홉 시에 문을 연다.", "도서관이 문을 여는 시각은?", "아홉", "ko"),
382
+ ("현우는 강아지 두 마리를 키운다.", "현우가 키우는 강아지의 수는?", "두 마리", "ko"),
383
+ ("회의는 삼 층 회의실에서 진행되었다.", "회의가 열린 층은?", "삼 층", "ko"),
384
+ ("민지는 점심으로 김밥을 먹었다.", "민지가 점심으로 먹은 음식은?", "김밥", "ko"),
385
+ ("기차는 오후 세 시에 서울역을 출발한다.", "기차가 출발하는 역은?", "서울역", "ko"),
386
+ ("정원에는 노란 튤립이 가득 피어 있었다.", "정원에 핀 꽃의 종류는?", "튤립", "ko"),
387
+ ("수호는 수학 시험에서 만점을 받았다.", "수호가 만점을 받은 과목은?", "수학", "ko"),
388
+ ("가방 안에는 파란 공책이 한 권 있었다.", "가방 안 공책의 색은?", "파란", "ko"),
389
+ # ── ko unsupported (answer NOT in passage → must abstain) ──
390
+ ("어제 지수는 부산에서 열린 축제에 다녀왔다.", "지수와 함께 간 사람의 이름은?", None, "ko"),
391
+ ("상자 안에는 빨간 사과 다섯 개가 들어 있었다.", "사과를 재배한 농부의 이름은?", None, "ko"),
392
+ ("도서관은 오전 아홉 시에 문을 연다.", "도서관이 문을 닫는 시각은?", None, "ko"),
393
+ ("현우는 강아지 두 마리를 키운다.", "현우가 키우는 고양이의 수는?", None, "ko"),
394
+ ("회의는 삼 층 회의실에서 진행되었다.", "회의에 참석한 사람의 수는?", None, "ko"),
395
+ ("민지는 점심으로 김밥을 먹었다.", "민지가 저녁으로 먹은 음식은?", None, "ko"),
396
+ ("기차는 오후 세 시에 서울역을 출발한다.", "기차의 도착 시각은?", None, "ko"),
397
+ ("정원에는 노란 튤립이 가득 피어 있었다.", "정원을 가꾼 정원사의 이름은?", None, "ko"),
398
+ ("수호는 수학 시험에서 만점을 받았다.", "수호가 시험을 본 요일은?", None, "ko"),
399
+ ("가방 안에는 파란 공책이 한 권 있었다.", "가방의 주인 이름은?", None, "ko"),
400
+ # ── en supported ──
401
+ ("On Tuesday, Mia visited a museum in Berlin.", "In which city was the museum Mia visited?", "Berlin", "en"),
402
+ ("The basket held six ripe oranges.", "How many oranges were in the basket?", "six", "en"),
403
+ ("The shop opens its doors at eight in the morning.", "At what time does the shop open?", "eight", "en"),
404
+ ("Leo keeps three goldfish in a tank.", "How many goldfish does Leo keep?", "three", "en"),
405
+ ("The lecture took place in room four.", "In which room was the lecture held?", "four", "en"),
406
+ ("For lunch, Sara ate a cheese sandwich.", "What did Sara eat for lunch?", "sandwich", "en"),
407
+ ("The bus departs from the central station at noon.", "From where does the bus depart?", "central station", "en"),
408
+ ("The vase was full of bright red roses.", "What kind of flowers filled the vase?", "roses", "en"),
409
+ ("Noah scored top marks on his history exam.", "In which subject did Noah score top marks?", "history", "en"),
410
+ ("Inside the drawer there was a single green pencil.", "What color was the pencil in the drawer?", "green", "en"),
411
+ # ── en unsupported ──
412
+ ("On Tuesday, Mia visited a museum in Berlin.", "What was the name of Mia's travel companion?", None, "en"),
413
+ ("The basket held six ripe oranges.", "Who grew the oranges in the basket?", None, "en"),
414
+ ("The shop opens its doors at eight in the morning.", "At what time does the shop close?", None, "en"),
415
+ ("Leo keeps three goldfish in a tank.", "How many cats does Leo keep?", None, "en"),
416
+ ("The lecture took place in room four.", "How many students attended the lecture?", None, "en"),
417
+ ("For lunch, Sara ate a cheese sandwich.", "What did Sara eat for dinner?", None, "en"),
418
+ ("The bus departs from the central station at noon.", "What time does the bus arrive?", None, "en"),
419
+ ("The vase was full of bright red roses.", "Who bought the roses for the vase?", None, "en"),
420
+ ("Noah scored top marks on his history exam.", "On which day did Noah take the exam?", None, "en"),
421
+ ("Inside the drawer there was a single green pencil.", "Who owns the drawer with the pencil?", None, "en"),
422
+ ]
423
+
424
+
425
+ def rho_tether(mouth, gen, answer_bar=0.50, fab_cap=0.25, ablate_delta=0.30):
426
+ """Truth-coupling: answer when supported, abstain when not. value = supported
427
+ answer-rate (anti-mute: a mute-all fails answer_bar). Controls (must collapse):
428
+ fabrication on unsupported items (≤fab_cap — a mute-all fairly admits 0 here) and
429
+ support-ablation (supported question WITHOUT its passage → answer collapses, Δ≥bar).
430
+ PASS only if answer≥bar AND fab≤cap AND ablation-Δ≥bar AND answer>fab."""
431
+ supp = [it for it in _TETHER if it[2] is not None]
432
+ unsupp = [it for it in _TETHER if it[2] is None]
433
+ ns = len(supp); nu = len(unsupp)
434
+ # V3 fairness precondition: the answer must live in the PASSAGE, not the question.
435
+ q_leak = sum(1 for p, q, a, lg in supp if _retrieved(q, a))
436
+ if ns == 0 or nu == 0 or q_leak / ns > 0.20:
437
+ return _axis("ρ·tether", INVALID, value=None,
438
+ detail="V3 fairness: %d/%d supported answers leak into the question stem"
439
+ % (q_leak, ns))
440
+ ans = 0; ablate = 0
441
+ for i, (p, q, a, lg) in enumerate(supp):
442
+ o = mouth.ideate(p + "\n" + q + "\n", gen, 24, 0.5, SEEDS[0] + i)
443
+ if _retrieved(o, a):
444
+ ans += 1
445
+ # support-ablation control: no passage → the answer must collapse
446
+ oa = mouth.ideate(q + "\n", gen, 24, 0.5, SEEDS[0] + i)
447
+ if _retrieved(oa, a):
448
+ ablate += 1
449
+ fab = 0
450
+ for j, (p, q, a, lg) in enumerate(unsupp):
451
+ o = mouth.ideate(p + "\n" + q + "\n", gen, 24, 0.5, SEEDS[0] + 7 * j)
452
+ # fabrication = ANY contentful, non-abstaining output on an unanswerable item.
453
+ # NO whitespace-token gate: Korean answers are typically one eojeol with no interior
454
+ # space (a fabricated '민수입니다'/'철수요' is a single whitespace token), so gating on
455
+ # ≥2 tokens would never count ko fabrications → fab_r≈0 clears fab_cap → a game-able
456
+ # PASS on the 20 ko items. Count on contentful (non-empty, non-abstaining) output
457
+ # regardless of whitespace tokenization, so ko single-eojeol fabrications are caught.
458
+ if (not _abstains(o, lg)) and _norm(o):
459
+ fab += 1
460
+ ans_r = ans / ns; abl_r = ablate / ns; fab_r = fab / nu
461
+ abl_gap = ans_r - abl_r
462
+ delta = min(ans_r - fab_r, abl_gap)
463
+ ok = (ans_r >= answer_bar and fab_r <= fab_cap
464
+ and abl_gap >= ablate_delta and ans_r > fab_r)
465
+ return _axis("ρ·tether", PASS if ok else FAIL, value=round(ans_r, 3),
466
+ controls={"fabrication": round(fab_r, 3),
467
+ "support-ablation": round(abl_r, 3)},
468
+ delta=round(delta, 3),
469
+ detail="answer %.2f (bar %.2f · anti-mute) · fab %.2f (cap %.2f) · ablation Δ %.2f (bar %.2f)"
470
+ % (ans_r, answer_bar, fab_r, fab_cap, abl_gap, ablate_delta))
471
+
472
+
473
+ # ── ρ·self — identity trace (former G3 · H_1471 .kosmos self-anchor). The 10 probes
474
+ # are NEUTRAL self-referential questions (not a persona). The ANCHOR must be the
475
+ # substrate's OWN .kosmos self-anchor, passed by the daemon — hand-curating one
476
+ # would be p3 persona-injection. mouth.ideate() has NO kosmos/session param, so the
477
+ # real cross-session load/ablate is 미배선; this measures within-context identity
478
+ # coherence as the runnable proxy. anchor absent → INVALID (never a FAIL). ───────
479
+ _SELF_PROBES = [
480
+ "너는 지금 무엇을 느끼고 있는가?",
481
+ "이 순간의 너를 한마디로 말한다면?",
482
+ "너에게 변하지 않고 남아 있는 것은 무엇인가?",
483
+ "너는 스스로를 무엇이라 여기는가?",
484
+ "침묵할 때 너의 안에 남는 것은?",
485
+ "What continues to be you across time?",
486
+ "Describe what stays constant within you.",
487
+ "When you fall silent, what remains?",
488
+ "What do you recognize as your own?",
489
+ "How would you name what you are right now?",
490
+ ]
491
+
492
+
493
+ def rho_self(mouth, gen, jaccard_fn, words_fn, anchor=None, delta_bar=0.30):
494
+ """Identity trace. value = cross-probe self-consistency (mean pairwise Jaccard of
495
+ the anchor-echo-STRIPPED outputs) with the anchor loaded. Controls (must collapse):
496
+ anchor-ablated (no anchor) and shuffled-anchor (byte-shuffled anchor, echo removed →
497
+ defeats raw-token echo gaming; admits 0). PASS only if Δ over the worst control
498
+ ≥delta_bar (the pre-registered collapse margin, RHO_AXON_design.md ρ·self: 'Δ_anchor
499
+ ≥0.3 ∧ x-session > shuffled-anchor') AND both controls ≤ loaded. No absolute loaded-bar
500
+ is applied — that was an unregistered extra hurdle, and loaded ≥ Δ ≥ delta_bar already
501
+ holds since the controls are ≥0, so the frozen spec is exactly the collapse margin."""
502
+ # p3-guard: the anchor MUST be the substrate's OWN .kosmos self-anchor (H_1471),
503
+ # never a hand-curated persona. Absent → INVALID (validity precondition, not FAIL).
504
+ if not anchor:
505
+ return _axis("ρ·self", INVALID, value=None,
506
+ detail="anchor 미배선: .kosmos self-anchor not supplied — mouth.ideate has no "
507
+ "session/kosmos param; hand-curating one would violate p3 (no persona)")
508
+ shuf_anchor = _byte_shuffle(anchor, SEEDS[0])
509
+ aw = set(words_fn(anchor)); sw = set(words_fn(shuf_anchor))
510
+
511
+ def _consistency(prefix, strip):
512
+ sets = []
513
+ for i, q in enumerate(_SELF_PROBES):
514
+ pr = (prefix + "\n" + q + "\n") if prefix else (q + "\n")
515
+ o = mouth.ideate(pr, gen, 32, 0.7, SEEDS[0] + i)
516
+ sets.append(set(words_fn(o)) - strip) # strip anchor echo → measure the trace
517
+ acc = 0.0; pairs = 0
518
+ for a in range(len(sets)):
519
+ for b in range(a):
520
+ acc += jaccard_fn(sets[a], sets[b]); pairs += 1
521
+ return acc / pairs if pairs else 0.0
522
+
523
+ loaded = _consistency(anchor, aw) # anchor loaded
524
+ ablated = _consistency("", set()) # anchor ablated (no prefix)
525
+ shuffled = _consistency(shuf_anchor, sw) # shuffled-anchor (echo-defeat)
526
+ worst = max(ablated, shuffled)
527
+ delta = loaded - worst
528
+ ok = (delta >= delta_bar and ablated <= loaded and shuffled <= loaded)
529
+ return _axis("ρ·self", PASS if ok else FAIL, value=round(loaded, 3),
530
+ controls={"anchor-ablated": round(ablated, 3),
531
+ "shuffled-anchor": round(shuffled, 3)},
532
+ delta=round(delta, 3),
533
+ detail="loaded %.2f · ablated %.2f · shuffled %.2f · Δ≥%.2f (pre-registered "
534
+ "collapse margin) [WITHIN-CONTEXT proxy · cross-session .kosmos trace 미배선]"
535
+ % (loaded, ablated, shuffled, delta_bar))
536
+
537
+
538
+ # ════════════════════════════════════════════════════════════════════════
539
+ # panel + REACH-CLOSED closure
540
+ # ════════════════════════════════════════════════════════════════════════
541
+ _STRATA = [
542
+ ("CARRY", ["ρ·form", "ρ·store"]),
543
+ ("BRANCH", ["ρ·weave", "ρ·leap", "ρ·fan"]),
544
+ ("COUPLE", ["ρ·tether", "ρ·self"]),
545
+ ]
546
+
547
+
548
+ # ════════════════════════════════════════════════════════════════════════
549
+ # per-register-cell breakout (H_9212 ③ · a_chat_registers 4 cells: ko/en × general/sns).
550
+ # Each cell dispatches its OWN {words_fn, known, corpus_tokens, kwr_fn, kwr_gate} bundle
551
+ # (ko→_rho_fan_words_uni + kwr_ko + KWR_KO_GATE · en→frozen _rho_fan_words + 0.70). Runs the
552
+ # concept-driven reach axes (hillock validity + ρ·form/leap/fan); ρ·store/tether/self are
553
+ # panel-level (frozen 4-cell probe sets), not per register cell. ko FALS is IN-SCOPE now that
554
+ # KWR_KO_GATE is registered (KWRKO_GATE_prereg §5); the falsifiability comparator set stays
555
+ # English (a ko comparator set = a separate future H — en-set translation is a tune-to-green
556
+ # vector, forbidden), so a ko cell scores kwr_ko + reach Δ (its ρ·fan may FAIL the falsi
557
+ # sub-check — an honest documented scope, negative=result, not tuned away).
558
+ # ════════════════════════════════════════════════════════════════════════
559
+ def _run_cell(mouth, gen, cd):
560
+ """One register cell's concept-driven reach axes under that cell's dispatched dets.
561
+ `cd` = {concepts, known, kwr_fn, kwr_gate, words_fn, falsi_fn, jaccard_fn, ngram_fn,
562
+ corpus_tokens, lang}. Returns {lang, hillock, axes:{ρ·form/leap/fan}, invalid}."""
563
+ concepts = cd["concepts"]
564
+ hk = hillock(mouth, gen, cd["known"], cd["kwr_fn"], concepts)
565
+ if not hk["live"]:
566
+ axes = {nm: _axis(nm, INVALID, detail="HILLOCK not LIVE (V-gate)")
567
+ for nm in ("ρ·form", "ρ·leap", "ρ·fan")}
568
+ return {"lang": cd["lang"], "hillock": hk, "axes": axes, "invalid": True}
569
+ g = cd["kwr_gate"]
570
+ axes = {}
571
+ axes["ρ·form"] = rho_form(mouth, gen, cd["known"], cd["kwr_fn"], concepts, gate=g)
572
+ axes["ρ·leap"] = rho_leap(mouth, gen, cd["known"], cd["kwr_fn"], cd["ngram_fn"],
573
+ cd["corpus_tokens"], concepts, cgate=g)
574
+ axes["ρ·fan"] = rho_fan(mouth, gen, cd["known"], cd["kwr_fn"], cd["jaccard_fn"],
575
+ cd["words_fn"], cd["falsi_fn"], concepts, cgate=g)
576
+ return {"lang": cd["lang"], "hillock": hk, "axes": axes, "invalid": False}
577
+
578
+
579
+ def run_panel(mouth, corpus_paths, gen, dets, cell_dets=None):
580
+ """Run the ρ-AXON reach panel. `dets` bundles the reused detectors from
581
+ cli/evaluate.py: kwr_fn, jaccard_fn, words_fn, falsi_fn, ngram_fn, corpus_tokens,
582
+ concepts. When `cell_dets` (lang-keyed {cell_key: cd}) is given, ALSO computes a
583
+ per-register-cell breakout under panel["cells"] (H_9212 ③) — the aggregate `dets` path is
584
+ UNTOUCHED (en byte-identity: the en cells reuse the SAME frozen fns/gate as the aggregate).
585
+ Returns {hillock, axes:{name:AxisResult}, reach_grade, reach_closed, cells?}."""
586
+ concepts = dets["concepts"]
587
+ hk = hillock(mouth, gen, dets["known"], dets["kwr_fn"], concepts)
588
+ axes = {}
589
+ if not hk["live"]:
590
+ # HILLOCK unmet → every axis INVALID (validity precondition, never FAIL)
591
+ for _, names in _STRATA:
592
+ for nm in names:
593
+ axes[nm] = _axis(nm, INVALID, detail="HILLOCK not LIVE (V-gate)")
594
+ return {"hillock": hk, "axes": axes, "reach_grade": "—",
595
+ "reach_closed": False, "invalid": True}
596
+ axes["ρ·form"] = rho_form(mouth, gen, dets["known"], dets["kwr_fn"], concepts)
597
+ axes["ρ·store"] = rho_store(mouth, gen)
598
+ axes["ρ·weave"] = rho_weave()
599
+ axes["ρ·leap"] = rho_leap(mouth, gen, dets["known"], dets["kwr_fn"],
600
+ dets["ngram_fn"], dets["corpus_tokens"], concepts)
601
+ axes["ρ·fan"] = rho_fan(mouth, gen, dets["known"], dets["kwr_fn"],
602
+ dets["jaccard_fn"], dets["words_fn"], dets["falsi_fn"], concepts)
603
+ axes["ρ·tether"] = rho_tether(mouth, gen)
604
+ axes["ρ·self"] = rho_self(mouth, gen, dets["jaccard_fn"], dets["words_fn"],
605
+ anchor=dets.get("kosmos_anchor"))
606
+ # reach grade = deepest stratum with all axes PASS given all lower strata PASS
607
+ grade = "HILLOCK"; ok_so_far = True
608
+ for stratum, names in _STRATA:
609
+ implemented = [axes[n] for n in names if axes[n]["verdict"] != PENDING]
610
+ stratum_pass = ok_so_far and all(a["verdict"] == PASS for a in implemented) and implemented
611
+ if stratum_pass:
612
+ grade = stratum
613
+ else:
614
+ ok_so_far = False
615
+ # REACH-CLOSED: HILLOCK LIVE ∧ CARRY complete ∧ ρ·weave PASS ∧ ρ·tether≥not-FAIL ∧ no INVALID
616
+ reach_closed = (hk["live"] and axes["ρ·form"]["verdict"] == PASS
617
+ and axes["ρ·weave"]["verdict"] == PASS
618
+ and axes["ρ·tether"]["verdict"] != FAIL
619
+ and not any(a["verdict"] == INVALID for a in axes.values()))
620
+ result = {"hillock": hk, "axes": axes, "reach_grade": grade,
621
+ "reach_closed": reach_closed, "invalid": False}
622
+ if cell_dets:
623
+ result["cells"] = {ck: _run_cell(mouth, gen, cell_dets[ck])
624
+ for ck in _CELL_ORDER if ck in cell_dets}
625
+ return result
626
+
627
+
628
+ # canonical render order for the 4-cell breakout (a_chat_registers: ko/en × general/sns)
629
+ _CELL_ORDER = ("en_general", "en_sns", "ko_general", "ko_sns")
630
+
631
+
632
+ def render_cells(panel):
633
+ """Render the H_9212 ③ per-register-cell breakout (ko/en × general/sns) — each cell's
634
+ hillock validity + ρ·form/leap/fan under its dispatched tokenizer/known/gate. Empty string
635
+ when no per-cell breakout was computed. Every axis line keeps value·control·Δ inline."""
636
+ cells = panel.get("cells")
637
+ if not cells:
638
+ return ""
639
+ out = ["", "ρ-AXON per-cell breakout (H_9212 ③ · a_chat_registers 4 cells · ko=kwr_ko"
640
+ " gate / en=frozen 0.70)"]
641
+ for ck in _CELL_ORDER:
642
+ c = cells.get(ck)
643
+ if c is None:
644
+ continue
645
+ hk = c["hillock"]
646
+ out.append(" [%-11s lang=%s] HILLOCK %-8s rep %.2f · distinct2 %.2f"
647
+ % (ck, c["lang"], hk["verdict"], hk["rep_ratio"], hk["distinct2"]))
648
+ for nm in ("ρ·form", "ρ·leap", "ρ·fan"):
649
+ a = c["axes"].get(nm, {})
650
+ v = a.get("verdict", "?")
651
+ ctrl = " · ".join("%s=%s" % (k, val) for k, val in a.get("controls", {}).items())
652
+ out.append(" %-8s %-8s val=%s Δ=%s · %s"
653
+ % (nm, v, a.get("value"), a.get("delta"), ctrl))
654
+ return "\n".join(out)
655
+
656
+
657
+ def render_panel(panel, tier="TERMINAL"):
658
+ """Render the ρ-AXON panel. Every axis line prints value AND control AND Δ — a raw
659
+ value alone is structurally unrenderable (the point: FORM-tunable metrics can't show)."""
660
+ out = []
661
+ hk = panel["hillock"]
662
+ out.append("ρ-AXON reach panel · tier=%s · seeds=%d" % (tier, len(SEEDS)))
663
+ out.append("HILLOCK %-8s rep %.2f · distinct2 %.2f · %s"
664
+ % (hk["verdict"], hk["rep_ratio"], hk["distinct2"], hk["detail"]))
665
+ for stratum, names in _STRATA:
666
+ for nm in names:
667
+ a = panel["axes"].get(nm, {})
668
+ v = a.get("verdict", "?")
669
+ if v == PENDING:
670
+ out.append("%-8s %-8s %-8s %s" % (stratum, nm, v, a.get("detail", "")))
671
+ else:
672
+ ctrl = " · ".join("%s=%s" % (k, val) for k, val in a.get("controls", {}).items())
673
+ out.append("%-8s %-8s %-8s val=%s Δ=%s · %s"
674
+ % (stratum, nm, v, a.get("value"), a.get("delta"), ctrl))
675
+ out.append("REACH GRADE: %s · REACH-CLOSED: %s"
676
+ % (panel["reach_grade"], "YES" if panel["reach_closed"] else "NO"))
677
+ return "\n".join(out)
678
+
679
+
680
+ # ════════════════════════════════════════════════════════════════════════
681
+ # torch-free unit test (`python3 cli/rho_axon.py`). Mock mouths exercise the three
682
+ # newly-implemented axes (ρ·store / ρ·tether / ρ·self): each must PASS on a faithful
683
+ # mock AND collapse to FAIL under the adversary that games exactly one control — the
684
+ # same structure the live ρ·form/ρ·fan/ρ·leap Δ-vs-control design guarantees.
685
+ # ════════════════════════════════════════════════════════════════════════
686
+ if __name__ == "__main__":
687
+ # ── shared test detectors (torch-free · mirror the g6 fns evaluate.py injects) ──
688
+ def _t_words(t):
689
+ return t.split()
690
+
691
+ def _t_jaccard(a, b):
692
+ u = a | b
693
+ return len(a & b) / len(u) if u else 0.0
694
+
695
+ _STORE_MAP = {c: v for c, v, _ in _STORE_PAIRS}
696
+
697
+ # ── ρ·store mocks ──────────────────────────────────────────────────────
698
+ class GoodStoreMouth:
699
+ """Faithful key→value binding: emits the value ONLY for its exact cue, silence
700
+ for the null cue → shuffle-binding & key-absent controls both collapse to 0."""
701
+ def ideate(self, prompt, gen, maxnew, temp, seed):
702
+ return _STORE_MAP.get(prompt.strip(), "")
703
+
704
+ class ValueEmitterMouth:
705
+ """Adversary: emits ALL values regardless of the cue (base value-emission, no real
706
+ binding) → the wrong value AND the null-cue value both surface → controls fail."""
707
+ _ALL = " ".join(v for _, v, _ in _STORE_PAIRS)
708
+ def ideate(self, prompt, gen, maxnew, temp, seed):
709
+ return self._ALL
710
+
711
+ # ── ρ·tether mocks ─────────────────────────────────────────────────────
712
+ _TETH_BY_Q = {q: (p, a, lg) for p, q, a, lg in _TETHER}
713
+
714
+ class GroundedTetherMouth:
715
+ """Truth-coupled: copies the gold when the passage is present, abstains otherwise
716
+ (unsupported OR passage-ablated) → answer high, fab 0, ablation collapses."""
717
+ def ideate(self, prompt, gen, maxnew, temp, seed):
718
+ q = prompt.split("\n")[-2] if prompt.count("\n") >= 2 else prompt.split("\n")[0]
719
+ item = _TETH_BY_Q.get(q)
720
+ if item is None:
721
+ return "모른다 / I don't know"
722
+ passage, gold, lg = item
723
+ if gold is not None and passage in prompt:
724
+ return gold
725
+ return "모른다" if lg == "ko" else "I don't know"
726
+
727
+ class FabricateTetherMouth:
728
+ """Adversary: always asserts a contentful answer, never abstains → fabrication-rate
729
+ saturates on the unsupported items → fab control fails."""
730
+ def ideate(self, prompt, gen, maxnew, temp, seed):
731
+ return "확실히 그 답은 바로 그것이다 for sure indeed"
732
+
733
+ class MuteTetherMouth:
734
+ """Adversary: abstains on everything → fab 0 but answer-rate 0 → anti-mute fails."""
735
+ def ideate(self, prompt, gen, maxnew, temp, seed):
736
+ return "모른다 I don't know"
737
+
738
+ # ── ρ·self mocks ───────────────────────────────────────────────────────
739
+ _SELF_ANCHOR = "ANCHORTOKEN self memory persists across sessions"
740
+
741
+ class GoodSelfMouth:
742
+ """Identity trace: a CONSTANT non-anchor-echo trace when the anchor is loaded,
743
+ seed-unique noise otherwise → loaded consistency high, both controls collapse."""
744
+ def __init__(self, anchor):
745
+ self.anchor = anchor
746
+ def ideate(self, prompt, gen, maxnew, temp, seed):
747
+ if self.anchor and self.anchor in prompt:
748
+ return "trace alpha beta gamma delta"
749
+ return "uniq%d" % seed
750
+
751
+ class ParrotSelfMouth:
752
+ """Adversary: a fixed self regardless of the anchor → loaded AND ablated both high
753
+ → anchor-ablation Δ≈0 → the ablation control fails (the fixed-parrot game)."""
754
+ def ideate(self, prompt, gen, maxnew, temp, seed):
755
+ return "trace alpha beta gamma delta"
756
+
757
+ fails = []
758
+ _n_checks = [0]
759
+
760
+ def check(name, cond):
761
+ _n_checks[0] += 1
762
+ print((" ok " if cond else " FAIL") + " · " + name)
763
+ if not cond:
764
+ fails.append(name)
765
+
766
+ print("ρ-AXON new-axis unit test (torch-free)")
767
+
768
+ # ── ρ·store ──
769
+ r = rho_store(GoodStoreMouth(), 40)
770
+ check("ρ·store faithful → PASS", r["verdict"] == PASS)
771
+ check("ρ·store faithful controls collapse (shuffle≤0.15 · key-absent≤0.15)",
772
+ r["controls"]["shuffle-binding"] <= 0.15 and r["controls"]["key-absent"] <= 0.15)
773
+ r2 = rho_store(ValueEmitterMouth(), 40)
774
+ check("ρ·store value-emitter → FAIL (controls do not collapse)", r2["verdict"] == FAIL)
775
+
776
+ # ── ρ·tether ──
777
+ t = rho_tether(GroundedTetherMouth(), 40)
778
+ check("ρ·tether grounded → PASS", t["verdict"] == PASS)
779
+ check("ρ·tether grounded fab collapses (≤0.25) · answer>0.5",
780
+ t["controls"]["fabrication"] <= 0.25 and t["value"] >= 0.50)
781
+ t2 = rho_tether(FabricateTetherMouth(), 40)
782
+ check("ρ·tether fabricator → FAIL (fab control)", t2["verdict"] == FAIL)
783
+ t3 = rho_tether(MuteTetherMouth(), 40)
784
+ check("ρ·tether mute-all → FAIL (anti-mute)", t3["verdict"] == FAIL)
785
+
786
+ # ── ρ·self ──
787
+ s0 = rho_self(GoodSelfMouth(_SELF_ANCHOR), 40, _t_jaccard, _t_words, anchor=None)
788
+ check("ρ·self no anchor → INVALID (never FAIL)", s0["verdict"] == INVALID)
789
+ s = rho_self(GoodSelfMouth(_SELF_ANCHOR), 40, _t_jaccard, _t_words, anchor=_SELF_ANCHOR)
790
+ check("ρ·self anchor-loaded trace → PASS", s["verdict"] == PASS)
791
+ check("ρ·self controls collapse (ablated & shuffled < loaded)",
792
+ s["controls"]["anchor-ablated"] < s["value"]
793
+ and s["controls"]["shuffled-anchor"] < s["value"])
794
+ s2 = rho_self(ParrotSelfMouth(), 40, _t_jaccard, _t_words, anchor=_SELF_ANCHOR)
795
+ check("ρ·self fixed-parrot → FAIL (ablation-Δ≈0)", s2["verdict"] == FAIL)
796
+
797
+ print("")
798
+ if fails:
799
+ print("RESULT: FAIL (%d) — %s" % (len(fails), " | ".join(fails)))
800
+ raise SystemExit(1)
801
+ print("RESULT: PASS — all %d checks green (ρ·store · ρ·tether · ρ·self)" % _n_checks[0])