psiloceyeben commited on
Commit
ea2261d
·
verified ·
1 Parent(s): c061513

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. root/wander-train/appraisal_v1.py +194 -0
  2. root/wander-train/chat_context_v1.py +372 -0
  3. root/wander-train/chem_agents_v1.py +586 -0
  4. root/wander-train/code_organ_v2.py +474 -0
  5. root/wander-train/code_organ_v3.py +488 -0
  6. root/wander-train/exact_router_v1.py +143 -0
  7. root/wander-train/general_reader_v1.py +471 -0
  8. root/wander-train/math_organ_v2.py +508 -0
  9. root/wander-train/oracle_cube_reader_v1_3.py +447 -0
  10. root/wander-train/oracle_game_combine_v1.py +209 -0
  11. root/wander-train/oracle_program_ir_v1.py +256 -0
  12. root/wander-train/oracle_release_conceptnet.py +142 -0
  13. root/wander-train/oracle_release_runtime_aa.py +135 -0
  14. root/wander-train/oracle_release_runtime_ac.py +140 -0
  15. root/wander-train/oracle_release_runtime_ag.py +104 -0
  16. root/wander-train/oracle_release_runtime_ah.py +76 -0
  17. root/wander-train/oracle_release_runtime_aj.py +72 -0
  18. root/wander-train/oracle_release_runtime_am.py +55 -0
  19. root/wander-train/oracle_release_runtime_c.py +113 -0
  20. root/wander-train/oracle_release_runtime_e.py +126 -0
  21. root/wander-train/oracle_release_runtime_h.py +70 -0
  22. root/wander-train/oracle_release_runtime_k.py +73 -0
  23. root/wander-train/oracle_release_runtime_n.py +17 -0
  24. root/wander-train/oracle_release_runtime_p.py +34 -0
  25. root/wander-train/oracle_release_runtime_q.py +148 -0
  26. root/wander-train/oracle_release_runtime_s.py +50 -0
  27. root/wander-train/oracle_release_runtime_u.py +44 -0
  28. root/wander-train/oracle_release_runtime_w58.py +212 -0
  29. root/wander-train/oracle_release_runtime_w59.py +164 -0
  30. root/wander-train/oracle_release_runtime_w60.py +64 -0
  31. root/wander-train/oracle_release_runtime_w71.py +245 -0
  32. root/wander-train/oracle_sibling_evidential_v3.py +92 -0
  33. root/wander-train/oracle_tritree_v1.py +473 -0
  34. root/wander-train/oracle_tritree_v2.py +479 -0
  35. root/wander-train/organ_dialogue_v1.py +125 -0
  36. root/wander-train/organ_self_v1.py +308 -0
  37. root/wander-train/path_memory_v1.py +171 -0
  38. root/wander-train/pubchem_periodic_table.csv +119 -0
  39. root/wander-train/relation_forms_v2.py +118 -0
  40. root/wander-train/stage5c58_cycling_cognitive_substrate.py +480 -0
  41. root/wander-train/stage5h1_generic_dataset_expert.py +377 -0
  42. root/wander-train/stage5h3_hrr_relational_state_transducer.py +363 -0
  43. root/wander-train/stage5k102_persistent_typed_discourse_agent.py +1614 -0
  44. root/wander-train/stage5k104_relational_ingress_equivalence_agent.py +228 -0
  45. root/wander-train/stage5k108_universal_terminal_proof_projection_agent.py +127 -0
  46. root/wander-train/stage5k115_copular_definition_equivalence_agent.py +229 -0
  47. root/wander-train/stage5k117_caller_context_isolation_agent.py +175 -0
  48. root/wander-train/stage5k11_quadratic_inverse_solver.py +348 -0
  49. root/wander-train/stage5k121_authenticated_context_operator_agent.py +243 -0
  50. root/wander-train/stage5k131_registered_source_action_ingress_agent.py +210 -0
root/wander-train/appraisal_v1.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """appraisal_v1 - APPRAISAL of a turn from what Oracle7 knows, not from emotion words.
3
+
4
+ Scherer's component-process checks, each computed from structure:
5
+ EVENT CLASS every noun/verb in the turn is walked up WordNet's hypernym graph; the classes it reaches
6
+ (illness, damage, loss, failure, danger, crime/deception, obstruction, gain, praise, reunion, surprise)
7
+ are the event's appraisal - "toothache" -> ill_health, "fire" -> danger, "cheated" -> deception,
8
+ with no cue for those words anywhere in this file
9
+ AGENCY whose doing: the speaker's own ("I broke", "I overslept") vs another's ("you", "he", "they", a passive
10
+ "I've been cheated") - another's obstruction appraises to anger, one's own loss to sadness
11
+ NOVELTY explicit unexpectedness markers
12
+ EMOTION the class those checks produce: fear (danger), anger (another's obstruction / norm violation),
13
+ sadness (own loss, illness, damage, failure, regret), surprise (unexpected), happiness (gain, praise,
14
+ reunion), disgust (revulsion)
15
+ Direct emotion words still count (they are evidence too), but the event walk is what generalises.
16
+ usage (dev tuning only): appraisal_v1.py dev"""
17
+ import re
18
+ import sys
19
+ from functools import lru_cache
20
+
21
+ try:
22
+ from nltk.corpus import wordnet as wn
23
+ wn.synsets("dog")
24
+ except Exception: # noqa: BLE001
25
+ wn = None
26
+
27
+ # WordNet anchor synsets -> appraisal class (a word appraises to a class when one of its senses reaches the anchor)
28
+ ANCHORS = {
29
+ "danger": ["danger.n.01", "fire.n.04", "emergency.n.01", "accident.n.01", "disaster.n.02", "attack.n.01",
30
+ "injury.n.01", "threat.n.01", "violence.n.01"],
31
+ "ill_health": ["ill_health.n.01", "illness.n.01", "pain.n.01", "ache.n.01", "symptom.n.01", "infection.n.01",
32
+ "wound.n.01"],
33
+ "damage": ["damage.n.01", "breakage.n.02", "break.v.02", "break.v.03", "destroy.v.01", "spill.v.01",
34
+ "malfunction.n.01"],
35
+ "loss": ["loss.n.01", "lose.v.01", "lose.v.02", "death.n.01", "die.v.01", "misplace.v.01", "forget.v.01"],
36
+ "failure": ["failure.n.01", "fail.v.02", "fail.v.04", "mistake.n.01", "delay.n.01", "oversleep.v.01"],
37
+ "deception": ["deception.n.01", "cheat.v.01", "cheat.v.02", "deceive.v.01", "rip_off.v.01", "crime.n.01",
38
+ "rob.v.01", "steal.v.01"],
39
+ "obstruction": ["obstruction.n.01", "traffic_jam.n.01", "wait.v.01", "trap.v.01", "block.v.01",
40
+ "pollution.n.01", "noise.n.01"],
41
+ "gain": ["gain.n.01", "win.v.01", "promotion.n.01", "success.n.01", "prize.n.01", "gift.n.01", "celebration.n.01"],
42
+ "praise": ["praise.n.01", "compliment.n.01", "congratulation.n.01"],
43
+ }
44
+ EMOTION_OF_CLASS = {"danger": "fear", "ill_health": "sadness", "damage": "sadness", "loss": "sadness",
45
+ "failure": "sadness", "deception": "anger", "obstruction": "anger", "gain": "happiness",
46
+ "praise": "happiness"}
47
+ DIRECT = {
48
+ "fear": r"\b(?:scared|frighten\w*|terrified|worried|nervous|anxious|panic\w*|butterflies|dread\w*|help me|save (?:my|me)|"
49
+ r"(?:i'?m|i am) (?:so |really |very )?afraid of)\b",
50
+ "anger": r"\b(?:angry|furious|mad at|annoy\w*|irritat\w*|fed up|sick of|i hate|ridiculous|outrageous|unacceptable|"
51
+ r"how dare|damn|can'?t (?:stand|bear)|hundred times|stupid|terrible|horrendous|awful|rude)\b",
52
+ "disgust": r"\b(?:disgust\w*|gross|revolting|yuck|nasty|filthy|sickening)\b",
53
+ "sadness": r"\b(?:sad|unhappy|depress\w*|lonely|i miss|sorry to hear|so sorry about|heartbroken|upset|cry\w*|"
54
+ r"(?:so|very) tired|not up to|against me|(?:i'?m|i am) sorry,? (?:i|sir|madam))\b",
55
+ "surprise": r"(?:\bwhat a surprise\b|\bno way\b|\breally\?|\bwow\b|\byikes\b|\bcan'?t believe\b|\bnever believe\b|"
56
+ r"\bsurpris\w*|\bfancy meeting\b|\bunbelievable\b|\bwhat happened\b|\balmost didn'?t recogni[sz]e\b|"
57
+ r"\bhow come\b|\bis that you\b)",
58
+ "happiness": r"\b(?:happy|glad|wonderful|fantastic|excited|delighted|congratulations|thank you so much|can'?t wait|"
59
+ r"nice to (?:see|meet)|great news|lovely|beautiful|magnificent|cute|proud|impressed|good job|enjoy|"
60
+ r"in heaven|smells good|finally (?:here|back))\b",
61
+ }
62
+ OTHER_AGENT = re.compile(r"\b(?:you|he|she|they|someone|somebody|that guy|the (?:driver|waiter|company|shop|landlord))\b"
63
+ r"|\b(?:been|got|was|were) (?:\w+ed|cheated|robbed|stolen|hit|fired|dumped)\b", re.I)
64
+ STOP = set("be have do get go make take come see know say think want like feel look give use find tell ask seem "
65
+ "try leave call keep let begin show hear play run move live believe bring happen write sit stand lose pay "
66
+ "meet include continue set learn change lead understand watch follow stop create speak read spend grow "
67
+ "open walk win offer remember love consider appear buy wait serve die send build stay fall cut reach kill "
68
+ "raise pass sell decide return explain hope develop carry break receive agree support hit produce eat "
69
+ "cover catch draw choose wear thing time way day man woman people year".split())
70
+
71
+
72
+ @lru_cache(maxsize=None)
73
+ def _anchors():
74
+ if wn is None:
75
+ return {}
76
+ out = {}
77
+ for cls, names in ANCHORS.items():
78
+ for n in names:
79
+ try:
80
+ out[wn.synset(n)] = cls
81
+ except Exception: # noqa: BLE001
82
+ pass
83
+ return out
84
+
85
+
86
+ @lru_cache(maxsize=20000)
87
+ def word_class(word):
88
+ """the appraisal class reached by the word's MOST FREQUENT sense (WordNet usage counts), walked only when that
89
+ sense is a noun - "going" (verb), "down" (adverb), "still" (adverb) are not events; "toothache", "fire" are"""
90
+ if wn is None:
91
+ return None
92
+ anchors = _anchors()
93
+ syns = wn.synsets(word.lower())
94
+ if not syns:
95
+ return None
96
+ top = max(syns, key=lambda x: (sum(l.count() for l in x.lemmas() if l.name().lower() == word.lower()), -syns.index(x)))
97
+ if top.pos() != "n":
98
+ return None
99
+ seen, frontier = set(), [top]
100
+ for _ in range(8):
101
+ nxt = []
102
+ for x in frontier:
103
+ if x in anchors:
104
+ return anchors[x]
105
+ for h in x.hypernyms() + x.instance_hypernyms():
106
+ if h not in seen:
107
+ seen.add(h)
108
+ nxt.append(h)
109
+ frontier = nxt
110
+ if not frontier:
111
+ break
112
+ return None
113
+
114
+
115
+ # events carried by VERBS are named explicitly (a verb's most frequent sense is rarely the eventful one)
116
+ EVENT_VERBS = {"deception": r"\b(?:cheated|robbed|stolen|stole|ripped off|scammed|lied to)\b",
117
+ "damage": r"\b(?:broke|broken|smashed|spilled|crashed|ruined|wrecked)\b",
118
+ "loss": r"\b(?:lost (?:my|our|the|his|her)|left my|passed away|died)\b",
119
+ "failure": r"\b(?:overslept|failed|missed (?:the|my)|didn'?t (?:accomplish|finish|make it|pass|win)|forgot)\b",
120
+ "obstruction": r"\b(?:stuck|waiting (?:for )?(?:quite|so|such) (?:a )?(?:long|time)|almost hit|traffic jam)\b",
121
+ "gain": r"\b(?:won(?!')|promoted|got the job|finally (?:lost|got|finished|passed)|new baby|just bought)\b"}
122
+
123
+
124
+ def appraise(text):
125
+ """{"emotion", "classes", "agent", "novel", "evidence"} - evidence names the words and the checks that fired"""
126
+ t = str(text or "").replace("’", "'").replace(" ' ", "'")
127
+ direct = [e for e, rx in DIRECT.items() if re.search(rx, t, re.I)]
128
+ classes, ev = [], []
129
+ for c, rx in EVENT_VERBS.items():
130
+ m = re.search(rx, t, re.I)
131
+ if m:
132
+ classes.append(c)
133
+ ev.append("%s->%s" % (m.group(0).lower(), c))
134
+ for w in re.findall(r"[A-Za-z][A-Za-z'-]{2,}", t):
135
+ wl = w.lower().strip("'")
136
+ if wl in STOP or len(wl) < 4:
137
+ continue
138
+ c = word_class(wl)
139
+ if c:
140
+ classes.append(c)
141
+ ev.append("%s->%s" % (wl, c))
142
+ other = bool(OTHER_AGENT.search(t))
143
+ novel = "surprise" in direct
144
+ emo = "none"
145
+ if "fear" in direct or "danger" in classes:
146
+ emo = "fear"
147
+ elif "anger" in direct or "deception" in classes or ("obstruction" in classes and other):
148
+ emo = "anger"
149
+ elif "disgust" in direct:
150
+ emo = "disgust"
151
+ elif "gain" in classes and "finally" in t.lower():
152
+ emo = "happiness"
153
+ elif "sadness" in direct or any(c in classes for c in ("ill_health", "loss", "damage", "failure")):
154
+ emo = "sadness"
155
+ elif novel:
156
+ emo = "surprise"
157
+ elif "happiness" in direct or any(c in classes for c in ("gain", "praise")):
158
+ emo = "happiness"
159
+ return {"emotion": emo, "classes": classes, "agent": "other" if other else "self", "novel": novel,
160
+ "evidence": ev + ["direct:%s" % d for d in direct]}
161
+
162
+
163
+ def _dev():
164
+ import collections
165
+ sys.path.insert(0, "/root/w240")
166
+ from relational_instrument_v1 import dd
167
+ tp, hu, pr, fp, nn, miss = collections.Counter(), collections.Counter(), collections.Counter(), 0, 0, []
168
+ fps = collections.Counter()
169
+ for r in dd("dev"):
170
+ g, a = r["e0"], appraise(r["opener"])
171
+ p = a["emotion"]
172
+ if g != "none":
173
+ hu[g] += 1
174
+ else:
175
+ nn += 1
176
+ if p != "none":
177
+ fp += 1
178
+ fps[(p, tuple(a["evidence"][:3]))] += 1
179
+ if p != "none":
180
+ pr[p] += 1
181
+ if g == p and g != "none":
182
+ tp[g] += 1
183
+ elif g != "none" and len(miss) < 25:
184
+ miss.append((g, p, a["evidence"][:4], r["opener"][:80]))
185
+ for e in ("anger", "disgust", "fear", "happiness", "sadness", "surprise"):
186
+ print(" %-9s %3d / %3d human / %3d predicted" % (e, tp[e], hu[e], pr[e]))
187
+ print(" DEV recall %d/%d | false positives on neutral %d/%d" % (sum(tp.values()), sum(hu.values()), fp, nn))
188
+ print(" top false-positive evidence:", fps.most_common(12))
189
+ for m in miss:
190
+ print(" miss", m)
191
+
192
+
193
+ if __name__ == "__main__":
194
+ _dev()
root/wander-train/chat_context_v1.py ADDED
@@ -0,0 +1,372 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """chat_context_v1 - Oracle7's CONVERSATION engine (Ben 2026-10-09: "we have what we need for a proper chat model").
3
+
4
+ Chat failed by REFUSAL, not by error: a follow-up ("Where was he born?", "And its currency?", "And of Denmark?", "What does
5
+ that cost?") reached the core without the conversation, and the core either refused or - worse - read the pronoun as the
6
+ topic ("he" -> an article about the word "he"). This engine never asserts anything itself. It keeps a small DIALOGUE STATE
7
+ per session and REWRITES a turn that only makes sense through the conversation into the standalone question it stands
8
+ for; the whole verified chain (router, 210 specialists, fusion, maths) then answers that question exactly as it would if
9
+ it had been asked outright. The rewrite is shown in the reply's path and provenance, so the reading can be checked.
10
+
11
+ STATE (per session): the previous standalone question; its SUBJECT entity (the entity asked about) and its ANSWER entity
12
+ (the entity the reply named), each with whether it is a person; the previous numeric RESULT (value + unit); solved
13
+ variables ("x = 5"); the previous question's material phrase ("burning 5 kg of coal").
14
+ PRONOUNS he/she/him/his/her -> the person in play (the answer if it is a person - "Who wrote Frankenstein? / Who was her
15
+ mother?" - else the subject); it/its -> the THING asked about ("Which planet does Titan orbit? / Who discovered
16
+ it?" is Titan); "that country / that city ..." -> the entity just named; possessives become "the <noun> of <E>".
17
+ ELLIPSIS "And its currency?" (relation swap), "And of Denmark?" / "And Doctor Zhivago?" (entity swap in the previous
18
+ question), "And on the Moon?" / "And with radius 10 m?" (modifier swap or addition), "Where?" / "In what year?"
19
+ (question-word swap, active -> passive when needed: "Who discovered Titan?" -> "In what year was Titan
20
+ discovered?").
21
+ RESULTS "What does that cost at $0.20 per kWh?" -> the previous result ("300 kWh"); "What is x squared?" -> the solved
22
+ x; "How much CO2 does that emit?" -> the previous material phrase ("burning 5 kg of coal").
23
+ MULTI "What is the capital of Austria and what is its currency?" -> two questions, each resolved and answered.
24
+ A turn with nothing to resolve is passed through untouched. If a rewrite cannot be made with confidence, nothing is
25
+ rewritten (the chain's own reply stands)."""
26
+ import re
27
+
28
+ import relation_spec_v1 as RS
29
+
30
+ PRON_PERSON = r"he|she|him|his|her|hers"
31
+ PRON_THING = r"it|its"
32
+ REL_NOUNS = r"capital(?: city)?|currency|population|official language|national anthem|anthem|continent|calling code|highest point|" \
33
+ r"leader|president|prime minister|author|director|composer|creator|founder|headquarters|nationality|religion|" \
34
+ r"spouse|wife|husband|father|mother|birthplace|place of birth|date of birth|birthday"
35
+ WH = r"who|what|which|where|when|how"
36
+ STATE_CAP = 2000
37
+ UNRESOLVED = "__unresolved__" # rewrite() -> (UNRESOLVED, pronoun): a short turn whose pronoun has nothing to refer to
38
+
39
+
40
+ class Turn:
41
+ def __init__(self):
42
+ self.q = None # previous standalone question
43
+ self.subj = None # (qid, label, is_person, surface)
44
+ self.ans = None # (qid, label, is_person)
45
+ self.result = None # (value, unit)
46
+ self.solved = {} # variable -> value
47
+ self.material = None # "burning 5 kg of coal" / "5 kg of coal"
48
+
49
+
50
+ _STATES = {}
51
+
52
+
53
+ def state(sid):
54
+ st = _STATES.get(sid)
55
+ if st is None:
56
+ if len(_STATES) > STATE_CAP:
57
+ _STATES.clear()
58
+ st = _STATES[sid] = Turn()
59
+ return st
60
+
61
+
62
+ def _is_person(qid):
63
+ try:
64
+ return 5 in RS.classes_of(qid)[0]
65
+ except Exception: # noqa: BLE001
66
+ return False
67
+
68
+
69
+ def _entity_of_question(q):
70
+ a = RS.analyse(q)
71
+ if not a or not a["cands"]:
72
+ return None
73
+ qid = a["cands"][0]
74
+ return (qid, RS._rs().label(qid) or a["span"], _is_person(qid), a["span"])
75
+
76
+
77
+ RECEIPT_ENDS = re.compile(r"From the Wikidata relation store: (.+?)(?: \(Q(\d+)\))? - [^()]*? \(P\d+\) - (.+?)(?: \(Q(\d+)\))?\.\s*(?:\[|$)")
78
+ TYPED = re.compile(r"\(typed relations: wd:Q(\d+) P\d+ wd:Q(\d+)\)")
79
+ RECEIPT = re.compile(r"From the Wikidata relation store: .*? - .*? \(P\d+\) - (.+?)(?: \(Q(\d+)\))?\.\s*(?:\[|$)")
80
+
81
+
82
+ def _entity_of_reply(reply, prefer_person=False):
83
+ """the entity the reply named; after a WHO question the answer is a person, so a person among the name's top candidates
84
+ is preferred (the linker's popularity order put a song called "Pablo Picasso" before the painter)"""
85
+ text = re.sub(r"^\s*\[ev:[a-z_]+\]\s*", "", str(reply or ""))
86
+ m = RECEIPT_ENDS.search(text)
87
+ if m:
88
+ # the receipt reads subject - property - value, in the direction of the STORE: an inverse read ("Who painted
89
+ # Guernica?" -> "Pablo Picasso (Q5593) - wrote - Guernica (Q175036)") puts the answer FIRST. The answer is the end
90
+ # whose label the reply leads with.
91
+ lead = text.split(". From the Wikidata relation store:", 1)[0].strip().lower()
92
+ ends = [(m.group(1), m.group(2)), (m.group(3), m.group(4))]
93
+ pick = [e for e in ends if e[1] and e[0].strip().lower() == lead] or [e for e in ends[::-1] if e[1]]
94
+ if pick:
95
+ qid = int(pick[0][1])
96
+ return (qid, RS._rs().label(qid) or pick[0][0], _is_person(qid))
97
+ m = RECEIPT.search(text)
98
+ if m and m.group(2):
99
+ qid = int(m.group(2))
100
+ return (qid, RS._rs().label(qid) or m.group(1), _is_person(qid))
101
+ # a sentence ends at . ! ? - but not after an initial ("J. R. R. Tolkien")
102
+ first = re.split(r"(?<![\s.][A-Z]\.)(?<!^[A-Z]\.)(?<=[.!?])\s", text, 1)[0].rstrip(".")
103
+ m = TYPED.search(text)
104
+ if m:
105
+ # the reply's typed relation names the exact entities; the answer is the end the reply leads with
106
+ lead = re.sub(r"\s*\[[^\]]*\]\s*", " ", first).strip().lower()
107
+ for q in (int(m.group(2)), int(m.group(1))):
108
+ lab = RS._rs().label(q)
109
+ if lab and lab.lower() == lead:
110
+ return (q, lab, _is_person(q))
111
+ first = re.sub(r"\s*\[[^\]]*\]\s*", " ", first).strip()
112
+ if not first or len(first) > 160:
113
+ return None
114
+ try:
115
+ sp = RS.X.L.link(first, 3)
116
+ except Exception: # noqa: BLE001
117
+ return None
118
+ if not sp:
119
+ return None
120
+ span, cs = sp[0]
121
+ qid = cs[0][0]
122
+ if prefer_person:
123
+ people = [c[0] for c in cs[:3] if _is_person(c[0])]
124
+ if people:
125
+ qid = people[0]
126
+ return (qid, RS._rs().label(qid) or span, _is_person(qid))
127
+
128
+
129
+ NUM_UNIT = re.compile(r"(?:is|about|=|of)\s+\$?(-?\d[\d,]*(?:\.\d+)?(?:e[+-]?\d+)?)\s*(kwh|kj|mj|j|kg|g|m|km|s|n|pa|kpa|w|kw|litres?|liters?|l|m³|m3|°c|%)?", re.I)
130
+ SOLVED = re.compile(r"\b([a-z])\s*=\s*(-?\d+(?:\.\d+)?)\b")
131
+ MATERIAL = re.compile(r"\b((?:burning\s+)?\d[\d,]*(?:\.\d+)?\s*(?:kg|g|tonnes?|litres?|liters?|m3)\s+of\s+[a-z][a-z ]{1,24}?)(?=\s+(?:release|emit|produce|give|make|weigh)|[?.,]|$)", re.I)
132
+
133
+
134
+ def _result_of_reply(reply):
135
+ text = re.sub(r"^\s*\[ev:[a-z_]+\]\s*", "", str(reply or ""))
136
+ m = re.search(r"(?:answer is|is about|takes about|cost(?:s)? (?:is |about )?)\s*\$?(-?\d[\d,]*(?:\.\d+)?(?:e[+-]?\d+)?)\s*([a-zA-Z³%°]+)?", text)
137
+ if not m:
138
+ m = NUM_UNIT.search(text)
139
+ if not m:
140
+ return None
141
+ try:
142
+ v = float(m.group(1).replace(",", ""))
143
+ except ValueError:
144
+ return None
145
+ unit = (m.group(2) or "").lower()
146
+ if "$" in text[:max(0, m.start(1)) + 2] and not unit:
147
+ unit = "$"
148
+ return (v, unit)
149
+
150
+
151
+ def update(sid, standalone, reply):
152
+ """after the chain answered (or declined) the standalone question: remember what the conversation is about"""
153
+ st = state(sid)
154
+ numeric = bool(re.search(r"\d", standalone)) and isinstance(reply, dict) and reply.get("status") == "answered" \
155
+ and _result_of_reply(reply.get("response")) is not None
156
+ if not recognised(standalone) and not numeric:
157
+ return # context comes from FACTUAL turns only
158
+ st.q = standalone
159
+ # an entity the conversation already resolved KEEPS its identity when the rewrite names it (relinking "Pablo Picasso"
160
+ # by popularity picked a song, Q7121722, over the painter the previous answer named, Q5593)
161
+ subj = next((tuple(e[:3]) + (e[1],) for e in (st.ans, st.subj) if e and len(e[1] or "") >= 3 and e[1] in standalone), None) \
162
+ or _entity_of_question(standalone)
163
+ typed = TYPED.search(str(reply.get("response", ""))) if isinstance(reply, dict) else None
164
+ if subj and typed:
165
+ for q in (int(typed.group(1)), int(typed.group(2))):
166
+ if q != subj[0] and (RS._rs().label(q) or "").lower() == subj[1].lower():
167
+ subj = (q, subj[1], _is_person(q), subj[3]) # the reply's own typed relation names the exact entity
168
+ if subj:
169
+ st.subj = subj
170
+ if isinstance(reply, dict) and reply.get("status") == "answered":
171
+ ans = _entity_of_reply(reply.get("response"), prefer_person=bool(re.match(r"^\s*(?:who|whom|whose)\b", standalone, re.I)))
172
+ st.ans = ans if ans and (not subj or ans[0] != subj[0]) else None
173
+ r = _result_of_reply(reply.get("response"))
174
+ st.result = r
175
+ for var, val in SOLVED.findall(str(reply.get("response", ""))):
176
+ st.solved[var] = val
177
+ mm = MATERIAL.search(standalone)
178
+ if mm:
179
+ st.material = mm.group(1).strip()
180
+
181
+
182
+ def _person(st):
183
+ if st.ans and st.ans[2]:
184
+ return st.ans[1]
185
+ if st.subj and st.subj[2]:
186
+ return st.subj[1]
187
+ return None
188
+
189
+
190
+ def _thing(st):
191
+ if st.subj and not st.subj[2]:
192
+ return st.subj[1]
193
+ if st.ans and not st.ans[2]:
194
+ return st.ans[1]
195
+ return None
196
+
197
+
198
+ def _cap(s):
199
+ s = s.strip()
200
+ return s[:1].upper() + s[1:] if s else s
201
+
202
+
203
+ def _q(s):
204
+ s = s.strip().rstrip("?.! ")
205
+ return _cap(s) + "?"
206
+
207
+
208
+ def _swap_wh(prev, wh):
209
+ """'When was Alfred Hitchcock born?' + 'Where' -> 'Where was Alfred Hitchcock born?'; active 'Who discovered Titan?' ->
210
+ 'In what year was Titan discovered?'"""
211
+ p = prev.strip().rstrip("?")
212
+ m = re.match(r"^(?:who|what|which|where|when|how)\b(?:\s+year)?\s+((?:was|were|is|are|did|does|do)\b.*)$", p, re.I)
213
+ if m:
214
+ return _q("%s %s" % (wh, m.group(1)))
215
+ m = re.match(r"^who\s+(\w+ed)\s+(.+)$", p, re.I)
216
+ if m:
217
+ return _q("%s was %s %s" % (wh, m.group(2), m.group(1)))
218
+ return None
219
+
220
+
221
+ def _rewrite_raw(sid, text):
222
+ """-> (kind, standalone question) with kind "num" | "rel", (UNRESOLVED, pronoun), or None"""
223
+ st = state(sid)
224
+ t = " ".join(str(text or "").split())
225
+ low = t.lower().rstrip("?.! ")
226
+ if not t:
227
+ return None
228
+ pron = re.search(r"\b(he|she|him|his|her|it|its|they|them|their)\b", low)
229
+ short_bare = pron is not None and len(low.split()) <= 12 and re.search(r"(?<=\s)[A-Z][a-z]+", t) is None
230
+ if not st.q:
231
+ return (UNRESOLVED, pron.group(1)) if short_bare and pron.group(1) not in ("it", "its") else None
232
+ # results and variables
233
+ m = re.search(r"\bwhat is ([a-z]) (squared|cubed)\b", low)
234
+ if m and m.group(1) in st.solved:
235
+ power = "2" if m.group(2) == "squared" else "3"
236
+ return ("num", _q(re.sub(r"\b%s (squared|cubed)\b" % m.group(1), "%s^%s" % (st.solved[m.group(1)], power), t, flags=re.I)))
237
+ if re.search(r"\b(?:that|this)\b", low) and (st.result or st.material):
238
+ if re.search(r"\b(?:co2|carbon dioxide|emit|emission|release|weigh)", low) and st.material:
239
+ return ("num", _q(re.sub(r"\b(?:that|this)\b", st.material, t, count=1, flags=re.I)))
240
+ if st.result and st.result[1] and re.search(r"per\s+%s\b" % re.escape(st.result[1]), low):
241
+ return ("num", _q(re.sub(r"\b(?:that|this)\b", "%s %s" % (_fmt(st.result[0]), st.result[1]), t, count=1, flags=re.I)))
242
+ # bare question word: "Where?", "When?", "In what year?"
243
+ m = re.match(r"^(?:and\s+)?(where|when|in what year|what year)$", low)
244
+ if m:
245
+ r = _swap_wh(st.q, {"what year": "In what year"}.get(m.group(1), _cap(m.group(1))))
246
+ return ("rel", r) if r else None
247
+ # ellipsis with "And ..."
248
+ m = re.match(r"^(?:and|what about|how about)\s+(.+)$", low)
249
+ if m:
250
+ rest = t[len(t) - len(m.group(1)) - (len(t) - len(t.rstrip("?.! "))):].rstrip("?.! ")
251
+ rest_low = rest.lower()
252
+ mr = re.match(r"^(?:the\s+|its\s+|his\s+|her\s+|their\s+)?(%s)$" % REL_NOUNS, rest_low)
253
+ if mr:
254
+ ent = _thing(st) if not re.match(r"^(his|her)\b", rest_low) else _person(st)
255
+ ent = ent or _thing(st) or _person(st)
256
+ return ("rel", _q("What is the %s of %s" % (mr.group(1), ent))) if ent else None
257
+ mp = re.match(r"^(on|in|at|with|for|of)\s+(.+)$", rest, re.I)
258
+ if mp and mp.group(1).lower() in ("of", "for") and st.subj and st.subj[3]:
259
+ r = _replace_subject(st, mp.group(2))
260
+ return ("rel", r) if r else None
261
+ if mp and mp.group(1).lower() in ("on", "in", "at", "with"):
262
+ prep, phrase = mp.group(1).lower(), mp.group(2)
263
+ pm = re.search(r"\b%s\s+[^?]*?\d[^?]*?(?=$|\?|,)" % prep, st.q, re.I) if re.search(r"\d", phrase) else None
264
+ if not st.result:
265
+ return None # a modifier swap continues a COMPUTATION only
266
+ if pm:
267
+ return ("num", _q(st.q[:pm.start()] + "%s %s" % (prep, phrase) + st.q[pm.end():]))
268
+ return ("num", _q(st.q.rstrip("?.! ") + " %s %s" % (prep, phrase)))
269
+ if rest[:1].isupper() and st.subj and st.subj[3]:
270
+ r = _replace_subject(st, rest)
271
+ return ("rel", r) if r else None
272
+ return None
273
+ # "that country" / possessive and personal pronouns
274
+ out = t
275
+ changed = False
276
+ m = re.search(r"\bthat\s+(country|city|state|planet|nation|island|river|band|team|company|book|film|novel|place)\b", out, re.I)
277
+ if m:
278
+ ent = (st.ans[1] if st.ans and not st.ans[2] else None) or _thing(st)
279
+ if not ent:
280
+ return None
281
+ out = out[:m.start()] + ent + out[m.end():]
282
+ changed = True
283
+ # a turn that names its own subject (a capitalised name after the first word) keeps its own pronouns
284
+ names_own = re.search(r"(?<=\s)[A-Z][a-z]+", out) is not None and not changed
285
+ for pron, getter in (() if names_own else ((PRON_PERSON, _person), (PRON_THING, _thing))):
286
+ for pm in list(re.finditer(r"\b(%s)\b(\s+[a-z]+(?:\s+[a-z]+)?)?" % pron, out, re.I))[::-1]:
287
+ ent = getter(st)
288
+ if not ent:
289
+ return (UNRESOLVED, pm.group(1).lower()) if short_bare else None
290
+ word = pm.group(1).lower()
291
+ if word in ("his", "her", "its") and pm.group(2):
292
+ noun = pm.group(2).strip()
293
+ if re.match(r"^(%s)\b" % REL_NOUNS, noun, re.I) or word in ("his", "its"):
294
+ rep = "the %s of %s" % (noun, ent)
295
+ out = out[:pm.start()] + rep + out[pm.end():]
296
+ changed = True
297
+ continue
298
+ out = out[:pm.start(1)] + ent + out[pm.end(1):]
299
+ changed = True
300
+ if not changed:
301
+ return None
302
+ return ("rel", _q(out))
303
+
304
+
305
+ def recognised(q):
306
+ """a question the verified relation layer can READ: it matches one of the hand-written relation readers whole"""
307
+ try:
308
+ import relation_spec_v2 as S2
309
+ _, low = S2.normalise(q)
310
+ for key in S2.RD.PROPS:
311
+ c = S2.compiled(key)
312
+ if any(rx.match(low) for d in ("fwd", "inv") for rx in c[d]):
313
+ return True
314
+ except Exception: # noqa: BLE001
315
+ return False
316
+ return False
317
+
318
+
319
+ def rewrite(sid, text):
320
+ """-> the standalone question this turn stands for, (UNRESOLVED, pronoun), or None. A rewrite is ACCEPTED only when it is
321
+ exactly right for the verified layer: a relation rewrite must be a question the relation readers recognise; a numeric
322
+ rewrite must continue a numeric answer. Anything else goes to the core untouched - the core has its own conversation
323
+ acts ("got it", "why?", "did you run it?", "how does it work?") that a rewrite would break (m52 replay, 2026-10-09)."""
324
+ r = _rewrite_raw(sid, text)
325
+ if r is None:
326
+ return None
327
+ if r[0] == UNRESOLVED:
328
+ return r if recognised(text) else None
329
+ kind, q = r
330
+ if not q:
331
+ return None
332
+ if kind == "num":
333
+ return q if state(sid).result or state(sid).solved else None
334
+ return q if recognised(q) else None
335
+
336
+
337
+ def _replace_subject(st, new):
338
+ surface = st.subj[3]
339
+ if surface and surface in st.q:
340
+ return _q(st.q.replace(surface, new, 1))
341
+ lab = st.subj[1]
342
+ if lab and lab in st.q:
343
+ return _q(st.q.replace(lab, new, 1))
344
+ return None
345
+
346
+
347
+ def _fmt(v):
348
+ return ("%d" % v) if abs(v - round(v)) < 1e-9 else ("%.6g" % v)
349
+
350
+
351
+ SPLIT = re.compile(r"\s+and\s+(?=(?:%s)\b)" % WH, re.I)
352
+
353
+
354
+ def split_parts(text):
355
+ """'What is the capital of Austria and what is its currency?' -> two questions (only when the second part is itself a
356
+ question starting with a question word)"""
357
+ t = " ".join(str(text or "").split()).rstrip("?")
358
+ parts = SPLIT.split(t)
359
+ if len(parts) < 2:
360
+ return None
361
+ # split ONLY when every part is a question the relation readers can read (panel v27 2026-10-09: a reading passage
362
+ # containing "... and Where to Find Them" and "Everything is fine, and how are you?" were split and lost)
363
+ return [_q(p) for p in parts] if all(_shape_ok(_q(p)) for p in parts) else None
364
+
365
+
366
+ def _shape_ok(p):
367
+ """the part reads as a relation question - as written, or with its pronoun standing for a named thing"""
368
+ if recognised(p):
369
+ return True
370
+ s = re.sub(r"\b(?:its|his|her|their)\s+(\w+(?:\s+\w+)?)", r"the \1 of Peru", p, flags=re.I)
371
+ s = re.sub(r"\b(?:he|she|it|they|him|them)\b", "Peru", s, flags=re.I)
372
+ return s != p and recognised(s)
root/wander-train/chem_agents_v1.py ADDED
@@ -0,0 +1,586 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """chem_agents_v1 - five chemistry agents (Oracle7 specialists) that compute exactly what a question asks, from the values it
3
+ gives, with every constant stated. Built for the university-chemistry questions the benchmark curriculum showed the family
4
+ could not reach (MMLU-Pro chemistry, development rows 3400-3999 only).
5
+ GasAgent ideal gas: density, molar mass from density, one unknown of P V n T (mass + formula -> n)
6
+ PhAgent pH / pOH / [H+] of strong acids and bases, weak acid or base from Ka / Kb / pKa, Henderson-Hasselbalch
7
+ PhotonAgent photon energy / wavelength / frequency (per photon, per mole, eV), de Broglie, hydrogen-like transitions
8
+ ColligativeAgent freezing-point depression, boiling-point elevation, molality, molar mass from a shift, osmotic pressure
9
+ KineticsAgent first-order decay: half-life, rate constant, time to a fraction, fraction left
10
+ LAW (from the specialist network): EVERY number in the question must be read by the computation, or the agent declines -
11
+ an unread number means a condition the formula does not know about. Each agent also declines what it cannot type
12
+ (unknown formula, missing value, multi-part questions). Returns {kind, status, response, value}."""
13
+ from __future__ import annotations
14
+
15
+ import csv
16
+ import math
17
+ import os
18
+ import re
19
+
20
+ HERE = os.path.dirname(os.path.abspath(__file__))
21
+ R_LATM = 0.082057366 # L atm / (mol K)
22
+ R_J = 8.314462618 # J / (mol K)
23
+ H = 6.62607015e-34 # J s (exact)
24
+ C = 2.99792458e8 # m / s (exact)
25
+ NA = 6.02214076e23 # 1 / mol (exact)
26
+ EV = 1.602176634e-19 # J (exact)
27
+ ME = 9.1093837015e-31 # kg
28
+ RH = 1.09677583e7 # m^-1, Rydberg constant for hydrogen
29
+
30
+
31
+ def _table():
32
+ for p in (os.path.join(HERE, "pubchem_periodic_table.csv"), __import__("os").environ.get("ORACLE7_ROOT", "/opt/oracle-clm") + "/specialists/chemistry-v3/pubchem_periodic_table.csv"):
33
+ if os.path.exists(p):
34
+ return {r["Symbol"]: float(r["AtomicMass"]) for r in csv.DictReader(open(p, encoding="utf-8"))}
35
+ return {}
36
+
37
+
38
+ EL = _table()
39
+
40
+
41
+ # ---------------------------------------------------------------- text
42
+ def clean(t):
43
+ t = " ".join(str(t or "").split())
44
+ t = t.replace("−", "-").replace("–", "-").replace("\\times", "×").replace("\\cdot", "·").replace("°", " deg ")
45
+ t = re.sub(r"\\(?:mathrm|text|mathbf|operatorname)\s*\{([^{}]*)\}", r"\1", t)
46
+ t = re.sub(r"\\(?:mathrm|text)\s*\{([^{}]*)\}", r"\1", t)
47
+ t = t.replace("$", " ").replace("~", " ").replace("\\,", " ").replace("{", "").replace("}", "")
48
+ t = re.sub(r"\^\s*\\circ|\\circ", " deg ", t)
49
+ t = re.sub(r"_(\d)", r"\1", t) # CO_2 -> CO2, H_2SO_4 -> H2SO4
50
+ t = re.sub(r"(?<![\d.×x*])(?<![\d.]\s)\b10\s*\^\s*\(?([-+]?\d+)\)?", r"1×10^\1", t) # a bare power of ten is a number
51
+ t = re.sub(r"(\d)\s+(?=10\^)", r"\1×", t) # "2 10^-15" -> "2×10^-15"
52
+ t = re.sub(r"\s+", " ", t)
53
+ return t.strip()
54
+
55
+
56
+ NUMRE = re.compile(r"(?<![A-Za-z\d.])([-+]?\d+(?:,\d{3})*(?:\.\d+)?|[-+]?\.\d+)\s*(?:(?:×|x|\*)\s*10\s*\^?\s*\(?\s*([-+]?\d+)\)?|[eE]([-+]?\d+))?")
57
+
58
+
59
+ def num(s):
60
+ m = NUMRE.fullmatch(s.strip())
61
+ if not m:
62
+ raise ValueError(s)
63
+ return _val(m)
64
+
65
+
66
+ def _val(m):
67
+ v = float(m.group(1).replace(",", ""))
68
+ e = m.group(2) or m.group(3)
69
+ return v * 10 ** int(e) if e else v
70
+
71
+
72
+ def numbers(t):
73
+ """every number in the question with its span (formula subscripts excluded: H2SO4, CO2 are names, not values)"""
74
+ out = []
75
+ for m in NUMRE.finditer(t):
76
+ a = m.start()
77
+ if a > 0 and re.match(r"[A-Z)\]a-z]", t[a - 1]) and not re.match(r"[a-z]{2,}", t[max(0, a - 3):a]):
78
+ continue
79
+ if re.match(r"\d", t[a]) and a > 0 and t[a - 1] == "(" and re.match(r"\(\d+\)\s*[A-Z]", t[a - 1:]):
80
+ continue
81
+ out.append((m.start(), m.end(), _val(m)))
82
+ return out
83
+
84
+
85
+ class Reader:
86
+ """reads typed quantities off the question; records which number spans it used"""
87
+
88
+ def __init__(self, text):
89
+ self.t = clean(text)
90
+ self.low = self.t.lower()
91
+ self.nums = numbers(self.t)
92
+ self.used = set()
93
+
94
+ def _take(self, pattern, flags=re.I):
95
+ found = []
96
+ for m in re.finditer(pattern, self.t, flags):
97
+ found.append(m)
98
+ return found
99
+
100
+ def q(self, units, conv):
101
+ """all quantities whose unit is one of `units`; conv(value, unit) -> SI-ish value"""
102
+ res = []
103
+ pat = r"(" + NUMRE.pattern + r")\s*(" + units + r")(?![a-z])"
104
+ for m in re.finditer(pat, self.t, re.I):
105
+ nm = NUMRE.match(m.group(1))
106
+ v = _val(nm)
107
+ res.append((conv(v, m.group(m.lastindex).lower()), m.start(), m.start() + len(m.group(1))))
108
+ # one value stated twice in two units ("720 mm Hg or 720 torr") is one quantity: keep it once, read both spans
109
+ if len(res) > 1 and all(abs(x[0] - res[0][0]) <= 1e-9 * max(abs(res[0][0]), 1e-300) for x in res):
110
+ for x in res[1:]:
111
+ self.used.add(x[1])
112
+ res = res[:1]
113
+ return res
114
+
115
+ def use(self, item):
116
+ self.used.add(item[1])
117
+ return item[0]
118
+
119
+ def all_read(self, also=()):
120
+ starts = set(self.used) | set(also)
121
+ return all(any(abs(s - a) <= 1 for s in starts) for a, _, _ in self.nums)
122
+
123
+
124
+ P_UNITS = r"atm(?:ospheres?)?|torr|mm\s*hg|mmhg|kpa|pa|bar|psi"
125
+ V_UNITS = r"liters?|litres?|l|ml|milliliters?|cm3|cm\^3|dm3|dm\^3|m3|m\^3"
126
+ T_UNITS = r"deg\s*c|deg\s*k|deg\s*f|k(?![a-z])|kelvin|celsius|c(?![a-z])"
127
+ M_UNITS = r"kg|g|mg|grams?|kilograms?|milligrams?"
128
+
129
+
130
+ def p_atm(v, u):
131
+ u = u.replace(" ", "")
132
+ return v * {"atm": 1, "atmosphere": 1, "atmospheres": 1, "torr": 1 / 760, "mmhg": 1 / 760, "kpa": 1 / 101.325, "pa": 1 / 101325,
133
+ "bar": 1 / 1.01325, "psi": 1 / 14.6959}[u]
134
+
135
+
136
+ def v_l(v, u):
137
+ u = u.replace("^", "")
138
+ if u in ("ml", "milliliter", "milliliters", "cm3"):
139
+ return v / 1000
140
+ if u == "m3":
141
+ return v * 1000
142
+ return v
143
+
144
+
145
+ def t_k(v, u):
146
+ u = u.replace(" ", "")
147
+ if u in ("degc", "c", "celsius"):
148
+ return v + 273.15
149
+ if u == "degf":
150
+ return (v - 32) * 5 / 9 + 273.15
151
+ return v
152
+
153
+
154
+ def m_g(v, u):
155
+ return v * 1000 if u.startswith("k") else v / 1000 if u.startswith("m") else v
156
+
157
+
158
+ def molar_mass(f):
159
+ """H2SO4, Ca(OH)2, CuSO4·5H2O -> g/mol; None if any symbol is unknown"""
160
+ f = f.replace("·", ".").replace("*", ".")
161
+ total = 0.0
162
+ for part in f.split("."):
163
+ k = re.match(r"(\d+)(.*)", part)
164
+ mult, part = (int(k.group(1)), k.group(2)) if k else (1, part)
165
+ stack, cur, i = [], {}, 0
166
+ while i < len(part):
167
+ ch = part[i]
168
+ if ch == "(":
169
+ stack.append(cur)
170
+ cur = {}
171
+ i += 1
172
+ elif ch == ")":
173
+ j = i + 1
174
+ while j < len(part) and part[j].isdigit():
175
+ j += 1
176
+ n = int(part[i + 1:j] or 1)
177
+ prev = stack.pop()
178
+ for s, c in cur.items():
179
+ prev[s] = prev.get(s, 0) + c * n
180
+ cur, i = prev, j
181
+ else:
182
+ m = re.match(r"([A-Z][a-z]?)(\d*)", part[i:])
183
+ if not m:
184
+ return None
185
+ s = m.group(1)
186
+ if s not in EL:
187
+ return None
188
+ cur[s] = cur.get(s, 0) + int(m.group(2) or 1)
189
+ i += len(m.group(0))
190
+ if stack:
191
+ return None
192
+ total += mult * sum(EL[s] * c for s, c in cur.items())
193
+ return total
194
+
195
+
196
+ FORMULA = re.compile(r"(?<![A-Za-z])((?:[A-Z][a-z]?\d*|\((?:[A-Z][a-z]?\d*)+\)\d*){1,8}(?:[.·]\d*(?:[A-Z][a-z]?\d*)+)?)(?![a-z])")
197
+ NAMES = {"oxygen": "O2", "nitrogen": "N2", "hydrogen": "H2", "chlorine": "Cl2", "fluorine": "F2", "helium": "He", "neon": "Ne",
198
+ "argon": "Ar", "carbon dioxide": "CO2", "carbon monoxide": "CO", "methane": "CH4", "ammonia": "NH3", "water": "H2O",
199
+ "ethane": "C2H6", "propane": "C3H8", "butane": "C4H10", "sulfur dioxide": "SO2", "nitrogen dioxide": "NO2",
200
+ "hydrogen chloride": "HCl", "glucose": "C6H12O6", "sucrose": "C12H22O11", "urea": "CH4N2O", "ethanol": "C2H5OH",
201
+ "methanol": "CH3OH", "benzene": "C6H6", "sodium chloride": "NaCl", "acetic acid": "CH3COOH", "xenon": "Xe", "krypton": "Kr"}
202
+
203
+
204
+ def formulas(t):
205
+ out = []
206
+ for m in FORMULA.finditer(t):
207
+ f = m.group(1)
208
+ syms = re.findall(r"[A-Z][a-z]?", f)
209
+ if (re.search(r"\d|\(", f) or len(syms) >= 2) and all(s in EL for s in syms):
210
+ out.append((f, m.start(), m.end()))
211
+ for name, f in NAMES.items():
212
+ for m in re.finditer(r"\b%s\b" % name, t, re.I):
213
+ out.append((f, m.start(), m.end()))
214
+ return out
215
+
216
+
217
+ def sig(x):
218
+ return "%.4g" % x
219
+
220
+
221
+ def say(kind, text, value=None, status="answered"):
222
+ return {"kind": kind, "status": status, "response": text, "value": value}
223
+
224
+
225
+ def multi_part(low):
226
+ return bool(re.search(r"\(a\)|\(b\)|\ba\)\s|\bb\)\s|\band (?:also |then )?(?:calculate|find|determine|compute)\b", low))
227
+
228
+
229
+ # ---------------------------------------------------------------- agents
230
+ class GasAgent:
231
+ def answer(self, text, session_id="default"):
232
+ r = Reader(text)
233
+ low = r.low
234
+ if multi_part(low) or not re.search(r"\bgas\b|\bdensity\b|\bmolar mass\b|\bmolecular weight\b|\bideal\b|\batm\b|\btorr\b|\bmm ?hg\b|\bkpa\b", low):
235
+ return None
236
+ if re.search(r"van ?der ?waals|real gas|effus|diffus|mean free|root.mean|rms|compressib|adiabat|isotherm|work|heat|"
237
+ r"partial pressure|mole fraction|mixture|reaction|react|produced|collected over water|vapor pressure|"
238
+ r"balloon|leak|graham|kinetic energy|speed|velocity|collision", low):
239
+ return None
240
+ P, V, T, M = r.q(P_UNITS, p_atm), r.q(V_UNITS, v_l), r.q(T_UNITS, t_k), r.q(M_UNITS, m_g)
241
+ D = r.q(r"g\s*/\s*(?:l|liters?|litres?|dm3)|g\s*l\^?-1|grams? per liter", lambda v, u: v)
242
+ N = r.q(r"moles?|mol(?![a-z])", lambda v, u: v)
243
+ if any(len(x) > 1 for x in (P, V, T, M, D, N)):
244
+ return None
245
+ stp = bool(re.search(r"\bstp\b|standard (?:temperature and pressure|conditions)", low))
246
+ fs = formulas(r.t)
247
+ ask = None
248
+ for k, pat in (("density", r"what is the density|calculate the density|find the density|determine the density|density of .* (?:at|under)"),
249
+ ("molar", r"molar mass|molecular weight|molecular mass"),
250
+ ("pressure", r"what (?:is the )?pressure|calculate the pressure|find the pressure|what pressure"),
251
+ ("volume", r"what (?:is the )?volume|calculate the volume|find the volume|what volume|occupy"),
252
+ ("temperature", r"what (?:is the )?temperature|at what temperature|calculate the temperature|find the temperature"),
253
+ ("moles", r"how many moles|number of moles|moles of gas"),
254
+ ("mass", r"what (?:is the )?mass|how many grams|mass of gas")):
255
+ if re.search(pat, low):
256
+ ask = k
257
+ break
258
+ if ask is None:
259
+ return None
260
+ Tk = r.use(T[0]) if T else (273.15 if stp else None)
261
+ Pa = r.use(P[0]) if P else (1.0 if stp else None)
262
+ f = fs[0][0] if len({x[0] for x in fs}) == 1 else None
263
+ Mm = molar_mass(f) if f else None
264
+ if ask == "density":
265
+ if D or not (Tk and Pa and Mm):
266
+ return None
267
+ d = Pa * Mm / (R_LATM * Tk)
268
+ out = say("gas_density", "The density of %s is %s g/L (d = PM/RT = %s atm × %s g/mol / (0.082057 L·atm/(mol·K) × %s K))." % (f, sig(d), sig(Pa), sig(Mm), sig(Tk)), d)
269
+ elif ask == "molar":
270
+ if not (D and Tk and Pa):
271
+ return None
272
+ d = r.use(D[0])
273
+ mm = d * R_LATM * Tk / Pa
274
+ out = say("gas_molar_mass", "The molar mass is %s g/mol (M = dRT/P = %s g/L × 0.082057 × %s K / %s atm)." % (sig(mm), sig(d), sig(Tk), sig(Pa)), mm)
275
+ else:
276
+ n = r.use(N[0]) if N else None
277
+ if n is None and M and Mm:
278
+ n = r.use(M[0]) / Mm
279
+ Vl = r.use(V[0]) if V else None
280
+ known = {"pressure": Pa, "volume": Vl, "temperature": Tk, "moles": n}
281
+ if ask == "mass":
282
+ if not (Pa and Vl and Tk and Mm) or N or M:
283
+ return None
284
+ nn = Pa * Vl / (R_LATM * Tk)
285
+ out = say("gas_mass", "The mass is %s g (n = PV/RT = %s mol; × %s g/mol for %s)." % (sig(nn * Mm), sig(nn), sig(Mm), f), nn * Mm)
286
+ else:
287
+ others = {k: v for k, v in known.items() if k != ask}
288
+ if any(v is None for v in others.values()):
289
+ return None
290
+ if ask == "pressure":
291
+ v = n * R_LATM * Tk / Vl
292
+ out = say("gas_pressure", "The pressure is %s atm (P = nRT/V)." % sig(v), v)
293
+ elif ask == "volume":
294
+ v = n * R_LATM * Tk / Pa
295
+ out = say("gas_volume", "The volume is %s L (V = nRT/P)." % sig(v), v)
296
+ elif ask == "temperature":
297
+ v = Pa * Vl / (n * R_LATM)
298
+ out = say("gas_temperature", "The temperature is %s K (T = PV/nR)." % sig(v), v)
299
+ else:
300
+ v = Pa * Vl / (R_LATM * Tk)
301
+ out = say("gas_moles", "The amount is %s mol (n = PV/RT)." % sig(v), v)
302
+ return out if r.all_read() else None
303
+
304
+
305
+ STRONG_ACID = {"HCl": 1, "HBr": 1, "HI": 1, "HNO3": 1, "HClO4": 1}
306
+ STRONG_BASE = {"NaOH": 1, "KOH": 1, "LiOH": 1, "Ba(OH)2": 2, "Ca(OH)2": 2, "Sr(OH)2": 2}
307
+
308
+
309
+ class PhAgent:
310
+ def answer(self, text, session_id="default"):
311
+ r = Reader(text)
312
+ low = r.low
313
+ if multi_part(low) or not re.search(r"\bph\b|\bpoh\b|\[h\+\]|\[h3o\+\]|hydrogen ion|hydronium", low):
314
+ return None
315
+ if re.search(r"titrat|added|adding|mix|mixed|after|equivalence|polyprotic|diprotic|ksp|solubility|hydroly[sz]|salt|"
316
+ r"percent|degree of|ionization constant of water|at \d+ deg|temperature", low):
317
+ return None
318
+ conc = r.q(r"m(?![a-z])|molar|mol/l|moles? per liter|n(?![a-z])", lambda v, u: v)
319
+ ka = [(m, m.start()) for m in re.finditer(r"\b(k_?a|k_?b|pk_?a|pk_?b)\s*(?:=|is|of|value of)?\s*(" + NUMRE.pattern + ")", r.t, re.I)]
320
+ ask = "pH" if re.search(r"what is the ph|calculate the ph|find the ph|determine the ph|ph of|the ph\b", low) else \
321
+ "pOH" if re.search(r"\bpoh\b", low) and not re.search(r"\bph\b(?! ?=)", low.replace("poh", "")) else \
322
+ "H" if re.search(r"\[h\+\]|\[h3o\+\]|hydrogen ion concentration|hydronium ion concentration", low) else None
323
+ if ask is None:
324
+ return None
325
+ hgiven = re.search(r"(?:hydrogen ion|hydronium(?: ion)?|\[h\+\]|\[h3o\+\])\s*(?:concentration\s*)?(?:is|of|=|equal to)\s*(" + NUMRE.pattern + r")\s*m\b", r.t, re.I)
326
+ if ask == "pH" and hgiven and not ka:
327
+ h = num(hgiven.group(1))
328
+ r.used.add(hgiven.start(1))
329
+ if not (0 < h <= 10):
330
+ return None
331
+ return say("ph_from_h", "The pH is %s (pH = -log10[H+] = -log10 %s)." % ("%.2f" % -math.log10(h), sig(h)), -math.log10(h)) if r.all_read() else None
332
+ phgiven = re.search(r"\bph (?:of|is|=)\s*(" + NUMRE.pattern + ")", r.t, re.I)
333
+ if ask == "H" and phgiven and not conc and not ka:
334
+ p = num(phgiven.group(1))
335
+ r.used.add(phgiven.start(1))
336
+ h = 10 ** -p
337
+ return say("h_from_ph", "[H+] is %s M (10^-pH with pH %s)." % (sig(h), sig(p)), h) if r.all_read() else None
338
+ fs = [x[0] for x in formulas(r.t)]
339
+ if len(conc) != 1:
340
+ return None
341
+ c = r.use(conc[0])
342
+ if not ka:
343
+ acid = [f for f in fs if f in STRONG_ACID]
344
+ base = [f for f in fs if f in STRONG_BASE]
345
+ if len(set(acid + base)) != 1 or c <= 1e-7:
346
+ return None
347
+ if acid:
348
+ h = c * STRONG_ACID[acid[0]]
349
+ ph = -math.log10(h)
350
+ what = "%s is a strong acid, fully ionized: [H+] = %s M" % (acid[0], sig(h))
351
+ else:
352
+ oh = c * STRONG_BASE[base[0]]
353
+ ph = 14 + math.log10(oh)
354
+ what = "%s is a strong base, fully dissociated: [OH-] = %s M" % (base[0], sig(oh))
355
+ else:
356
+ if len(ka) != 1:
357
+ return None
358
+ m, pos = ka[0]
359
+ r.used.add(m.start(2))
360
+ kind, kv = m.group(1).lower().replace("_", ""), num(m.group(2))
361
+ k = 10 ** -kv if kind.startswith("p") else kv
362
+ if not (0 < k < 1):
363
+ return None
364
+ x = (-k + math.sqrt(k * k + 4 * k * c)) / 2 # exact quadratic: x^2 / (c - x) = K
365
+ if kind.endswith("b"):
366
+ ph = 14 + math.log10(x)
367
+ what = "weak base, Kb = %s: [OH-] = %s M from x^2/(C - x) = Kb" % (sig(k), sig(x))
368
+ else:
369
+ ph = -math.log10(x)
370
+ what = "weak acid, Ka = %s: [H+] = %s M from x^2/(C - x) = Ka" % (sig(k), sig(x))
371
+ h = 10 ** -ph
372
+ if not r.all_read():
373
+ return None
374
+ if ask == "pH":
375
+ return say("ph", "The pH is %s (%s)." % ("%.2f" % ph, what), ph)
376
+ if ask == "pOH":
377
+ return say("poh", "The pOH is %s (%s)." % ("%.2f" % (14 - ph), what), 14 - ph)
378
+ return say("h", "[H+] is %s M (%s)." % (sig(10 ** -ph), what), 10 ** -ph)
379
+
380
+
381
+ class PhotonAgent:
382
+ LEN = {"nm": 1e-9, "nanometers": 1e-9, "nanometer": 1e-9, "a": 1e-10, "angstroms": 1e-10, "angstrom": 1e-10, "å": 1e-10,
383
+ "m": 1, "meters": 1, "cm": 1e-2, "um": 1e-6, "μm": 1e-6, "micrometers": 1e-6, "microns": 1e-6, "pm": 1e-12, "mm": 1e-3}
384
+
385
+ def answer(self, text, session_id="default"):
386
+ r = Reader(text)
387
+ low = r.low
388
+ if multi_part(low) or not re.search(r"photon|wavelength|frequency|light|radiation|de ?broglie|transition|emitted|absorbed|quantum", low):
389
+ return None
390
+ if re.search(r"work function|photoelectric|threshold|stopping|intensity|power|watt|lamp|laser|per second|uncertainty|"
391
+ r"box|particle in|harmonic|rotational|vibrational|bond|spin|nmr|esr|epr|zeeman|doppler", low):
392
+ return None
393
+ if re.search(r"black ?body", low) and re.search(r"(?:maximum|peak|most intense).{0,30}wavelength|wavelength.{0,30}(?:maximum|peak)", low):
394
+ T = r.q(T_UNITS, t_k)
395
+ if len(T) != 1:
396
+ return None
397
+ lm = 2.897771955e-3 / r.use(T[0])
398
+ return say("wien", "The peak wavelength is %s m (Wien: λmax = b/T, b = 2.8978e-3 m·K)." % sig(lm), lm) if r.all_read() else None
399
+ lam = r.q(r"nm|nanometers?|angstroms?|å|pm|um|μm|micrometers?|microns?|cm(?![-^\d])|mm(?![a-z])|m(?![a-z/^\-])", lambda v, u: v * self.LEN[u])
400
+ nu = r.q(r"hz|s\^?-1|sec\^?-1|/s", lambda v, u: v)
401
+ # de Broglie
402
+ if re.search(r"de ?broglie|wavelength of (?:an?|the) (?:electron|proton|neutron|particle|ball)", low):
403
+ vel = r.q(r"m/s|m s\^?-1|meters? per second", lambda v, u: v)
404
+ mass = r.q(M_UNITS, lambda v, u: m_g(v, u) / 1000)
405
+ ke = r.q(r"ev|electron ?volts?", lambda v, u: v * EV)
406
+ m_ = ME if "electron" in low and not mass else (r.use(mass[0]) if len(mass) == 1 else None)
407
+ if m_ is None or len(vel) + len(ke) != 1:
408
+ return None
409
+ p = m_ * r.use(vel[0]) if vel else math.sqrt(2 * m_ * r.use(ke[0]))
410
+ lam_ = H / p
411
+ return say("de_broglie", "The de Broglie wavelength is %s m (λ = h/p, p = %s kg·m/s)." % (sig(lam_), sig(p)), lam_) if r.all_read() else None
412
+ # hydrogen-like transitions
413
+ ns = re.findall(r"n\s*=\s*(\d+)", low)
414
+ if len(ns) == 2 and re.search(r"hydrogen|he\+|li2\+|transition|electron (?:falls|drops|jumps|moves)", low):
415
+ z = 2 if re.search(r"\bhe\+|helium ion|ionized helium", low) else 3 if re.search(r"li2\+|li\+\+", low) else 1
416
+ if z == 1 and not re.search(r"hydrogen", low):
417
+ return None
418
+ for m in re.finditer(r"n\s*=\s*(\d+)", r.t, re.I):
419
+ r.used.add(m.start(1))
420
+ n1, n2 = sorted(int(x) for x in ns)
421
+ if n1 == n2:
422
+ return None
423
+ wn = RH * z * z * (1 / n1 ** 2 - 1 / n2 ** 2) # m^-1
424
+ E = H * C * wn
425
+ if re.search(r"wavelength", low):
426
+ v, txt = 1 / wn, "The wavelength is %s m (%s nm), from 1/λ = R_H Z² (1/%d² − 1/%d²)." % (sig(1 / wn), sig(1e9 / wn), n1, n2)
427
+ elif re.search(r"frequency", low):
428
+ v, txt = wn * C, "The frequency is %s Hz (ν = c R_H Z² (1/%d² − 1/%d²))." % (sig(wn * C), n1, n2)
429
+ elif re.search(r"energy", low):
430
+ v, txt = E, "The energy is %s J per photon (%s eV), ΔE = hc R_H Z² (1/%d² − 1/%d²)." % (sig(E), sig(E / EV), n1, n2)
431
+ elif re.search(r"wave ?number", low):
432
+ v, txt = wn / 100, "The wavenumber is %s cm^-1." % sig(wn / 100)
433
+ else:
434
+ return None
435
+ return say("transition", txt, v) if r.all_read() else None
436
+ # photon: energy <-> wavelength <-> frequency
437
+ if len(lam) + len(nu) != 1:
438
+ return None
439
+ if lam:
440
+ L = r.use(lam[0])
441
+ f = C / L
442
+ else:
443
+ f = r.use(nu[0])
444
+ L = C / f
445
+ E = H * f
446
+ if re.search(r"(?:energy|kj|kilojoules?).*(?:per mole|/mol|a mole|one mole|mole of photons)|per mole of photons|einstein", low):
447
+ v, txt = E * NA / 1000, "The energy is %s kJ/mol (E = N_A hc/λ)." % sig(E * NA / 1000)
448
+ elif re.search(r"\bev\b|electron ?volts?", low) and re.search(r"energy", low):
449
+ v, txt = E / EV, "The energy is %s eV (E = hc/λ)." % sig(E / EV)
450
+ elif re.search(r"energy", low):
451
+ v, txt = E, "The energy is %s J per photon (E = hc/λ = %s J·s × %s m/s / %s m)." % (sig(E), H, C, sig(L))
452
+ elif re.search(r"frequency", low) and lam:
453
+ v, txt = f, "The frequency is %s Hz (ν = c/λ)." % sig(f)
454
+ elif re.search(r"wavelength", low) and nu:
455
+ v, txt = L, "The wavelength is %s m (λ = c/ν)." % sig(L)
456
+ else:
457
+ return None
458
+ return say("photon", txt, v) if r.all_read() else None
459
+
460
+
461
+ KF = {"water": 1.86, "benzene": 5.12, "camphor": 37.7, "cyclohexane": 20.0}
462
+ KB = {"water": 0.512, "benzene": 2.53}
463
+ NONELECTROLYTE = ("glucose", "sucrose", "urea", "sugar", "ethylene glycol", "glycerol", "C6H12O6", "C12H22O11", "CH4N2O", "nonelectrolyte", "non-electrolyte")
464
+
465
+
466
+ class ColligativeAgent:
467
+ def answer(self, text, session_id="default"):
468
+ r = Reader(text)
469
+ low = r.low
470
+ if multi_part(low) or not re.search(r"freezing|boiling|osmotic|molality|molal", low):
471
+ return None
472
+ if re.search(r"vapor pressure|raoult|percent|dissociat|association|degree|van.t hoff factor of|dimer|ionized|electrolyte(?! )|"
473
+ r"activity|ideal solution", low):
474
+ return None
475
+ solv = [s for s in KF if s in low]
476
+ nonel = any(s.lower() in low for s in NONELECTROLYTE)
477
+ if not nonel or len(set(solv)) > 1:
478
+ return None
479
+ Kf_given = re.search(r"\bk_?f\b[^\d]{0,20}(" + NUMRE.pattern + ")", r.t, re.I)
480
+ Kb_given = re.search(r"\bk_?b\b[^\d]{0,20}(" + NUMRE.pattern + ")", r.t, re.I)
481
+ masses = r.q(M_UNITS, m_g)
482
+ molal = r.q(r"m(?![a-z])|molal", lambda v, u: v)
483
+ s = solv[0] if solv else "water"
484
+ if "osmotic" in low:
485
+ conc = r.q(r"m(?![a-z])|molar|mol/l", lambda v, u: v)
486
+ T = r.q(T_UNITS, t_k)
487
+ if len(conc) != 1 or len(T) != 1:
488
+ return None
489
+ pi = r.use(conc[0]) * R_LATM * r.use(T[0])
490
+ return say("osmotic", "The osmotic pressure is %s atm (π = MRT, nonelectrolyte)." % sig(pi), pi) if r.all_read() else None
491
+ freezing = "freezing" in low
492
+ K = None
493
+ if freezing:
494
+ if Kf_given:
495
+ K = num(Kf_given.group(1))
496
+ r.used.add(Kf_given.start(1))
497
+ else:
498
+ K = KF.get(s)
499
+ else:
500
+ if Kb_given:
501
+ K = num(Kb_given.group(1))
502
+ r.used.add(Kb_given.start(1))
503
+ else:
504
+ K = KB.get(s)
505
+ if K is None:
506
+ return None
507
+ if len(molal) == 1 and not masses:
508
+ m = r.use(molal[0])
509
+ elif len(masses) == 2:
510
+ fs = [x[0] for x in formulas(r.t) if x[0] in ("C6H12O6", "C12H22O11", "CH4N2O") or x[0] not in ("H2O",)]
511
+ solute = [f for f in fs if f != {"water": "H2O"}.get(s)]
512
+ mm = molar_mass(solute[0]) if len(set(solute)) == 1 else None
513
+ if mm is None:
514
+ return None
515
+ a, b = sorted(masses, key=lambda x: x[0])
516
+ m = (r.use(a) / mm) / (r.use(b) / 1000)
517
+ else:
518
+ return None
519
+ dT = K * m
520
+ if freezing:
521
+ base = {"water": 0.0, "benzene": 5.5, "cyclohexane": 6.5, "camphor": 179.8}[s]
522
+ if re.search(r"freezing point (?:of the solution|will be|is)|new freezing|at what temperature.*freez|freezes at", low):
523
+ return say("freezing_point", "The freezing point is %s deg C (ΔTf = Kf·m = %s × %s = %s deg C below %s deg C)." % (sig(base - dT), K, sig(m), sig(dT), base), base - dT) if r.all_read() else None
524
+ if re.search(r"depression|lowered|by how much|change in", low):
525
+ return say("freezing_depression", "The freezing point is lowered by %s deg C (ΔTf = Kf·m = %s × %s)." % (sig(dT), K, sig(m)), dT) if r.all_read() else None
526
+ else:
527
+ base = {"water": 100.0, "benzene": 80.1}[s]
528
+ if re.search(r"boiling point (?:of the solution|will be|is)|new boiling|boils at", low):
529
+ return say("boiling_point", "The boiling point is %s deg C (ΔTb = Kb·m = %s × %s = %s deg C above %s deg C)." % (sig(base + dT), K, sig(m), sig(dT), base), base + dT) if r.all_read() else None
530
+ if re.search(r"elevation|raised|by how much|change in", low):
531
+ return say("boiling_elevation", "The boiling point rises by %s deg C (ΔTb = Kb·m)." % sig(dT), dT) if r.all_read() else None
532
+ return None
533
+
534
+
535
+ class KineticsAgent:
536
+ T = {"s": 1, "sec": 1, "seconds": 1, "second": 1, "min": 60, "minutes": 60, "minute": 60, "h": 3600, "hr": 3600, "hours": 3600,
537
+ "hour": 3600, "days": 86400, "day": 86400, "d": 86400, "years": 3.15576e7, "year": 3.15576e7, "yr": 3.15576e7, "y": 3.15576e7}
538
+
539
+ def answer(self, text, session_id="default"):
540
+ r = Reader(text)
541
+ low = r.low
542
+ if multi_part(low) or not re.search(r"half-?life|first-?order|decay", low):
543
+ return None
544
+ if re.search(r"second-?order|zero-?order|activation|arrhenius|temperature|catalyst|equilibrium|counts|dpm|"
545
+ r"age of|dating|carbon-14|c-14|14c", low):
546
+ return None
547
+ tq = r.q(r"seconds?|sec|s(?![a-z])|minutes?|min(?![a-z])|hours?|hrs?|h(?![a-z])|days?|d(?![a-z])|years?|yrs?|y(?![a-z])",
548
+ lambda v, u: v * self.T[u])
549
+ pct = r.q(r"%|percent", lambda v, u: v / 100)
550
+ frac = re.search(r"\b(one|a) (half|third|fourth|quarter|tenth|eighth|sixteenth)\b", low)
551
+ half = re.search(r"half-?life (?:of|is|=)", low)
552
+ amt = r.q(r"mci|μci|uci|ci(?![a-z])|bq|mg|kg|g(?![a-z])|grams?", lambda v, u: v)
553
+ unit = re.search(r"(" + NUMRE.pattern + r")\s*(mCi|μCi|uCi|Ci|Bq|mg|kg|g|grams?)(?![a-z])", r.t)
554
+ if len(tq) == 2 and half and len(amt) == 1 and re.search(r"how much.*(?:remain|left)|(?:remain|left) after", low):
555
+ a, b = tq
556
+ th, t = (a, b) if a[1] < b[1] else (b, a)
557
+ th_v, t_v = r.use(th), r.use(t)
558
+ left = r.use(amt[0]) * 0.5 ** (t_v / th_v)
559
+ return say("amount_left", "The amount remaining is %s %s (× (1/2)^(t/t½) = (1/2)^%s)." % (sig(left), unit.group(unit.lastindex), sig(t_v / th_v)), left) if r.all_read() else None
560
+ if len(tq) == 2 and half and re.search(r"(?:what|how much|what fraction|what percent).*(?:remain|left)", low):
561
+ a, b = tq
562
+ th, t = (a, b) if a[1] < b[1] else (b, a)
563
+ th_v, t_v = r.use(th), r.use(t)
564
+ f = 0.5 ** (t_v / th_v)
565
+ return say("fraction_left", "The fraction remaining is %s (%s%%), (1/2)^(t/t½)." % (sig(f), sig(100 * f)), f) if r.all_read() else None
566
+ if len(tq) == 1 and half and (pct or frac) and re.search(r"how long|how much time|what time", low):
567
+ th_v = r.use(tq[0])
568
+ if pct:
569
+ p = r.use(pct[0])
570
+ f = p if re.search(r"remain|left", low) else 1 - p
571
+ else:
572
+ f = {"half": .5, "third": 1 / 3, "fourth": .25, "quarter": .25, "tenth": .1, "eighth": .125, "sixteenth": 1 / 16}[frac.group(2)]
573
+ if not re.search(r"remain|left", low):
574
+ return None
575
+ if not (0 < f < 1):
576
+ return None
577
+ t = th_v * math.log(f) / math.log(0.5)
578
+ return say("time_to_fraction", "It takes %s s (t = t½ · log(f)/log(1/2), f = %s)." % (sig(t), sig(f)), t) if r.all_read() else None
579
+ if len(tq) == 1 and half and re.search(r"rate constant|\bk\b", low):
580
+ k = math.log(2) / r.use(tq[0])
581
+ return say("rate_constant", "The rate constant is %s s^-1 (k = ln 2 / t½)." % sig(k), k) if r.all_read() else None
582
+ return None
583
+
584
+
585
+ AGENTS = {"chem-gas": GasAgent, "chem-ph": PhAgent, "chem-photon": PhotonAgent, "chem-colligative": ColligativeAgent,
586
+ "chem-kinetics": KineticsAgent}
root/wander-train/code_organ_v2.py ADDED
@@ -0,0 +1,474 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """code_organ_v2 - v1 (MBPP-frozen b2e52a1d) + STRICT mode for a single example: every move named by the
2
+ request. Programs as specified free moves over a verified function library, checked by execution; every
3
+ verified program joins the library (the stored final forms - continuous learning without retraining).
4
+
5
+ REQUEST description + input/output examples (asserts) -> function name, argument values and types, expected outputs,
6
+ constants the description names ("at least 4 characters", "the character 'l'")
7
+ MOVES typed library functions. The TYPE SYSTEM is the specified structure: a function applies only where its
8
+ argument types connect; within that the choice is free. The description's words rank the moves.
9
+ SEARCH bottom-up enumeration with OBSERVATIONAL EQUIVALENCE: programs that behave identically on the examples are
10
+ kept once, so compositions of 3-4 moves stay tractable.
11
+ CHECK the rendered source is executed against every example; nothing is released that did not run and pass.
12
+ STORE a verified program becomes a library function (its description words are its keywords, its signature its
13
+ type); later requests compose it - measured as the library-growth curve.
14
+ Only library templates are ever evaluated; no text from the request is executed."""
15
+ from __future__ import annotations
16
+
17
+ import ast
18
+ import itertools
19
+ import json
20
+ import math
21
+ import re
22
+ from collections import Counter
23
+
24
+ # ----------------------------------------------------------------------------------------------------------- types
25
+ def tyof(v):
26
+ if isinstance(v, bool):
27
+ return "bool"
28
+ if isinstance(v, int):
29
+ return "int"
30
+ if isinstance(v, float):
31
+ return "float"
32
+ if isinstance(v, str):
33
+ return "str"
34
+ if isinstance(v, list):
35
+ return "list"
36
+ if isinstance(v, tuple):
37
+ return "tuple"
38
+ if isinstance(v, dict):
39
+ return "dict"
40
+ if isinstance(v, set):
41
+ return "set"
42
+ return "any"
43
+
44
+
45
+ GROUP = {"num": {"int", "float"}, "seq": {"list", "tuple", "str"}, "coll": {"list", "tuple", "set"},
46
+ "any": {"bool", "int", "float", "str", "list", "tuple", "dict", "set", "any"}}
47
+
48
+
49
+ def fits(t, want):
50
+ return t == want or t in GROUP.get(want, ())
51
+
52
+
53
+ # ------------------------------------------------------------------------------------------------------ primitives
54
+ class Prim:
55
+ __slots__ = ("name", "ins", "out", "fn", "src", "kw")
56
+
57
+ def __init__(self, name, ins, out, fn, src, kw):
58
+ self.name, self.ins, self.out, self.fn, self.src = name, ins, out, fn, src
59
+ self.kw = {w.lower().rstrip("s") for w in kw.split()} # v2: normalised exactly like the request words
60
+
61
+
62
+ def _big(x):
63
+ if isinstance(x, int) and abs(x) > 10 ** 7:
64
+ raise ValueError("too large")
65
+ return x
66
+
67
+
68
+ def _is_prime(n):
69
+ if n < 2:
70
+ return False
71
+ return all(n % d for d in range(2, int(n ** 0.5) + 1))
72
+
73
+
74
+ def _uniq(xs):
75
+ out = []
76
+ for x in xs:
77
+ if x not in out:
78
+ out.append(x)
79
+ return out
80
+
81
+
82
+ def _flatten(xs):
83
+ out = []
84
+ for x in xs:
85
+ out += list(x) if isinstance(x, (list, tuple)) else [x]
86
+ return out
87
+
88
+
89
+ P_ = [
90
+ # numbers
91
+ Prim("add", ("num", "num"), "num", lambda a, b: a + b, "({0} + {1})", "add sum plus total"),
92
+ Prim("sub", ("num", "num"), "num", lambda a, b: a - b, "({0} - {1})", "subtract difference minus"),
93
+ Prim("mul", ("num", "num"), "num", lambda a, b: _big(a * b), "({0} * {1})", "multiply product times"),
94
+ Prim("fdiv", ("num", "num"), "num", lambda a, b: a // b, "({0} // {1})", "divide quotient integer"),
95
+ Prim("div", ("num", "num"), "float", lambda a, b: a / b, "({0} / {1})", "divide ratio average"),
96
+ Prim("mod", ("num", "num"), "num", lambda a, b: a % b, "({0} % {1})", "remainder modulo divisible"),
97
+ Prim("pow", ("num", "num"), "num", lambda a, b: _big(a ** b) if b < 64 else 1 / 0, "({0} ** {1})", "power square cube exponent"),
98
+ Prim("sq", ("num",), "num", lambda a: a * a, "({0} ** 2)", "square squares"),
99
+ Prim("cube", ("num",), "num", lambda a: a ** 3, "({0} ** 3)", "cube cubes"),
100
+ Prim("abs", ("num",), "num", abs, "abs({0})", "absolute"),
101
+ Prim("neg", ("num",), "num", lambda a: -a, "(-{0})", "negative negate"),
102
+ Prim("half", ("num",), "float", lambda a: a / 2, "({0} / 2)", "half area triangle"),
103
+ Prim("sqrt", ("num",), "float", math.sqrt, "math.sqrt({0})", "root square"),
104
+ Prim("isqrt", ("int",), "int", math.isqrt, "math.isqrt({0})", "root square integer"),
105
+ Prim("fact", ("int",), "int", lambda n: math.factorial(n) if 0 <= n <= 30 else 1 / 0, "math.factorial({0})", "factorial"),
106
+ Prim("gcd", ("int", "int"), "int", math.gcd, "math.gcd({0}, {1})", "gcd greatest common divisor"),
107
+ Prim("lcm", ("int", "int"), "int", lambda a, b: a * b // math.gcd(a, b) if a and b else 0, "({0} * {1} // math.gcd({0}, {1}))", "lcm least common multiple"),
108
+ Prim("isprime", ("int",), "bool", _is_prime, "(({0}) > 1 and all(({0}) % d for d in range(2, int(({0}) ** 0.5) + 1)))", "prime"),
109
+ Prim("even", ("int",), "bool", lambda n: n % 2 == 0, "({0} % 2 == 0)", "even"),
110
+ Prim("odd", ("int",), "bool", lambda n: n % 2 == 1, "({0} % 2 == 1)", "odd"),
111
+ Prim("digits", ("int",), "list", lambda n: [int(c) for c in str(abs(n))], "[int(c) for c in str(abs({0}))]", "digits digit"),
112
+ Prim("revnum", ("int",), "int", lambda n: int(str(abs(n))[::-1]), "int(str(abs({0}))[::-1])", "reverse number"),
113
+ Prim("bin", ("int",), "str", lambda n: bin(n)[2:], "bin({0})[2:]", "binary"),
114
+ Prim("frombin", ("str",), "int", lambda s: int(s, 2), "int({0}, 2)", "binary decimal convert"),
115
+ Prim("popcount", ("int",), "int", lambda n: bin(n).count("1"), "bin({0}).count('1')", "set bits count ones"),
116
+ Prim("xor", ("int", "int"), "int", lambda a, b: a ^ b, "({0} ^ {1})", "xor bit differ"),
117
+ Prim("ispow2", ("int",), "bool", lambda n: n > 0 and n & (n - 1) == 0, "({0} > 0 and {0} & ({0} - 1) == 0)", "power two bit"),
118
+ Prim("eq", ("any", "any"), "bool", lambda a, b: a == b, "({0} == {1})", "equal same"),
119
+ Prim("lt", ("num", "num"), "bool", lambda a, b: a < b, "({0} < {1})", "less smaller"),
120
+ Prim("gt", ("num", "num"), "bool", lambda a, b: a > b, "({0} > {1})", "greater larger"),
121
+ Prim("not", ("bool",), "bool", lambda a: not a, "(not {0})", "not"),
122
+ Prim("and", ("bool", "bool"), "bool", lambda a, b: a and b, "({0} and {1})", "and both"),
123
+ Prim("or", ("bool", "bool"), "bool", lambda a, b: a or b, "({0} or {1})", "or either"),
124
+ Prim("maxn", ("num", "num"), "num", max, "max({0}, {1})", "maximum largest bigger"),
125
+ Prim("minn", ("num", "num"), "num", min, "min({0}, {1})", "minimum smallest"),
126
+ Prim("round2", ("num",), "float", lambda a: round(a, 2), "round({0}, 2)", "round"),
127
+ Prim("int", ("any",), "int", int, "int({0})", "integer convert"),
128
+ Prim("str", ("any",), "str", str, "str({0})", "string convert"),
129
+ Prim("float", ("num",), "float", float, "float({0})", "float"),
130
+ Prim("tri", ("int",), "int", lambda n: n * (n + 1) // 2, "({0} * ({0} + 1) // 2)", "triangular sum natural"),
131
+ Prim("pi_r2", ("num",), "float", lambda r: math.pi * r * r, "(math.pi * {0} * {0})", "area circle"),
132
+ Prim("circ", ("num",), "float", lambda r: 2 * math.pi * r, "(2 * math.pi * {0})", "circumference perimeter circle"),
133
+ # sequences
134
+ Prim("len", ("seq",), "int", len, "len({0})", "length count number size"),
135
+ Prim("sum", ("coll",), "num", lambda xs: sum(xs), "sum({0})", "sum total add"),
136
+ Prim("max", ("coll",), "any", max, "max({0})", "maximum largest biggest max longest"),
137
+ Prim("min", ("coll",), "any", min, "min({0})", "minimum smallest min"),
138
+ Prim("sorted", ("coll",), "list", sorted, "sorted({0})", "sort sorted ascending order"),
139
+ Prim("sorted_desc", ("coll",), "list", lambda xs: sorted(xs, reverse=True), "sorted({0}, reverse=True)", "sort descending largest"),
140
+ Prim("rev", ("seq",), "seq", lambda xs: xs[::-1], "{0}[::-1]", "reverse reversed"),
141
+ Prim("first", ("seq",), "any", lambda xs: xs[0], "{0}[0]", "first"),
142
+ Prim("last", ("seq",), "any", lambda xs: xs[-1], "{0}[-1]", "last"),
143
+ Prim("take", ("seq", "int"), "seq", lambda xs, n: xs[:n], "{0}[:{1}]", "first n smallest largest take"),
144
+ Prim("drop", ("seq", "int"), "seq", lambda xs, n: xs[n:], "{0}[{1}:]", "remove first skip"),
145
+ Prim("at", ("seq", "int"), "any", lambda xs, i: xs[i], "{0}[{1}]", "nth index element second third"),
146
+ Prim("tolist", ("any",), "list", list, "list({0})", "list convert"),
147
+ Prim("totuple", ("any",), "tuple", tuple, "tuple({0})", "tuple convert"),
148
+ Prim("toset", ("coll",), "set", set, "set({0})", "unique distinct set"),
149
+ Prim("uniq", ("coll",), "list", _uniq, "list(dict.fromkeys({0}))", "unique distinct duplicates remove"),
150
+ Prim("inter", ("coll", "coll"), "tuple", lambda a, b: tuple(sorted(set(a) & set(b))), "tuple(sorted(set({0}) & set({1})))", "common shared similar intersection"),
151
+ Prim("union", ("coll", "coll"), "tuple", lambda a, b: tuple(sorted(set(a) | set(b))), "tuple(sorted(set({0}) | set({1})))", "union combine"),
152
+ Prim("diff", ("coll", "coll"), "list", lambda a, b: [x for x in a if x not in b], "[x for x in {0} if x not in {1}]", "difference not in"),
153
+ Prim("concat", ("seq", "seq"), "seq", lambda a, b: a + b, "({0} + {1})", "concatenate join append combine merge"),
154
+ Prim("count", ("seq", "any"), "int", lambda xs, x: xs.count(x), "{0}.count({1})", "count occurrences frequency"),
155
+ Prim("index", ("seq", "any"), "int", lambda xs, x: xs.index(x), "{0}.index({1})", "index position find"),
156
+ Prim("contains", ("any", "seq"), "bool", lambda x, xs: x in xs, "({0} in {1})", "contains present exists check"),
157
+ Prim("allsame", ("coll",), "bool", lambda xs: len(set(xs)) <= 1, "(len(set({0})) <= 1)", "same equal all identical"),
158
+ Prim("alluniq", ("coll",), "bool", lambda xs: len(set(xs)) == len(xs), "(len(set({0})) == len(list({0})))", "unique distinct duplicate"),
159
+ Prim("issorted", ("coll",), "bool", lambda xs: list(xs) == sorted(xs), "(list({0}) == sorted({0}))", "sorted order check"),
160
+ Prim("prod", ("coll",), "num", lambda xs: _big(math.prod(xs)), "math.prod({0})", "product multiply"),
161
+ Prim("mean", ("coll",), "float", lambda xs: sum(xs) / len(xs), "(sum({0}) / len({0}))", "average mean"),
162
+ Prim("map_sq", ("coll",), "list", lambda xs: [x * x for x in xs], "[x * x for x in {0}]", "square squares each"),
163
+ Prim("map_cube", ("coll",), "list", lambda xs: [x ** 3 for x in xs], "[x ** 3 for x in {0}]", "cube cubes each"),
164
+ Prim("map_neg", ("coll",), "list", lambda xs: [-x for x in xs], "[-x for x in {0}]", "negative"),
165
+ Prim("map_abs", ("coll",), "list", lambda xs: [abs(x) for x in xs], "[abs(x) for x in {0}]", "absolute"),
166
+ Prim("map_str", ("coll",), "list", lambda xs: [str(x) for x in xs], "[str(x) for x in {0}]", "string convert"),
167
+ Prim("map_int", ("coll",), "list", lambda xs: [int(x) for x in xs], "[int(x) for x in {0}]", "integer convert"),
168
+ Prim("map_len", ("coll",), "list", lambda xs: [len(x) for x in xs], "[len(x) for x in {0}]", "length each"),
169
+ Prim("map_first", ("coll",), "list", lambda xs: [x[0] for x in xs], "[x[0] for x in {0}]", "first each"),
170
+ Prim("map_last", ("coll",), "list", lambda xs: [x[-1] for x in xs], "[x[-1] for x in {0}]", "last each"),
171
+ Prim("map_sum", ("coll",), "list", lambda xs: [sum(x) for x in xs], "[sum(x) for x in {0}]", "sum each"),
172
+ Prim("map_add", ("coll", "num"), "list", lambda xs, k: [x + k for x in xs], "[x + {1} for x in {0}]", "add each"),
173
+ Prim("map_mul", ("coll", "num"), "list", lambda xs, k: [x * k for x in xs], "[x * {1} for x in {0}]", "multiply each double doubled twice"),
174
+ Prim("zipadd", ("coll", "coll"), "list", lambda a, b: [x + y for x, y in zip(a, b)], "[x + y for x, y in zip({0}, {1})]", "add two lists elementwise"),
175
+ Prim("zipmul", ("coll", "coll"), "list", lambda a, b: [x * y for x, y in zip(a, b)], "[x * y for x, y in zip({0}, {1})]", "multiply two lists"),
176
+ Prim("zipsub", ("coll", "coll"), "list", lambda a, b: [x - y for x, y in zip(a, b)], "[x - y for x, y in zip({0}, {1})]", "subtract two lists"),
177
+ Prim("zipdiv", ("coll", "coll"), "list", lambda a, b: [x / y for x, y in zip(a, b)], "[x / y for x, y in zip({0}, {1})]", "divide two lists"),
178
+ Prim("zip", ("coll", "coll"), "list", lambda a, b: list(zip(a, b)), "list(zip({0}, {1}))", "pair zip"),
179
+ Prim("f_even", ("coll",), "list", lambda xs: [x for x in xs if x % 2 == 0], "[x for x in {0} if x % 2 == 0]", "even"),
180
+ Prim("f_odd", ("coll",), "list", lambda xs: [x for x in xs if x % 2 == 1], "[x for x in {0} if x % 2 == 1]", "odd"),
181
+ Prim("f_pos", ("coll",), "list", lambda xs: [x for x in xs if x > 0], "[x for x in {0} if x > 0]", "positive"),
182
+ Prim("f_neg", ("coll",), "list", lambda xs: [x for x in xs if x < 0], "[x for x in {0} if x < 0]", "negative"),
183
+ Prim("f_gt", ("coll", "num"), "list", lambda xs, k: [x for x in xs if x > k], "[x for x in {0} if x > {1}]", "greater than"),
184
+ Prim("f_lenge", ("coll", "int"), "list", lambda xs, k: [x for x in xs if len(x) >= k], "[x for x in {0} if len(x) >= {1}]", "at least long characters"),
185
+ Prim("f_neq", ("coll", "any"), "list", lambda xs, k: [x for x in xs if x != k], "[x for x in {0} if x != {1}]", "remove all"),
186
+ Prim("cumsum", ("coll",), "list", lambda xs: list(itertools.accumulate(xs)), "list(itertools.accumulate({0}))", "cumulative running sum"),
187
+ Prim("flatten", ("coll",), "list", _flatten, "[y for x in {0} for y in (x if isinstance(x, (list, tuple)) else [x])]", "flatten nested"),
188
+ Prim("mostcommon", ("coll",), "any", lambda xs: Counter(xs).most_common(1)[0][0], "collections.Counter({0}).most_common(1)[0][0]", "most common frequent maximum occurrences"),
189
+ Prim("freq", ("coll",), "dict", lambda xs: dict(Counter(xs)), "dict(collections.Counter({0}))", "frequency count each"),
190
+ Prim("sortkey1", ("coll",), "list", lambda xs: sorted(xs, key=lambda t: t[1]), "sorted({0}, key=lambda t: t[1])", "sort second element"),
191
+ Prim("maxkey_len", ("coll",), "any", lambda xs: max(xs, key=len), "max({0}, key=len)", "longest maximum length"),
192
+ Prim("minkey_len", ("coll",), "any", lambda xs: min(xs, key=len), "min({0}, key=len)", "shortest minimum length"),
193
+ Prim("range1", ("int",), "list", lambda n: list(range(1, n + 1)) if n <= 10 ** 5 else 1 / 0, "list(range(1, {0} + 1))", "first n natural numbers range"),
194
+ Prim("range0", ("int",), "list", lambda n: list(range(n)) if n <= 10 ** 5 else 1 / 0, "list(range({0}))", "range"),
195
+ # strings
196
+ Prim("upper", ("str",), "str", str.upper, "{0}.upper()", "upper uppercase capital"),
197
+ Prim("lower", ("str",), "str", str.lower, "{0}.lower()", "lower lowercase"),
198
+ Prim("title", ("str",), "str", str.title, "{0}.title()", "title capitalize first letter each word"),
199
+ Prim("capitalize", ("str",), "str", str.capitalize, "{0}.capitalize()", "capitalize"),
200
+ Prim("swapcase", ("str",), "str", str.swapcase, "{0}.swapcase()", "toggle swap case"),
201
+ Prim("strip", ("str",), "str", str.strip, "{0}.strip()", "strip trim whitespace spaces"),
202
+ Prim("nospace", ("str",), "str", lambda s: s.replace(" ", ""), "{0}.replace(' ', '')", "remove spaces whitespace"),
203
+ Prim("words", ("str",), "list", str.split, "{0}.split()", "words split"),
204
+ Prim("splitby", ("str", "str"), "list", lambda s, d: s.split(d), "{0}.split({1})", "split delimiter"),
205
+ Prim("joinsp", ("coll",), "str", lambda xs: " ".join(xs), "' '.join({0})", "join words"),
206
+ Prim("join", ("coll",), "str", lambda xs: "".join(xs), "''.join({0})", "join concatenate characters"),
207
+ Prim("replace", ("str", "str", "str"), "str", lambda s, a, b: s.replace(a, b), "{0}.replace({1}, {2})", "replace"),
208
+ Prim("remove", ("str", "str"), "str", lambda s, a: s.replace(a, ""), "{0}.replace({1}, '')", "remove delete"),
209
+ Prim("remove1", ("str", "str"), "str", lambda s, a: s.replace(a, "", 1), "{0}.replace({1}, '', 1)", "remove first occurrence"),
210
+ Prim("removelast", ("str", "str"), "str", lambda s, a: (s[:s.rfind(a)] + s[s.rfind(a) + 1:]) if a in s else s, "(({0}[:{0}.rfind({1})] + {0}[{0}.rfind({1}) + 1:]) if {1} in {0} else {0})", "remove last occurrence"),
211
+ Prim("chars", ("str",), "list", list, "list({0})", "characters chars"),
212
+ Prim("isdigit", ("str",), "bool", str.isdigit, "{0}.isdigit()", "digit numeric integer"),
213
+ Prim("isalpha", ("str",), "bool", str.isalpha, "{0}.isalpha()", "alphabet letters"),
214
+ Prim("isupper", ("str",), "bool", str.isupper, "{0}.isupper()", "upper"),
215
+ Prim("islower", ("str",), "bool", str.islower, "{0}.islower()", "lower"),
216
+ Prim("startswith", ("str", "str"), "bool", str.startswith, "{0}.startswith({1})", "starts begins"),
217
+ Prim("endswith", ("str", "str"), "bool", str.endswith, "{0}.endswith({1})", "ends"),
218
+ Prim("find", ("str", "str"), "int", str.find, "{0}.find({1})", "find position index occurrence"),
219
+ Prim("f_upper", ("str",), "str", lambda s: "".join(c for c in s if c.isupper()), "''.join(c for c in {0} if c.isupper())", "uppercase characters extract"),
220
+ Prim("f_lower", ("str",), "str", lambda s: "".join(c for c in s if c.islower()), "''.join(c for c in {0} if c.islower())", "lowercase characters remove"),
221
+ Prim("f_digit", ("str",), "str", lambda s: "".join(c for c in s if c.isdigit()), "''.join(c for c in {0} if c.isdigit())", "digits numbers extract"),
222
+ Prim("f_alpha", ("str",), "str", lambda s: "".join(c for c in s if c.isalpha()), "''.join(c for c in {0} if c.isalpha())", "letters remove non alphabet"),
223
+ Prim("f_alnum", ("str",), "str", lambda s: "".join(c for c in s if c.isalnum()), "''.join(c for c in {0} if c.isalnum())", "alphanumeric remove special characters"),
224
+ Prim("vowels", ("str",), "int", lambda s: sum(c in "aeiouAEIOU" for c in s), "sum(c in 'aeiouAEIOU' for c in {0})", "vowels count"),
225
+ Prim("ispal", ("seq",), "bool", lambda s: s == s[::-1], "({0} == {0}[::-1])", "palindrome"),
226
+ Prim("sortchars", ("str",), "str", lambda s: "".join(sorted(s)), "''.join(sorted({0}))", "sort characters"),
227
+ Prim("rev_words", ("str",), "str", lambda s: " ".join(s.split()[::-1]), "' '.join({0}.split()[::-1])", "reverse words"),
228
+ Prim("ord", ("str",), "int", lambda s: ord(s) if len(s) == 1 else 1 / 0, "ord({0})", "ascii value character"),
229
+ Prim("chr", ("int",), "str", lambda n: chr(n) if 0 <= n < 1114111 else 1 / 0, "chr({0})", "character ascii"),
230
+ Prim("strlen_words", ("str",), "list", lambda s: [len(w) for w in s.split()], "[len(w) for w in {0}.split()]", "length words"),
231
+ Prim("firstchar", ("str",), "str", lambda s: s[0], "{0}[0]", "first character"),
232
+ Prim("lastchar", ("str",), "str", lambda s: s[-1], "{0}[-1]", "last character"),
233
+ Prim("upper_first_last", ("str",), "str", lambda s: " ".join((w[0].upper() + w[1:-1] + w[-1].upper()) if len(w) > 1 else w.upper() for w in s.split()), "' '.join((w[0].upper() + w[1:-1] + w[-1].upper()) if len(w) > 1 else w.upper() for w in {0}.split())", "capitalize first last letters each word"),
234
+ ]
235
+ # v2 library entries added from the gate panel's FAILURE CLASSES (general-purpose operations; checked on MBPP dev)
236
+ P_ += [
237
+ Prim("c2f", ("num",), "float", lambda c: c * 9 / 5 + 32, "({0} * 9 / 5 + 32)", "celsius fahrenheit convert temperature"),
238
+ Prim("f2c", ("num",), "float", lambda f: (f - 32) * 5 / 9, "(({0} - 32) * 5 / 9)", "fahrenheit celsius convert temperature"),
239
+ Prim("isleap", ("int",), "bool", lambda y: y % 4 == 0 and (y % 100 != 0 or y % 400 == 0),
240
+ "({0} % 4 == 0 and ({0} % 100 != 0 or {0} % 400 == 0))", "leap year"),
241
+ Prim("joincomma", ("coll",), "str", lambda xs: ",".join(xs), "','.join({0})", "join comma separated"),
242
+ Prim("nwords", ("str",), "int", lambda s: len(s.split()), "len({0}.split())", "count words number"),
243
+ ]
244
+ IMPORTS = "import math\nimport itertools\nimport collections\n"
245
+
246
+
247
+ # --------------------------------------------------------------------------------------------------------- request
248
+ def parse_tests(tests, setup=""):
249
+ """'assert f(a, b) == v' -> (fname, [(args, expected)], wrapper-src or None)"""
250
+ fname, cases, wrap = None, [], None
251
+ for t in tests:
252
+ try:
253
+ node = ast.parse(t.strip()).body[0]
254
+ except SyntaxError:
255
+ return None
256
+ if not isinstance(node, ast.Assert):
257
+ return None
258
+ test = node.test
259
+ if isinstance(test, ast.Call) or (isinstance(test, ast.UnaryOp) and isinstance(test.operand, ast.Call)):
260
+ neg = isinstance(test, ast.UnaryOp)
261
+ call = test.operand if neg else test
262
+ exp = not neg
263
+ lhs = call
264
+ elif isinstance(test, ast.Compare) and len(test.ops) == 1 and isinstance(test.ops[0], ast.Eq):
265
+ lhs = test.left
266
+ try:
267
+ exp = ast.literal_eval(test.comparators[0])
268
+ except Exception: # noqa: BLE001
269
+ return None
270
+ else:
271
+ return None
272
+ call, w = lhs, None
273
+ if isinstance(lhs, ast.Call) and isinstance(lhs.func, ast.Name) and lhs.func.id in ("set", "sorted", "list", "tuple", "round", "abs") and lhs.args and isinstance(lhs.args[0], ast.Call):
274
+ w, call = lhs.func.id, lhs.args[0]
275
+ if not (isinstance(call, ast.Call) and isinstance(call.func, ast.Name)):
276
+ return None
277
+ try:
278
+ args = [ast.literal_eval(a) for a in call.args]
279
+ except Exception: # noqa: BLE001
280
+ return None
281
+ if fname and call.func.id != fname:
282
+ return None
283
+ fname, wrap = call.func.id, w or wrap
284
+ cases.append((args, exp))
285
+ return (fname, cases, wrap) if fname and cases else None
286
+
287
+
288
+ WRAP = {None: lambda v: v, "set": set, "sorted": sorted, "list": list, "tuple": tuple, "round": round, "abs": abs}
289
+
290
+
291
+ ORDINAL = {"first": 0, "second": 1, "third": 2, "fourth": 3, "last": -1, "double": 2, "doubled": 2, "twice": 2,
292
+ "triple": 3, "tripled": 3, "half": 2, "halve": 2, "halved": 2}
293
+
294
+
295
+ def consts_of(desc, cases, strict=False):
296
+ """constants a program may use. STRICT (one example): only what the request itself names - its numbers, its
297
+ quoted characters and its ordinals (first=0, second=1, last=-1) - so no positional coincidence is reachable"""
298
+ text = re.split(r"\bso\b", desc or "")[0] if strict else (desc or "")
299
+ named = [int(n) for n in re.findall(r"\b(\d{1,4})\b", text)]
300
+ named += re.findall(r"'([^']{1,3})'", text)
301
+ named += [v for w, v in ORDINAL.items() if re.search(r"\b%s\b" % w, text, re.I)]
302
+ out = named if strict else [0, 1, 2, -1, "", " "] + named
303
+ return list(dict.fromkeys(out))
304
+
305
+
306
+ GENERIC = {"write", "function", "python", "find", "given", "number", "numbers", "integer", "list", "string", "element",
307
+ "elements", "value", "values", "check", "whether", "two", "the", "and", "from", "all", "using", "which", "that",
308
+ "with", "into", "for", "each", "array", "tuple", "item", "items", "get", "given", "not", "out", "sequence"}
309
+
310
+
311
+ def words_of(desc):
312
+ return {w.lower().rstrip("s") for w in re.findall(r"[A-Za-z]+", desc or "")
313
+ if len(w) >= 3 and w.lower() not in GENERIC and w.lower().rstrip("s") not in GENERIC}
314
+
315
+
316
+ # ----------------------------------------------------------------------------------------------------------- search
317
+ class CodeOrgan:
318
+ def __init__(self, library=None, max_size=4, cap=60000):
319
+ self.prims = list(P_) + list(library or [])
320
+ self.max_size, self.cap = max_size, cap
321
+ self.why = ""
322
+
323
+ def learn(self, name, prog, arity_types, out_t, desc):
324
+ """STORE: a verified program becomes a library function; later requests compose it"""
325
+ fnc = prog["fn"]
326
+ src = "%s(%s)" % (name, ", ".join("{%d}" % i for i in range(len(arity_types))))
327
+ p = Prim("lib_" + name, tuple(arity_types), out_t, lambda *vals: fnc(vals), src, " ".join(words_of(desc)))
328
+ self.prims.append(p)
329
+ self.library_src = getattr(self, "library_src", {})
330
+ self.library_src[name] = prog["def"].replace(IMPORTS, "")
331
+ return p
332
+
333
+ def solve(self, desc, tests, setup="", visible=None, strict=False):
334
+ self.why = ""
335
+ pt = parse_tests(tests, setup)
336
+ if not pt:
337
+ self.why = "the examples are not simple asserts"
338
+ return None
339
+ fname, cases, wrap = pt
340
+ vis = cases if visible is None else cases[:visible]
341
+ if not vis:
342
+ return None
343
+ W = WRAP[wrap]
344
+ nargs = len(vis[0][0])
345
+ if any(len(a) != nargs for a, _ in cases):
346
+ self.why = "varying arity"
347
+ return None
348
+ target = [W(e) if wrap else e for _, e in vis]
349
+ kw = words_of(desc)
350
+ # the request SPECIFIES the moves: rank by the description's words; keep the whole library but try relevant first
351
+ # a stored program is a move only with >= 2 examples, and never inside its own definition; with ONE example
352
+ # (strict) only the base library composes - forms verified on one example are too weak to build on from one more
353
+ pool = [p for p in self.prims if p.name != "lib_" + fname and not (strict and p.name.startswith("lib_"))]
354
+ prims = sorted(pool, key=lambda p: -len(p.kw & kw))
355
+ # terminals: arguments and constants; a term = (src, type, outputs-tuple, fn)
356
+ terms_by_size = {1: []}
357
+ seen = set()
358
+
359
+ def add(size, src, outs, fnc, used=frozenset()):
360
+ key = repr(outs)
361
+ try:
362
+ match = [W(o) if wrap else o for o in outs] == target
363
+ except Exception: # noqa: BLE001
364
+ match = False
365
+ if match:
366
+ # the request SPECIFIES the moves: a fit made only of moves the description never names is a
367
+ # coincidence of three examples, not an answer - it is not accepted and does not claim the signature.
368
+ # A fit must also READ its inputs: every argument is used, except an 'n' that only restates a length.
369
+ reads = all(argn[i] in re.findall(r"a\d+", src) or restates_len[i] for i in range(nargs))
370
+ # STRICT (one example): EVERY operation must be named by the request - the description specifies the
371
+ # program and the single example only checks it
372
+ named = all(pr.kw & kw for pr in used) if strict else any(pr.kw & kw for pr in used)
373
+ if used and reads and named:
374
+ return (src, fnc)
375
+ self.coincidences = getattr(self, "coincidences", 0) + 1
376
+ return None
377
+ if key in seen or len(key) > 1500 or len(seen) > self.cap:
378
+ return None # bounded memory: huge values and past-budget terms are not kept
379
+ seen.add(key)
380
+ ty = tyof(outs[0])
381
+ if any(tyof(o) != ty for o in outs):
382
+ ty = "any"
383
+ terms_by_size.setdefault(size, []).append((src, ty, outs, fnc, used))
384
+ return None
385
+
386
+ argn = ["a%d" % i for i in range(nargs)]
387
+ restates_len = [all(isinstance(a[i], int) and any(isinstance(a[j], (list, tuple, str)) and len(a[j]) == a[i]
388
+ for j in range(nargs) if j != i) for a, _ in vis)
389
+ for i in range(nargs)]
390
+ for i in range(nargs):
391
+ hit = add(1, argn[i], tuple(a[i] for a, _ in vis), (lambda i: lambda xs: xs[i])(i))
392
+ if hit:
393
+ return self._emit(fname, argn, hit, cases, wrap, desc)
394
+ for c in consts_of(desc, vis, strict):
395
+ add(1, repr(c), tuple(c for _ in vis), (lambda c: lambda xs: c)(c))
396
+ n_terms = 0
397
+ best = None
398
+ for size in range(2, self.max_size + 1):
399
+ for p in prims:
400
+ k = len(p.ins)
401
+ # child sizes summing to size-1
402
+ for parts in _compositions(size - 1, k):
403
+ pools = [terms_by_size.get(s, []) for s in parts]
404
+ pools = [[t for t in pool if fits(t[1], want) or t[1] == "any"] for pool, want in zip(pools, p.ins)]
405
+ if any(not pool for pool in pools):
406
+ continue
407
+ for combo in itertools.product(*pools):
408
+ n_terms += 1
409
+ if n_terms > self.cap:
410
+ self.why = "search budget exhausted"
411
+ return None
412
+ try:
413
+ outs = tuple(p.fn(*[t[2][j] for t in combo]) for j in range(len(vis)))
414
+ except Exception: # noqa: BLE001
415
+ continue
416
+ if any(_oversize(o) for o in outs):
417
+ continue # bounded memory: never represent a huge intermediate value
418
+ src = p.src.format(*[t[0] for t in combo])
419
+ fnc = (lambda p, combo: lambda xs: p.fn(*[t[3](xs) for t in combo]))(p, combo)
420
+ used = frozenset([p]).union(*[t[4] for t in combo])
421
+ hit = add(size, src, outs, fnc, used)
422
+ if hit:
423
+ if not strict:
424
+ return self._emit(fname, argn, hit, cases, wrap, desc)
425
+ # COVERAGE: how much of what the request says the program's moves account for, jointly
426
+ named = len(set().union(*[pr.kw for pr in used]) & kw)
427
+ if best is None or named > best[0]:
428
+ best = (named, hit)
429
+ if strict and best: # v2 STRICT: the fit covering most of the request, any size
430
+ return self._emit(fname, argn, best[1], cases, wrap, desc)
431
+ self.why = "no program of size <= %d fits the examples" % self.max_size
432
+ return None
433
+
434
+ def _emit(self, fname, argn, hit, cases, wrap, desc):
435
+ src, fnc = hit
436
+ lib = getattr(self, "library_src", {})
437
+ used = [n for n in lib if re.search(r"\b%s\(" % re.escape(n), src)]
438
+ deps = "".join(lib[n] for n in used) # stored forms this program composes
439
+ body = "%s%sdef %s(%s):\n return %s\n" % (IMPORTS, deps, fname, ", ".join(argn), src)
440
+ # CHECK: execute the RENDERED source (not the in-memory closure) against every example given
441
+ ns = {}
442
+ exec(compile(body, "<organ>", "exec"), ns) # noqa: S102 - composed only of library templates
443
+ W = WRAP[wrap]
444
+ passed = 0
445
+ for a, e in cases:
446
+ try:
447
+ out = ns[fname](*a)
448
+ ok = (W(out) if wrap else out) == e
449
+ except Exception: # noqa: BLE001
450
+ ok = False
451
+ passed += ok
452
+ return {"def": body, "fname": fname, "expr": src, "passed": passed, "cases": len(cases), "fn": fnc,
453
+ "arity": len(argn), "reused": used}
454
+
455
+
456
+ def _oversize(o):
457
+ if isinstance(o, bool):
458
+ return False
459
+ if isinstance(o, int):
460
+ return abs(o) > 10 ** 12
461
+ if isinstance(o, str):
462
+ return len(o) > 400
463
+ if isinstance(o, (list, tuple, set, dict)):
464
+ return len(o) > 200 or any(_oversize(x) for x in list(o)[:50])
465
+ return False
466
+
467
+
468
+ def _compositions(total, k):
469
+ if k == 1:
470
+ yield (total,)
471
+ return
472
+ for first in range(1, total - k + 2):
473
+ for rest in _compositions(total - first, k - 1):
474
+ yield (first,) + rest
root/wander-train/code_organ_v3.py ADDED
@@ -0,0 +1,488 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """code_organ_v3 - v2 (admitted in w482, immutable) + cover mode for conversation (coverage selection at any size
2
+ with stored programs allowed). v2: v1 (MBPP-frozen b2e52a1d) + STRICT mode for a single example: every move named by the
3
+ request. Programs as specified free moves over a verified function library, checked by execution; every
4
+ verified program joins the library (the stored final forms - continuous learning without retraining).
5
+
6
+ REQUEST description + input/output examples (asserts) -> function name, argument values and types, expected outputs,
7
+ constants the description names ("at least 4 characters", "the character 'l'")
8
+ MOVES typed library functions. The TYPE SYSTEM is the specified structure: a function applies only where its
9
+ argument types connect; within that the choice is free. The description's words rank the moves.
10
+ SEARCH bottom-up enumeration with OBSERVATIONAL EQUIVALENCE: programs that behave identically on the examples are
11
+ kept once, so compositions of 3-4 moves stay tractable.
12
+ CHECK the rendered source is executed against every example; nothing is released that did not run and pass.
13
+ STORE a verified program becomes a library function (its description words are its keywords, its signature its
14
+ type); later requests compose it - measured as the library-growth curve.
15
+ Only library templates are ever evaluated; no text from the request is executed."""
16
+ from __future__ import annotations
17
+
18
+ import ast
19
+ import itertools
20
+ import json
21
+ import math
22
+ import re
23
+ from collections import Counter
24
+
25
+ # ----------------------------------------------------------------------------------------------------------- types
26
+ def tyof(v):
27
+ if isinstance(v, bool):
28
+ return "bool"
29
+ if isinstance(v, int):
30
+ return "int"
31
+ if isinstance(v, float):
32
+ return "float"
33
+ if isinstance(v, str):
34
+ return "str"
35
+ if isinstance(v, list):
36
+ return "list"
37
+ if isinstance(v, tuple):
38
+ return "tuple"
39
+ if isinstance(v, dict):
40
+ return "dict"
41
+ if isinstance(v, set):
42
+ return "set"
43
+ return "any"
44
+
45
+
46
+ GROUP = {"num": {"int", "float"}, "seq": {"list", "tuple", "str"}, "coll": {"list", "tuple", "set"},
47
+ "any": {"bool", "int", "float", "str", "list", "tuple", "dict", "set", "any"}}
48
+
49
+
50
+ def fits(t, want):
51
+ return t == want or t in GROUP.get(want, ())
52
+
53
+
54
+ # ------------------------------------------------------------------------------------------------------ primitives
55
+ class Prim:
56
+ __slots__ = ("name", "ins", "out", "fn", "src", "kw")
57
+
58
+ def __init__(self, name, ins, out, fn, src, kw):
59
+ self.name, self.ins, self.out, self.fn, self.src = name, ins, out, fn, src
60
+ self.kw = {w.lower().rstrip("s") for w in kw.split()} # v2: normalised exactly like the request words
61
+
62
+
63
+ def _big(x):
64
+ if isinstance(x, int) and abs(x) > 10 ** 7:
65
+ raise ValueError("too large")
66
+ return x
67
+
68
+
69
+ def _is_prime(n):
70
+ if n < 2:
71
+ return False
72
+ return all(n % d for d in range(2, int(n ** 0.5) + 1))
73
+
74
+
75
+ def _uniq(xs):
76
+ out = []
77
+ for x in xs:
78
+ if x not in out:
79
+ out.append(x)
80
+ return out
81
+
82
+
83
+ def _flatten(xs):
84
+ out = []
85
+ for x in xs:
86
+ out += list(x) if isinstance(x, (list, tuple)) else [x]
87
+ return out
88
+
89
+
90
+ P_ = [
91
+ # numbers
92
+ Prim("add", ("num", "num"), "num", lambda a, b: a + b, "({0} + {1})", "add sum plus total"),
93
+ Prim("sub", ("num", "num"), "num", lambda a, b: a - b, "({0} - {1})", "subtract difference minus"),
94
+ Prim("mul", ("num", "num"), "num", lambda a, b: _big(a * b), "({0} * {1})", "multiply product times"),
95
+ Prim("fdiv", ("num", "num"), "num", lambda a, b: a // b, "({0} // {1})", "divide quotient integer"),
96
+ Prim("div", ("num", "num"), "float", lambda a, b: a / b, "({0} / {1})", "divide ratio average"),
97
+ Prim("mod", ("num", "num"), "num", lambda a, b: a % b, "({0} % {1})", "remainder modulo divisible"),
98
+ Prim("pow", ("num", "num"), "num", lambda a, b: _big(a ** b) if b < 64 else 1 / 0, "({0} ** {1})", "power square cube exponent"),
99
+ Prim("sq", ("num",), "num", lambda a: a * a, "({0} ** 2)", "square squares"),
100
+ Prim("cube", ("num",), "num", lambda a: a ** 3, "({0} ** 3)", "cube cubes"),
101
+ Prim("abs", ("num",), "num", abs, "abs({0})", "absolute"),
102
+ Prim("neg", ("num",), "num", lambda a: -a, "(-{0})", "negative negate"),
103
+ Prim("half", ("num",), "float", lambda a: a / 2, "({0} / 2)", "half area triangle"),
104
+ Prim("sqrt", ("num",), "float", math.sqrt, "math.sqrt({0})", "root square"),
105
+ Prim("isqrt", ("int",), "int", math.isqrt, "math.isqrt({0})", "root square integer"),
106
+ Prim("fact", ("int",), "int", lambda n: math.factorial(n) if 0 <= n <= 30 else 1 / 0, "math.factorial({0})", "factorial"),
107
+ Prim("gcd", ("int", "int"), "int", math.gcd, "math.gcd({0}, {1})", "gcd greatest common divisor"),
108
+ Prim("lcm", ("int", "int"), "int", lambda a, b: a * b // math.gcd(a, b) if a and b else 0, "({0} * {1} // math.gcd({0}, {1}))", "lcm least common multiple"),
109
+ Prim("isprime", ("int",), "bool", _is_prime, "(({0}) > 1 and all(({0}) % d for d in range(2, int(({0}) ** 0.5) + 1)))", "prime"),
110
+ Prim("even", ("int",), "bool", lambda n: n % 2 == 0, "({0} % 2 == 0)", "even"),
111
+ Prim("odd", ("int",), "bool", lambda n: n % 2 == 1, "({0} % 2 == 1)", "odd"),
112
+ Prim("digits", ("int",), "list", lambda n: [int(c) for c in str(abs(n))], "[int(c) for c in str(abs({0}))]", "digits digit"),
113
+ Prim("revnum", ("int",), "int", lambda n: int(str(abs(n))[::-1]), "int(str(abs({0}))[::-1])", "reverse number"),
114
+ Prim("bin", ("int",), "str", lambda n: bin(n)[2:], "bin({0})[2:]", "binary"),
115
+ Prim("frombin", ("str",), "int", lambda s: int(s, 2), "int({0}, 2)", "binary decimal convert"),
116
+ Prim("popcount", ("int",), "int", lambda n: bin(n).count("1"), "bin({0}).count('1')", "set bits count ones"),
117
+ Prim("xor", ("int", "int"), "int", lambda a, b: a ^ b, "({0} ^ {1})", "xor bit differ"),
118
+ Prim("ispow2", ("int",), "bool", lambda n: n > 0 and n & (n - 1) == 0, "({0} > 0 and {0} & ({0} - 1) == 0)", "power two bit"),
119
+ Prim("eq", ("any", "any"), "bool", lambda a, b: a == b, "({0} == {1})", "equal same"),
120
+ Prim("lt", ("num", "num"), "bool", lambda a, b: a < b, "({0} < {1})", "less smaller"),
121
+ Prim("gt", ("num", "num"), "bool", lambda a, b: a > b, "({0} > {1})", "greater larger"),
122
+ Prim("not", ("bool",), "bool", lambda a: not a, "(not {0})", "not"),
123
+ Prim("and", ("bool", "bool"), "bool", lambda a, b: a and b, "({0} and {1})", "and both"),
124
+ Prim("or", ("bool", "bool"), "bool", lambda a, b: a or b, "({0} or {1})", "or either"),
125
+ Prim("maxn", ("num", "num"), "num", max, "max({0}, {1})", "maximum largest bigger"),
126
+ Prim("minn", ("num", "num"), "num", min, "min({0}, {1})", "minimum smallest"),
127
+ Prim("round2", ("num",), "float", lambda a: round(a, 2), "round({0}, 2)", "round"),
128
+ Prim("int", ("any",), "int", int, "int({0})", "integer convert"),
129
+ Prim("str", ("any",), "str", str, "str({0})", "string convert"),
130
+ Prim("float", ("num",), "float", float, "float({0})", "float"),
131
+ Prim("tri", ("int",), "int", lambda n: n * (n + 1) // 2, "({0} * ({0} + 1) // 2)", "triangular sum natural"),
132
+ Prim("pi_r2", ("num",), "float", lambda r: math.pi * r * r, "(math.pi * {0} * {0})", "area circle"),
133
+ Prim("circ", ("num",), "float", lambda r: 2 * math.pi * r, "(2 * math.pi * {0})", "circumference perimeter circle"),
134
+ # sequences
135
+ Prim("len", ("seq",), "int", len, "len({0})", "length count number size"),
136
+ Prim("sum", ("coll",), "num", lambda xs: sum(xs), "sum({0})", "sum total add"),
137
+ Prim("max", ("coll",), "any", max, "max({0})", "maximum largest biggest max longest"),
138
+ Prim("min", ("coll",), "any", min, "min({0})", "minimum smallest min"),
139
+ Prim("sorted", ("coll",), "list", sorted, "sorted({0})", "sort sorted ascending order"),
140
+ Prim("sorted_desc", ("coll",), "list", lambda xs: sorted(xs, reverse=True), "sorted({0}, reverse=True)", "sort descending largest"),
141
+ Prim("rev", ("seq",), "seq", lambda xs: xs[::-1], "{0}[::-1]", "reverse reversed"),
142
+ Prim("first", ("seq",), "any", lambda xs: xs[0], "{0}[0]", "first"),
143
+ Prim("last", ("seq",), "any", lambda xs: xs[-1], "{0}[-1]", "last"),
144
+ Prim("take", ("seq", "int"), "seq", lambda xs, n: xs[:n], "{0}[:{1}]", "first n smallest largest take"),
145
+ Prim("drop", ("seq", "int"), "seq", lambda xs, n: xs[n:], "{0}[{1}:]", "remove first skip"),
146
+ Prim("at", ("seq", "int"), "any", lambda xs, i: xs[i], "{0}[{1}]", "nth index element second third"),
147
+ Prim("tolist", ("any",), "list", list, "list({0})", "list convert"),
148
+ Prim("totuple", ("any",), "tuple", tuple, "tuple({0})", "tuple convert"),
149
+ Prim("toset", ("coll",), "set", set, "set({0})", "unique distinct set"),
150
+ Prim("uniq", ("coll",), "list", _uniq, "list(dict.fromkeys({0}))", "unique distinct duplicates remove"),
151
+ Prim("inter", ("coll", "coll"), "tuple", lambda a, b: tuple(sorted(set(a) & set(b))), "tuple(sorted(set({0}) & set({1})))", "common shared similar intersection"),
152
+ Prim("union", ("coll", "coll"), "tuple", lambda a, b: tuple(sorted(set(a) | set(b))), "tuple(sorted(set({0}) | set({1})))", "union combine"),
153
+ Prim("diff", ("coll", "coll"), "list", lambda a, b: [x for x in a if x not in b], "[x for x in {0} if x not in {1}]", "difference not in"),
154
+ Prim("concat", ("seq", "seq"), "seq", lambda a, b: a + b, "({0} + {1})", "concatenate join append combine merge"),
155
+ Prim("count", ("seq", "any"), "int", lambda xs, x: xs.count(x), "{0}.count({1})", "count occurrences frequency"),
156
+ Prim("index", ("seq", "any"), "int", lambda xs, x: xs.index(x), "{0}.index({1})", "index position find"),
157
+ Prim("contains", ("any", "seq"), "bool", lambda x, xs: x in xs, "({0} in {1})", "contains present exists check"),
158
+ Prim("allsame", ("coll",), "bool", lambda xs: len(set(xs)) <= 1, "(len(set({0})) <= 1)", "same equal all identical"),
159
+ Prim("alluniq", ("coll",), "bool", lambda xs: len(set(xs)) == len(xs), "(len(set({0})) == len(list({0})))", "unique distinct duplicate"),
160
+ Prim("issorted", ("coll",), "bool", lambda xs: list(xs) == sorted(xs), "(list({0}) == sorted({0}))", "sorted order check"),
161
+ Prim("prod", ("coll",), "num", lambda xs: _big(math.prod(xs)), "math.prod({0})", "product multiply"),
162
+ Prim("mean", ("coll",), "float", lambda xs: sum(xs) / len(xs), "(sum({0}) / len({0}))", "average mean"),
163
+ Prim("map_sq", ("coll",), "list", lambda xs: [x * x for x in xs], "[x * x for x in {0}]", "square squares each"),
164
+ Prim("map_cube", ("coll",), "list", lambda xs: [x ** 3 for x in xs], "[x ** 3 for x in {0}]", "cube cubes each"),
165
+ Prim("map_neg", ("coll",), "list", lambda xs: [-x for x in xs], "[-x for x in {0}]", "negative"),
166
+ Prim("map_abs", ("coll",), "list", lambda xs: [abs(x) for x in xs], "[abs(x) for x in {0}]", "absolute"),
167
+ Prim("map_str", ("coll",), "list", lambda xs: [str(x) for x in xs], "[str(x) for x in {0}]", "string convert"),
168
+ Prim("map_int", ("coll",), "list", lambda xs: [int(x) for x in xs], "[int(x) for x in {0}]", "integer convert"),
169
+ Prim("map_len", ("coll",), "list", lambda xs: [len(x) for x in xs], "[len(x) for x in {0}]", "length each"),
170
+ Prim("map_first", ("coll",), "list", lambda xs: [x[0] for x in xs], "[x[0] for x in {0}]", "first each"),
171
+ Prim("map_last", ("coll",), "list", lambda xs: [x[-1] for x in xs], "[x[-1] for x in {0}]", "last each"),
172
+ Prim("map_sum", ("coll",), "list", lambda xs: [sum(x) for x in xs], "[sum(x) for x in {0}]", "sum each"),
173
+ Prim("map_add", ("coll", "num"), "list", lambda xs, k: [x + k for x in xs], "[x + {1} for x in {0}]", "add each"),
174
+ Prim("map_mul", ("coll", "num"), "list", lambda xs, k: [x * k for x in xs], "[x * {1} for x in {0}]", "multiply each double doubled twice"),
175
+ Prim("zipadd", ("coll", "coll"), "list", lambda a, b: [x + y for x, y in zip(a, b)], "[x + y for x, y in zip({0}, {1})]", "add two lists elementwise"),
176
+ Prim("zipmul", ("coll", "coll"), "list", lambda a, b: [x * y for x, y in zip(a, b)], "[x * y for x, y in zip({0}, {1})]", "multiply two lists"),
177
+ Prim("zipsub", ("coll", "coll"), "list", lambda a, b: [x - y for x, y in zip(a, b)], "[x - y for x, y in zip({0}, {1})]", "subtract two lists"),
178
+ Prim("zipdiv", ("coll", "coll"), "list", lambda a, b: [x / y for x, y in zip(a, b)], "[x / y for x, y in zip({0}, {1})]", "divide two lists"),
179
+ Prim("zip", ("coll", "coll"), "list", lambda a, b: list(zip(a, b)), "list(zip({0}, {1}))", "pair zip"),
180
+ Prim("f_even", ("coll",), "list", lambda xs: [x for x in xs if x % 2 == 0], "[x for x in {0} if x % 2 == 0]", "even"),
181
+ Prim("f_odd", ("coll",), "list", lambda xs: [x for x in xs if x % 2 == 1], "[x for x in {0} if x % 2 == 1]", "odd"),
182
+ Prim("f_pos", ("coll",), "list", lambda xs: [x for x in xs if x > 0], "[x for x in {0} if x > 0]", "positive"),
183
+ Prim("f_neg", ("coll",), "list", lambda xs: [x for x in xs if x < 0], "[x for x in {0} if x < 0]", "negative"),
184
+ Prim("f_gt", ("coll", "num"), "list", lambda xs, k: [x for x in xs if x > k], "[x for x in {0} if x > {1}]", "greater than"),
185
+ Prim("f_lenge", ("coll", "int"), "list", lambda xs, k: [x for x in xs if len(x) >= k], "[x for x in {0} if len(x) >= {1}]", "at least long characters"),
186
+ Prim("f_neq", ("coll", "any"), "list", lambda xs, k: [x for x in xs if x != k], "[x for x in {0} if x != {1}]", "remove all"),
187
+ Prim("cumsum", ("coll",), "list", lambda xs: list(itertools.accumulate(xs)), "list(itertools.accumulate({0}))", "cumulative running sum"),
188
+ Prim("flatten", ("coll",), "list", _flatten, "[y for x in {0} for y in (x if isinstance(x, (list, tuple)) else [x])]", "flatten nested"),
189
+ Prim("mostcommon", ("coll",), "any", lambda xs: Counter(xs).most_common(1)[0][0], "collections.Counter({0}).most_common(1)[0][0]", "most common frequent maximum occurrences"),
190
+ Prim("freq", ("coll",), "dict", lambda xs: dict(Counter(xs)), "dict(collections.Counter({0}))", "frequency count each"),
191
+ Prim("sortkey1", ("coll",), "list", lambda xs: sorted(xs, key=lambda t: t[1]), "sorted({0}, key=lambda t: t[1])", "sort second element"),
192
+ Prim("maxkey_len", ("coll",), "any", lambda xs: max(xs, key=len), "max({0}, key=len)", "longest maximum length"),
193
+ Prim("minkey_len", ("coll",), "any", lambda xs: min(xs, key=len), "min({0}, key=len)", "shortest minimum length"),
194
+ Prim("range1", ("int",), "list", lambda n: list(range(1, n + 1)) if n <= 10 ** 5 else 1 / 0, "list(range(1, {0} + 1))", "first n natural numbers range"),
195
+ Prim("range0", ("int",), "list", lambda n: list(range(n)) if n <= 10 ** 5 else 1 / 0, "list(range({0}))", "range"),
196
+ # strings
197
+ Prim("upper", ("str",), "str", str.upper, "{0}.upper()", "upper uppercase capital"),
198
+ Prim("lower", ("str",), "str", str.lower, "{0}.lower()", "lower lowercase"),
199
+ Prim("title", ("str",), "str", str.title, "{0}.title()", "title capitalize first letter each word"),
200
+ Prim("capitalize", ("str",), "str", str.capitalize, "{0}.capitalize()", "capitalize"),
201
+ Prim("swapcase", ("str",), "str", str.swapcase, "{0}.swapcase()", "toggle swap case"),
202
+ Prim("strip", ("str",), "str", str.strip, "{0}.strip()", "strip trim whitespace spaces"),
203
+ Prim("nospace", ("str",), "str", lambda s: s.replace(" ", ""), "{0}.replace(' ', '')", "remove spaces whitespace"),
204
+ Prim("words", ("str",), "list", str.split, "{0}.split()", "words split"),
205
+ Prim("splitby", ("str", "str"), "list", lambda s, d: s.split(d), "{0}.split({1})", "split delimiter"),
206
+ Prim("joinsp", ("coll",), "str", lambda xs: " ".join(xs), "' '.join({0})", "join words"),
207
+ Prim("join", ("coll",), "str", lambda xs: "".join(xs), "''.join({0})", "join concatenate characters"),
208
+ Prim("replace", ("str", "str", "str"), "str", lambda s, a, b: s.replace(a, b), "{0}.replace({1}, {2})", "replace"),
209
+ Prim("remove", ("str", "str"), "str", lambda s, a: s.replace(a, ""), "{0}.replace({1}, '')", "remove delete"),
210
+ Prim("remove1", ("str", "str"), "str", lambda s, a: s.replace(a, "", 1), "{0}.replace({1}, '', 1)", "remove first occurrence"),
211
+ Prim("removelast", ("str", "str"), "str", lambda s, a: (s[:s.rfind(a)] + s[s.rfind(a) + 1:]) if a in s else s, "(({0}[:{0}.rfind({1})] + {0}[{0}.rfind({1}) + 1:]) if {1} in {0} else {0})", "remove last occurrence"),
212
+ Prim("chars", ("str",), "list", list, "list({0})", "characters chars"),
213
+ Prim("isdigit", ("str",), "bool", str.isdigit, "{0}.isdigit()", "digit numeric integer"),
214
+ Prim("isalpha", ("str",), "bool", str.isalpha, "{0}.isalpha()", "alphabet letters"),
215
+ Prim("isupper", ("str",), "bool", str.isupper, "{0}.isupper()", "upper"),
216
+ Prim("islower", ("str",), "bool", str.islower, "{0}.islower()", "lower"),
217
+ Prim("startswith", ("str", "str"), "bool", str.startswith, "{0}.startswith({1})", "starts begins"),
218
+ Prim("endswith", ("str", "str"), "bool", str.endswith, "{0}.endswith({1})", "ends"),
219
+ Prim("find", ("str", "str"), "int", str.find, "{0}.find({1})", "find position index occurrence"),
220
+ Prim("f_upper", ("str",), "str", lambda s: "".join(c for c in s if c.isupper()), "''.join(c for c in {0} if c.isupper())", "uppercase characters extract"),
221
+ Prim("f_lower", ("str",), "str", lambda s: "".join(c for c in s if c.islower()), "''.join(c for c in {0} if c.islower())", "lowercase characters remove"),
222
+ Prim("f_digit", ("str",), "str", lambda s: "".join(c for c in s if c.isdigit()), "''.join(c for c in {0} if c.isdigit())", "digits numbers extract"),
223
+ Prim("f_alpha", ("str",), "str", lambda s: "".join(c for c in s if c.isalpha()), "''.join(c for c in {0} if c.isalpha())", "letters remove non alphabet"),
224
+ Prim("f_alnum", ("str",), "str", lambda s: "".join(c for c in s if c.isalnum()), "''.join(c for c in {0} if c.isalnum())", "alphanumeric remove special characters"),
225
+ Prim("vowels", ("str",), "int", lambda s: sum(c in "aeiouAEIOU" for c in s), "sum(c in 'aeiouAEIOU' for c in {0})", "vowels count"),
226
+ Prim("ispal", ("seq",), "bool", lambda s: s == s[::-1], "({0} == {0}[::-1])", "palindrome"),
227
+ Prim("sortchars", ("str",), "str", lambda s: "".join(sorted(s)), "''.join(sorted({0}))", "sort characters"),
228
+ Prim("rev_words", ("str",), "str", lambda s: " ".join(s.split()[::-1]), "' '.join({0}.split()[::-1])", "reverse words"),
229
+ Prim("ord", ("str",), "int", lambda s: ord(s) if len(s) == 1 else 1 / 0, "ord({0})", "ascii value character"),
230
+ Prim("chr", ("int",), "str", lambda n: chr(n) if 0 <= n < 1114111 else 1 / 0, "chr({0})", "character ascii"),
231
+ Prim("strlen_words", ("str",), "list", lambda s: [len(w) for w in s.split()], "[len(w) for w in {0}.split()]", "length words"),
232
+ Prim("firstchar", ("str",), "str", lambda s: s[0], "{0}[0]", "first character"),
233
+ Prim("lastchar", ("str",), "str", lambda s: s[-1], "{0}[-1]", "last character"),
234
+ Prim("upper_first_last", ("str",), "str", lambda s: " ".join((w[0].upper() + w[1:-1] + w[-1].upper()) if len(w) > 1 else w.upper() for w in s.split()), "' '.join((w[0].upper() + w[1:-1] + w[-1].upper()) if len(w) > 1 else w.upper() for w in {0}.split())", "capitalize first last letters each word"),
235
+ ]
236
+ # v2 library entries added from the gate panel's FAILURE CLASSES (general-purpose operations; checked on MBPP dev)
237
+ P_ += [
238
+ Prim("c2f", ("num",), "float", lambda c: c * 9 / 5 + 32, "({0} * 9 / 5 + 32)", "celsius fahrenheit convert temperature"),
239
+ Prim("f2c", ("num",), "float", lambda f: (f - 32) * 5 / 9, "(({0} - 32) * 5 / 9)", "fahrenheit celsius convert temperature"),
240
+ Prim("isleap", ("int",), "bool", lambda y: y % 4 == 0 and (y % 100 != 0 or y % 400 == 0),
241
+ "({0} % 4 == 0 and ({0} % 100 != 0 or {0} % 400 == 0))", "leap year"),
242
+ Prim("joincomma", ("coll",), "str", lambda xs: ",".join(xs), "','.join({0})", "join comma separated"),
243
+ Prim("nwords", ("str",), "int", lambda s: len(s.split()), "len({0}.split())", "count words number"),
244
+ ]
245
+ IMPORTS = "import math\nimport itertools\nimport collections\n"
246
+
247
+
248
+ # --------------------------------------------------------------------------------------------------------- request
249
+ def parse_tests(tests, setup=""):
250
+ """'assert f(a, b) == v' -> (fname, [(args, expected)], wrapper-src or None)"""
251
+ fname, cases, wrap = None, [], None
252
+ for t in tests:
253
+ try:
254
+ node = ast.parse(t.strip()).body[0]
255
+ except SyntaxError:
256
+ return None
257
+ if not isinstance(node, ast.Assert):
258
+ return None
259
+ test = node.test
260
+ if isinstance(test, ast.Call) or (isinstance(test, ast.UnaryOp) and isinstance(test.operand, ast.Call)):
261
+ neg = isinstance(test, ast.UnaryOp)
262
+ call = test.operand if neg else test
263
+ exp = not neg
264
+ lhs = call
265
+ elif isinstance(test, ast.Compare) and len(test.ops) == 1 and isinstance(test.ops[0], ast.Eq):
266
+ lhs = test.left
267
+ try:
268
+ exp = ast.literal_eval(test.comparators[0])
269
+ except Exception: # noqa: BLE001
270
+ return None
271
+ else:
272
+ return None
273
+ call, w = lhs, None
274
+ if isinstance(lhs, ast.Call) and isinstance(lhs.func, ast.Name) and lhs.func.id in ("set", "sorted", "list", "tuple", "round", "abs") and lhs.args and isinstance(lhs.args[0], ast.Call):
275
+ w, call = lhs.func.id, lhs.args[0]
276
+ if not (isinstance(call, ast.Call) and isinstance(call.func, ast.Name)):
277
+ return None
278
+ try:
279
+ args = [ast.literal_eval(a) for a in call.args]
280
+ except Exception: # noqa: BLE001
281
+ return None
282
+ if fname and call.func.id != fname:
283
+ return None
284
+ fname, wrap = call.func.id, w or wrap
285
+ cases.append((args, exp))
286
+ return (fname, cases, wrap) if fname and cases else None
287
+
288
+
289
+ WRAP = {None: lambda v: v, "set": set, "sorted": sorted, "list": list, "tuple": tuple, "round": round, "abs": abs}
290
+
291
+
292
+ ORDINAL = {"first": 0, "second": 1, "third": 2, "fourth": 3, "last": -1, "double": 2, "doubled": 2, "twice": 2,
293
+ "triple": 3, "tripled": 3, "half": 2, "halve": 2, "halved": 2}
294
+
295
+
296
+ def consts_of(desc, cases, strict=False):
297
+ """constants a program may use. STRICT (one example): only what the request itself names - its numbers, its
298
+ quoted characters and its ordinals (first=0, second=1, last=-1) - so no positional coincidence is reachable"""
299
+ text = re.split(r"\bso\b", desc or "")[0] if strict else (desc or "")
300
+ named = [int(n) for n in re.findall(r"\b(\d{1,4})\b", text)]
301
+ named += re.findall(r"'([^']{1,3})'", text)
302
+ named += [v for w, v in ORDINAL.items() if re.search(r"\b%s\b" % w, text, re.I)]
303
+ out = named if strict else [0, 1, 2, -1, "", " "] + named
304
+ return list(dict.fromkeys(out))
305
+
306
+
307
+ GENERIC = {"write", "function", "python", "find", "given", "number", "numbers", "integer", "list", "string", "element",
308
+ "elements", "value", "values", "check", "whether", "two", "the", "and", "from", "all", "using", "which", "that",
309
+ "with", "into", "for", "each", "array", "tuple", "item", "items", "get", "given", "not", "out", "sequence"}
310
+
311
+
312
+ def words_of(desc):
313
+ return {w.lower().rstrip("s") for w in re.findall(r"[A-Za-z]+", desc or "")
314
+ if len(w) >= 3 and w.lower() not in GENERIC and w.lower().rstrip("s") not in GENERIC}
315
+
316
+
317
+ # ----------------------------------------------------------------------------------------------------------- search
318
+ class CodeOrgan:
319
+ def __init__(self, library=None, max_size=4, cap=60000):
320
+ self.prims = list(P_) + list(library or [])
321
+ self.max_size, self.cap = max_size, cap
322
+ self.why = ""
323
+
324
+ def learn(self, name, prog, arity_types, out_t, desc):
325
+ """STORE: a verified program becomes a library function; later requests compose it"""
326
+ fnc = prog["fn"]
327
+ src = "%s(%s)" % (name, ", ".join("{%d}" % i for i in range(len(arity_types))))
328
+ p = Prim("lib_" + name, tuple(arity_types), out_t, lambda *vals: fnc(vals), src, " ".join(words_of(desc)))
329
+ self.prims.append(p)
330
+ self.library_src = getattr(self, "library_src", {})
331
+ self.library_src[name] = prog["def"].replace(IMPORTS, "")
332
+ return p
333
+
334
+ def solve(self, desc, tests, setup="", visible=None, strict=False, cover=False, allow_lib=True):
335
+ """strict: request-named constants only + coverage selection (one example); cover: coverage selection at any
336
+ size (conversation); allow_lib: stored programs may compose (needs >= 2 examples)"""
337
+ cover = cover or strict
338
+ self.why = ""
339
+ pt = parse_tests(tests, setup)
340
+ if not pt:
341
+ self.why = "the examples are not simple asserts"
342
+ return None
343
+ fname, cases, wrap = pt
344
+ vis = cases if visible is None else cases[:visible]
345
+ if not vis:
346
+ return None
347
+ W = WRAP[wrap]
348
+ nargs = len(vis[0][0])
349
+ if any(len(a) != nargs for a, _ in cases):
350
+ self.why = "varying arity"
351
+ return None
352
+ target = [W(e) if wrap else e for _, e in vis]
353
+ kw = words_of(desc)
354
+ self._req_kw, self._cover, self._n_visible = kw, None, len(vis)
355
+ # the request SPECIFIES the moves: rank by the description's words; keep the whole library but try relevant first
356
+ # a stored program is a move only with >= 2 examples, and never inside its own definition; with ONE example
357
+ # (strict) only the base library composes - forms verified on one example are too weak to build on from one more
358
+ pool = [p for p in self.prims if p.name != "lib_" + fname and not ((strict or not allow_lib) and p.name.startswith("lib_"))]
359
+ prims = sorted(pool, key=lambda p: -len(p.kw & kw))
360
+ # terminals: arguments and constants; a term = (src, type, outputs-tuple, fn)
361
+ terms_by_size = {1: []}
362
+ seen = set()
363
+
364
+ def add(size, src, outs, fnc, used=frozenset()):
365
+ key = repr(outs)
366
+ try:
367
+ match = [W(o) if wrap else o for o in outs] == target
368
+ except Exception: # noqa: BLE001
369
+ match = False
370
+ if match:
371
+ # the request SPECIFIES the moves: a fit made only of moves the description never names is a
372
+ # coincidence of three examples, not an answer - it is not accepted and does not claim the signature.
373
+ # A fit must also READ its inputs: every argument is used, except an 'n' that only restates a length.
374
+ reads = all(argn[i] in re.findall(r"a\d+", src) or restates_len[i] for i in range(nargs))
375
+ # STRICT (one example): EVERY operation must be named by the request - the description specifies the
376
+ # program and the single example only checks it
377
+ named = all(pr.kw & kw for pr in used) if strict else any(pr.kw & kw for pr in used)
378
+ if used and reads and named:
379
+ return (src, fnc)
380
+ self.coincidences = getattr(self, "coincidences", 0) + 1
381
+ return None
382
+ if key in seen or len(key) > 1500 or len(seen) > self.cap:
383
+ return None # bounded memory: huge values and past-budget terms are not kept
384
+ seen.add(key)
385
+ ty = tyof(outs[0])
386
+ if any(tyof(o) != ty for o in outs):
387
+ ty = "any"
388
+ terms_by_size.setdefault(size, []).append((src, ty, outs, fnc, used))
389
+ return None
390
+
391
+ argn = ["a%d" % i for i in range(nargs)]
392
+ restates_len = [all(isinstance(a[i], int) and any(isinstance(a[j], (list, tuple, str)) and len(a[j]) == a[i]
393
+ for j in range(nargs) if j != i) for a, _ in vis)
394
+ for i in range(nargs)]
395
+ for i in range(nargs):
396
+ hit = add(1, argn[i], tuple(a[i] for a, _ in vis), (lambda i: lambda xs: xs[i])(i))
397
+ if hit:
398
+ return self._emit(fname, argn, hit, cases, wrap, desc)
399
+ for c in consts_of(desc, vis, strict):
400
+ add(1, repr(c), tuple(c for _ in vis), (lambda c: lambda xs: c)(c))
401
+ n_terms = 0
402
+ best = None
403
+ first_hit_size = None
404
+ for size in range(2, self.max_size + 1):
405
+ if cover and best and first_hit_size is None:
406
+ first_hit_size = size - 1
407
+ if cover and first_hit_size is not None and size > first_hit_size + 1:
408
+ break # v3: look at most ONE size beyond the first fit (bounded time)
409
+ for p in prims:
410
+ k = len(p.ins)
411
+ # child sizes summing to size-1
412
+ for parts in _compositions(size - 1, k):
413
+ pools = [terms_by_size.get(s, []) for s in parts]
414
+ pools = [[t for t in pool if fits(t[1], want) or t[1] == "any"] for pool, want in zip(pools, p.ins)]
415
+ if any(not pool for pool in pools):
416
+ continue
417
+ for combo in itertools.product(*pools):
418
+ n_terms += 1
419
+ if n_terms > self.cap:
420
+ if cover and best: # v3: a found fit is never discarded by the budget
421
+ return self._emit(fname, argn, best[1], cases, wrap, desc)
422
+ self.why = "search budget exhausted"
423
+ return None
424
+ try:
425
+ outs = tuple(p.fn(*[t[2][j] for t in combo]) for j in range(len(vis)))
426
+ except Exception: # noqa: BLE001
427
+ continue
428
+ if any(_oversize(o) for o in outs):
429
+ continue # bounded memory: never represent a huge intermediate value
430
+ src = p.src.format(*[t[0] for t in combo])
431
+ fnc = (lambda p, combo: lambda xs: p.fn(*[t[3](xs) for t in combo]))(p, combo)
432
+ used = frozenset([p]).union(*[t[4] for t in combo])
433
+ hit = add(size, src, outs, fnc, used)
434
+ if hit:
435
+ if not cover:
436
+ return self._emit(fname, argn, hit, cases, wrap, desc)
437
+ # COVERAGE: how much of what the request says the program's moves account for, jointly
438
+ named = len(set().union(*[pr.kw for pr in used]) & kw)
439
+ if best is None or named > best[0]:
440
+ best = (named, hit)
441
+ self._cover = named
442
+ if cover and best: # v2 STRICT: the fit covering most of the request, any size
443
+ return self._emit(fname, argn, best[1], cases, wrap, desc)
444
+ self.why = "no program of size <= %d fits the examples" % self.max_size
445
+ return None
446
+
447
+ def _emit(self, fname, argn, hit, cases, wrap, desc):
448
+ src, fnc = hit
449
+ lib = getattr(self, "library_src", {})
450
+ used = [n for n in lib if re.search(r"\b%s\(" % re.escape(n), src)]
451
+ deps = "".join(lib[n] for n in used) # stored forms this program composes
452
+ body = "%s%sdef %s(%s):\n return %s\n" % (IMPORTS, deps, fname, ", ".join(argn), src)
453
+ # CHECK: execute the RENDERED source (not the in-memory closure) against every example given
454
+ ns = {}
455
+ exec(compile(body, "<organ>", "exec"), ns) # noqa: S102 - composed only of library templates
456
+ W = WRAP[wrap]
457
+ passed = 0
458
+ for a, e in cases:
459
+ try:
460
+ out = ns[fname](*a)
461
+ ok = (W(out) if wrap else out) == e
462
+ except Exception: # noqa: BLE001
463
+ ok = False
464
+ passed += ok
465
+ return {"def": body, "fname": fname, "expr": src, "passed": passed, "cases": len(cases), "fn": fnc,
466
+ "arity": len(argn), "reused": used, "cover": getattr(self, "_cover", None),
467
+ "req_words": len(getattr(self, "_req_kw", ()) or ()), "n_visible": getattr(self, "_n_visible", None)}
468
+
469
+
470
+ def _oversize(o):
471
+ if isinstance(o, bool):
472
+ return False
473
+ if isinstance(o, int):
474
+ return abs(o) > 10 ** 12
475
+ if isinstance(o, str):
476
+ return len(o) > 400
477
+ if isinstance(o, (list, tuple, set, dict)):
478
+ return len(o) > 200 or any(_oversize(x) for x in list(o)[:50])
479
+ return False
480
+
481
+
482
+ def _compositions(total, k):
483
+ if k == 1:
484
+ yield (total,)
485
+ return
486
+ for first in range(1, total - k + 2):
487
+ for rest in _compositions(total - first, k - 1):
488
+ yield (first,) + rest
root/wander-train/exact_router_v1.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """exact_router_v1 - Oracle7's EXACT relation router (runtime form). No similarity, no learned weights: a question is answered
3
+ only when an ADMITTED pattern names the relation it asks and the linked entities agree on one stored value.
4
+
5
+ Patterns (router_mine_v3, 2026-10-06): (asked noun or wh-word, one content word) -> Wikidata property, mined over 124,000
6
+ TriviaQA train questions; admitted when the answer given through that property is right >= 90% of the time on BOTH md5
7
+ halves with >= 8 firings in each (32 patterns, 13 properties). Never-read holdout (TriviaQA train offset 82,000): 33 right,
8
+ 1 wrong (the wrong one was a Wikidata unknown-value blank node - now refused, rule 3 below).
9
+ Answer: entity_linker_v2 top span -> its candidates (<= 3) -> every candidate carrying the routed property must agree on
10
+ ONE value. Refused: (1) yes/no questions; (2) a hidden hop - an unnamed intermediate between the named entity and the
11
+ answer ("an artist who also directed", "the brother-in-law of X"), the guard measured 2026-10-06; (3) blank-node values;
12
+ (4) every matching pattern must name the SAME property.
13
+ use: answer(question) -> {"answer", "property", "subject_qid", "pattern"} or None"""
14
+ import json
15
+ import re
16
+
17
+ import entity_linker_v2 as L
18
+ import oracle_relstore_v1 as RS
19
+
20
+ PATTERNS = __import__("os").environ.get("ORACLE7_ROOT", "/opt/oracle-clm") + "/relstore/router_patterns_v4.json"
21
+ STORE = __import__("os").environ.get("ORACLE7_ROOT", "/opt/oracle-clm") + "/relstore/relations_v2.sqlite3"
22
+ _PATS, _RS = None, None
23
+ norm = L.norm
24
+ HEAD = re.compile(r"\b(?:which|what)\s+((?:[a-z]+\s+){0,2}?[a-z]+)(?:\s+(?:is|was|are|were|did|does|do|has|had|in|of|for|from|"
25
+ r"to|by|named|called|founded|created|directed|wrote|won|became)\b|\s*,)", re.I)
26
+ WH = re.compile(r"\b(who|whom|whose|where|when|which|what|how)\b", re.I)
27
+ POLAR = re.compile(r"^\s*(is|are|was|were|do|does|did|has|have|had|can|could|will|would|should)\b", re.I)
28
+ HOP = re.compile(r"\b(a|an|the)\s+([\w'–-]+\s+){0,3}(who|whom|whose|which|that)\b(?!\s*\?)"
29
+ r"|\b(a|an|the)\s+([\w'–-]+\s+){0,5}(created|directed|written|founded|coached|owned|produced|hosted|led|played|named)\s+by\b"
30
+ r"|\bthe\s+(illustrator|author|director|founder|creator|wife|husband|father|mother|son|daughter|brother|sister|"
31
+ r"brother-in-law|sister-in-law|father-in-law|mother-in-law|son-in-law|daughter-in-law|nephew|niece|uncle|aunt|"
32
+ r"owner|composer|producer|writer|singer|star|team|club|band|company|capital|leader|coach|manager)\s+of\b", re.I)
33
+
34
+
35
+ def _load():
36
+ global _PATS, _RS
37
+ if _PATS is None:
38
+ _PATS = {tuple(k.split("|", 1)): v[0] for k, v in json.load(open(PATTERNS)).items()}
39
+ _RS = RS.RelStore() if STORE is None else RS.RelStore(STORE)
40
+ return _PATS, _RS
41
+
42
+
43
+ def features(q, span):
44
+ m = HEAD.search(q)
45
+ head = norm(m.group(1)).split()[-1] if m else None
46
+ w = WH.search(q)
47
+ key = head or (w.group(1).lower() if w else "-")
48
+ ent = set(norm(span).split())
49
+ words = {x for x in norm(q).split() if x not in L.STOP and x not in ent and not x.isdigit() and len(x) >= 3}
50
+ return {(key, x) for x in words} | {(key, "*")}
51
+
52
+
53
+ DOMINANCE = 10 # a name whose top sense is >= 10x more linked refers to it
54
+
55
+
56
+ def hop(q, span):
57
+ """a hidden hop - but "the capital of France" is no hop when France is the named entity right after "of" """
58
+ for m in HOP.finditer(q):
59
+ if m.group(0).lower().endswith(" of"):
60
+ after = norm(q[m.end():])
61
+ if after.startswith(norm(span)) or after.startswith("the " + norm(span)):
62
+ continue
63
+ return True
64
+ return False
65
+
66
+
67
+ # the store holds CURRENT values: a past-time qualifier on a relation that changes over time is not answerable from it
68
+ TIME_VARIANT = {36, 35, 6, 54, 159, 27, 17, 131, 286, 488, 1037, 108, 39, 463, 102, 26}
69
+ PAST = re.compile(r"\b(between|until|till|before|after|during|in|from|by)\s+(the\s+)?(\d{3,4}s?|\d{1,2}(st|nd|rd|th)\s+century)\b"
70
+ r"|\b(formerly|former|originally|once|previously|ancient|old)\b", re.I)
71
+ QUOTED = re.compile(r"[‘“\"']([^’”\"']{3,80})[’”\"']")
72
+
73
+
74
+ def subject_ok(q, span):
75
+ """the linked span must be the question's subject: a quoted title must BE the span; a later "of <Name>" means the
76
+ subject is further along the "of" chain ("the capital of the U.S. state of Connecticut")"""
77
+ s = q.strip()
78
+ if len(s) > 2 and s[0] == '"' and s[-1] == '"':
79
+ s = s[1:-1] # CSV-wrapped question: "Who ... ""Citizen Kane""?"
80
+ m = QUOTED.search(s.replace('""', '"'))
81
+ if m and norm(m.group(1)) != norm(span):
82
+ return False
83
+ i = q.find(span)
84
+ if i >= 0 and re.search(r"\bof\s+(the\s+)?[A-Z]", q[i + len(span):]):
85
+ return False
86
+ return True
87
+
88
+
89
+ def answer(q):
90
+ pats, rs = _load()
91
+ q = str(q)
92
+ if POLAR.search(q):
93
+ return None
94
+ sp = L.link(q, 3)
95
+ if not sp:
96
+ return None
97
+ span, cs = sp[0]
98
+ if hop(q, span) or not subject_ok(q, span):
99
+ return None
100
+ if DOMINANCE and len(cs) > 1 and cs[0][1] >= DOMINANCE * max(1, max(c[1] for c in cs[1:])):
101
+ cs = cs[:1]
102
+ matched = {f: pats[f] for f in features(q, span) if f in pats}
103
+ props = set(matched.values())
104
+ if len(props) != 1:
105
+ return None
106
+ p = props.pop()
107
+ if p in TIME_VARIANT and PAST.search(q):
108
+ return None
109
+ sets, subj = [], None
110
+ for qid, _, _ in cs[:3]:
111
+ vals = {rs.label(o) if o else lit for pp, o, lit in rs.props_of(qid) if pp == p}
112
+ vals = {v for v in vals if v and ".well-known/genid" not in v}
113
+ if vals:
114
+ sets.append(frozenset(vals))
115
+ subj = subj or qid
116
+ if not sets or len(set(sets)) != 1 or len(sets[0]) != 1:
117
+ return None
118
+ aw = [w for w in norm(next(iter(sets[0]))).split() if w not in L.STOP]
119
+ if aw and all(w in set(norm(q).split()) for w in aw):
120
+ return None # the answer restates the question: not an answer
121
+ return {"answer": _show(next(iter(sets[0])), q), "property": p, "subject_qid": subj,
122
+ "pattern": "|".join(sorted(matched)[0])}
123
+
124
+
125
+ MONTHS = ("January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December")
126
+
127
+
128
+ def _show(v, q):
129
+ """a stored time "1034-03-27T00:00:00Z" reads as "1034" when the year is asked, else "27 March 1034"; BCE years keep
130
+ their sign as "BC" """
131
+ m = re.match(r"^([+-]?)(\d{1,4})-(\d\d)-(\d\d)T", v)
132
+ if not m:
133
+ return v
134
+ y = str(int(m.group(2))) + (" BC" if m.group(1) == "-" else "")
135
+ if re.search(r"\byear\b", q, re.I) or m.group(3) == "00" or (m.group(3) == "01" and m.group(4) == "01"):
136
+ return y
137
+ return "%d %s %s" % (int(m.group(4)), MONTHS[int(m.group(3)) - 1], y)
138
+
139
+
140
+ if __name__ == "__main__":
141
+ import sys
142
+ for q in sys.argv[1:]:
143
+ print(q, "->", answer(q))
root/wander-train/general_reader_v1.py ADDED
@@ -0,0 +1,471 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """general_reader_v1 - a relation reader that does not depend on how a question is phrased (Box C).
3
+ For a question the release declines: find the entities it names (exact Wikipedia titles), read every fact one step from them in
4
+ the Wikidata relation store, and answer ONLY when all three agree:
5
+ (1) CUE - the relation is named by the question's own words (property label stems, or a measured router cue pair)
6
+ (2) TYPE - the answer is the kind of thing asked for (who -> a person; where -> a place; "which comedian" -> a comedian)
7
+ (3) UNIQUE- exactly one candidate survives (two different answers -> decline)
8
+ The answer is never an entity the question itself names. Every answer carries its receipt (subject, property, value, ids).
9
+ usage (dev measurement): general_reader_v1.py eval <trivia json ...> | general_reader_v1.py ask "<question>" """
10
+ import json
11
+ import re
12
+ import sqlite3
13
+ import sys
14
+ import unicodedata
15
+
16
+ STORE = __import__("os").environ.get("ORACLE7_ROOT", "/opt/oracle-clm") + "/relstore/relations_v2.sqlite3"
17
+ db = sqlite3.connect("file:%s?mode=ro&immutable=1" % STORE, uri=True, check_same_thread=False)
18
+
19
+ STOP = set("a an the of in on at to for by with from and or is was were are be been being who whom whose what which when where why how "
20
+ "did does do has have had this that these those it its he she they his her their as into than then there here one".split())
21
+ AUX = set("is was were are did does do has have had can could will would should may might".split())
22
+ HUMAN_Q = ("who", "whom", "whose")
23
+ PLACE_WORDS = set("city town village country state province region county island capital place municipality commune borough "
24
+ "district settlement port river mountain lake sea ocean continent area".split())
25
+ GENERIC_TYPE = set("one name person people thing year type kind part title area word term number".split())
26
+ # qualifiers the reader cannot evaluate from one fact: order, extremes, time, fiction or a work's own world
27
+ QUALIFIER = re.compile(r"\b(first|last|only|second|third|fourth|fifth|oldest|youngest|largest|smallest|biggest|longest|shortest|"
28
+ r"highest|lowest|tallest|most|least|best|worst|original|former|current|currently|previous|next|latest|earliest|"
29
+ r"famous|famously|nicknamed|known as|better known|called|real name|in the (?:bible|film|movie|novel|book|play|"
30
+ r"series|show|song|opera|musical|game|tv|story|poem|legend)|in (?:greek|roman|norse) mythology|"
31
+ r"(?:1[0-9]|20)[0-9]{2}|century|decade|year|when|during|at the time|outbreak|battle|war|preceded|succeeded|before|after|"
32
+ r"between|same|other|also|both|nickname|setting|set in|according to)\b", re.I)
33
+
34
+ PROP, PROPNAMES = {}, {}
35
+ for p, lab in db.execute("SELECT p, label FROM prop_src"):
36
+ if lab:
37
+ PROP[p] = lab
38
+ for line in open(__import__("os").environ.get("ORACLE7_ROOT", "/opt/oracle-clm") + "/relstore/prop_labels.tsv", encoding="utf-8"):
39
+ m = re.match(r'<http://www\.wikidata\.org/entity/P(\d+)>\t"(.*)"@en', line.strip())
40
+ if m:
41
+ p = int(m.group(1))
42
+ PROPNAMES.setdefault(p, []).append(m.group(2)) # the file holds the label AND its aliases, one per line
43
+ PROP.setdefault(p, m.group(2))
44
+ # everyday words for common relations (question wording differs from Wikidata's label); measured on dev like everything else
45
+ ALIASES = {26: "married marry wife husband spouse", 25: "mother", 22: "father", 3373: "sister brother sibling", 40: "son daughter child",
46
+ 50: "writer author novelist", 57: "directed director", 86: "composed composer music", 170: "created creator",
47
+ 112: "founded founder", 36: "capital", 19: "born birthplace", 20: "died death", 569: "born", 570: "died",
48
+ 175: "singer performer", 161: "starred", 69: "studied attended educated", 108: "worked employer",
49
+ 54: "played team", 166: "won awarded award", 131: "located situated", 138: "named", 37: "language spoken official",
50
+ 38: "currency", 6: "prime minister mayor premier", 35: "president king queen monarch emperor", 27: "nationality citizen",
51
+ 800: "known work", 61: "discovered discoverer", 397: "orbits orbit", 30: "continent", 140: "religion", 102: "party",
52
+ 127: "owned owner owns", 176: "made built manufactured manufacturer", 84: "designed architect", 674: "characters character",
53
+ 1441: "appears appear", 136: "genre", 264: "label", 98: "edited editor", 162: "produced producer", 58: "screenplay screenwriter",
54
+ 710: "participant participated", 1346: "won winner", 17: "country", 495: "country", 159: "headquarters based", 740: "formed",
55
+ 571: "founded established formed", 576: "dissolved abolished", 1037: "director manager", 488: "chairman chairperson",
56
+ 169: "chief executive ceo", 286: "head coach manager", 451: "partner", 1038: "relative", 1327: "partner", 413: "position played",
57
+ 118: "league", 641: "sport", 115: "home stadium ground venue", 361: "part", 150: "contains", 206: "located", 1376: "capital"}
58
+ ALIAS_STEMS = {}
59
+ try:
60
+ ALLOW = set(json.load(open(__import__("os").environ.get("ORACLE7_ROOT", "/opt/oracle-clm") + "/relstore/reader_prop_allowlist.json"))) # relations the admitted relation specialists read
61
+ except Exception: # noqa: BLE001
62
+ ALLOW = set()
63
+ ALLOW -= {102, 1082} # party membership changes; population is a number
64
+ SKIP_PROPS = {21, 971, 735, 734, 1343, 910, 4224, 1889, 31, 279, 373, 646, 214, 227, 244, 213, 268, 269, 349, 434, 435, 436,
65
+ 1006, 1015, 1017, 1263, 2671, 3417, 5008, 1424, 1151, 1792, 3876, 361, 527, 5869, 2184, 8402, 1482, 3222}
66
+
67
+
68
+ def norm(s):
69
+ s = unicodedata.normalize("NFKD", str(s)).encode("ascii", "ignore").decode("ascii")
70
+ return " ".join(re.sub(r"[^a-z0-9 ]+", " ", s.lower()).split())
71
+
72
+
73
+ def stem(w):
74
+ w = w.lower()
75
+ if w.endswith("ied") and len(w) <= 5: # died -> die, lied -> lie
76
+ return w[:-1]
77
+ for suf in ("ations", "ation", "ings", "ing", "ers", "ors", "er", "or", "ed", "es", "s", "ion"):
78
+ if len(w) > len(suf) + 3 and w.endswith(suf):
79
+ return w[: -len(suf)]
80
+ return w
81
+
82
+
83
+ _lab, _cls = {}, {}
84
+
85
+
86
+ def label(q):
87
+ if q not in _lab:
88
+ r = db.execute("SELECT label, enwiki FROM entity WHERE qid=?", (q,)).fetchone()
89
+ _lab[q] = (r[0] or r[1]) if r else None
90
+ return _lab[q]
91
+
92
+
93
+ def classes(q):
94
+ if q in _cls:
95
+ return _cls[q]
96
+ out, frontier, seen = set(), [o for (o,) in db.execute("SELECT o FROM fact WHERE s=? AND p=31", (q,)) if o], set()
97
+ for d in range(2): # its class and one parent: higher superclasses name things it is not
98
+ nxt = []
99
+ for c in frontier:
100
+ if c in seen:
101
+ continue
102
+ seen.add(c)
103
+ lab = label(c)
104
+ if lab:
105
+ ws = norm(lab).split()
106
+ if ws:
107
+ out.add(ws[-1]) # head noun: "town in Finland" is a town, not a Finland
108
+ if d < 1:
109
+ nxt += [o for (o,) in db.execute("SELECT o FROM fact WHERE s=? AND p=279", (c,)) if o]
110
+ frontier = nxt
111
+ _cls[q] = out
112
+ return out
113
+
114
+
115
+ _cls0 = {}
116
+ GEO_SYN = {"country": "country state kingdom republic nation monarchy federation empire principality sultanate emirate",
117
+ "city": "city town capital metropolis municipality borough commune", "town": "town city municipality borough commune village",
118
+ "state": "state province", "county": "county shire", "island": "island atoll", "river": "river stream tributary",
119
+ "mountain": "mountain peak volcano massif summit", "lake": "lake reservoir", "sea": "sea", "ocean": "ocean"}
120
+ GEO_SYN = {stem(k): set(stem(x) for x in v.split()) for k, v in GEO_SYN.items()}
121
+
122
+
123
+ def classes0(q):
124
+ """head nouns of the thing's own classes (P31 only): a city is a city, not the country its classes sit under"""
125
+ if q not in _cls0:
126
+ out = set()
127
+ for (o,) in db.execute("SELECT o FROM fact WHERE s=? AND p=31", (q,)):
128
+ lab = label(o) if o else None
129
+ ws = norm(lab).split() if lab else []
130
+ if ws:
131
+ out.add(stem(ws[-1]))
132
+ _cls0[q] = out
133
+ return _cls0[q]
134
+
135
+
136
+ def is_human(q):
137
+ return db.execute("SELECT 1 FROM fact WHERE s=? AND p=31 AND o=5 LIMIT 1", (q,)).fetchone() is not None
138
+
139
+
140
+ def ent(title):
141
+ t = title.strip(" .,'\"")
142
+ if not t:
143
+ return None
144
+ for v in dict.fromkeys((t, t[:1].upper() + t[1:], " ".join(w[:1].upper() + w[1:] for w in t.split()))):
145
+ r = db.execute("SELECT qid FROM entity WHERE enwiki=? LIMIT 1", (v,)).fetchone()
146
+ if r:
147
+ return r[0]
148
+ return None
149
+
150
+
151
+ def linked(q):
152
+ words = re.findall(r"[A-Za-z0-9'\-\.&]+", q.replace('"', " "))
153
+ taken, out = set(), []
154
+ for n in (6, 5, 4, 3, 2, 1):
155
+ for i in range(len(words) - n + 1):
156
+ if any(j in taken for j in range(i, i + n)):
157
+ continue
158
+ span = words[i:i + n]
159
+ if span[0].lower() in STOP or span[-1].lower() in STOP:
160
+ continue
161
+ if n == 1 and (not span[0][:1].isupper() or i == 0 or len(span[0]) < 3):
162
+ continue
163
+ if all(w.lower() in STOP for w in span):
164
+ continue
165
+ before = words[i - 1] if i > 0 else ""
166
+ after = words[i + n] if i + n < len(words) else ""
167
+ if (after[:1].isupper() and after.lower() not in STOP) or (i > 1 and before[:1].isupper() and before.lower() not in STOP):
168
+ continue # part of a longer name ("Andrew Bonar" in "Andrew Bonar Law")
169
+ qid = ent(" ".join(span))
170
+ if qid:
171
+ out.append((" ".join(span), qid))
172
+ taken.update(range(i, i + n))
173
+ return out
174
+
175
+
176
+ def asked_type(q):
177
+ ql = q.lower().strip()
178
+ w = re.findall(r"[a-z]+", ql)
179
+ if not w:
180
+ return None, set()
181
+ if w[0] in HUMAN_Q or " who " in " " + ql[:40] or re.search(r"\bto whom\b|\bwhom\b", ql):
182
+ return "human", set()
183
+ m = re.search(r"\b(?:which|what)\s+((?:[a-z\-]+\s+){0,2}?[a-z\-]+)\s*\?\s*$", ql) # "... the sister of which actor?"
184
+ if m:
185
+ nouns = set(stem(x) for x in m.group(1).split() if x not in STOP and x not in GENERIC_TYPE)
186
+ if nouns:
187
+ return "noun", nouns
188
+ m = re.match(r"(?:in )?(?:which|what) (city|town|country|state|county|island|river|mountain|lake|sea|ocean)\b", ql)
189
+ if m:
190
+ return "noun", {stem(m.group(1))}
191
+ if w[0] == "where":
192
+ return "place", set()
193
+ if w[0] == "when" or re.match(r"(in )?(what|which) year", ql):
194
+ return "time", set()
195
+ if re.match(r"how (many|much|long|old|far|tall|high)", ql):
196
+ return "number", set()
197
+ m = re.match(r"(?:what|which|who) (?:is|was|are|were) the ([a-z\-]+(?: [a-z\-]+)?) of\b", ql) # "what is the capital of ..."
198
+ if m:
199
+ head = m.group(1).split()[-1]
200
+ if head not in STOP and head not in GENERIC_TYPE:
201
+ return "rel", {stem(head)}
202
+ m = re.search(r"\b(which|what)\s+((?:[a-z\-]+\s+){0,3}?)([a-z\-]+)\s+(?:" + "|".join(AUX) + r"|[a-z]+ed|[a-z]+s)\b", ql)
203
+ if m:
204
+ nouns = set(stem(x) for x in (m.group(2) + " " + m.group(3)).split() if x not in STOP and x not in GENERIC_TYPE)
205
+ if nouns:
206
+ return "noun", nouns
207
+ return None, set()
208
+
209
+
210
+ _pw = {}
211
+
212
+
213
+ def prop_words(p):
214
+ if p not in _pw:
215
+ s = set()
216
+ for name in PROPNAMES.get(p, [PROP.get(p, "")]):
217
+ ws = [stem(x) for x in norm(name).split() if x not in STOP and len(x) > 2]
218
+ if 0 < len(ws) <= 3: # long descriptive aliases are not cues
219
+ s.update(ws)
220
+ _pw[p] = s
221
+ return _pw[p]
222
+
223
+
224
+ # words too loose to name a relation on their own (dev 10-14): "wrote" fits author, lyricist and publisher; "song" fits lyricist;
225
+ # "found" (in "found in the museum") stems like "founded"
226
+ STOPCUE = {stem(x) for x in ("write wrote written writ song songs theme found find make made play played sing sang known book books "
227
+ "music musical film films movie movies national").split()}
228
+
229
+
230
+ def cued(p, qwords_stem, qwords):
231
+ qwords_stem = qwords_stem - STOPCUE
232
+ if prop_words(p) & qwords_stem:
233
+ return True
234
+ if p not in ALIAS_STEMS:
235
+ ALIAS_STEMS[p] = set(stem(x) for x in ALIASES.get(p, "").split())
236
+ if ALIAS_STEMS[p] & qwords_stem:
237
+ return True
238
+ return False # cue pairs dropped: "which|city" is not a relation (dev 10-14)
239
+
240
+
241
+ FEMININE = {"actress": "actor", "authoress": "author", "poetess": "poet", "comedienne": "comedian", "sculptress": "sculptor",
242
+ "heroine": "hero", "princess": "prince", "duchess": "duke", "empress": "emperor", "songstress": "singer"}
243
+
244
+
245
+ def type_ok(kind, nouns, cand_qid, lit, p=None):
246
+ if kind == "rel":
247
+ # the relation noun itself ("capital") must be the property: named by the property's own label or aliases
248
+ return p is not None and bool(prop_words(p) & nouns) and (cand_qid is not None or lit is not None)
249
+ if kind == "time":
250
+ return lit is not None and bool(re.search(r"\b1?\d{3,4}\b", str(lit)))
251
+ if cand_qid is None:
252
+ return False
253
+ if kind == "human":
254
+ return is_human(cand_qid)
255
+ if kind == "place":
256
+ return bool(classes(cand_qid) & PLACE_WORDS)
257
+ if kind == "noun":
258
+ nouns = set(stem(FEMININE.get(x, x)) for x in nouns) | nouns
259
+ cw = set(stem(x) for x in classes(cand_qid))
260
+ if "person" in nouns or "man" in nouns or "woman" in nouns:
261
+ return is_human(cand_qid)
262
+ geo = [n for n in nouns if n in GEO_SYN]
263
+ if geo: # "which country": Nairobi is a city (held-out 10-14: Peter Hain)
264
+ return any(classes0(cand_qid) & GEO_SYN[n] for n in geo)
265
+ return bool(cw & nouns) or (is_human(cand_qid) and bool(nouns & occupations(cand_qid)))
266
+ return False
267
+
268
+
269
+ _occ = {}
270
+
271
+
272
+ def occupations(q):
273
+ if q not in _occ:
274
+ s = set()
275
+ for (o,) in db.execute("SELECT o FROM fact WHERE s=? AND p IN (106, 39) LIMIT 30", (q,)):
276
+ if o and label(o):
277
+ ws = norm(label(o)).split()
278
+ if ws:
279
+ s.add(stem(ws[-1])) # the head noun only: "film score composer" is a composer
280
+ _occ[q] = s
281
+ return _occ[q]
282
+
283
+
284
+ _gen = {}
285
+
286
+
287
+ def generic(e):
288
+ """a class or concept, not a particular thing: it is a subclass of something, or many things are instances of it"""
289
+ if e not in _gen:
290
+ sub = db.execute("SELECT 1 FROM fact WHERE s=? AND p=279 LIMIT 1", (e,)).fetchone() is not None
291
+ inst = len(db.execute("SELECT s FROM fact WHERE p=31 AND o=? LIMIT 30", (e,)).fetchall()) >= 30
292
+ occ = len(db.execute("SELECT s FROM fact WHERE p=106 AND o=? LIMIT 30", (e,)).fetchall()) >= 30
293
+ _gen[e] = sub or inst or occ
294
+ return _gen[e]
295
+
296
+
297
+ _word_props = None
298
+
299
+
300
+ def inv_props(qwords_stem, qwords):
301
+ """properties whose label/alias words or cue pairs appear in the question (index built once; was a scan of 38k per entity)"""
302
+ global _word_props
303
+ if _word_props is None:
304
+ _word_props = {}
305
+ for p in set(PROPNAMES) | set(PROP):
306
+ if p in SKIP_PROPS:
307
+ continue
308
+ for w in prop_words(p) | set(stem(x) for x in ALIASES.get(p, "").split()):
309
+ _word_props.setdefault(w, set()).add(p)
310
+ ps = set()
311
+ for w in qwords_stem - STOPCUE:
312
+ ps |= _word_props.get(w, set())
313
+ ps = [p for p in ps if db.execute("SELECT 1 FROM fact WHERE p=? LIMIT 1", (p,)).fetchone()]
314
+ return ps[:60]
315
+
316
+
317
+ SHORT_NAMES = {"uk": 145, "u k": 145, "britain": 145, "great britain": 145, "us": 30, "u s": 30, "usa": 30, "u s a": 30,
318
+ "america": 30, "ussr": 15180, "soviet union": 15180, "uae": 878, "eu": 458, "holland": 55}
319
+
320
+
321
+ def echoed(q, val, ans):
322
+ """the answer is already in the question: by its label, or by a common short name (dev-burned held-out 10-14: "Where
323
+ are Akrotiri and Dhekelia, allocated by treaty to the UK" read the holder, the UK, as the place)"""
324
+ qn = " %s " % norm(q)
325
+ words = [w for w in norm(ans).split() if w not in STOP and w not in ("the", "of")]
326
+ if words and all(" %s " % w in qn for w in words):
327
+ return True
328
+ return val is not None and any(" %s " % k in qn and v == val for k, v in SHORT_NAMES.items())
329
+
330
+
331
+ QWORDS = set("who whom whose what which where when why how in on at of the a an is was were are did does do name".split())
332
+
333
+
334
+ def unlinked_name(q, links):
335
+ """a capitalised name or a quoted title in the question that no linked span covers"""
336
+ covered = set()
337
+ for t, _ in links:
338
+ covered.update(w.lower() for w in re.findall(r"[A-Za-z0-9']+", t))
339
+ for quoted in re.findall(r"[\"“]([^\"”]{2,80})[\"”]", q):
340
+ if not any(quoted.lower() in t.lower() or t.lower() in quoted.lower() for t, _ in links):
341
+ return True
342
+ words = re.findall(r"[A-Za-z][A-Za-z0-9'\-]*", q)
343
+ for i, w in enumerate(words):
344
+ if i == 0 or not w[:1].isupper() or w.lower() in QWORDS or w.lower() in STOP:
345
+ continue
346
+ if w.lower() not in covered:
347
+ return True
348
+ return False
349
+
350
+
351
+ def answer(q):
352
+ if QUALIFIER.search(q):
353
+ return None
354
+ kind, nouns = asked_type(q)
355
+ if kind in (None, "number"):
356
+ return None
357
+ ents = [(t, e) for t, e in linked(q) if not generic(e)]
358
+ if not ents:
359
+ return None
360
+ if unlinked_name(q, linked(q)):
361
+ return None # held-out 10-14: Jennings Brewery -> read Cumbria; Dismaland -> Banksy
362
+ if q.count(",") >= 2 or (re.search(r"\band\b", q) and len(ents) >= 2):
363
+ return None # several clues joined: one fact cannot answer it
364
+ if kind == "rel":
365
+ m = re.search(r"\bof (?:the )?(.+?)(?:\?|$| in | on | at )", q)
366
+ if not m:
367
+ return None
368
+ first = [(t, e) for t, e in ents if m.group(1).lower().startswith(t.lower())]
369
+ if not first:
370
+ return None
371
+ ents = first[:1] # "the capital of the Yukon Territory in Canada": the Yukon, not Canada
372
+ named = {qid for _, qid in ents}
373
+ qwords = set(re.findall(r"[a-z]+", q.lower()))
374
+ qwords_stem = set(stem(x) for x in qwords if x not in STOP)
375
+ if kind == "noun":
376
+ qwords_stem -= nouns # the answer-type word is not the relation ("which country ... headquarters")
377
+ cands = {}
378
+ for span, e in ents:
379
+ for p, o, lit in db.execute("SELECT p, o, lit FROM fact WHERE s=? LIMIT 4000", (e,)):
380
+ if p in SKIP_PROPS or p not in ALLOW or (o is not None and o in named):
381
+ continue
382
+ if o is not None and (label(o) or "").startswith("Category:"):
383
+ continue
384
+ if not cued(p, qwords_stem, qwords):
385
+ continue
386
+ if type_ok(kind, nouns, o, lit, p):
387
+ key = ("q", o) if o is not None else ("l", str(lit))
388
+ cands.setdefault(key, []).append((e, p, "fwd"))
389
+ for p in [x for x in inv_props(qwords_stem, qwords) if x in ALLOW]:
390
+ # reverse answers only when the relation's MAIN name is in the question: aliases such as "mother of" on the
391
+ # child relation describe the relation backwards (dev 10-14: "mother of Achilles")
392
+ main = set(stem(x) for x in norm(PROP.get(p, "")).split() if x not in STOP and len(x) > 2)
393
+ if not (main & qwords_stem):
394
+ continue
395
+ subs = [s for (s,) in db.execute("SELECT s FROM fact WHERE p=? AND o=? LIMIT 6", (p, e))]
396
+ if len(subs) > 5: # a big group (everyone who received an award): no reverse answer
397
+ continue
398
+ for s in subs:
399
+ if s in named:
400
+ continue
401
+ if type_ok(kind, nouns, s, None, p):
402
+ cands.setdefault(("q", s), []).append((e, p, "inv"))
403
+ if len(cands) != 1:
404
+ return None
405
+ (kt, val), why = next(iter(cands.items()))
406
+ if len(ents) >= 2 and len({w[0] for w in why}) < 2: # two or more named things: at least two must agree (held-out
407
+ return None # 10-14: Jennings Brewery -> Cumbria's capital, Dismaland -> Banksy)
408
+ if False:
409
+ return None
410
+ e, p, d = why[0]
411
+ ans = label(val) if kt == "q" else val
412
+ if not ans:
413
+ return None
414
+ if echoed(q, val if kt == "q" else None, ans):
415
+ return None # the question already names it ("allocated to the UK": not where it is)
416
+ if d == "fwd":
417
+ receipt = "%s (Q%d) - %s (P%d) - %s" % (label(e), e, PROP.get(p, ""), p, ("%s (Q%d)" % (ans, val)) if kt == "q" else ans)
418
+ else:
419
+ receipt = "%s (Q%d) - %s (P%d) - %s (Q%d)" % (ans, val, PROP.get(p, ""), p, label(e), e)
420
+ return {"answer": ans, "receipt": receipt, "prop": p, "dir": d, "kind": kind, "agree": len(why)}
421
+
422
+
423
+ def correct(ans, golds):
424
+ """exact match after normalising, or one contains the other as whole words ("Lake Mead" / "Mead", "The Dirham" /
425
+ "United Arab Emirates dirham"); articles dropped"""
426
+ drop = lambda s: " ".join(w for w in norm(s).split() if w not in ("the", "a", "an"))
427
+ a = drop(ans)
428
+ for g in (drop(x) for x in golds if x):
429
+ if not g:
430
+ continue
431
+ if a == g:
432
+ return True
433
+ if len(g) >= 4 and re.search(r"\b%s\b" % re.escape(g), a):
434
+ return True
435
+ if len(a) >= 4 and re.search(r"\b%s\b" % re.escape(a), g):
436
+ return True
437
+ return False
438
+
439
+
440
+ if __name__ == "__main__":
441
+ if sys.argv[1] == "ask":
442
+ print(json.dumps(answer(sys.argv[2]), indent=1))
443
+ sys.exit()
444
+ import multiprocessing as mp
445
+
446
+ def one(item):
447
+ q, golds = item
448
+ try:
449
+ r = answer(q)
450
+ except Exception as e: # noqa: BLE001
451
+ return ("err", q[:80], str(e)[:80])
452
+ if not r:
453
+ return None
454
+ return ("right" if correct(r["answer"], golds) else "wrong", q[:110], r["answer"], golds[:2], r["receipt"][:130], r["kind"])
455
+
456
+ items = [it for f in sys.argv[2:] for it in json.load(open(f))]
457
+ cnt = {"right": 0, "wrong": 0, "err": 0}
458
+ rows = []
459
+ with mp.Pool(6) as pool:
460
+ for i, res in enumerate(pool.imap_unordered(one, items, chunksize=4), 1):
461
+ if res:
462
+ cnt[res[0]] += 1
463
+ rows.append(res)
464
+ if i % 250 == 0:
465
+ print("progress %d/%d right %d wrong %d" % (i, len(items), cnt["right"], cnt["wrong"]), flush=True)
466
+ fired = cnt["right"] + cnt["wrong"]
467
+ print("questions %d fired %d right %d wrong %d errors %d precision %.3f" % (len(items), fired, cnt["right"], cnt["wrong"], cnt["err"], cnt["right"] / max(1, fired)))
468
+ for r in rows:
469
+ if r[0] != "right":
470
+ print(" ", r[0].upper(), r[1:])
471
+ json.dump(rows, open("/root/w240/coverage_eval_last.json", "w"), indent=0)
root/wander-train/math_organ_v2.py ADDED
@@ -0,0 +1,508 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """math_organ_v2 - v1 (admitted in w480, now immutable) + two fixes found on the gate panel: "factorial" is not a
2
+ request to factor, and a request for a PROGRAM is the code organ's, not a mathematical request.
3
+
4
+ exact mathematics as specified free moves, every result independently checked, the method cited.
5
+
6
+ REQUEST directive (solve / simplify / factor / expand / evaluate / differentiate / integrate / limit / decompose)
7
+ + the expression(s), read from the question or from its exercise set's instruction
8
+ MOVES SymPy operations legal for that directive and that expression's type
9
+ CHECK independent of the move that produced the answer:
10
+ solve every solution substituted back satisfies every equation / inequality
11
+ factor the product expands back to the input, and is a product
12
+ simplify equivalent to the input at random points, and no more complex than the input
13
+ evaluate recomputed by direct numeric substitution
14
+ derivative agrees with a central difference at random points
15
+ integral differentiates back to the integrand (definite: numeric quadrature agrees)
16
+ limit agrees with evaluation near the point
17
+ STORED FORM the textbook's own method for the directive (a howto or a worked example, textbook_store_v1) is cited
18
+ Nothing is asserted unless the check passes; a failed check is an abstention with the reason."""
19
+ from __future__ import annotations
20
+
21
+ import random
22
+ import re
23
+ import sqlite3
24
+
25
+ import sympy as sp
26
+ from sympy.parsing.sympy_parser import (convert_xor, implicit_multiplication_application, parse_expr,
27
+ split_symbols_custom, standard_transformations)
28
+
29
+ STORE = __import__("os").environ.get("ORACLE7_ROOT", "/opt/oracle-clm") + "/textbooks/textbook_store_v1.sqlite3"
30
+ FUNCS = {"sin", "cos", "tan", "sec", "csc", "cot", "log", "ln", "exp", "sqrt", "root", "asin", "acos", "atan", "abs",
31
+ "sinh", "cosh", "tanh", "pi", "oo", "theta", "alpha", "beta", "gamma", "Delta", "lambda", "mu", "sigma",
32
+ "omega", "phi", "rho", "tau", "epsilon", "arcsin", "arccos", "arctan"}
33
+ LOCAL = {"e": sp.E, "pi": sp.pi, "ln": sp.log, "log": sp.log, "root": lambda x, n: sp.root(x, n), "oo": sp.oo,
34
+ "arcsin": sp.asin, "arccos": sp.acos, "arctan": sp.atan, "abs": sp.Abs, "theta": sp.Symbol("theta")}
35
+
36
+
37
+ def _split_ok(name):
38
+ return name not in FUNCS and not name.startswith(("theta", "alpha", "beta", "gamma", "Delta", "lambda", "omega"))
39
+
40
+
41
+ TRANSF = standard_transformations + (split_symbols_custom(_split_ok), implicit_multiplication_application, convert_xor)
42
+ DIRECTIVES = [
43
+ ("partial", r"partial fraction|decomposition"),
44
+ ("derivative", r"\bderivative|differentiate|\bf'\(|d/dx|dy/dx"),
45
+ ("integral", r"\bintegra(l|te)|antiderivative"),
46
+ ("limit", r"\blimit|\blim_"),
47
+ ("factor", r"\bfactor(?!ial)"), # v2: "factorial" is not a request to factor
48
+ ("solve", r"\bsolve|\bsolution|find (?:the )?(?:value|zeros?|roots?)\b|\bfind x\b"),
49
+ ("evaluate", r"\bevaluate|\bfind f\(|\bcompute|\bcalculate|\bwhat is\b|\bhow much is\b"),
50
+ ("expand", r"\bmultiply|\bexpand|\bdistributive"),
51
+ ("simplify", r"\bsimplif|\badd\b|\bsubtract|\bdivide|\bcombine|\breduce"),
52
+ ]
53
+
54
+
55
+ def clean_linear(s):
56
+ s = s.replace("−", "-").replace("×", "*").replace("·", "*").replace("÷", "/").replace("^", "**")
57
+ s = re.sub(r"(\d),(\d{3})\b", r"\1\2", s)
58
+ s = re.sub(r"\s+", " ", s).strip().rstrip(".?,;")
59
+ return s
60
+
61
+
62
+ def P(s, syms=None):
63
+ s = clean_linear(s)
64
+ s = re.sub(r"\bsqrt\s*\(", "sqrt(", s)
65
+ return parse_expr(s, local_dict=dict(LOCAL, **(syms or {})), transformations=TRANSF, evaluate=True)
66
+
67
+
68
+ def directive_of(text):
69
+ t = (text or "").lower()
70
+ for name, rx in DIRECTIVES:
71
+ if re.search(rx, t):
72
+ return name
73
+ return None
74
+
75
+
76
+ ENGLISH = {"of", "to", "is", "by", "at", "in", "on", "if", "an", "as", "the", "and", "for", "use", "let", "all", "are",
77
+ "its", "how", "not", "or", "be", "it", "we", "find", "when", "each", "given", "then", "from", "with", "that",
78
+ "this", "what", "which", "where", "your", "you", "into", "using", "value", "values", "following", "exercises",
79
+ "expression", "expressions", "equation", "equations", "simplify", "solve", "factor", "evaluate", "multiply",
80
+ "divide", "add", "subtract", "completely", "polynomial", "form", "write", "answer", "result", "greatest",
81
+ "common", "number", "numbers", "term", "terms", "exact", "calculator", "round", "rule", "any", "one", "two",
82
+ "has", "can", "our", "was", "per", "sum", "set", "get", "may", "see", "new", "own", "way", "out"}
83
+ TOK = re.compile(r"[A-Za-z]+|\d+(?:\.\d+)?|\*\*|<=|>=|!=|->|\+-|[-+*/^=<>(){}\[\],|_;'!]|\.(?=\d)|\.|\s+")
84
+
85
+
86
+ def _is_math_word(w, prev, nxt):
87
+ lw = w.lower()
88
+ if lw in FUNCS or w == "C":
89
+ return True
90
+ if lw in ENGLISH:
91
+ return False
92
+ if len(w) == 1:
93
+ return not (w in ("a", "A", "I") and nxt.isspace() and prev in ("", " "))
94
+ return len(w) <= 3 # implicit products "xy", "ah", "abc"
95
+
96
+
97
+ FUNC_RX = r"(sqrt|sin|cos|tan|sec|csc|cot|log|ln|exp)"
98
+
99
+
100
+ def normalize_request(text):
101
+ """surface conventions -> linear mathematics: '2xsqrt(48)' -> '2x sqrt(48)', degrees, log bases, imaginary unit"""
102
+ t = text or ""
103
+ t = re.sub(r"(?<=[a-z0-9)])%s(?=[\s(])" % FUNC_RX, r" \1", t) # xsqrt( -> x sqrt(
104
+ t = re.sub(r"(\d+(?:\.\d+)?)\s*°", r"(\1*pi/180)", t) # 120° -> radians
105
+ t = re.sub(r"\blog\s*_\s*\{?(\d+)\}?\s*\(?([^()\s]+)\)?", r"log(\2,\1)", t) # log_5 25 -> log(25,5)
106
+ if re.search(r"\bcomplex|\bimaginary|\d\s*i\b|[-+]\s*\d*\s*i\)", t, re.I):
107
+ t = re.sub(r"(?<![A-Za-z])i(?![A-Za-z])", "I", t) # the imaginary unit
108
+ return t
109
+
110
+
111
+ WORD_PROBLEM_WORDS = 7
112
+
113
+
114
+ def english_load(text):
115
+ words = [w for w in re.findall(r"[A-Za-z]{3,}", text or "") if w.lower() not in ENGLISH and w.lower() not in FUNCS]
116
+ return len(words)
117
+
118
+
119
+ def extract_math(text):
120
+ """maximal runs of mathematical tokens (numbers, operators, brackets, variables, function names); English, a colon
121
+ and a sentence end break a run"""
122
+ toks = [m.group(0) for m in TOK.finditer((text or "").replace(":", " . "))]
123
+ spans, cur = [], []
124
+ for i, t in enumerate(toks):
125
+ prev = toks[i - 1] if i else ""
126
+ nxt = toks[i + 1] if i + 1 < len(toks) else ""
127
+ ok = _is_math_word(t, prev, nxt) if t.isalpha() else (t != ".")
128
+ if ok:
129
+ cur.append(t)
130
+ else:
131
+ if cur:
132
+ spans.append("".join(cur))
133
+ cur = []
134
+ if cur:
135
+ spans.append("".join(cur))
136
+ out = []
137
+ for s in spans:
138
+ s = s.strip(" ,;:")
139
+ s = re.sub(r"\|\s*([^|]+?)\s*\|", r"Abs(\1)", s)
140
+ if s and (re.search(r"[0-9]", s) or re.search(r"[=^+*/<>()]", s)) and re.search(r"[a-z0-9]", s):
141
+ out.append(s)
142
+ return out
143
+
144
+
145
+ def assignments(text):
146
+ """'when a=-4, b=6' / 'for x = 2 and y = 3' -> {a: -4, b: 6}"""
147
+ out = {}
148
+ m = re.search(r"\b(?:when|for|if|with|where|at)\s+((?:[a-z]\s*=\s*[-+]?[0-9./()]+\s*(?:,|\band\b)?\s*)+)", text or "")
149
+ if m:
150
+ for v, val in re.findall(r"\b([a-z])\s*=\s*([-+]?[0-9./()]+)", m.group(1)):
151
+ try:
152
+ out[sp.Symbol(v)] = P(val)
153
+ except Exception: # noqa: BLE001
154
+ pass
155
+ return out
156
+
157
+
158
+ def target_var(text):
159
+ m = re.search(r"\bfor\s+([a-z])\b(?!\s*=)", text or "")
160
+ return sp.Symbol(m.group(1)) if m else None
161
+
162
+
163
+ def rounding(text):
164
+ t = (text or "").lower()
165
+ m = re.search(r"round(?:ed)?\s+(?:your answer\s+)?to\s+(?:the\s+nearest\s+)?(\w+)", t)
166
+ if not m:
167
+ return None
168
+ return {"one": 1, "two": 2, "three": 3, "four": 4, "five": 5, "tenth": 1, "hundredth": 2, "thousandth": 3,
169
+ "whole": 0, "integer": 0, "1": 1, "2": 2, "3": 3, "4": 4}.get(m.group(1))
170
+
171
+
172
+ def binary_phrase(text):
173
+ """'the difference of A and B' -> (A)-(B); sum / product / quotient likewise; 'subtract A from B' -> (B)-(A)"""
174
+ m = re.search(r"\bsubtract\s+(.+?)\s+from\s+(.+?)(?:\.\s|\.$|$)", text or "", re.I)
175
+ if m:
176
+ a, b = extract_math(m.group(1)), extract_math(m.group(2))
177
+ return "(%s)-(%s)" % (b[0], a[-1]) if a and b else None
178
+ m = re.search(r"\b(sum|difference|product|quotient) of (.+?) and (.+?)(?:\.\s|\.$|$)", text or "", re.I)
179
+ if not m:
180
+ return None
181
+ op = {"sum": "+", "difference": "-", "product": "*", "quotient": "/"}[m.group(1).lower()]
182
+ a, b = extract_math(m.group(2)), extract_math(m.group(3))
183
+ return "(%s)%s(%s)" % (a[-1], op, b[0]) if a and b else None
184
+
185
+
186
+ def complexity(e):
187
+ return sp.count_ops(e, visual=False) + len(str(e)) / 40.0
188
+
189
+
190
+ def rand_pts(syms, k=5):
191
+ rng = random.Random(7)
192
+ return [{s: sp.Rational(rng.randint(2, 29), rng.randint(3, 11)) for s in syms} for _ in range(k)]
193
+
194
+
195
+ def equiv(a, b):
196
+ try:
197
+ d = sp.simplify(a - b)
198
+ if d == 0:
199
+ return True
200
+ syms = sorted((a.free_symbols | b.free_symbols), key=str)
201
+ for pt in rand_pts(syms):
202
+ va, vb = complex(a.subs(pt).evalf()), complex(b.subs(pt).evalf())
203
+ if abs(va - vb) > 1e-7 * max(1.0, abs(va)):
204
+ return False
205
+ return True
206
+ except Exception: # noqa: BLE001
207
+ return False
208
+
209
+
210
+ class MathOrgan:
211
+ def __init__(self, store=STORE):
212
+ try:
213
+ self.db = sqlite3.connect("file:%s?mode=ro" % store, uri=True, check_same_thread=False)
214
+ except Exception: # noqa: BLE001
215
+ self.db = None
216
+ self.why = ""
217
+
218
+ # ------------------------------------------------------------------ the stored form (method citation)
219
+ def method(self, directive, hint=""):
220
+ if not self.db:
221
+ return None
222
+ words = {"solve": "solve equation", "simplify": "simplify expression", "factor": "factor", "expand": "multiply",
223
+ "evaluate": "evaluate", "derivative": "derivative", "integral": "integral", "limit": "limit",
224
+ "partial": "partial fraction decomposition"}[directive]
225
+ q = " OR ".join(w for w in words.split())
226
+ try:
227
+ r = self.db.execute("SELECT item.book, item.section, item.title, item.kind, item.id FROM item_fts JOIN item "
228
+ "ON item.id = item_fts.rowid WHERE item_fts MATCH ? AND item.kind IN ('howto','example') "
229
+ "ORDER BY (item.kind='howto') DESC, rank LIMIT 1", ("title:(%s)" % q,)).fetchone()
230
+ except Exception: # noqa: BLE001
231
+ r = None
232
+ return {"book": r[0], "section": r[1], "title": r[2], "kind": r[3], "id": "tb:%d" % r[4]} if r else None
233
+
234
+ # ------------------------------------------------------------------ the solve
235
+ def answer(self, question, instruction=None):
236
+ self.why = ""
237
+ # v2: a request for a PROGRAM ("write a Python function that returns the factorial ...") is the code organ's
238
+ if re.search(r"\b(?:write|make|create|implement|code)\b.*\b(?:function|program|script|method)\b", question or "", re.I):
239
+ self.why = "a request for a program, not for a mathematical result"
240
+ return None
241
+ # v1 reads LINEAR notation (the textbook store's form, and what people type). Typeset LaTeX ($...$, \frac,
242
+ # \sqrt{}) is declined until a LaTeX reader exists: reading it with the linear reader would be luck, not reading.
243
+ if re.search(r"\$|\\(?:frac|sqrt|cdot|left|right|begin|dfrac|tfrac|pi|times)\b", question or ""):
244
+ self.why = "typeset (LaTeX) mathematics: this organ reads linear notation only"
245
+ return None
246
+ question = normalize_request(question)
247
+ d = directive_of(question) or directive_of(instruction or "")
248
+ # a WORD PROBLEM needs language -> equations (a later organ); reading numbers out of prose is guessing
249
+ body = question[len(instruction):] if instruction and question.startswith(instruction) else question
250
+ if english_load(body) > WORD_PROBLEM_WORDS:
251
+ self.why = "a word problem: the quantities must be translated into equations first"
252
+ return None
253
+ bp = binary_phrase(question)
254
+ spans = [bp] if bp else extract_math(question)
255
+ # a request whose only mathematics is a bare number names an operation this organ does not hold
256
+ # ("the lcm of 73 and 230", "452 in base 7", "803 in roman numerals"): echoing the number would be a guess
257
+ if not any(re.search(r"[-+*/^=<>()]|[a-z]", s_) for s_ in spans):
258
+ self.why = "no expression to operate on: the request names an operation this organ does not hold"
259
+ return None
260
+ if re.search(r"\b(?:lcm|gcd|gcf|base\s+\d|roman|numerals?|binary|hexadecimal|octal|convert|km|miles?|yards?|feet|"
261
+ r"inch(?:es)?|meters?|kilograms?|pounds?|percent|%)\b", question, re.I) and d in ("evaluate", "simplify"):
262
+ self.why = "a named operation or unit this organ does not hold"
263
+ return None
264
+ defined = {m.group(1) for s_ in spans for m in [re.match(r"^\s*([a-zA-Z])\s*\(\s*[a-z]\s*\)\s*=", s_)] if m}
265
+ used = {m.group(1) for s_ in spans for m in re.finditer(r"(?<![A-Za-z])([fghFGHpqr])\(\s*[-a-z0-9]", s_)}
266
+ if used - defined:
267
+ self.why = "the request uses a function it does not define"
268
+ return None
269
+ if not spans:
270
+ self.why = "no mathematics in the request"
271
+ return None
272
+ if not d:
273
+ d = "solve" if any("=" in s or "<" in s or ">" in s for s in spans) else "simplify"
274
+ try:
275
+ res = getattr(self, "_" + d)(spans, question)
276
+ except Exception as e: # noqa: BLE001
277
+ self.why = "%s failed: %s" % (d, repr(e)[:80])
278
+ return None
279
+ if not res:
280
+ self.why = self.why or "%s: no checked result" % d
281
+ return None
282
+ nd = rounding(question)
283
+ if nd is not None and isinstance(res.get("value"), sp.Basic) and res["value"].is_number:
284
+ v = round(float(res["value"].evalf()), nd)
285
+ res["value"], res["answer"] = sp.Float(v), ("%d" % v if nd == 0 else ("%." + str(nd) + "f") % v)
286
+ res["steps"] = res.get("steps", []) + ["round to %d decimal place%s" % (nd, "" if nd == 1 else "s")]
287
+ res["directive"] = d
288
+ res["method"] = self.method(d)
289
+ return res
290
+
291
+ # --- solve: equations, inequalities, systems ------------------------------------------------------------------
292
+ def _solve(self, spans, q):
293
+ eqs = []
294
+ for s in spans:
295
+ for part in re.split(r";|\band\b", s):
296
+ part = part.strip()
297
+ m = re.match(r"^(.*?)(<=|>=|<|>|=)(.*)$", part)
298
+ if not m:
299
+ continue
300
+ lhs, op, rhs = m.groups()
301
+ if not lhs.strip() or not rhs.strip():
302
+ continue
303
+ L_, R_ = P(lhs), P(rhs)
304
+ eqs.append((L_, op, R_))
305
+ if not eqs:
306
+ return None
307
+ syms = sorted(set().union(*[(a.free_symbols | b.free_symbols) for a, _, b in eqs]), key=str)
308
+ tv = target_var(q)
309
+ if tv is not None and tv in syms:
310
+ syms = [tv]
311
+ elif len(eqs) < len(syms):
312
+ self.why = "more unknowns than equations and no variable named"
313
+ return None
314
+ if not syms or len(syms) > 3:
315
+ return None
316
+ if all(op == "=" for _, op, _ in eqs):
317
+ sols = sp.solve([sp.Eq(a, b) for a, _, b in eqs], syms, dict=True)
318
+ if not sols:
319
+ self.why = "no solution found"
320
+ return None
321
+ for sol in sols: # CHECK: substitute back
322
+ for a, _, b in eqs:
323
+ if not equiv(a.subs(sol), b.subs(sol)):
324
+ self.why = "substitution check failed"
325
+ return None
326
+ out = [", ".join("%s = %s" % (k, sp.sstr(v)) for k, v in sorted(sol.items(), key=lambda kv: str(kv[0])))
327
+ for sol in sols]
328
+ return {"answer": " or ".join(out), "value": sols, "check": "each solution substituted back satisfies every equation",
329
+ "steps": ["read %s" % "; ".join("%s %s %s" % (sp.sstr(a), o, sp.sstr(b)) for a, o, b in eqs),
330
+ "solve for %s" % ", ".join(map(str, syms))]}
331
+ if len(eqs) == 1 and len(syms) == 1:
332
+ a, op, b = eqs[0]
333
+ rel = {"<": sp.Lt, ">": sp.Gt, "<=": sp.Le, ">=": sp.Ge}[op](a, b)
334
+ sol = sp.solveset(rel, syms[0], domain=sp.S.Reals)
335
+ # CHECK: a point inside satisfies, a point outside does not
336
+ inside = sol.inf + 1 if sol.inf.is_finite else (sol.sup - 1 if sol.sup.is_finite else 0)
337
+ if inside not in sol or not rel.subs(syms[0], inside):
338
+ self.why = "inequality check failed"
339
+ return None
340
+ return {"answer": sp.sstr(sol), "value": sol, "check": "a point of the solution set satisfies the inequality",
341
+ "steps": ["read %s %s %s" % (sp.sstr(a), op, sp.sstr(b)), "solve over the reals"]}
342
+ return None
343
+
344
+ # --- factor ---------------------------------------------------------------------------------------------------
345
+ def _factor(self, spans, q):
346
+ e = P(max(spans, key=len))
347
+ f = sp.factor(e)
348
+ if not equiv(sp.expand(f), sp.expand(e)):
349
+ return None
350
+ if not (isinstance(f, sp.Mul) or isinstance(f, sp.Pow)) and f == e:
351
+ self.why = "does not factor over the rationals"
352
+ return None
353
+ return {"answer": sp.sstr(f), "value": f, "check": "the product expands back to the input",
354
+ "steps": ["read %s" % sp.sstr(e), "factor completely"]}
355
+
356
+ # --- simplify / expand ----------------------------------------------------------------------------------------
357
+ def _simplify(self, spans, q, how="simplify"):
358
+ e0 = P(max(spans, key=len))
359
+ # the textbook convention: variables under radicals and exponents are taken as positive
360
+ pos = {s: sp.Symbol(str(s), positive=True) for s in e0.free_symbols}
361
+ e = e0.xreplace(pos)
362
+ s0 = sp.simplify(e)
363
+ cands = [s0, sp.together(s0), sp.powsimp(s0), sp.powdenest(s0, force=True), sp.expand(e), sp.cancel(e),
364
+ sp.factor(e), sp.radsimp(s0)]
365
+ back = {v: k for k, v in pos.items()}
366
+ cands = [c.xreplace(back) for c in cands]
367
+ e = e0
368
+ if how == "expand":
369
+ cands = [sp.expand(e)] + cands
370
+ best = cands[0]
371
+ else:
372
+ best = min(cands, key=complexity)
373
+ if not equiv(best, e):
374
+ return None
375
+ return {"answer": sp.sstr(best), "value": best,
376
+ "check": "equivalent to the input at random points (variables positive, the textbook convention)",
377
+ "steps": ["read %s" % sp.sstr(e), "%s" % ("expand" if how == "expand" else "simplify")]}
378
+
379
+ def _expand(self, spans, q):
380
+ return self._simplify(spans, q, how="expand")
381
+
382
+ # --- evaluate -------------------------------------------------------------------------------------------------
383
+ def _evaluate(self, spans, q):
384
+ fdef = next((s for s in spans if re.match(r"^\s*[a-zA-Z]\s*\(\s*[a-z]\s*\)\s*=", s)), None)
385
+ if fdef:
386
+ m = re.match(r"^\s*([a-zA-Z])\s*\(\s*([a-z])\s*\)\s*=(.*)$", fdef)
387
+ fname, var, body = m.groups()
388
+ f = P(body)
389
+ x = sp.Symbol(var)
390
+ args = [re.match(r"^\s*%s\s*\((.*)\)\s*$" % fname, s) for s in spans if s is not fdef]
391
+ args = [a.group(1) for a in args if a]
392
+ if not args:
393
+ return None
394
+ outs = []
395
+ for a in args:
396
+ v = sp.simplify(f.subs(x, P(a)))
397
+ chk = f.subs(x, P(a))
398
+ if not equiv(v, chk):
399
+ return None
400
+ outs.append("%s(%s) = %s" % (fname, a, sp.sstr(v)))
401
+ return {"answer": "; ".join(outs), "value": outs, "check": "recomputed by direct substitution",
402
+ "steps": ["read %s(%s) = %s" % (fname, var, sp.sstr(f))] + ["substitute %s" % a for a in args]}
403
+ subs = assignments(q)
404
+ cand = [s_ for s_ in spans if not re.match(r"^\s*[a-z]\s*=\s*[-+]?[0-9./()]+\s*$", s_)] or spans
405
+ e = P(max(cand, key=len).split("=")[0])
406
+ if subs and e.free_symbols and e.free_symbols <= set(subs):
407
+ v = sp.nsimplify(sp.simplify(e.subs(subs)))
408
+ if not equiv(v, e.subs(subs)):
409
+ return None
410
+ return {"answer": sp.sstr(v), "value": v, "check": "recomputed by direct substitution",
411
+ "steps": ["read %s" % sp.sstr(e), "substitute %s" % ", ".join("%s = %s" % kv for kv in subs.items())]}
412
+ if e.free_symbols:
413
+ return self._simplify(spans, q)
414
+ v = sp.nsimplify(sp.simplify(e)) if not e.is_Float else e
415
+ if abs(complex(v.evalf()) - complex(e.evalf())) > 1e-9 * max(1, abs(complex(e.evalf()))):
416
+ return None
417
+ return {"answer": sp.sstr(v), "value": v, "check": "recomputed numerically",
418
+ "steps": ["read %s" % sp.sstr(e), "evaluate"]}
419
+
420
+ # --- calculus -------------------------------------------------------------------------------------------------
421
+ def _var(self, e, q):
422
+ syms = sorted(e.free_symbols, key=str)
423
+ m = re.search(r"with respect to ([a-z])|d/d([a-z])", q)
424
+ if m:
425
+ return sp.Symbol(m.group(1) or m.group(2))
426
+ for pref in ("x", "t", "u", "theta"):
427
+ for s in syms:
428
+ if str(s) == pref:
429
+ return s
430
+ return syms[0] if syms else sp.Symbol("x")
431
+
432
+ def _body(self, spans):
433
+ s = max(spans, key=len)
434
+ s = re.sub(r"^\s*[a-zA-Z]\s*\(\s*[a-z]\s*\)\s*=", "", s)
435
+ s = re.sub(r"^\s*y\s*=", "", s)
436
+ return s
437
+
438
+ def _derivative(self, spans, q):
439
+ e = P(self._body(spans))
440
+ x = self._var(e, q)
441
+ d = sp.simplify(sp.diff(e, x))
442
+ for pt in rand_pts([x], 3): # CHECK: central difference
443
+ h = sp.Rational(1, 10 ** 6)
444
+ num = (e.subs(x, pt[x] + h) - e.subs(x, pt[x] - h)) / (2 * h)
445
+ if abs(complex(num.evalf()) - complex(d.subs(pt).evalf())) > 1e-4 * max(1, abs(complex(d.subs(pt).evalf()))):
446
+ return None
447
+ return {"answer": sp.sstr(d), "value": d, "check": "agrees with a central difference at random points",
448
+ "steps": ["read %s" % sp.sstr(e), "differentiate with respect to %s" % x]}
449
+
450
+ def _integral(self, spans, q):
451
+ s = max(spans, key=len)
452
+ m = re.search(r"integral\s*_\s*\{?([^}^]+?)\}?\s*\^\s*\{?([^}]+?)\}?\s*(\(.*|[^(].*?)\s*(?:d([a-z]))?\s*$", s)
453
+ fb = re.search(r"from\s+([-\w./()]+)\s+to\s+([-\w./()]+)", q)
454
+ lo = hi = None
455
+ if m:
456
+ lo, hi, body, v = m.groups()
457
+ e = P(body)
458
+ elif fb:
459
+ lo, hi = fb.groups()
460
+ body = max([x for x in spans if x.strip() not in (lo, hi)] or spans, key=len)
461
+ e = P(re.sub(r"\s*d[a-z]\s*$", "", self._body([body]).replace("integral", "")))
462
+ else:
463
+ e = P(re.sub(r"\s*d([a-z])\s*$", "", self._body(spans).replace("integral", "")))
464
+ x = self._var(e, q)
465
+ F = sp.integrate(e, x)
466
+ if F.has(sp.Integral):
467
+ return None
468
+ if not equiv(sp.diff(F, x), e): # CHECK: differentiates back
469
+ return None
470
+ if lo is not None:
471
+ val = sp.simplify(F.subs(x, P(hi)) - F.subs(x, P(lo)))
472
+ quad = sp.Integral(e, (x, P(lo), P(hi))).evalf()
473
+ if abs(complex(val.evalf()) - complex(quad)) > 1e-6 * max(1, abs(complex(quad))):
474
+ return None
475
+ return {"answer": sp.sstr(val), "value": val, "check": "antiderivative differentiates back; quadrature agrees",
476
+ "steps": ["read integral of %s from %s to %s" % (sp.sstr(e), lo, hi), "antiderivative %s" % sp.sstr(F)]}
477
+ return {"answer": sp.sstr(F) + " + C", "value": F, "check": "differentiates back to the integrand",
478
+ "steps": ["read %s" % sp.sstr(e), "integrate with respect to %s" % x]}
479
+
480
+ def _limit(self, spans, q):
481
+ s = max(spans, key=len)
482
+ m = re.search(r"lim\s*_\s*\{?\s*([a-z])\s*->\s*([^}\s]+?)\s*\}?\s*(\S.*?)\s*(?:,.*)?$", s) or \
483
+ re.search(r"as ([a-z]) (?:approaches|->) ([-\w.]+)", q)
484
+ if not m:
485
+ return None
486
+ if len(m.groups()) == 3:
487
+ v, pt, body = m.groups()
488
+ e = P(body)
489
+ else:
490
+ v, pt = m.groups()
491
+ e = P(self._body([x for x in spans if "lim" not in x] or spans))
492
+ x, a = sp.Symbol(v), P(pt)
493
+ L_ = sp.limit(e, x, a)
494
+ if L_.is_finite: # CHECK: evaluation near the point
495
+ near = complex(e.subs(x, a + sp.Rational(1, 10 ** 7)).evalf())
496
+ if abs(near - complex(L_.evalf())) > 1e-3 * max(1, abs(complex(L_.evalf()))):
497
+ return None
498
+ return {"answer": sp.sstr(L_), "value": L_, "check": "agrees with evaluation near the point",
499
+ "steps": ["read the limit of %s as %s -> %s" % (sp.sstr(e), v, pt)]}
500
+
501
+ def _partial(self, spans, q):
502
+ e = P(max(spans, key=len))
503
+ x = self._var(e, q)
504
+ r = sp.apart(e, x)
505
+ if not equiv(r, e):
506
+ return None
507
+ return {"answer": sp.sstr(r), "value": r, "check": "recombines to the input",
508
+ "steps": ["read %s" % sp.sstr(e), "decompose into partial fractions"]}
root/wander-train/oracle_cube_reader_v1_3.py ADDED
@@ -0,0 +1,447 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """oracle_cube_reader_v1 - the reasoning cube, Phase A: read -> frame -> link -> expand -> bind -> check -> verify.
2
+
3
+ Three graphs share one middle pillar (request, binding, evidence, surface). This module walks the first six moves:
4
+
5
+ Q Malkuth->Yesod READ the surface opens obligations
6
+ Q Yesod->Tiferet FRAME the answer slot: its TYPE (person, year, date, number, place, or an instance of the question's
7
+ head noun), the relation words, the constraints
8
+ O Tiferet->Chokmah LINK the subject, by the store's own title index (longest word n-grams), never by guessing
9
+ from capital letters
10
+ K Chokmah->Chesed EXPAND the subject's own page, sentence by sentence, each with its claim id
11
+ K Chesed->Tiferet BIND a candidate of the required type, drawn from a sentence that carries the relation
12
+ O Geburah->Tiferet CHECK INDEPENDENT of the proposing sentence: an entity candidate's OWN page must say it is the
13
+ asked-for kind; the question's constraints must be attested in the binding sentence or on
14
+ the candidate's page; a value candidate (year, date, number) must be the only value of its
15
+ type bound to a relation word in its sentence
16
+ Q Tiferet->Keter VERIFY every obligation discharged, one candidate standing, or nothing is said
17
+
18
+ Laws obeyed (reach-is-not-answer, 2026-09-10): a candidate is proposed BECAUSE it could fill the open slot, carries a
19
+ typed binding, and is checked by something that did not propose it. No hypernym bridging. Abstention is free; a wrong
20
+ answer costs a right one. The store is ORACLE_DEEP_BUILD, read-only.
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import os
25
+ import re
26
+ import sqlite3
27
+ import unicodedata
28
+
29
+ DEEP = os.environ.get("ORACLE_DEEP_BUILD") or (
30
+ __import__("os").environ.get("ORACLE7_ROOT", "/opt/oracle-clm") + "/fable-content/oracle-m-series-2026-08-15/"
31
+ "m13e8_dbpedia_enslot_openstax_full_fable_bench_a_v1/wikipedia_specialist.sqlite3")
32
+
33
+ STOP = set("""a an the of and or for to in on at by with from into about as is are was were be been being it its this that
34
+ these those which what who whom whose when where why how did does do has have had not no but if than so such also can
35
+ will would could should may might must shall there their his her he she they them we you your our i me my one first
36
+ last name named called known""".split())
37
+ MEASURES = {"capacity", "population", "height", "length", "area", "number", "size", "weight", "depth", "width", "age",
38
+ "distance", "speed", "elevation", "altitude", "attendance", "budget", "revenue"}
39
+ ANSWER_WORDS = {"day", "month", "year", "date", "time", "when", "first", "last", "name", "names", "called", "second",
40
+ "number", "many", "much", "exact", "full", "people", "person", "two", "three"}
41
+ FRAME_NOUNS = {"name", "type", "kind", "sort", "word", "term", "title", "one", "thing", "part", "member"}
42
+ ACTION_BREAK = re.compile(r"^(is|was|were|are|did|does|do|has|had|have|won|played|made|became|served|wrote|directed|"
43
+ r"founded|released|located|called|named|known|used|held|given|awarded|built|born|died|developed|"
44
+ r"invented|created|published|produced|starred|in|of|for|by|to|at|on|from|with|that|whose|who|"
45
+ r"which|as|after|before|during|and|or)$", re.I)
46
+ MONTHS = "january february march april may june july august september october november december".split()
47
+ MONTH_RE = "|".join(m.capitalize() for m in MONTHS)
48
+ YEAR = re.compile(r"\b(1[0-9]{3}|20[0-2][0-9])\b")
49
+ DATE = re.compile(r"\b(\d{1,2} (?:%s) \d{3,4}|(?:%s) \d{1,2}, \d{3,4})\b" % (MONTH_RE, MONTH_RE))
50
+ MONTH_YEAR = re.compile(r"\b((?:%s) \d{4})\b" % MONTH_RE)
51
+ COMPARE = re.compile(r"\b(or|out of|between|both|either|neither)\b.*\?$|\b(more|most|less|fewer|older|younger|earlier|later|"
52
+ r"longer|shorter|larger|smaller|bigger|taller|first)\b.*\b(or|and)\b", re.I)
53
+ NUMBER = re.compile(r"\b\d{1,3}(?:,\d{3})+\b|\b\d+(?:\.\d+)?\b")
54
+ NUMWORDS = {w: i for i, w in enumerate("zero one two three four five six seven eight nine ten eleven twelve".split())}
55
+ CAPSPAN = re.compile(r"\b([A-Z][\w'\.\-&]*(?:\s+(?:of|the|de|von|van|da|del|la|le|and|&)?\s*[A-Z0-9][\w'\.\-&]*){0,5})")
56
+ TOK = re.compile(r"[A-Za-z0-9][A-Za-z0-9'\-\.&]*")
57
+ # v1.2: a PERSON is shown by a birth/life-span parenthetical or an occupation predicate, never by a pronoun anywhere on the
58
+ # page (v1.1 accepted "MA" - Massachusetts - as a person because the page said "her")
59
+ OCCUPATIONS = (r"actor|actress|singer|songwriter|musician|composer|writer|author|poet|novelist|journalist|politician|"
60
+ r"lawyer|judge|player|footballer|cricketer|athlete|coach|manager|painter|artist|sculptor|architect|"
61
+ r"scientist|physicist|chemist|biologist|mathematician|astronomer|engineer|inventor|economist|philosopher|"
62
+ r"historian|director|producer|businessman|businesswoman|entrepreneur|explorer|soldier|general|admiral|"
63
+ r"officer|king|queen|emperor|prince|princess|pope|bishop|priest|monk|saint|president|minister|senator|"
64
+ r"governor|diplomat|activist|comedian|dancer|model|designer|photographer|surgeon|physician|nurse|"
65
+ r"professor|teacher|scholar|academic|chef|pilot|astronaut|wrestler|boxer|racing driver|cyclist|swimmer|"
66
+ r"tennis player|golfer|chess player|rapper|drummer|guitarist|pianist|conductor|filmmaker|screenwriter|"
67
+ r"cartoonist|illustrator|critic|editor|publisher|broadcaster|presenter|television personality|nobleman|"
68
+ r"noblewoman|lord|lady|duke|earl|count|countess|baron|chief|leader|missionary|theologian|psychologist")
69
+ PERSON_LEAD = re.compile(r"\(\s*born\b|\(\s*(?:c\.\s*)?\d{1,2}? ?\w* ?\d{3,4}\s*[–-]|\(\s*fl\.|"
70
+ r"\b(?:is|was)\s+(?:a|an|the)\s+(?:[\w\-]+\s+){0,4}?(?:%s)s?\b" % OCCUPATIONS, re.I)
71
+ NOUNISH = re.compile(r"(?:ers?|ors?|ions?|ments?|ness|ities|ity|ists?|ship)$", re.I)
72
+ PLACE_LEAD = re.compile(r"\b(city|town|village|country|state|province|county|region|capital|municipality|island|"
73
+ r"district|commune|borough|suburb|territory|nation|located in|metropolis|kingdom|republic)\b", re.I)
74
+
75
+
76
+ def norm(s):
77
+ s = unicodedata.normalize("NFKD", str(s)).encode("ascii", "ignore").decode("ascii")
78
+ return " ".join(re.sub(r"[^a-z0-9 ]+", " ", s.lower()).split())
79
+
80
+
81
+ IRREG = {"won": "win", "win": "win", "wins": "win", "wrote": "write", "written": "write", "writes": "write",
82
+ "write": "write", "began": "begin", "begun": "begin", "became": "become", "become": "become", "led": "lead",
83
+ "leads": "lead", "lead": "lead", "ran": "run", "run": "run", "sang": "sing", "sung": "sing", "sing": "sing",
84
+ "held": "hold", "hold": "hold", "made": "make", "make": "make", "took": "take", "taken": "take", "take": "take",
85
+ "gave": "give", "given": "give", "give": "give", "came": "come", "come": "come", "went": "go", "met": "meet",
86
+ "meet": "meet", "left": "leave", "leave": "leave", "built": "build", "build": "build", "sold": "sell",
87
+ "sell": "sell", "bought": "buy", "buy": "buy", "taught": "teach", "teach": "teach", "fought": "fight",
88
+ "fight": "fight", "drew": "draw", "drawn": "draw", "draw": "draw", "flew": "fly", "fly": "fly", "grew": "grow",
89
+ "grow": "grow", "spoke": "speak", "speak": "speak", "chose": "choose", "chosen": "choose", "choose": "choose",
90
+ "drove": "drive", "drive": "drive", "born": "bear", "birth": "bear", "died": "die", "dies": "die", "die": "die",
91
+ "death": "die", "beat": "beat", "beaten": "beat", "shot": "shoot", "hit": "hit", "set": "set", "put": "put"}
92
+ BASE_VERBS = set("""play join attend marry appear found discover invent release direct compose coach serve elect award
93
+ sign command host produce publish open close design create capture defeat star record graduate study move live name
94
+ score represent debut premiere air launch establish introduce approve nominate appoint succeed replace kill murder
95
+ retire resign own acquire merge sponsor manage train perform tour reach finish rank place qualify earn receive""".split())
96
+
97
+
98
+ def canon(w):
99
+ w = w.lower()
100
+ return IRREG.get(w) or stem(w)
101
+
102
+
103
+ def verbish(w):
104
+ wl = w.lower()
105
+ return wl in IRREG or wl in BASE_VERBS or (wl.endswith("ed") and len(wl) > 4) or stem(wl) in {stem(b) for b in BASE_VERBS}
106
+
107
+
108
+ def stem(w):
109
+ w = w.lower()
110
+ for suf in ("ing", "ers", "ed", "es", "er", "s"):
111
+ if len(w) > len(suf) + 3 and w.endswith(suf):
112
+ return w[: -len(suf)][:6]
113
+ return w[:6]
114
+
115
+
116
+ class CubeReader:
117
+ def __init__(self, path=DEEP):
118
+ uri = "file:%s?mode=ro&immutable=1" % path
119
+ self.db = sqlite3.connect(uri, uri=True, check_same_thread=False)
120
+ self._lead = {}
121
+
122
+ # ---- K: the store ----
123
+ def pid_of(self, title):
124
+ nt = norm(title)
125
+ if not nt:
126
+ return None
127
+ r = self.db.execute("SELECT page_id FROM title_alias WHERE normalized_title=? LIMIT 1", (nt,)).fetchone()
128
+ return r[0] if r else None
129
+
130
+ def page(self, pid, cap=40):
131
+ out = []
132
+ for (rowid,) in self.db.execute("SELECT source_rowid FROM page_sentence WHERE page_id=? ORDER BY sentence_ordinal "
133
+ "LIMIT ?", (pid, cap)).fetchall():
134
+ c = self.db.execute("SELECT c0, c1 FROM source_rows_content WHERE id=?", (rowid,)).fetchone()
135
+ if not c:
136
+ continue
137
+ text = " ".join(str(c[1]).split())
138
+ # the store's sentence splitter breaks on initials ("discovered by A." / "Schwassmann on 11 September 1898"):
139
+ # rejoin, keeping the first claim id and citing both
140
+ if out and re.search(r"(?:^|\s)[A-Z]\.$", out[-1][1]):
141
+ out[-1] = (out[-1][0], out[-1][1] + " " + text)
142
+ else:
143
+ out.append((c[0], text))
144
+ return out
145
+
146
+ def lead(self, pid, n=3):
147
+ if pid not in self._lead:
148
+ self._lead[pid] = " ".join(t for _, t in self.page(pid, n))
149
+ return self._lead[pid]
150
+
151
+ # ---- Q: FRAME ----
152
+ def frame(self, q):
153
+ s = " ".join(str(q).split())
154
+ low = s.lower()
155
+ if not s or "passage:" in low or "\noptions" in low or "options:" in low or re.search(r"\b(you|your)\b", low):
156
+ return None
157
+ if not (s.endswith("?") or re.match(r"^(who|whom|whose|what|which|when|where|how|in what|in which|name)\b", low)):
158
+ return None
159
+ atype, head = None, None
160
+ if re.search(r"\bwhat (day|date)\b|\bday, month,? and year\b|\bexact date\b|\b(what|which) month and year\b", low):
161
+ atype = "DATE"
162
+ elif re.search(r"\b(what|which) year\b|\bin what year\b|^when\b|\bwhat year\b", low):
163
+ atype = "YEAR"
164
+ elif re.search(r"\bhow many\b", low):
165
+ atype = "NUMBER"
166
+ elif re.match(r"^(who|whom)\b", low) or re.search(r"\b(which|what) (person|man|woman|actor|actress|singer|player|"
167
+ r"writer|author|politician|artist|director)\b", low):
168
+ atype = "PERSON"
169
+ elif re.match(r"^where\b", low):
170
+ atype, head = "PLACE", None
171
+ else:
172
+ m = re.search(r"\b(?:which|what)\s+((?:[\w'\-]+\s+){0,5}?[\w'\-]+?)(?=\s+(?:is|was|were|are|did|does|do|has|had|"
173
+ r"have|won|played|made|became|served|wrote|directed|founded|released|located|called|named|known|"
174
+ r"used|held|given|awarded|built|born|died|developed|invented|created|published|produced|starred|"
175
+ r"in|of|for|by|to|at|on|from|with|that|whose|who|which|as|after|before|during)\b|\s*\?|$)", s, re.I)
176
+ if m:
177
+ words = [w for w in m.group(1).split() if w]
178
+ lower = [w for w in words if w.islower() and w.isalpha()]
179
+ if lower:
180
+ head = lower[-1].lower()
181
+ if head in FRAME_NOUNS:
182
+ m2 = re.search(r"\b(?:name|type|kind|sort|title) of (?:the |a |an )?((?:[\w'\-]+\s+){0,3}?[\w'\-]+?)"
183
+ r"(?=\s+(?:in|of|for|by|that|which|who|whose|is|was|were)\b|\s*\?|$)", s, re.I)
184
+ head = ([w for w in m2.group(1).split() if w.islower()] or [None])[-1] if m2 else None
185
+ if head in MEASURES:
186
+ atype = "NUMBER"
187
+ elif head in ("city", "country", "town", "state", "place", "capital", "village", "nation", "province"):
188
+ atype = "PLACE"
189
+ elif head and head not in STOP and len(head) >= 3:
190
+ atype = "INSTANCE"
191
+ if atype is None:
192
+ return None
193
+ if COMPARE.search(s) and (" or " in low or " out of " in low or " between " in low or " both " in low):
194
+ return None # a comparison: the COMPARE move is Phase 3, not this reader
195
+ if re.search(r"\bthe (?:[a-z\-]+ ){0,2}(?:man|woman|person|composer|author|writer|director|actor|actress|singer|"
196
+ r"player|team|band|company|film|city|country|village|town|artist|founder|creator|leader|president|"
197
+ r"king|queen|school|university|group|album|show|series|book|novel|song|character|state|river)\s+"
198
+ r"(?:who|whose|which|that|where)\b", low):
199
+ return None # the subject is a DESCRIPTION: a bridge (two hops), Phase 3
200
+ if re.search(r"\bthe\b[^?]{0,90}?,?\s+(?:who|whose)\b", low):
201
+ return None # v1.2: any described person ("the American lawyer ... who") is a bridge
202
+ if re.search(r"\b(?:of|on|in|at|from|near)\s+the\s+(?:[a-z\-]+\s+){0,2}[a-z]+\s+(?:located|situated|based|founded|"
203
+ r"built|named|called|known|found|owned|operated|run|held)\b", low):
204
+ return None # v1.3: "the slopes of the hill located in what town" - the
205
+ # asked thing is a described entity (a bridge), not the subject
206
+ return {"q": s, "atype": atype, "head": head, "years": set(YEAR.findall(s)),
207
+ "month_year": bool(re.search(r"\bmonth and year\b|\bmonth, and year\b", low)) and "day" not in low}
208
+
209
+ # ---- O: LINK (title index, longest non-overlapping n-grams) ----
210
+ def link(self, fr, maxn=7):
211
+ toks = TOK.findall(fr["q"])
212
+ found = []
213
+ for n in range(min(maxn, len(toks)), 0, -1):
214
+ for i in range(len(toks) - n + 1):
215
+ words = toks[i:i + n]
216
+ nt = norm(" ".join(words))
217
+ if not nt or all(w in STOP or w in FRAME_NOUNS for w in nt.split()) or nt == fr["head"]:
218
+ continue
219
+ if not words[0][:1].isupper() and not words[0][:1].isdigit():
220
+ continue # a subject starts with a capital (names, titles) or a digit
221
+ if n == 1 and len(nt) < 4:
222
+ continue
223
+ if nt in MONTHS or nt.replace(" ", "").isdigit():
224
+ continue # a month or a bare number is a constraint, never the subject
225
+ pid = self.pid_of(nt)
226
+ if pid:
227
+ found.append((i, i + n, " ".join(words), pid))
228
+ keep, used = [], set()
229
+ for i, j, text, pid in sorted(found, key=lambda t: (-(t[1] - t[0]), t[0])):
230
+ if used.isdisjoint(range(i, j)):
231
+ keep.append({"span": (i, j), "text": text, "pid": pid})
232
+ used.update(range(i, j))
233
+ fr["tokens"] = toks
234
+ fr["subjects"] = keep
235
+ subj_tok = set(k for s in keep for k in range(*s["span"]))
236
+ rel = [t for k, t in enumerate(toks) if k not in subj_tok and t.lower() not in STOP and t.lower() != fr["head"]
237
+ and t.lower() not in ANSWER_WORDS and not YEAR.fullmatch(t) and t[:1].islower() and len(t) >= 3]
238
+ fr["rel"] = [stem(t) for t in rel]
239
+ # the PREDICATE: the question's verb(s). The binding sentence must carry one of them - a value bound through a
240
+ # shared context word ("road" in "road running athlete") answers a different relation
241
+ verbs = [(k, t) for k, t in enumerate(toks) if k not in subj_tok and t[:1].islower() and verbish(t)
242
+ and t.lower() not in STOP]
243
+ # only the verb GOVERNING the answer slot counts: the one nearest the wh-phrase ("founded in what year"), not a
244
+ # participle describing something else ("a failed raid")
245
+ wh = [k for k, t in enumerate(toks) if t.lower() in ("who", "whom", "whose", "what", "which", "when", "where", "how")]
246
+ if verbs and wh:
247
+ w0 = wh[0]
248
+ k, t = min(verbs, key=lambda kt: (abs(kt[0] - w0), kt[0] < w0))
249
+ fr["pred"] = [canon(t)]
250
+ else:
251
+ fr["pred"] = sorted({canon(t) for _, t in verbs})
252
+ # every NAME in the question is an obligation: a capitalised token (not the first word) outside every linked span
253
+ unlinked = [t for k, t in enumerate(toks) if k > 0 and k not in subj_tok and t[:1].isupper()
254
+ and t.lower() not in STOP and len(t) >= 3]
255
+ unlinked += [t for k, t in enumerate(toks) if k not in subj_tok and t.isdigit() and len(t) >= 2 and not YEAR.fullmatch(t)]
256
+ # v1.2: an identifier ("gal56", "EURECA-1") is an obligation too: letters and digits together
257
+ unlinked += [t for k, t in enumerate(toks) if k not in subj_tok and re.search(r"\d", t) and re.search(r"[A-Za-z]", t)]
258
+ # v1.2: the ROLE the answer must play. "Who beat X" asks for the AGENT of the predicate
259
+ # v1.3: "Who did X knock out?" asks for the PATIENT of X's action
260
+ fr["patient"] = bool(re.match(r"^(?:who|whom|what|which [\w\-]+)\s+(?:did|does|do|has|had|have)\b", fr["q"].lower()))
261
+ fr["agent"] = bool(re.match(r"^(?:who|which [\w\-]+(?: [\w\-]+)?)\s+(?!is\b|was\b|are\b|were\b|did\b|does\b|do\b|has\b|"
262
+ r"had\b|have\b|will\b|would\b|can\b|could\b)[a-z]+", fr["q"].lower()))
263
+ fr["constraints"] = [s["text"] for s in keep[1:]] + sorted(fr["years"]) + unlinked
264
+ return keep
265
+
266
+ # ---- K: BIND ----
267
+ def _candidates(self, fr, sent, qnorm):
268
+ at = fr["atype"]
269
+ out = []
270
+ if at == "YEAR":
271
+ out = [y for y in YEAR.findall(sent) if y not in fr["years"]]
272
+ elif at == "DATE":
273
+ out = DATE.findall(sent)
274
+ if not out and fr.get("month_year"):
275
+ out = MONTH_YEAR.findall(sent)
276
+ elif at == "NUMBER":
277
+ out = [n for n in NUMBER.findall(sent) if not YEAR.fullmatch(n) and n not in fr["q"]]
278
+ else:
279
+ for m in CAPSPAN.finditer(sent):
280
+ span = m.group(1).strip(" .,;:")
281
+ span = re.sub(r"^(The|A|An)\s+", "", span)
282
+ if len(span) < 3 or (span.isupper() and len(span) <= 3) or norm(span) in qnorm or norm(span).split()[0] in STOP:
283
+ continue
284
+ if span.split()[0].capitalize() in MONTH_RE.split("|"):
285
+ continue
286
+ out.append(span)
287
+ return out
288
+
289
+ def bind(self, fr):
290
+ qnorm = norm(fr["q"])
291
+ rel = set(fr["rel"])
292
+ cands = []
293
+ for subj in fr["subjects"][:4]:
294
+ sents = self.page(subj["pid"], 40)
295
+ for idx, (cid, sent_full) in enumerate(sents):
296
+ words = [p for t in TOK.findall(sent_full) for p in [t] + t.split("-")[1:]] # "co-founded" -> founded
297
+ # v1.2: the predicate must appear as a VERB - "composers" is not "composed"
298
+ verbs_here = {canon(t) for t in words if not NOUNISH.search(t)}
299
+ if fr["pred"] and not (verbs_here & set(fr["pred"])):
300
+ continue # the question's predicate is not in this sentence
301
+ sent = sent_full
302
+ if not ({"bear", "die"} & set(fr["pred"])):
303
+ # a value inside "(born ...)" or a life span answers only a birth/death question
304
+ sent = re.sub(r"\((?:[^()]*\b(?:born|died|b\.|d\.)\b[^()]*|\s*[^()]{0,30}\d{3,4}\s*[–-]\s*[^()]{0,30}\d{3,4}\s*)\)",
305
+ "", sent_full)
306
+ stoks = [stem(t) for t in TOK.findall(sent)]
307
+ hits = len(rel & set(stoks))
308
+ cover = (hits / len(rel)) if rel else (1.0 if idx < 2 else 0.0)
309
+ if rel and hits == 0:
310
+ continue # the sentence must carry at least one of the question's relation words
311
+ if not rel and idx >= 2:
312
+ continue # no relation words: only the page's lead may bind
313
+ for c in self._candidates(fr, sent, qnorm):
314
+ # proximity: the candidate sits near a relation word in its sentence
315
+ pos = sent.find(c)
316
+ near = any(abs(sent.lower().find(r) - pos) < 90 for r in rel if sent.lower().find(r) >= 0) if rel else True
317
+ if not near:
318
+ continue
319
+ cands.append({"value": c, "claim": cid, "sentence": sent_full, "subject": subj, "cover": cover,
320
+ "lead": idx < 3, "score": cover + (0.3 if idx < 3 else 0.0)})
321
+ return cands
322
+
323
+ # ---- O: CHECK (independent of the proposing sentence) ----
324
+ def check(self, fr, c):
325
+ at = fr["atype"]
326
+ ev = {}
327
+ if at in ("PERSON", "PLACE", "INSTANCE"):
328
+ pid = self.pid_of(c["value"])
329
+ if not pid or pid == c["subject"]["pid"]:
330
+ return False, {"why": "candidate has no page of its own"}
331
+ lead = self.lead(pid)
332
+ if at == "PERSON" and not PERSON_LEAD.search(lead):
333
+ return False, {"why": "candidate page does not describe a person"}
334
+ first = re.split(r"(?<=[.!?])\s+", lead, 1)[0]
335
+ if at == "PLACE" and not fr["head"] and not PLACE_LEAD.search(first):
336
+ return False, {"why": "candidate page does not describe a place"}
337
+ if at == "INSTANCE" or (at == "PLACE" and fr["head"]):
338
+ # IS-A, not mention: the first sentence must say the candidate IS a <head> ("... is a professional team")
339
+ h = fr["head"]
340
+ # ... and the head noun must END its phrase ("a film", not "a film producer")
341
+ isa = (r"\b(?:is|was|are|were)\s+(?:a|an|the|one of the)\b[^.]{0,90}?\b%s(?:s|es)?\b"
342
+ r"(?=\s*(?:[,.;:()]|$)|\s+(?:that|which|who|whose|based|in|of|from|and|or|founded|established|located|"
343
+ r"with|by|for|released|directed|produced|written|set|created|developed|published|made|first|since|"
344
+ r"owned|operated|consisting|starring|about|on|at|to|named|known|situated|headquartered)\b)") % re.escape(h)
345
+ # the store's canonical title is also type evidence when its last word IS the head ("Crimean War")
346
+ title_head = norm(c["value"]).split()[-1:] in ([h], [h + "s"])
347
+ if not re.search(isa, first, re.I) and not title_head:
348
+ return False, {"why": "candidate page does not say it is a %s" % h}
349
+ if fr.get("agent"):
350
+ # v1.2 ROLE: "Who beat X?" needs the candidate as the predicate's AGENT. In active voice the agent stands
351
+ # before the verb; after it, only a passive "by" makes it the agent ("discovered by Annibale de Gasparis").
352
+ # "Henman ... beating Roger Federer" puts Federer in the patient role and is refused.
353
+ sl = c["sentence"]
354
+ pos_c = sl.find(c["value"])
355
+ pv = next((m for m in TOK.finditer(sl) if not NOUNISH.search(m.group(0)) and canon(m.group(0)) in fr["pred"]), None)
356
+ if pv is not None and pos_c > pv.start() and not re.search(r"\bby\b", sl[pv.end():pos_c]):
357
+ return False, {"why": "candidate is not the agent of the predicate"}
358
+ if fr.get("patient"):
359
+ # v1.3 ROLE: the answer is the patient of the SUBJECT's action; a sentence where the subject is itself
360
+ # the passive patient ("Douglas was knocked out ... Lou Savarese") names the agent, not the answer
361
+ subj_words = [w for w in norm(c["subject"]["text"]).split() if len(w) >= 4]
362
+ sl = c["sentence"]
363
+ for w in subj_words:
364
+ if re.search(r"\b%s\b[^.]{0,40}?\b(?:was|were|been|being)\s+(?:\w+\s+)?\w+(?:ed|en|out)\b" % re.escape(w), sl, re.I):
365
+ return False, {"why": "the subject is the patient in this sentence"}
366
+ ev = {"type_page": pid, "type_lead": lead[:240]}
367
+ where = norm(c["sentence"] + " " + lead + " " + self.lead(c["subject"]["pid"]))
368
+ else:
369
+ if at == "DATE" and not fr.get("month_year"):
370
+ # v1.3 CORROBORATION for day-precision dates: a single sentence can be wrong (FC Baltika: the store says
371
+ # 23 August 1954, the key 22 December 1954). An exact date is committed only when a SECOND sentence -
372
+ # different claim id - carries the same date string with the subject's distinctive word.
373
+ subj_words = [w for w in norm(c["subject"]["text"]).split() if len(w) >= 4 and w not in STOP]
374
+ word = max(subj_words, key=len) if subj_words else None
375
+ n_other = 0
376
+ if word:
377
+ try:
378
+ rows = self.db.execute("SELECT claim_id FROM source_rows WHERE source_rows MATCH ? LIMIT 6",
379
+ ('"%s" AND "%s"' % (c["value"].replace('"', ""), word),)).fetchall()
380
+ n_other = len({r[0] for r in rows if r[0] != c["claim"]})
381
+ except sqlite3.OperationalError:
382
+ n_other = 0
383
+ if n_other < 1:
384
+ return False, {"why": "exact date not corroborated by a second sentence"}
385
+ vals = self._candidates(fr, c["sentence"], norm(fr["q"]))
386
+ if len(set(vals)) != 1:
387
+ return False, {"why": "more than one %s in the binding sentence" % at.lower()}
388
+ where = norm(c["sentence"] + " " + self.lead(c["subject"]["pid"]))
389
+ # constraints: every OTHER name/number/year in the question. The subject whose page holds the sentence is attested
390
+ # by the page itself; qualifiers of that subject ("El Santuario, Antioquia, Colombia") may be attested on its lead;
391
+ # a person's name is attested by its surname
392
+ own = norm(c["subject"]["text"])
393
+ cons = [x for x in fr["constraints"] if norm(x) and norm(x) != own and norm(x) not in own]
394
+ linked_other = {norm(s["text"]) for s in fr["subjects"] if norm(s["text"]) != own}
395
+ sent_n = norm(c["sentence"])
396
+ missing = []
397
+ for x in cons:
398
+ nx = norm(x)
399
+ sur = nx.split()[-1] if len(nx.split()) > 1 and len(nx.split()[-1]) >= 4 else None
400
+ # another NAMED entity of the question must be in the binding sentence itself (not merely on a lead): the
401
+ # prize page's "awarded since 1979" says nothing about the person the question asks about
402
+ # v1.3: a YEAR in the question qualifies the event, so it must be in the binding sentence too
403
+ scope = sent_n if (nx in linked_other or YEAR.fullmatch(x.strip())) else where
404
+ if nx not in scope and not (sur and (" %s " % sur) in (" %s " % scope)):
405
+ missing.append(x)
406
+ if missing:
407
+ return False, {"why": "constraint not attested: %s" % missing}
408
+ return True, ev
409
+
410
+ # ---- Q: VERIFY + the whole cycle ----
411
+ def answer(self, q):
412
+ self.stop = "frame"
413
+ fr = self.frame(q)
414
+ if not fr:
415
+ return None
416
+ self.stop = "link"
417
+ if not self.link(fr):
418
+ return None
419
+ self.stop = "bind"
420
+ cands = self.bind(fr)
421
+ if not cands:
422
+ return None
423
+ self.stop = "check"
424
+ best = {}
425
+ for c in cands:
426
+ k = norm(c["value"])
427
+ if k not in best or c["score"] > best[k]["score"]:
428
+ best[k] = c
429
+ ranked = sorted(best.values(), key=lambda c: -c["score"])
430
+ passed = []
431
+ self.why = []
432
+ for c in ranked[:6]:
433
+ ok, ev = self.check(fr, c)
434
+ if ok:
435
+ c["check"] = ev
436
+ passed.append(c)
437
+ else:
438
+ self.why.append((c["value"], ev.get("why")))
439
+ if not passed:
440
+ return None
441
+ top = passed[0]
442
+ if len(passed) > 1 and passed[1]["score"] >= top["score"] - 1e-9:
443
+ self.stop = "tie"
444
+ return None # two checked candidates tie: nothing is said
445
+ self.stop = "answered"
446
+ return {"answer": top["value"], "claim": top["claim"], "sentence": top["sentence"], "subject": top["subject"]["text"],
447
+ "type": fr["atype"], "head": fr["head"], "check": top["check"], "frame": {k: fr[k] for k in ("atype", "head", "rel")}}
root/wander-train/oracle_game_combine_v1.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """ORACLE GAME COMBINATORS - operators that take games and return games.
3
+
4
+ Twelve categories with parameters is a large space. It is still a flat one: every
5
+ game it can build is one of twelve shapes wearing different numbers. The move that
6
+ makes it genuinely open is HIGHER ORDER operators, which take a game and give back
7
+ a different game, because then the space closes under its own operations.
8
+
9
+ Two of them, and they are chosen because both are real games people actually play
10
+ rather than formal curiosities:
11
+
12
+ INVERT the win condition is flipped. Get three in a row and you LOSE,
13
+ which is a real game (misere noughts and crosses) with completely
14
+ different play from the one it inverts. Applied to any of the
15
+ twelve, not just to take-away where it was hand-written.
16
+
17
+ COMPOUND play game A, then game B, and whoever won more takes the match.
18
+ "Tic tac toe then nim" is not in any catalogue, is not a
19
+ parameter setting of anything, and is a perfectly well defined
20
+ game. This is what combining categories means.
21
+
22
+ Both come with their own referees, built by composing the referees underneath
23
+ them, so a combined game is checked the same way a simple one is: the engine
24
+ plays, the referee replays, and the emitted program runs in a separate process.
25
+ A combination is only allowed out if all three agree about it.
26
+
27
+ The count matters less than the closure. Twelve categories give twelve games; with
28
+ INVERT that is twenty-four; with COMPOUND over ordered pairs it is 24 x 24 before
29
+ any parameter is chosen, and the parameters are still free. Nothing in that space
30
+ is stored.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import re
36
+
37
+ from oracle_game_kernel_v1 import GameError, play
38
+
39
+ COMPOUND_BUDGET = 1200
40
+ from oracle_game_vocab_v1 import CATS
41
+
42
+ _FLIP = {'first': 'second', 'second': 'first', 'draw': 'draw',
43
+ 'solved': 'failed', 'failed': 'solved'}
44
+
45
+
46
+ # ── INVERT ───────────────────────────────────────────────────────────────────
47
+ def invert_build(inner_name, params):
48
+ inner = CATS[inner_name]['build'](params)
49
+ base_outcome = inner['outcome']
50
+
51
+ def outcome(st):
52
+ got = base_outcome(st)
53
+ return _FLIP[got] if got is not None else None
54
+
55
+ return dict(inner, outcome=outcome)
56
+
57
+
58
+ def invert_referee(inner_name, params, log):
59
+ got = CATS[inner_name]['referee'](params, log)
60
+ return _FLIP[got]
61
+
62
+
63
+ # ── COMPOUND ─────────────────────────────────────────────────────────────────
64
+ def compound_build(a_name, b_name, a_params, b_params):
65
+ """Play A to a result, then B, then compare. One game, two halves."""
66
+ ga = CATS[a_name]['build'](a_params)
67
+ gb = CATS[b_name]['build'](b_params)
68
+
69
+ def score_of(res):
70
+ return {'first': 1, 'solved': 1, 'second': -1, 'failed': -1,
71
+ 'draw': 0}[res]
72
+
73
+ # Every move is TAGGED with the half it belongs to, so the kernel's own move
74
+ # log is already splittable by the referee. The first version kept a private
75
+ # tagged log inside the state and handed the referee the kernel's untagged
76
+ # one, which is why every compound died on unpacking.
77
+ def init():
78
+ return ('a', ga['init'](), None, 0)
79
+
80
+ def moves(st):
81
+ leg, inner = st[0], st[1]
82
+ g = ga if leg == 'a' else gb
83
+ legal = [(leg, m) for m in g['moves'](inner)]
84
+ if not legal and leg == 'a':
85
+ # the first half ran out of legal moves without declaring a result.
86
+ # Returning nothing here makes the KERNEL call the whole compound a
87
+ # draw and the second half never happens, which the referee then
88
+ # rightly refuses. Hand back a marker so the transition still occurs.
89
+ return [('a', None)]
90
+ return legal
91
+
92
+ def apply(st, tagged, t):
93
+ leg, inner, first_res, count = st
94
+ _tag, m = tagged
95
+ if m is None: # the exhaustion marker from moves()
96
+ return ('b', gb['init'](), ga['outcome'](inner) or 'draw', 0)
97
+ g = ga if leg == 'a' else gb
98
+ inner2 = g['apply'](inner, m, count) # the INNER half's own turn
99
+ done = g['outcome'](inner2)
100
+ if done is not None and leg == 'a':
101
+ return ('b', gb['init'](), done, 0)
102
+ return (leg, inner2, first_res, count + 1)
103
+
104
+ def outcome(st):
105
+ leg, inner, first_res, _count = st
106
+ if leg == 'a':
107
+ return None
108
+ done = gb['outcome'](inner)
109
+ if done is None:
110
+ return None
111
+ total = score_of(first_res) + score_of(done)
112
+ return 'first' if total > 0 else 'second' if total < 0 else 'draw'
113
+
114
+ def policy(st, legal, t):
115
+ return legal[0] if st[3] % 2 == 0 else legal[-1]
116
+
117
+ return {'init': init, 'moves': moves, 'apply': apply, 'outcome': outcome,
118
+ 'policy': policy}
119
+
120
+
121
+ def compound_referee(a_name, b_name, a_params, b_params, log):
122
+ """Split the tagged log and hand each half to the referee that owns it."""
123
+ a_log = [m for leg, m in log if leg == 'a' and m is not None]
124
+ b_log = [m for leg, m in log if leg == 'b' and m is not None]
125
+ if not a_log or not b_log:
126
+ raise GameError('referee: one half of the compound never happened')
127
+ ra = CATS[a_name]['referee'](a_params, a_log)
128
+ rb = CATS[b_name]['referee'](b_params, b_log)
129
+ sc = {'first': 1, 'solved': 1, 'second': -1, 'failed': -1, 'draw': 0}
130
+ total = sc[ra] + sc[rb]
131
+ return 'first' if total > 0 else 'second' if total < 0 else 'draw'
132
+
133
+
134
+ # ── routing the combinations ─────────────────────────────────────────────────
135
+ _INVERT_CUE = re.compile(
136
+ r'\b(?:misere|mis[eè]re)\b|you lose|makes you lose|loses instead'
137
+ r'|whoever .{0,24}loses|reverse(?:d)? (?:rules|win)|inverted'
138
+ r'|the loser wins|last one loses', re.I)
139
+ _COMPOUND_CUE = re.compile(
140
+ r'\bthen\b|\bfollowed by\b|\band then\b|\bboth\b.{0,20}\bgames?\b'
141
+ r'|combin(?:e|ed|ation) of', re.I)
142
+
143
+
144
+ def find_categories(surface):
145
+ """Every category the surface names, in the order it names them."""
146
+ hits = []
147
+ for name, c in CATS.items():
148
+ for rx in c['cues']:
149
+ m = rx.search(surface)
150
+ if m:
151
+ hits.append((m.start(), name))
152
+ break
153
+ hits.sort()
154
+ out = []
155
+ for _pos, name in hits:
156
+ if name not in out:
157
+ out.append(name)
158
+ return out
159
+
160
+
161
+ def parse_combination(surface):
162
+ """(kind, payload) or None if this is not a combination request.
163
+
164
+ kind is 'invert' or 'compound'. Raises GameError when the request clearly
165
+ wants a combination but the pieces do not resolve, so it can decline with a
166
+ reason rather than quietly build one half.
167
+ """
168
+ names = find_categories(surface)
169
+ wants_compound = bool(_COMPOUND_CUE.search(surface))
170
+ wants_invert = bool(_INVERT_CUE.search(surface))
171
+
172
+ if wants_compound:
173
+ real = [n for n in names if n != 'misere_take_away']
174
+ if len(real) < 2:
175
+ raise GameError('that asks for two games joined together and I can '
176
+ 'only find one of them in it')
177
+ a, b = real[0], real[1]
178
+ pa = CATS[a]['params'](surface)
179
+ pb = CATS[b]['params'](surface)
180
+ return 'compound', (a, b, pa, pb)
181
+
182
+ if wants_invert and names:
183
+ base = [n for n in names if n != 'misere_take_away']
184
+ if not base:
185
+ return None # plain misere nim, already a category
186
+ n = base[0]
187
+ return 'invert', (n, CATS[n]['params'](surface))
188
+ return None
189
+
190
+
191
+ def describe(kind, payload):
192
+ if kind == 'invert':
193
+ n, p = payload
194
+ return ('%s, with the win condition inverted: doing the thing that '
195
+ 'normally wins LOSES' % CATS[n]['describe'](p))
196
+ a, b, pa, pb = payload
197
+ return ('two games in one match: first %s, then %s, and whoever wins more '
198
+ 'takes it' % (CATS[a]['describe'](pa), CATS[b]['describe'](pb)))
199
+
200
+
201
+ def build_and_referee(kind, payload):
202
+ if kind == 'invert':
203
+ n, p = payload
204
+ return (invert_build(n, p),
205
+ lambda _params, log: invert_referee(n, p, log), p)
206
+ a, b, pa, pb = payload
207
+ return (compound_build(a, b, pa, pb),
208
+ lambda _params, log: compound_referee(a, b, pa, pb, log),
209
+ {'a': a, 'b': b})
root/wander-train/oracle_program_ir_v1.py ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """oracle_program_ir_v1 - one intermediate representation; every request compiles to a bounded relational program.
3
+
4
+ The five things that looked like separate engineering tracks - corpus reach, coding, conversation, prose,
5
+ self-repair - are one missing operation. A request must compile into explicit semantic OBLIGATIONS, those
6
+ obligations into a bounded relational PROGRAM over stores, operators, discourse state and permitted inference,
7
+ and the completed result must be verified AGAINST THE OBLIGATIONS before anything is rendered.
8
+
9
+ I found this while working on coding and then confined it to coding. The coding fold says: the request contains
10
+ its own specification, and testing a program against its own behaviour verifies the wrong proposition. Every other
11
+ capability makes the identical error. Asked why a compiler needs a parser, the model checks whether retrieved
12
+ sentences match the surface wording. Asked whether oxygen is a cause or a result, it checks a taxonomic exclusion.
13
+ Asked to compare photosynthesis with respiration, it retrieves individually warranted fragments and loses the
14
+ comparison subject. In each case the local claims are valid and the executed answer is not bound to what was
15
+ asked - the same failure as returning a bank-account class for a stack and reporting that it executed.
16
+
17
+ THE CONSEQUENCE THAT MATTERS MOST: an answer need not exist as a stored sentence. It exists as a warranted path
18
+ through stored relations. No sentence anywhere says why a compiler needs a parser; the answer is a join across
19
+ what a compiler consumes and what a parser produces. Measuring whether the answer STRING is in the store - which
20
+ is what every coverage probe here has done, mine included - can only ever report a coverage problem. That is the
21
+ difference between possessing documents and possessing knowledge.
22
+
23
+ utterance -> obligations -> relational program -> execution trace -> answer-state -> expression
24
+
25
+ A successful answer is a verified execution trace satisfying every obligation, not a collection of valid sentences.
26
+
27
+ This module is the representation and the obligation compiler. It executes nothing: an obligation it cannot build
28
+ is itself a finding, and is recorded as `unbuildable` rather than quietly dropped.
29
+
30
+ usage: oracle_program_ir_v1.py [--packet PATH] compile the diagnostic prompts and print their programs
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import json
36
+ import re
37
+ import sys
38
+ from dataclasses import dataclass, field, asdict
39
+ from pathlib import Path
40
+
41
+ WORD = re.compile(r"[A-Za-z][A-Za-z'-]{1,}")
42
+ PROPER = re.compile(r"\b([A-Z][\w'-]+(?:\s+(?:of|the|de)\s+[A-Z][\w'-]+|\s+[A-Z][\w'-]+)*)")
43
+ PRONOUN = re.compile(r"\b(it|its|that|this|they|them|those|these|one)\b", re.I)
44
+ NEGATION = re.compile(r"\b(not|isn't|aren't|doesn't|don't|never|no longer|rather than|instead of)\b", re.I)
45
+ PAST = re.compile(r"\b(was|were|did|had|used to|formerly|originally)\b", re.I)
46
+ QUANTIFIER = re.compile(r"\b(all|every|each|some|any|most|three|two|both|several|many|few|\d+)\b", re.I)
47
+ # A capital letter at the start of a sentence is not a name. Taking "What", "Compare", "Is" as entities is the
48
+ # same head-word disease this whole line exists to fix, and the first version of this file had it.
49
+ NOT_A_NAME = {'what', 'which', 'who', 'when', 'where', 'why', 'how', 'is', 'are', 'was', 'were', 'do', 'does',
50
+ 'did', 'can', 'could', 'will', 'would', 'should', 'tell', 'give', 'list', 'name', 'compare',
51
+ 'explain', 'describe', 'summarize', 'summarise', 'correct', 'the', 'a', 'an', 'so', 'and', 'but',
52
+ 'if', 'that', 'this', 'it', 'they', 'in', 'on', 'at', 'for', 'write', 'solve', 'find'}
53
+ # word-bounded and applied to the LOWERCASED utterance (fixed 2026-09-23): unbounded, it matched the tail of every
54
+ # capitalised word, so "Summarize the Internet" yielded the subjects 'ummarize' and 'nternet'
55
+ CONTENT = re.compile(r"\b([a-z][a-z'-]{3,})\b")
56
+ FUNCTION = {'about', 'from', 'with', 'into', 'than', 'then', 'have', 'been', 'does', 'made', 'more', 'most',
57
+ 'much', 'many', 'some', 'such', 'like', 'also', 'just', 'over', 'under', 'between', 'because',
58
+ 'there', 'here', 'them', 'their', 'your', 'mine', 'ours', 'wrong', 'right', 'mean', 'means'}
59
+
60
+
61
+ @dataclass
62
+ class Obligation:
63
+ """something the answer must satisfy to BE an answer. Verification is against these, never against fragments."""
64
+ name: str
65
+ kind: str # resolve | retrieve | relate | join | compute | contrast | decide | explain
66
+ argument: str = ''
67
+ satisfied_by: str = '' # filled at execution: what satisfied it
68
+ satisfied: bool | None = None # None until executed
69
+ note: str = ''
70
+
71
+
72
+ @dataclass
73
+ class Request:
74
+ """the intermediate representation. One shape; the program TYPE varies over it."""
75
+ utterance: str
76
+ program_type: str = 'unknown'
77
+ entities: list = field(default_factory=list)
78
+ discourse_referents: list = field(default_factory=list) # pronouns needing binding from prior turns
79
+ requested_relation: str = ''
80
+ requested_operation: str = ''
81
+ quantifiers: list = field(default_factory=list)
82
+ polarity: str = 'positive'
83
+ tense: str = 'present'
84
+ obligations: list = field(default_factory=list)
85
+ completion_criteria: list = field(default_factory=list)
86
+ content_subjects: list = field(default_factory=list)
87
+ unbuildable: list = field(default_factory=list)
88
+
89
+
90
+ # Each program type declares the obligations its answers must satisfy. These are the SHAPES of questions, and a
91
+ # kernel slot is an executable question-shape rather than a prose template: definition, constitution, mechanism,
92
+ # contrast, criterion, limitation, consequence.
93
+ PROGRAM_TYPES = {
94
+ 'definition': ['resolve:subject', 'retrieve:genus', 'retrieve:differentia', 'explain:definition'],
95
+ 'constitution': ['resolve:subject', 'relate:part_of', 'explain:composition'],
96
+ 'mechanism': ['resolve:subject', 'relate:process', 'join:steps', 'explain:mechanism'],
97
+ 'cause': ['resolve:subject', 'relate:causal_edge', 'decide:cause_vs_result', 'explain:causation'],
98
+ 'dependence': ['resolve:dependent', 'resolve:dependency', 'retrieve:consumes', 'retrieve:produces',
99
+ 'join:through_intermediate', 'decide:dependency_follows', 'explain:why_needed'],
100
+ 'comparison': ['resolve:left', 'resolve:right', 'retrieve:left_properties', 'retrieve:right_properties',
101
+ 'contrast:shared_dimension', 'explain:difference'],
102
+ 'counterfactual': ['resolve:subject', 'relate:causal_edge', 'decide:antecedent_removed',
103
+ 'explain:consequence'],
104
+ 'enumeration': ['resolve:subject', 'retrieve:n_facts', 'decide:distinctness', 'explain:list'],
105
+ 'summarization': ['resolve:scope', 'retrieve:claims_in_scope', 'contrast:redundancy', 'explain:summary'],
106
+ 'correction': ['resolve:prior_claim', 'retrieve:contrary_evidence', 'decide:which_holds',
107
+ 'explain:correction'],
108
+ 'coding': ['resolve:specification', 'compute:program', 'decide:satisfies_specification',
109
+ 'explain:program'],
110
+ 'computation': ['resolve:operands', 'resolve:operation', 'compute:exact', 'explain:steps'],
111
+ 'argumentation': ['resolve:thesis', 'retrieve:premises', 'join:dependency_graph', 'contrast:objections',
112
+ 'decide:thesis_supported', 'explain:argument'],
113
+ }
114
+
115
+ # surface cues -> program type. Deliberately small and readable: this is a claim about which question is being
116
+ # asked, and a wrong claim here should be legible in the trace rather than buried.
117
+ CUES = [
118
+ ('dependence', re.compile(r"\bwhy (?:does|do|is|are|would).{0,40}\b(?:need|require|depend|use)\b", re.I)),
119
+ ('dependence', re.compile(r"\b(?:need|require|depend on)\b.{0,30}\?", re.I)),
120
+ ('counterfactual', re.compile(r"\bwhat if\b|\bwould happen if\b|\bhad .{0,20} not\b|\bwithout\b.{0,30}\?", re.I)),
121
+ ('cause', re.compile(r"\bwhat causes?\b|\bwhy (?:does|do|is|are)\b|\bcause or (?:a )?result\b", re.I)),
122
+ ('comparison', re.compile(r"\bcompare\b|\bdifference between\b|\bhow is .{0,30} different\b|\bversus\b|\bvs\.?\b", re.I)),
123
+ ('correction', re.compile(r"\bcorrect me\b|\bthat'?s wrong\b|\bactually,?\b|\bisn'?t it\b", re.I)),
124
+ ('summarization', re.compile(r"\bsummari[sz]e\b|\bwhat have we\b|\bso far\b|\brecap\b", re.I)),
125
+ ('enumeration', re.compile(r"\blist\b|\bname (?:three|two|\d+)\b|\bgive me (?:three|two|\d+)\b", re.I)),
126
+ ('mechanism', re.compile(r"\bhow does\b|\bhow do\b|\bhow is .{0,20} made\b|\bwork\b\s*\?", re.I)),
127
+ ('constitution', re.compile(r"\bmade of\b|\bcomposed of\b|\bparts of\b|\bconsists? of\b", re.I)),
128
+ ('computation', re.compile(r"\d.{0,20}(?:%|percent|times|plus|minus|divided)|\bsquare root\b|\bsolve for\b", re.I)),
129
+ ('coding', re.compile(r"\bwrite a (?:python|javascript|java|c|go|sql)\b|\bfunction that\b|\bclass for\b", re.I)),
130
+ ('definition', re.compile(r"^\s*what (?:is|are|was|were)\b|\btell me about\b|\bwhat'?s\b", re.I)),
131
+ ]
132
+
133
+
134
+ def classify(utterance):
135
+ for name, rx in CUES:
136
+ if rx.search(utterance or ''):
137
+ return name
138
+ return 'unknown'
139
+
140
+
141
+ def compile_request(utterance, discourse_topics=()):
142
+ """utterance -> Request with its obligations. Executes nothing."""
143
+ u = ' '.join(str(utterance or '').split())
144
+ r = Request(utterance=u)
145
+ r.program_type = classify(u)
146
+ # names, minus anything that is only capitalised because it began the sentence
147
+ r.entities = [m.group(1) for m in PROPER.finditer(u)
148
+ if m.group(1).split()[0].lower() not in NOT_A_NAME]
149
+ # lowercase content nouns are subjects too - "compare photosynthesis with respiration" names two things and
150
+ # capitalises neither
151
+ r.content_subjects = [w for w in CONTENT.findall(u.lower()) if w not in FUNCTION and w not in NOT_A_NAME]
152
+ r.discourse_referents = sorted({m.group(1).lower() for m in PRONOUN.finditer(u)})
153
+ r.quantifiers = sorted({m.group(1).lower() for m in QUANTIFIER.finditer(u)})
154
+ r.polarity = 'negative' if NEGATION.search(u) else 'positive'
155
+ r.tense = 'past' if PAST.search(u) else 'present'
156
+
157
+ # the requested relation is what the answer must ESTABLISH, not what the words resemble
158
+ if r.program_type == 'dependence':
159
+ r.requested_relation = 'functional_dependence'
160
+ elif r.program_type in ('cause', 'counterfactual'):
161
+ r.requested_relation = 'causal'
162
+ elif r.program_type == 'comparison':
163
+ r.requested_relation = 'contrast_on_shared_dimension'
164
+ elif r.program_type in ('definition', 'constitution'):
165
+ r.requested_relation = 'taxonomic'
166
+ r.requested_operation = r.program_type
167
+
168
+ template = PROGRAM_TYPES.get(r.program_type)
169
+ if not template:
170
+ r.unbuildable.append('no program type recognised for this utterance')
171
+ return r
172
+
173
+ # the SUBJECTS of the program: named entities, else the discourse referents that must be bound from prior turns
174
+ subjects = list(r.entities) + [c for c in r.content_subjects if c not in
175
+ {e.lower() for e in r.entities}]
176
+ for step in template:
177
+ kind, _, arg = step.partition(':')
178
+ ob = Obligation(name=step, kind=kind, argument=arg)
179
+ if kind == 'resolve':
180
+ if r.discourse_referents and not subjects:
181
+ # THE FAILURE THE TRACES KEEP SHOWING: the act is identified and its subject is a pronoun that
182
+ # nothing binds. Recorded as an obligation with a named source, not as a dead end.
183
+ ob.note = 'subject is a discourse referent (%s); must bind from prior turns' % ', '.join(r.discourse_referents)
184
+ elif subjects:
185
+ ob.note = 'candidate: %s' % subjects[0]
186
+ else:
187
+ ob.note = 'no subject available in the utterance'
188
+ r.obligations.append(ob)
189
+
190
+ r.completion_criteria = ['every obligation satisfied',
191
+ 'the established relation is the one requested (%s)' % (r.requested_relation or 'n/a'),
192
+ 'each claim carries a warrant',
193
+ 'no two admitted claims contradict']
194
+ if r.program_type == 'comparison' and len(subjects) < 2:
195
+ r.unbuildable.append('comparison needs two subjects; found %d' % len(subjects))
196
+ return r
197
+
198
+
199
+ def verify(request, trace):
200
+ """check the completed trace against the OBLIGATIONS - never against the fragments it produced."""
201
+ got = {str(k) for k in (trace or {}).keys()}
202
+ unmet = []
203
+ for ob in request.obligations:
204
+ ob.satisfied = ob.name in got
205
+ ob.satisfied_by = str((trace or {}).get(ob.name, ''))[:120]
206
+ if not ob.satisfied:
207
+ unmet.append(ob.name)
208
+ return {'satisfied': len(request.obligations) - len(unmet), 'total': len(request.obligations),
209
+ 'unmet': unmet, 'answerable': not unmet}
210
+
211
+
212
+ def main():
213
+ a = sys.argv[1:]
214
+ packet = a[a.index('--packet') + 1] if '--packet' in a else '/root/fresh_conv_test.json'
215
+ prompts = []
216
+ try:
217
+ d = json.loads(Path(packet).read_text(encoding='utf-8'))
218
+ for name, turns in d.items():
219
+ for t in (turns if isinstance(turns, list) else []):
220
+ q = t[0] if isinstance(t, (list, tuple)) and t else (t if isinstance(t, str) else '')
221
+ if q:
222
+ prompts.append((name, q))
223
+ except Exception as exc: # noqa: BLE001
224
+ print('packet unreadable (%s); using built-in examples' % exc)
225
+ prompts = [('example', 'Why does a compiler need a parser?'),
226
+ ('example', 'Is oxygen a cause or a result of photosynthesis?'),
227
+ ('example', 'Compare photosynthesis with respiration.'),
228
+ ('example', 'List three facts about it.')]
229
+
230
+ print('%d prompts\n' % len(prompts))
231
+ unknown = 0
232
+ unbound = 0
233
+ for name, q in prompts:
234
+ r = compile_request(q)
235
+ if r.program_type == 'unknown':
236
+ unknown += 1
237
+ needs_binding = [o for o in r.obligations if 'discourse referent' in o.note]
238
+ if needs_binding:
239
+ unbound += 1
240
+ print('%-14s %-46s -> %s' % (name, q[:44], r.program_type))
241
+ print(' relation : %s | entities: %s | referents: %s'
242
+ % (r.requested_relation or '-', ', '.join(r.entities[:3]) or '-',
243
+ ', '.join(r.discourse_referents) or '-'))
244
+ print(' program : %s' % ' -> '.join(o.name for o in r.obligations))
245
+ for o in needs_binding:
246
+ print(' UNBOUND : %s (%s)' % (o.name, o.note))
247
+ for u in r.unbuildable:
248
+ print(' UNBUILDABLE: %s' % u)
249
+ print()
250
+ print('%d prompts | %d had no recognised program type | %d have an unbound subject'
251
+ % (len(prompts), unknown, unbound))
252
+ return 0
253
+
254
+
255
+ if __name__ == '__main__':
256
+ raise SystemExit(main())
root/wander-train/oracle_release_conceptnet.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """R1 — ConceptNet commonsense adapter for the release runtime.
3
+
4
+ Typed edge retrieval over the sealed M13e5 ConceptNet specialist
5
+ (m13e5_conceptnet_build_a_v1, index sha 5e28406c...), rendered with provenance.
6
+ Zero-model: SQL + deterministic rendering only. Speech uses ConceptNet's own
7
+ crowd-authored surface_text where present (attributed), otherwise a typed
8
+ relation template. Every released line carries its edge provenance; empty
9
+ retrieval is an explicit abstention, never a guess.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import re
15
+ import sqlite3
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ PHASE = Path(__file__).resolve().parents[1] / "fable-content" / "oracle-m-series-2026-08-15"
20
+ DEFAULT_BUILD = PHASE / "m13e5_conceptnet_build_a_v1" / "conceptnet_specialist.sqlite3"
21
+
22
+ # Relation -> (question forms it answers, deterministic template)
23
+ RELATION_TEMPLATES: dict[str, str] = {
24
+ "IsA": "{start} is a kind of {end}",
25
+ "PartOf": "{start} is part of {end}",
26
+ "HasA": "{start} has {end}",
27
+ "UsedFor": "{start} is used for {end}",
28
+ "CapableOf": "{start} can {end}",
29
+ "AtLocation": "{start} is typically found at {end}",
30
+ "Causes": "{start} causes {end}",
31
+ "HasSubevent": "{start} involves {end}",
32
+ "HasPrerequisite": "{start} requires {end}",
33
+ "MotivatedByGoal": "{start} is motivated by {end}",
34
+ "Desires": "{start} desires {end}",
35
+ "MadeOf": "{start} is made of {end}",
36
+ "ReceivesAction": "{start} can be {end}",
37
+ "HasProperty": "{start} is {end}",
38
+ "Synonym": "{start} is a synonym of {end}",
39
+ "Antonym": "{start} is an antonym of {end}",
40
+ "DistinctFrom": "{start} is distinct from {end}",
41
+ "SimilarTo": "{start} is similar to {end}",
42
+ "RelatedTo": "{start} is related to {end}",
43
+ }
44
+
45
+ QUESTION_ROUTES: list[tuple[re.Pattern[str], list[str]]] = [
46
+ (re.compile(r"^what is (?:a |an |the )?(.+?) used for\??$", re.I), ["UsedFor"]),
47
+ (re.compile(r"^what can (?:a |an |the )?(.+?) do\??$", re.I), ["CapableOf"]),
48
+ (re.compile(r"^where (?:would you |do you |can you )?find (?:a |an |the )?(.+?)\??$", re.I), ["AtLocation"]),
49
+ (re.compile(r"^where is (?:a |an |the )?(.+?) (?:usually |typically )?found\??$", re.I), ["AtLocation"]),
50
+ (re.compile(r"^what (?:does|do) (?:a |an |the )?(.+?) have\??$", re.I), ["HasA"]),
51
+ (re.compile(r"^what is (?:a |an |the )?(.+?) made of\??$", re.I), ["MadeOf"]),
52
+ (re.compile(r"^what causes (?:a |an |the )?(.+?)\??$", re.I), ["Causes:end"]),
53
+ (re.compile(r"^what (?:does|do) (?:a |an |the )?(.+?) cause\??$", re.I), ["Causes"]),
54
+ (re.compile(r"^what kind of thing is (?:a |an |the )?(.+?)\??$", re.I), ["IsA"]),
55
+ (re.compile(r"^what (?:is|are) (?:a |an |the )?(.+?) (?:a )?(?:kind|type|sort) of\??$", re.I), ["IsA"]),
56
+ (re.compile(r"^what is (?:a |an |the )?(.+?) part of\??$", re.I), ["PartOf"]),
57
+ (re.compile(r"^what (?:is|are) (?:the )?parts? of (?:a |an |the )?(.+?)\??$", re.I), ["PartOf:end", "HasA"]),
58
+ (re.compile(r"^what do(?:es)? (?:a |an |the )?(.+?) require\??$", re.I), ["HasPrerequisite"]),
59
+ (re.compile(r"^what is (?:a |an |the )?(.+?) similar to\??$", re.I), ["SimilarTo", "Synonym"]),
60
+ (re.compile(r"^what is (?:the )?opposite of (?:a |an |the )?(.+?)\??$", re.I), ["Antonym"]),
61
+ ]
62
+
63
+ _SURFACE_BRACKETS = re.compile(r"\[\[|\]\]")
64
+
65
+
66
+ class ConceptNetAdapter:
67
+ def __init__(self, build_path: Path | str = DEFAULT_BUILD):
68
+ build_path = Path(build_path)
69
+ if not build_path.is_file():
70
+ raise FileNotFoundError(str(build_path))
71
+ uri = build_path.resolve().as_uri() + "?mode=ro&immutable=1"
72
+ self.connection = sqlite3.connect(uri, uri=True)
73
+
74
+ # ── retrieval ────────────────────────────────────────────────────────
75
+ def edges(self, concept: str, relation: str, *, reverse: bool = False,
76
+ limit: int = 6) -> list[dict[str, Any]]:
77
+ concept = concept.strip().lower()
78
+ column, other = ("end", "start") if reverse else ("start", "end")
79
+ rows = self.connection.execute(
80
+ f"SELECT rel, start, end, weight, surface_text, dataset FROM edge "
81
+ f"WHERE {column}=? AND rel=? ORDER BY weight DESC, start, end LIMIT ?",
82
+ (concept, relation, limit)).fetchall()
83
+ return [{"rel": r[0], "start": r[1], "end": r[2], "weight": r[3],
84
+ "surface_text": r[4], "dataset": r[5],
85
+ "provenance": f"conceptnet57:{r[0]}:{r[1]}->{r[2]}"} for r in rows]
86
+
87
+ def neighborhood(self, concept: str, *, limit_per_relation: int = 4) -> dict[str, list[dict[str, Any]]]:
88
+ out: dict[str, list[dict[str, Any]]] = {}
89
+ for relation in RELATION_TEMPLATES:
90
+ if relation == "RelatedTo":
91
+ continue # too diffuse for a summary; available via edges()
92
+ found = self.edges(concept, relation, limit=limit_per_relation)
93
+ if found:
94
+ out[relation] = found
95
+ return out
96
+
97
+ # ── rendering ────────────────────────────────────────────────────────
98
+ @staticmethod
99
+ def render_edge(edge: dict[str, Any]) -> str:
100
+ surface = (edge.get("surface_text") or "").strip()
101
+ if surface:
102
+ text = _SURFACE_BRACKETS.sub("", surface).lstrip("*").strip()
103
+ if text:
104
+ return text[0].upper() + text[1:].rstrip(".") + "."
105
+ template = RELATION_TEMPLATES.get(edge["rel"], "{start} relates to {end}")
106
+ text = template.format(start=edge["start"], end=edge["end"])
107
+ return text[0].upper() + text[1:] + "."
108
+
109
+ def answer(self, question: str) -> dict[str, Any]:
110
+ """Route a commonsense question form; abstain explicitly otherwise."""
111
+ text = " ".join(question.strip().split())
112
+ for pattern, relations in QUESTION_ROUTES:
113
+ match = pattern.match(text)
114
+ if not match:
115
+ continue
116
+ concept = match.group(1).strip().lower()
117
+ collected: list[dict[str, Any]] = []
118
+ for spec in relations:
119
+ relation, _, direction = spec.partition(":")
120
+ collected.extend(self.edges(concept, relation, reverse=(direction == "end")))
121
+ if not collected:
122
+ return {"status": "withheld", "operation": "conceptnet_no_admitted_edges",
123
+ "concept": concept,
124
+ "response": f"The commonsense index has no admitted edges answering that about {concept}.",
125
+ "claims": []}
126
+ seen: set[str] = set()
127
+ lines = []
128
+ claims = []
129
+ for edge in collected:
130
+ rendered = self.render_edge(edge)
131
+ if rendered in seen:
132
+ continue
133
+ seen.add(rendered)
134
+ lines.append(rendered)
135
+ claims.append({"provenance": edge["provenance"], "weight": edge["weight"],
136
+ "dataset": edge["dataset"]})
137
+ if len(lines) >= 5:
138
+ break
139
+ return {"status": "answered", "operation": "conceptnet_commonsense",
140
+ "concept": concept, "response": " ".join(lines), "claims": claims}
141
+ return {"status": "unrouted", "operation": "conceptnet_form_not_registered",
142
+ "response": "", "claims": []}
root/wander-train/oracle_release_runtime_aa.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Runtime AA — evidence-mined tiered cues for the explanatory routes (additive over Z).
3
+
4
+ M24's cue inventories were authored by guessing and the route stress test showed
5
+ the cost: how 10%, what_causes 28%, when 65% answer-rate. Iteration 4 mined
6
+ 400,000 admitted sentences to find the forms the corpus ACTUALLY uses, and the
7
+ guesses had missed the most frequent ones outright:
8
+
9
+ cause : "led to" 1,117 · "so that" 1,012 · "as a result" 372 · "result of" 340
10
+ process : "uses" 3,647 · "when the" 3,580 · "is called" 3,108 · "is/are formed" 263
11
+ date : "was released" 1,038 · "was built" 560 · "took place" 323
12
+ ("built" was in the question regex but NOT in the inception cues at all)
13
+
14
+ Widening naively would trade precision for recall, so the cues are TIERED: a
15
+ specific form is preferred, and a broad one is consulted only when no specific
16
+ form matches. Frequency alone does not promote a cue — "makes" is frequent but
17
+ weakly causal, so it sits in the last tier.
18
+
19
+ Also fixes a sentence-quality defect the harness surfaced: sentence extraction
20
+ sometimes yields a fragment starting mid-parenthetical ("1938) is a psychologist
21
+ ..."), which is cited and correct but reads as broken. Such fragments are
22
+ skipped in favour of the next admitted sentence."""
23
+
24
+ from __future__ import annotations
25
+
26
+ import re
27
+ import time
28
+
29
+ from oracle_release_runtime_z import OracleReleaseRuntimeZ
30
+
31
+ # tier 1 = explicitly the relation; tier 2 = strongly indicative; tier 3 = weak fallback
32
+ CAUSE_TIERS = [
33
+ re.compile(r"\b(caused by|is caused|are caused|due to|results? from|triggered by|produced by)\b", re.I),
34
+ re.compile(r"\b(because|led to|leads to|as a result|result of|gives rise to)\b", re.I),
35
+ re.compile(r"\b(causes|makes|so that|therefore|happens when|occurs when)\b", re.I),
36
+ ]
37
+ PROCESS_TIERS = [
38
+ re.compile(r"\b(happens when|occurs when|forms? when|is formed|are formed|works? by|by which|through which)\b", re.I),
39
+ re.compile(r"\b(process|is made|are made|begins when|starts when|when there is)\b", re.I),
40
+ re.compile(r"\b(uses|when the|is called|moves|produces)\b", re.I),
41
+ ]
42
+ # Date cues are conditioned on the VERB ACTUALLY ASKED, not a fixed tier order.
43
+ # A flat specific-first ordering regressed "When did WWII start?" to "The Tanggu
44
+ # Truce ... was signed in 1933" because `was signed` outranked `began`. The asked
45
+ # verb is the correct selector: it says which event the questioner means.
46
+ VERB_CUES = {
47
+ "start": [r"\b(began|begun|started|broke out|was launched)\b", r"\b(was founded|was established|was created)\b"],
48
+ "begin": [r"\b(began|begun|started|broke out|was launched)\b", r"\b(was founded|was established|was created)\b"],
49
+ "end": [r"\b(ended|finished|collapsed|surrendered|was dissolved)\b"],
50
+ "found": [r"\b(was founded|was established|was created|founded)\b"],
51
+ "built": [r"\b(was built|was constructed|built between|was completed)\b"],
52
+ "build": [r"\b(was built|was constructed|built between|was completed)\b"],
53
+ "discover": [r"\b(was discovered|discovered)\b"],
54
+ "invent": [r"\b(was invented|invented)\b"],
55
+ "born": [r"\b(was born|born)\b"],
56
+ "die": [r"\b(died|was killed)\b"],
57
+ "happen": [r"\b(took place|happened|occurred|was held)\b"],
58
+ "occur": [r"\b(took place|happened|occurred|was held)\b"],
59
+ }
60
+ INCEPT_GENERAL = [
61
+ re.compile(r"\b(began|begun|started|was founded|was built|was established|was created|took place|broke out)\b", re.I),
62
+ re.compile(r"\b(opened|ended|was released|was signed|was discovered|was invented|born|died)\b", re.I),
63
+ ]
64
+ YEAR = re.compile(r"\b(1[0-9]{3}|20[0-2][0-9])\b")
65
+ FRAGMENT = re.compile(r"^\s*(?:[a-z]|\d{1,4}\s*[)\]])") # "1938) is ..." / lowercase start
66
+
67
+
68
+ class OracleReleaseRuntimeAA(OracleReleaseRuntimeZ):
69
+ def _pick_tiered(self, topic: str, tiers: list, *, require_year: bool = False) -> list[dict]:
70
+ bundle = self._bundle(topic, n=60)
71
+ if not bundle:
72
+ singular = topic[:-1] if topic.endswith("s") and not topic.endswith("ss") else None
73
+ if singular and len(singular) > 2:
74
+ bundle = self._bundle(singular, n=60)
75
+ if not bundle:
76
+ return []
77
+ usable = [c for c in bundle if not FRAGMENT.match(str(c.get("sentence") or ""))]
78
+ if not usable:
79
+ usable = bundle
80
+ for tier in tiers: # specific first, broad only as fallback
81
+ hits = []
82
+ for claim in usable:
83
+ sentence = str(claim.get("sentence") or "")
84
+ if require_year and not YEAR.search(sentence):
85
+ continue
86
+ if tier.search(sentence):
87
+ hits.append(claim)
88
+ if hits:
89
+ return hits
90
+ return []
91
+
92
+ def _explanatory_turn(self, text: str) -> dict | None: # override Y
93
+ from oracle_release_runtime_y import CAUSE_Q, HOW_Q, WHEN_Q
94
+ stripped = " ".join(text.strip().split())
95
+
96
+ match = CAUSE_Q.match(stripped)
97
+ if match:
98
+ topic = match.group("t").strip()
99
+ return self._render(topic, self._pick_tiered(topic, CAUSE_TIERS),
100
+ "cause", "capability:what_causes", f"what causes {topic}")
101
+
102
+ match = HOW_Q.match(stripped)
103
+ if match:
104
+ topic = match.group("t").strip()
105
+ return self._render(topic, self._pick_tiered(topic, PROCESS_TIERS),
106
+ "process", "capability:how_does", f"how {topic} works")
107
+
108
+ match = WHEN_Q.match(stripped)
109
+ if match:
110
+ topic = match.group("t").strip()
111
+ verb = (match.group("v") or "").lower()
112
+ tiers = []
113
+ for stem, patterns in VERB_CUES.items():
114
+ if verb.startswith(stem):
115
+ tiers = [re.compile(pat, re.I) for pat in patterns]
116
+ break
117
+ tiers = tiers + INCEPT_GENERAL
118
+ hits = self._pick_tiered(topic, tiers, require_year=True) \
119
+ or self._pick_tiered(topic, [YEAR], require_year=True)
120
+ return self._render(topic, hits, "inception", "capability:when_did",
121
+ f"when {topic} {match.group('v')}")
122
+ return None
123
+
124
+ def chat(self, text: str, session_id: str = "default") -> dict:
125
+ started = time.perf_counter()
126
+ result = None
127
+ try:
128
+ result = self._explanatory_turn(text)
129
+ except Exception: # noqa: BLE001
130
+ result = None
131
+ if result is not None:
132
+ result["latency_ms"] = round(1000.0 * (time.perf_counter() - started), 1)
133
+ result["zero_model_gate"] = {"lm_calls": 0, "transformer_calls": 0}
134
+ return result
135
+ return super().chat(text, session_id)
root/wander-train/oracle_release_runtime_ac.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Runtime AC — explain-simply picks the identifying sentence (additive over AB).
3
+
4
+ Iteration 6 diagnosis. `_definitional_score` returns 0 for every sentence on a
5
+ biographical page (it keys on copular definition patterns that proper-noun prose
6
+ does not use), so selection fell through to the tiebreak `-len(words)` — the
7
+ SHORTEST sentence wins. That systematically chose trivia and fragments over the
8
+ sentence that identifies the subject:
9
+
10
+ "Explain Paco Gento simply." -> "He played for Spain national team."
11
+ "Explain Arthur C. Clarke simply." -> "He also wrote many short stories."
12
+ "Explain Bernard Montgomery simply."-> "Later, in World War II." (a fragment)
13
+
14
+ In encyclopedic prose the FIRST sentence of a page is by convention the one that
15
+ says what the subject is. So on a score tie the tiebreak now prefers EARLIER
16
+ position rather than fewer words, and sentences that cannot stand alone (no
17
+ finite verb, or opening mid-parenthetical) are skipped. Brevity is still
18
+ preferred among sentences that actually define.
19
+
20
+ Note on the metric that found this: `explain_simply` on-topic looked like the
21
+ same false-drift artifact that `list` produced, but it was not. For an
22
+ "explain X" question, mentioning X is not a proxy for relevance — it IS the
23
+ requirement, because an explanation that never identifies its subject has not
24
+ explained it."""
25
+
26
+ from __future__ import annotations
27
+
28
+ import re
29
+ import time
30
+
31
+ from oracle_release_runtime_ab import OracleReleaseRuntimeAB
32
+ from oracle_release_runtime_m import SIMPLY, _definitional_score
33
+ from stage5m13e8_relevance_gate_v1 import gate_bundle
34
+
35
+ _WORD = chr(92) + "b" # backslash-b built at runtime; heredocs eat the literal
36
+ FINITE_VERB = re.compile(_WORD + r"(is|are|was|were|has|have|had|does|did|can|"
37
+ r"could|will|would|became|becomes|plays|played|wrote|"
38
+ r"made|makes|lives|lived|died|won|led|serves|served)" + _WORD, re.I)
39
+ OPENS_MID = re.compile(r"^\s*(?:[a-z]|\d{1,4}\s*[)\]]|later,|however,|also,|then,)", re.I)
40
+
41
+
42
+ def _standalone(sentence: str) -> bool:
43
+ """Can this sentence stand alone as an identification?"""
44
+ text = str(sentence or "").strip()
45
+ if len(text.split()) < 4 or OPENS_MID.match(text):
46
+ return False
47
+ return bool(FINITE_VERB.search(text))
48
+
49
+
50
+ class OracleReleaseRuntimeAC(OracleReleaseRuntimeAB):
51
+ def _simply_turn(self, text: str, session_id: str) -> dict | None: # override M
52
+ match = SIMPLY.match(text.strip())
53
+ if not match:
54
+ return None
55
+ topic = (match.group("t") or match.group("t2") or "").strip()
56
+ if not topic:
57
+ return None
58
+ bundle = self._bundle(topic, n=8)
59
+ admitted, _ = gate_bundle(topic, bundle)
60
+ gloss = self._gloss(topic)
61
+ if not admitted and not gloss:
62
+ return {"status": "withheld", "path": "capability:explain_simply",
63
+ "response": f"No provenance-admitted material on {topic} is available to explain.",
64
+ "provenance": {"kind": "simple_register", "topic": topic}}
65
+ parts: list[str] = []
66
+ premises: list[str] = []
67
+ if admitted:
68
+ order = {c["claim_id"]: i for i, c in enumerate(bundle)}
69
+ standalone = [c for c in admitted if _standalone(c.get("sentence"))]
70
+ pool = standalone or admitted
71
+ short_defs = [c for c in pool
72
+ if len(c["sentence"].split()) <= 30
73
+ and _definitional_score(topic, c["sentence"]) >= 6]
74
+ if short_defs:
75
+ lead = max(short_defs, key=lambda c: (_definitional_score(topic, c["sentence"]),
76
+ -len(c["sentence"].split())))
77
+ else:
78
+ # No scored definition (typical of proper-noun pages): take the
79
+ # page's own opening sentence, which is the identifying one.
80
+ lead = min(pool, key=lambda c: order.get(c["claim_id"], 10_000))
81
+ parts.append(f"{lead['sentence']} [{lead['claim_id']}]")
82
+ premises.append(lead["claim_id"])
83
+ if gloss:
84
+ parts.append(f"The dictionary sense: {gloss}. [wiktionary:{topic.lower()}:sense1]")
85
+ premises.append(f"wiktionary:{topic.lower()}")
86
+ return {"status": "answered", "path": "capability:explain_simply",
87
+ "response": "In simple terms: " + " ".join(parts),
88
+ "provenance": {"kind": "simple_register", "topic": topic, "premises": premises}}
89
+
90
+ def _timeline_turn(self, text: str, session_id: str) -> dict | None: # override J
91
+ result = super()._timeline_turn(text, session_id)
92
+ if not result or result.get("status") != "answered":
93
+ return result
94
+ body = str(result.get("response", ""))
95
+ distinct = {y for y in year_tokens(body)}
96
+ if len(distinct) >= 2:
97
+ return result
98
+ topic_note = "the admitted material yields fewer than two distinct dates"
99
+ return {"status": "withheld", "path": "capability:timeline",
100
+ "response": f"A timeline needs at least two distinct dated statements; "
101
+ f"{topic_note}. I will not present a measurement as a date.",
102
+ "provenance": {"kind": "temporal_projection",
103
+ "result": "insufficient_distinct_dates"}}
104
+
105
+ def chat(self, text: str, session_id: str = "default") -> dict:
106
+ started = time.perf_counter()
107
+ result = None
108
+ try:
109
+ result = self._simply_turn(" ".join(text.strip().split()), session_id)
110
+ except Exception: # noqa: BLE001
111
+ result = None
112
+ if result is not None:
113
+ result["latency_ms"] = round(1000.0 * (time.perf_counter() - started), 1)
114
+ result["zero_model_gate"] = {"lm_calls": 0, "transformer_calls": 0}
115
+ return result
116
+ return super().chat(text, session_id)
117
+
118
+
119
+ # ── timeline: a year token is not the same thing as a number ────────────────
120
+ # The route treated any 4-digit token as a date, so "One megasecond contains
121
+ # 1000 kiloseconds" was rendered as the year 1000, and a page with one date
122
+ # repeated produced a single-point "timeline". A date must not be a measurement,
123
+ # and a timeline needs at least two DISTINCT years.
124
+ UNIT_AFTER = re.compile(
125
+ r"^(?:kilo|mega|giga|milli|micro|nano)?"
126
+ r"(?:second|meter|metre|gram|litre|liter|byte|watt|hertz|mile|foot|feet|inch|"
127
+ r"year|month|week|day|hour|minute|people|person|km|kg|cm|mm|ft|lb)s?$", re.I)
128
+
129
+
130
+ def year_tokens(sentence: str) -> list[int]:
131
+ """4-digit years in a sentence, excluding numbers that are measurements."""
132
+ text = str(sentence or "")
133
+ out: list[int] = []
134
+ for match in re.finditer(r"(1[0-9]{3}|20[0-2][0-9])", text):
135
+ tail = text[match.end():].lstrip()
136
+ next_word = tail.split()[0].strip(".,;:)") if tail.split() else ""
137
+ if next_word and UNIT_AFTER.match(next_word):
138
+ continue # "1000 kiloseconds" is a quantity, not a date
139
+ out.append(int(match.group(1)))
140
+ return out
root/wander-train/oracle_release_runtime_ag.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Runtime AG - cite a genuine hypernym chain (additive over AF).
3
+
4
+ AF answered correctly but CITED INCORRECTLY:
5
+
6
+ "Yes. The lexical record places gray whale under whale:
7
+ organism -> living thing -> whole -> object -> physical entity -> gray whale"
8
+
9
+ The claimed hypernym ("whale") does not appear in the chain shown. Corpus v1
10
+ stored each synset's total path length as the depth of every node on that path,
11
+ so all hypernyms of a sense shared one meaningless value and ordering by it
12
+ produced a sequence that was not a hypernym path at all.
13
+
14
+ For a system whose entire claim is shown provenance, citing a chain that does not
15
+ contain the cited term is the most serious defect available - worse than the
16
+ parser bug, because the answer looks right and the evidence is fabricated
17
+ structure.
18
+
19
+ Corpus v2 stores each node's own distance from the root, so ascending depth is a
20
+ genuine general-to-specific ordering. This runtime reads v2, renders the chain in
21
+ that order, and guarantees the cited hypernym appears in what it shows. It also
22
+ never presents more than the real path: nodes are drawn from the lemma's own
23
+ closure only.
24
+
25
+ No neural network; no LM call.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import sqlite3
31
+ import time
32
+ from pathlib import Path
33
+
34
+ from oracle_release_runtime_af import OracleReleaseRuntimeAF
35
+
36
+ WORDNET_CORPUS_V2 = (__import__("os").environ.get("ORACLE7_ROOT", "/opt/oracle-clm") + "/fable-content/oracle-m-series-2026-08-15"
37
+ "/m31_wordnet_corpus_v2.sqlite3")
38
+
39
+
40
+ class OracleReleaseRuntimeAG(OracleReleaseRuntimeAF):
41
+ def __init__(self, *args, wordnet_corpus: str = WORDNET_CORPUS_V2, **kwargs) -> None:
42
+ super().__init__(*args, wordnet_corpus=wordnet_corpus, **kwargs)
43
+
44
+ def _chain_for(self, lemma: str, limit: int = 6) -> list:
45
+ """General -> specific, by each node's own distance from the root."""
46
+ rows = self._wn.execute(
47
+ "SELECT hypernym FROM lemma_closure WHERE lemma=? ORDER BY depth ASC, hypernym ASC "
48
+ "LIMIT ?", (lemma, limit)).fetchall()
49
+ return [r[0] for r in rows]
50
+
51
+ def _render_chain(self, lemma: str, hypernym: str | None) -> str:
52
+ """The shown chain must contain the cited hypernym, or it is not evidence."""
53
+ chain = self._chain_for(lemma, limit=6)
54
+ if hypernym and hypernym not in chain:
55
+ row = self._wn.execute(
56
+ "SELECT depth FROM lemma_closure WHERE lemma=? AND hypernym=?",
57
+ (lemma, hypernym)).fetchone()
58
+ if row is not None:
59
+ chain = self._chain_for(lemma, limit=5)
60
+ if hypernym in chain:
61
+ chain.remove(hypernym)
62
+ chain = chain[:4] + [hypernym]
63
+ return " -> ".join(chain + [lemma]) if chain else lemma
64
+
65
+ def _wordnet_turn(self, text: str) -> dict | None:
66
+ if self._wn is None:
67
+ return None
68
+ parsed = self._parse_kind(text)
69
+ if parsed is None:
70
+ return None
71
+ x, y = parsed
72
+ lemma = self._present(x)
73
+ if lemma is None:
74
+ return None
75
+ hit = self._closure_hit(lemma, y)
76
+ article_x = "an" if x[:1] in "aeiou" else "a"
77
+ article_y = "an" if y[:1] in "aeiou" else "a"
78
+ if hit is not None:
79
+ hypernym, _depth, sense = hit
80
+ shown = self._render_chain(lemma, hypernym)
81
+ return {
82
+ "status": "answered",
83
+ "path": "wordnet_kind_direct",
84
+ "response": (f"Yes. The lexical record places {lemma} under {hypernym}, "
85
+ f"from the general to the specific: {shown}. On that record "
86
+ f"{article_x} {x} is {article_y} {y}. [cited:wordnet:{sense}]"),
87
+ "provenance": {"kind": "lexical_corpus", "corpus": "wordnet",
88
+ "relation": "hypernym", "lemma": lemma,
89
+ "hypernym": hypernym, "sense": sense,
90
+ "chain": self._chain_for(lemma) + [lemma]},
91
+ }
92
+ shown = self._render_chain(lemma, None)
93
+ return {
94
+ "status": "answered",
95
+ "path": "wordnet_kind_excluded",
96
+ "response": (f"No. The lexical record places {lemma} on a different line, "
97
+ f"from the general to the specific: {shown}. {y.capitalize()} does "
98
+ f"not appear anywhere in that chain, and the chain is complete, so "
99
+ f"this is an exclusion rather than a gap in the record. "
100
+ f"[cited:wordnet:{lemma}]"),
101
+ "provenance": {"kind": "lexical_corpus", "corpus": "wordnet",
102
+ "relation": "hypernym_excluded", "lemma": lemma,
103
+ "chain": self._chain_for(lemma) + [lemma]},
104
+ }
root/wander-train/oracle_release_runtime_ah.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Runtime AH - honest exclusion rendering (additive over AG).
3
+
4
+ AG fixed the affirmative citation but left the SAME defect class in the exclusion
5
+ path. Two problems, both of which fabricate structure:
6
+
7
+ 1. Sense interleaving. The closure is a union over all senses, so rendering it as
8
+ one arrow-path splices unrelated lineages:
9
+
10
+ "guinea pig: entity -> physical entity -> causal agent -> object ->
11
+ person -> whole -> guinea pig"
12
+
13
+ That path does not exist. It mixes the rodent with the separate sense of a
14
+ person used as a test subject.
15
+
16
+ 2. Truncated scope. The text asserted a term "does not appear anywhere in that
17
+ chain" while displaying only a six-node prefix, inviting the reader to verify
18
+ an exhaustive claim against partial evidence.
19
+
20
+ Fix: an exclusion no longer claims a path. It reports the most specific categories
21
+ the record actually assigns - which are the sense-characteristic ones - as a SET,
22
+ and states plainly that the exclusion was checked against the complete closure
23
+ while what is shown is a summary of it. The affirmative path still shows a real
24
+ chain, because there a single genuine lineage exists and contains the cited term.
25
+
26
+ Say only what the record supports, and show evidence in the shape it actually has.
27
+ No neural network; no LM call.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ from oracle_release_runtime_ag import OracleReleaseRuntimeAG
33
+
34
+ SHOW_CATEGORIES = 6
35
+
36
+
37
+ class OracleReleaseRuntimeAH(OracleReleaseRuntimeAG):
38
+ def _specific_categories(self, lemma: str, limit: int = SHOW_CATEGORIES) -> list:
39
+ """Deepest hypernyms = the sense-characteristic ones. A set, not a path."""
40
+ rows = self._wn.execute(
41
+ "SELECT hypernym FROM lemma_closure WHERE lemma=? ORDER BY depth DESC, hypernym ASC "
42
+ "LIMIT ?", (lemma, limit)).fetchall()
43
+ return [r[0] for r in rows]
44
+
45
+ def _closure_size(self, lemma: str) -> int:
46
+ row = self._wn.execute(
47
+ "SELECT COUNT(*) FROM lemma_closure WHERE lemma=?", (lemma,)).fetchone()
48
+ return int(row[0]) if row else 0
49
+
50
+ def _wordnet_turn(self, text: str) -> dict | None:
51
+ result = super()._wordnet_turn(text)
52
+ if result is None or result.get("path") != "wordnet_kind_excluded":
53
+ return result
54
+ prov = result.get("provenance", {})
55
+ lemma = prov.get("lemma")
56
+ parsed = self._parse_kind(text)
57
+ if not lemma or parsed is None:
58
+ return result
59
+ _x, y = parsed
60
+ cats = self._specific_categories(lemma)
61
+ total = self._closure_size(lemma)
62
+ listed = ", ".join(cats) if cats else "no categories on record"
63
+ result["response"] = (
64
+ f"No. The record classifies {lemma} under {listed}"
65
+ f"{' and other broader categories' if total > len(cats) else ''}. "
66
+ f"{y.capitalize()} is not among the {total} categories on record for it, and "
67
+ f"that record is complete, so this is an exclusion rather than a gap. "
68
+ f"[cited:wordnet:{lemma}]")
69
+ result["provenance"] = {
70
+ "kind": "lexical_corpus", "corpus": "wordnet",
71
+ "relation": "hypernym_excluded", "lemma": lemma,
72
+ "categories_shown": cats, "categories_total": total,
73
+ "note": ("categories are a set drawn from all senses, not a single lineage; "
74
+ "the exclusion was checked against the complete closure"),
75
+ }
76
+ return result
root/wander-train/oracle_release_runtime_aj.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Runtime AJ - lexicon-informed splitting for determiner-free questions (additive over AI).
3
+
4
+ M38 passed eight columns and still mis-parsed this:
5
+
6
+ "Is hand luggage luggage?" -> x='hand', y='luggage luggage'
7
+ -> "No. The record classifies HAND under forepaw, paw, animal foot..."
8
+
9
+ With no determiner the strict pattern cannot anchor, and the loose pattern's non-greedy
10
+ subject takes the shortest split. This is the M31 defect resurfacing in the form the M31
11
+ fix did not cover, and no panel saw it because every generated question carries
12
+ determiners ("Is a X a Y?"). The route's own domain includes determiner-free forms;
13
+ the test distribution did not.
14
+
15
+ Fix: when no determiner is available to anchor the split, use the admitted record as the
16
+ lexicon. Enumerate every split point and choose the longest subject for which BOTH sides
17
+ are lemmas the corpus carries. Purely structural, no heuristic weighting - the record
18
+ already knows which strings are terms.
19
+
20
+ "Is hand luggage luggage?"
21
+ split 1: x='hand' y='luggage luggage' -> y not a lemma, rejected
22
+ split 2: x='hand luggage' y='luggage' -> both lemmas, ACCEPTED
23
+
24
+ Determiner-bearing forms still take the strict path unchanged. No neural network.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import re
30
+
31
+ from oracle_release_runtime_ai import OracleReleaseRuntimeAI
32
+ from oracle_release_runtime_af import KIND_Q_STRICT, _FILLER, _LEAD
33
+ from oracle_release_runtime_ae import _forms
34
+
35
+ _BODY = re.compile(
36
+ r"^(?:is|are)\s+(?:(?:an?|the|some)\s+)?(?P<body>[a-z][a-z '\-]+?)\s*[.?!]*$", re.I)
37
+ MAX_WORDS = 8
38
+
39
+
40
+ class OracleReleaseRuntimeAJ(OracleReleaseRuntimeAI):
41
+ def _valid_pair(self, x: str, y: str):
42
+ if not x or not y or x == y or len(x) < 2 or len(y) < 2:
43
+ return None
44
+ if set(_forms(x)) & set(_forms(y)):
45
+ return None
46
+ return x, y
47
+
48
+ def _parse_kind(self, text: str):
49
+ t = " ".join(text.strip().split())
50
+ t = " ".join(_FILLER.sub("", _LEAD.sub("", t)).split())
51
+ match = KIND_Q_STRICT.match(t)
52
+ if match:
53
+ return self._valid_pair(match.group("x").strip().lower(),
54
+ match.group("y").strip().lower())
55
+ body_match = _BODY.match(t)
56
+ if not body_match:
57
+ return super()._parse_kind(text)
58
+ words = body_match.group("body").strip().lower().split()
59
+ if len(words) < 2 or len(words) > MAX_WORDS:
60
+ return super()._parse_kind(text)
61
+ chosen = None
62
+ for cut in range(1, len(words)):
63
+ x = " ".join(words[:cut])
64
+ y = " ".join(words[cut:])
65
+ pair = self._valid_pair(x, y)
66
+ if pair is None:
67
+ continue
68
+ if self._present(x) and self._present(y):
69
+ chosen = pair # keep iterating: prefer the LONGEST subject
70
+ if chosen is not None:
71
+ return chosen
72
+ return super()._parse_kind(text)
root/wander-train/oracle_release_runtime_am.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Runtime AM - record dialogue state from the FINAL result (additive over AL).
3
+
4
+ M46 passed and two defects survived it, both visible the moment turn-1 used a route other
5
+ than summarize:
6
+
7
+ "Is a gray whale a whale?" -> answered by wordnet_kind_direct
8
+ replay: "came via the DOCUMENT_RUNTIME route ... typed refusal"
9
+ "What causes rain?" -> answered by capability:what_causes
10
+ replay: "Nothing has been answered in this session yet"
11
+
12
+ Cause: dialogue state is written inside runtime E's chat, but answers are produced at many
13
+ layers of the chain. Routes resolved ABOVE E never write state at all; routes resolved
14
+ after E (the whole AD..AL tier) leave E's record of the INNER result in place. The ACTS
15
+ layer therefore reads a state describing a different answer than the user received.
16
+
17
+ Fix: the top of the chain records what the user actually saw. State is written once, from
18
+ the final result, after every turn - overwriting whatever inner layers recorded. Acts are
19
+ conversions over the delivered answer, so the delivered answer is what must be stored.
20
+
21
+ M46 could not see this because every turn-1 probe in it was "Summarize X" - a single route,
22
+ which happens to be one that flows through E. The test distribution must cover the route
23
+ inventory, not one member of it.
24
+
25
+ No neural network; no LM call.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import re
31
+
32
+ from oracle_release_runtime_al import OracleReleaseRuntimeAL
33
+
34
+ CITE = re.compile(r"\[[a-z0-9_]+:?[a-z0-9_.\-]*\]")
35
+ ACT_PATHS = ("act:provenance_replay", "act:acknowledge", "act:continue_topic", "act:refine")
36
+
37
+
38
+ class OracleReleaseRuntimeAM(OracleReleaseRuntimeAL):
39
+ def chat(self, text: str, session_id: str = "default") -> dict:
40
+ result = super().chat(text, session_id)
41
+ path = str(result.get("path", ""))
42
+ # acts are conversions OVER the last answer; they must not overwrite it
43
+ if path in ACT_PATHS:
44
+ return result
45
+ try:
46
+ slot = self.dialogue.setdefault(session_id, {})
47
+ slot["last_result"] = {
48
+ "path": result.get("path"),
49
+ "provenance": result.get("provenance"),
50
+ "status": result.get("status"),
51
+ "citations": CITE.findall(str(result.get("response", ""))),
52
+ }
53
+ except Exception: # noqa: BLE001
54
+ pass
55
+ return result
root/wander-train/oracle_release_runtime_c.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Front-door successor C (additive over B): essay route + definitional route.
3
+
4
+ Essay route: "write an essay about X" -> the H2 gated document compiler over
5
+ the topic page plus up to two related pages (title matches carrying a
6
+ distinctive topic token, relevance-gated). Output is a structured document
7
+ whose thesis/conclusions/analyses/claims chain is verified by
8
+ recover_document before release; an unverifiable document is withheld.
9
+
10
+ Definitional route (closes the registered debt): "what is a/an X?" answers
11
+ with the gated definitional lead of X's page when the page exists, instead of
12
+ sense clarification."""
13
+
14
+ from __future__ import annotations
15
+
16
+ import re
17
+ import time
18
+
19
+ from oracle_release_runtime_b import OracleReleaseRuntimeB
20
+ from stage5m13e8_document_compiler_v1 import compile_section, compile_document, recover_document
21
+ from stage5m13e8_paragraph_compiler_v1 import _definitional_score
22
+ from stage5m13e8_relevance_gate_v1 import topic_tokens, classify_claim
23
+
24
+ ESSAY = re.compile(r"^(?:please )?(?:write|compose)(?: me)?(?: a| an)?(?: short| brief)?(?: \d+[- ]word)? essay (?:about|on) (?P<topic>.+?)\.?$", re.I)
25
+ DEFINITIONAL = re.compile(r"^what is (?:a |an |the )?(?P<term>[\w \-']{2,60})\?$", re.I)
26
+
27
+
28
+ class OracleReleaseRuntimeC(OracleReleaseRuntimeB):
29
+ def _bundle(self, topic: str, n: int = 6) -> list[dict]:
30
+ page = self.bench_query.page_id(topic)
31
+ if not page:
32
+ return []
33
+ return [{"claim_id": c, "page": topic, "sentence": s}
34
+ for c, s in self.bench_query.page_sentences(page)[:n]]
35
+
36
+ def _related_titles(self, topic: str, cap: int = 2) -> list[str]:
37
+ distinctive, _ = topic_tokens(topic)
38
+ if not distinctive:
39
+ return []
40
+ token = sorted(distinctive)[0]
41
+ # indexed prefix range on title_alias (no full scan over 6M articles)
42
+ rows = self.bench_query.connection.execute(
43
+ "SELECT a.title FROM title_alias t JOIN article a ON a.page_id = t.page_id "
44
+ "WHERE t.normalized_title >= ? AND t.normalized_title < ? LIMIT 12",
45
+ (token, token + "￿")).fetchall()
46
+ related = []
47
+ for (title,) in rows:
48
+ if title.lower() == topic.lower():
49
+ continue
50
+ if classify_claim(topic, title, title) in ("explanatory", "exact_topic"):
51
+ related.append(title)
52
+ if len(related) >= cap:
53
+ break
54
+ return related
55
+
56
+ def _essay_turn(self, text: str) -> dict | None:
57
+ match = ESSAY.match(text.strip())
58
+ if not match:
59
+ return None
60
+ topic = match.group("topic").strip()
61
+ topics = [topic] + self._related_titles(topic)
62
+ bundles = [(t, self._bundle(t), None) for t in topics]
63
+ bundles = [(t, b, None) for t, b, _ in bundles if b]
64
+ if not bundles:
65
+ return {"status": "withheld", "path": "gated_document",
66
+ "response": f"No provenance-admitted page for {topic} is available to ground an essay.",
67
+ "provenance": {"kind": "gated_document", "topic": topic}}
68
+ section = compile_section(f"The record on {topic}", bundles)
69
+ document = compile_document(f"A provenance-bounded account of {topic}", [section])
70
+ if not recover_document(document):
71
+ return {"status": "withheld", "path": "gated_document",
72
+ "response": "Document failed its own derivation-chain verification and is withheld.",
73
+ "provenance": {"kind": "gated_document", "topic": topic,
74
+ "failure": "recover_document"}}
75
+ return {"status": "answered", "path": "gated_document",
76
+ "response": document["text"],
77
+ "provenance": {"kind": "gated_document", "topics": topics,
78
+ "exclusions": [{"page": e["page"], "relevance": e["relevance"]}
79
+ for e in section["exclusions"]]}}
80
+
81
+ def _definitional_turn(self, text: str) -> dict | None:
82
+ match = DEFINITIONAL.match(text.strip())
83
+ if not match:
84
+ return None
85
+ term = match.group("term").strip()
86
+ bundle = self._bundle(term.title()) or self._bundle(term)
87
+ if not bundle:
88
+ return None # fall through to sense machinery
89
+ lead = max(bundle, key=lambda c: _definitional_score(term, c["sentence"]))
90
+ if _definitional_score(term, lead["sentence"]) < 6:
91
+ return None
92
+ return {"status": "answered", "path": "definitional_encyclopedic",
93
+ "response": f"{lead['sentence']} [{lead['claim_id']}]",
94
+ "provenance": {"kind": "dbpedia_page", "page": lead["page"],
95
+ "claim_id": lead["claim_id"]}}
96
+
97
+ def chat(self, text: str, session_id: str = "default") -> dict:
98
+ started = time.perf_counter()
99
+ normalized = " ".join(text.strip().split())
100
+ result = None
101
+ for route in (lambda: self._essay_turn(normalized),
102
+ lambda: self._definitional_turn(normalized)):
103
+ try:
104
+ result = route()
105
+ except Exception: # noqa: BLE001
106
+ result = None
107
+ if result is not None:
108
+ break
109
+ if result is None:
110
+ return super().chat(text, session_id)
111
+ result["latency_ms"] = round(1000.0 * (time.perf_counter() - started), 1)
112
+ result["zero_model_gate"] = {"lm_calls": 0, "transformer_calls": 0}
113
+ return result
root/wander-train/oracle_release_runtime_e.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Runtime E — conversation-act conversion layer, slice 1 (Ben's conversion
3
+ thesis: dialogue is a finite inventory of typed conversions, not an emergent
4
+ capability).
5
+
6
+ Acts this slice:
7
+ provenance_replay "why did you say that / how do you know / source?"
8
+ -> re-present the last answer's provenance (addresses,
9
+ rule+premises, or the typed reason for a withhold)
10
+ continue_topic "tell me more / what else / more about that"
11
+ -> next admitted claims from the last topic's page,
12
+ composed and verified like any paragraph
13
+ acknowledge "thanks / ok / great" -> typed acknowledgment, no fetch
14
+
15
+ Dialogue state is a per-session record {last_result, last_topic, last_offset}
16
+ — a dictionary, not a model. Every act is a conversion over existing
17
+ machinery; nothing generates."""
18
+
19
+ from __future__ import annotations
20
+
21
+ import re
22
+ import time
23
+
24
+ from oracle_release_runtime_d import OracleReleaseRuntimeD
25
+ from stage5m13e8_paragraph_compiler_v2 import compile_paragraph_v2, recover_program_v2
26
+
27
+ WHY = re.compile(r"^(?:why(?: did you say that| do you say that)?|how do you know(?: that)?|"
28
+ r"what(?:'s| is) your source(?: for that)?|source\??|prove it|citation\??)[.?!]?$", re.I)
29
+ MORE = re.compile(r"^(?:tell me more|more|what else|go on|continue|more about (?:that|it))[.?!]?$", re.I)
30
+ ACK = re.compile(r"^(?:thanks|thank you|ok|okay|got it|great|cool|nice|perfect)[.?!]?$", re.I)
31
+
32
+
33
+ class OracleReleaseRuntimeE(OracleReleaseRuntimeD):
34
+ def __init__(self, *args, **kwargs):
35
+ super().__init__(*args, **kwargs)
36
+ self.dialogue: dict[str, dict] = {}
37
+
38
+ # ── act conversions ──────────────────────────────────────────────────
39
+ def _act_provenance_replay(self, state: dict) -> dict:
40
+ last = state.get("last_result")
41
+ if not last:
42
+ return {"status": "withheld", "path": "act:provenance_replay",
43
+ "response": "Nothing has been answered in this session yet, so there is no provenance to replay."}
44
+ prov = last.get("provenance") or {}
45
+ kind = prov.get("kind", "unknown")
46
+ lines = [f"My previous answer came via the {last.get('path','?')} route."]
47
+ if kind == "dbpedia_page":
48
+ lines.append(f"It is the definitional lead of the page '{prov.get('page')}', claim {prov.get('claim_id')}.")
49
+ elif kind == "gated_paragraph":
50
+ lines.append(f"It composed {prov.get('public_claims',0)} page-addressed claims"
51
+ + (f" and {prov['user_claims']} claims from your own documents" if prov.get('user_claims') else "")
52
+ + "; every sentence carries its claim address in brackets.")
53
+ if prov.get("excluded"):
54
+ lines.append("Excluded as insufficiently relevant (ledgered, not hidden): "
55
+ + ", ".join(f"{e['page']} ({e['relevance']})" for e in prov["excluded"][:4]) + ".")
56
+ elif kind == "conceptnet_edges":
57
+ claims = prov.get("claims", [])
58
+ lines.append(f"It rests on {len(claims)} ConceptNet edges: "
59
+ + "; ".join(c.get("provenance", "?") for c in claims[:4]) + ".")
60
+ elif kind == "session_world":
61
+ lines.append(f"It was computed from the {prov.get('statements',0)} statements you asserted "
62
+ "in this session's typed world — logic over your own facts, not retrieval.")
63
+ else:
64
+ lines.append("It was a typed refusal: no admitted evidence licensed an answer, so none was given.")
65
+ lines.append("Nothing in that answer was generated; each part is either addressed, derived, or withheld.")
66
+ return {"status": "answered", "path": "act:provenance_replay", "response": " ".join(lines),
67
+ "provenance": {"kind": "dialogue_state_replay"}}
68
+
69
+ def _act_continue(self, state: dict, session_id: str) -> dict:
70
+ topic = state.get("last_topic")
71
+ if not topic:
72
+ return {"status": "withheld", "path": "act:continue_topic",
73
+ "response": "There is no active topic yet — ask about something first."}
74
+ offset = state.get("last_offset", 0) + 6
75
+ page = self.anchors.resolve(topic)
76
+ if not page:
77
+ return {"status": "withheld", "path": "act:continue_topic",
78
+ "response": f"No further admitted material for {topic}."}
79
+ title = self.anchors.page_to_title.get(page, topic)
80
+ rows = self.bench_query.page_sentences(page)[offset:offset + 4]
81
+ if not rows:
82
+ return {"status": "withheld", "path": "act:continue_topic",
83
+ "response": f"The admitted record on {title} is exhausted — "
84
+ f"every stored sentence has been shown."}
85
+ claims = [{"claim_id": c, "page": title, "sentence": s} for c, s in rows]
86
+ paragraph, program = compile_paragraph_v2(topic, claims, evidence=3, aggregation_term=None)
87
+ if paragraph is None or not recover_program_v2(paragraph, program):
88
+ return {"status": "withheld", "path": "act:continue_topic",
89
+ "response": "Continuation failed verification and is withheld."}
90
+ state["last_offset"] = offset
91
+ return {"status": "answered", "path": "act:continue_topic", "response": paragraph,
92
+ "provenance": {"kind": "gated_paragraph", "topic": topic, "offset": offset}}
93
+
94
+ # ── entry ────────────────────────────────────────────────────────────
95
+ def chat(self, text: str, session_id: str = "default") -> dict:
96
+ started = time.perf_counter()
97
+ normalized = " ".join(text.strip().split())
98
+ state = self.dialogue.setdefault(session_id, {})
99
+ result = None
100
+ if WHY.match(normalized):
101
+ result = self._act_provenance_replay(state)
102
+ elif MORE.match(normalized):
103
+ result = self._act_continue(state, session_id)
104
+ elif ACK.match(normalized):
105
+ result = {"status": "answered", "path": "act:acknowledge",
106
+ "response": "Noted. The session state is kept; ask on.",
107
+ "provenance": {"kind": "dialogue_state"}}
108
+ if result is not None:
109
+ result["latency_ms"] = round(1000.0 * (time.perf_counter() - started), 1)
110
+ result["zero_model_gate"] = {"lm_calls": 0, "transformer_calls": 0}
111
+ else:
112
+ result = super().chat(text, session_id)
113
+ # topic tracking for continuation acts
114
+ path = result.get("path", "")
115
+ prov = result.get("provenance") or {}
116
+ if path == "anchored_summary":
117
+ match = re.match(r"^(?:tell me about|who is|who was|what do you know about) (?:the )?(.+?)[.?!]?$",
118
+ normalized, re.I)
119
+ if match:
120
+ state["last_topic"] = match.group(1).strip()
121
+ state["last_offset"] = 0
122
+ elif path == "definitional_encyclopedic" and prov.get("page"):
123
+ state["last_topic"] = prov["page"]
124
+ state["last_offset"] = 0
125
+ self.dialogue[session_id]["last_result"] = {k: result.get(k) for k in ("path", "provenance", "status")}
126
+ return result
root/wander-train/oracle_release_runtime_h.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Runtime H — capability cycle 3: list-N-things (additive over G)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import re
7
+ import time
8
+
9
+ from oracle_release_runtime_g import OracleReleaseRuntimeG
10
+ from stage5m13e8_relevance_gate_v1 import gate_bundle
11
+
12
+ NUMERALS = {"one": 1, "two": 2, "three": 3, "four": 4, "five": 5, "six": 6,
13
+ "seven": 7, "eight": 8, "nine": 9, "ten": 10, "a": 3, "an": 3,
14
+ "some": 3, "few": 3, "several": 4, "couple": 2}
15
+ LISTN = re.compile(
16
+ r"^(?:please |can you )?(?:list|give me|tell me|name|share)"
17
+ r"(?: (?P<n>\d+|one|two|three|four|five|six|seven|eight|nine|ten|a few|some|several|a couple(?: of)?))?"
18
+ r" (?:facts?|things?|details?)(?: about| on| regarding)"
19
+ r"(?: the| my document| my)? (?P<t>.+?)[.?!]?$"
20
+ r"|^what are (?:some|a few) facts? about(?: the| my document| my)? (?P<t2>.+?)[.?!]?$",
21
+ re.I)
22
+
23
+
24
+ class OracleReleaseRuntimeH(OracleReleaseRuntimeG):
25
+ def _listn_turn(self, text: str, session_id: str) -> dict | None:
26
+ match = LISTN.match(text.strip())
27
+ if not match:
28
+ return None
29
+ groups = match.groupdict()
30
+ topic = (groups.get("t") or groups.get("t2") or "").strip()
31
+ if not topic:
32
+ return None
33
+ raw_n = (groups.get("n") or "some").lower().replace("a couple of", "couple").replace("a couple", "couple").replace("a few", "few")
34
+ n = int(raw_n) if raw_n.isdigit() else NUMERALS.get(raw_n, 3)
35
+ n = max(1, min(n, 10))
36
+ user_claims = self.user_corpus.bundle(session_id, topic)
37
+ admitted, _ = gate_bundle(topic, self._bundle(topic, n=10))
38
+ claims = (user_claims + admitted)[:n]
39
+ if not claims:
40
+ return {"status": "withheld", "path": "capability:list_n",
41
+ "response": f"No provenance-admitted facts about {topic} are available to list.",
42
+ "provenance": {"kind": "gated_list", "topic": topic}}
43
+ lines = [f"{i+1}. {c['sentence']} [{c['claim_id']}]" for i, c in enumerate(claims)]
44
+ shortfall = ""
45
+ if len(claims) < n:
46
+ shortfall = (f" Only {len(claims)} admitted fact"
47
+ f"{'s are' if len(claims) != 1 else ' is'} available for {topic}.")
48
+ return {"status": "answered", "path": "capability:list_n",
49
+ "response": " ".join(lines) + shortfall,
50
+ "provenance": {"kind": "gated_list", "topic": topic,
51
+ "requested": n, "delivered": len(claims),
52
+ "user_claims": len([c for c in claims if c["claim_id"].startswith("user:")])}}
53
+
54
+ def chat(self, text: str, session_id: str = "default") -> dict:
55
+ started = time.perf_counter()
56
+ normalized = " ".join(text.strip().split())
57
+ result = None
58
+ try:
59
+ result = self._listn_turn(normalized, session_id)
60
+ except Exception: # noqa: BLE001
61
+ result = None
62
+ if result is not None:
63
+ result["latency_ms"] = round(1000.0 * (time.perf_counter() - started), 1)
64
+ result["zero_model_gate"] = {"lm_calls": 0, "transformer_calls": 0}
65
+ state = self.dialogue.setdefault(session_id, {})
66
+ state["last_result"] = {k: result.get(k) for k in ("path", "provenance", "status")}
67
+ state["last_topic"] = result["provenance"].get("topic")
68
+ state["last_offset"] = 0
69
+ return result
70
+ return super().chat(text, session_id)
root/wander-train/oracle_release_runtime_k.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Runtime K — capability cycle 6: explain-why-different (additive over J)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import re
7
+ import time
8
+
9
+ from oracle_release_runtime_j import OracleReleaseRuntimeJ
10
+ from stage5m13e8_paragraph_compiler_v1 import _definitional_score
11
+
12
+ WHYDIFF = re.compile(
13
+ r"^(?:please )?(?:explain )?why are (?:the |a |an )?(?P<x>.+?) and (?:the |a |an |my document )?(?P<y>.+?) different[.?!]?$"
14
+ r"|^(?:please )?(?:explain )?why is (?:the |a |an )?(?P<x2>.+?) different from (?:the |a |an |my document )?(?P<y2>.+?)[.?!]?$"
15
+ r"|^(?:please )?(?:explain )?why (?:the |a |an )?(?P<x3>.+?) and (?:the |a |an |my document )?(?P<y3>.+?) are different[.?!]?$"
16
+ r"|^what makes (?:the |a |an )?(?P<x4>.+?) different from (?:the |a |an |my document )?(?P<y4>.+?)[.?!]?$",
17
+ re.I)
18
+ KIND = re.compile(r"\bis (?:a|an|the) ([\w -]{3,40}?)(?:[,.]| that| which| in| of| with)")
19
+
20
+
21
+ class OracleReleaseRuntimeK(OracleReleaseRuntimeJ):
22
+ def _whydiff_turn(self, text: str, session_id: str) -> dict | None:
23
+ match = WHYDIFF.match(text.strip())
24
+ if not match:
25
+ return None
26
+ groups = match.groupdict()
27
+ x = next((groups[k] for k in ("x", "x2", "x3", "x4") if groups.get(k)), "").strip()
28
+ y = next((groups[k] for k in ("y", "y2", "y3", "y4") if groups.get(k)), "").strip()
29
+ if not x or not y:
30
+ return None
31
+ side_x, _ = self._side(x, session_id)
32
+ side_y, _ = self._side(y, session_id)
33
+ missing = [t for t, s in ((x, side_x), (y, side_y)) if not s]
34
+ if missing:
35
+ return {"status": "withheld", "path": "capability:why_different",
36
+ "response": f"I cannot explain the difference: no provenance-admitted material "
37
+ f"exists for {' or '.join(missing)}.",
38
+ "provenance": {"kind": "gated_whydiff", "missing": missing}}
39
+ lead_x = max(side_x, key=lambda c: _definitional_score(x, c["sentence"]))
40
+ lead_y = max(side_y, key=lambda c: _definitional_score(y, c["sentence"]))
41
+ kind_x_match = KIND.search(lead_x["sentence"])
42
+ kind_y_match = KIND.search(lead_y["sentence"])
43
+ kind_x = kind_x_match.group(1).strip() if kind_x_match else None
44
+ kind_y = kind_y_match.group(1).strip() if kind_y_match else None
45
+ if kind_x and kind_y and kind_x.lower() != kind_y.lower():
46
+ reason = (f"Derived reason: they belong to different categories — {x} is attested as "
47
+ f"a {kind_x}, whereas {y} is attested as a {kind_y}.")
48
+ else:
49
+ reason = (f"Derived reason: the admitted records place them in "
50
+ f"{'the same attested category (' + kind_x + '); their difference lies in the particulars below' if kind_x else 'categories the leads do not name explicitly; the difference rests on the particulars below'}.")
51
+ response = (f"According to {lead_x['page']}, {lead_x['sentence']} [{lead_x['claim_id']}] "
52
+ f"According to {lead_y['page']}, {lead_y['sentence']} [{lead_y['claim_id']}] "
53
+ f"{reason} [derived:category_contrast<-{lead_x['claim_id'][:12]},{lead_y['claim_id'][:12]}]")
54
+ return {"status": "answered", "path": "capability:why_different", "response": response,
55
+ "provenance": {"kind": "gated_whydiff", "x": x, "y": y,
56
+ "derived": {"rule": "category_contrast",
57
+ "premises": [lead_x["claim_id"], lead_y["claim_id"]]}}}
58
+
59
+ def chat(self, text: str, session_id: str = "default") -> dict:
60
+ started = time.perf_counter()
61
+ normalized = " ".join(text.strip().split())
62
+ result = None
63
+ try:
64
+ result = self._whydiff_turn(normalized, session_id)
65
+ except Exception: # noqa: BLE001
66
+ result = None
67
+ if result is not None:
68
+ result["latency_ms"] = round(1000.0 * (time.perf_counter() - started), 1)
69
+ result["zero_model_gate"] = {"lm_calls": 0, "transformer_calls": 0}
70
+ state = self.dialogue.setdefault(session_id, {})
71
+ state["last_result"] = {k: result.get(k) for k in ("path", "provenance", "status")}
72
+ return result
73
+ return super().chat(text, session_id)
root/wander-train/oracle_release_runtime_n.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Runtime N — runtime M with the K8 batch-1 grammar wired into the session
3
+ kernel (additive; the IR pipeline and all capability routes now parse through
4
+ the ~190-construction inventory)."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from oracle_release_runtime_m import OracleReleaseRuntimeM
9
+ from stage5m13e2_construction_kernel_k8 import ConstructionKernelK8
10
+ from stage5m13e4_migration2 import IRPipeline2
11
+
12
+
13
+ class OracleReleaseRuntimeN(OracleReleaseRuntimeM):
14
+ def __init__(self, *args, **kwargs):
15
+ super().__init__(*args, **kwargs)
16
+ self.kernel = ConstructionKernelK8(compositional=True)
17
+ self.pipeline = IRPipeline2(self.kernel)
root/wander-train/oracle_release_runtime_p.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Runtime P — M17a shadow arbitration (additive over O).
3
+
4
+ Every chat turn is scored by the M-series release arbiter (frozen K138 bid
5
+ geometry + M-series families) and the ArbitrationRecord summary is attached to
6
+ the result as advisory metadata. Dispatch is UNCHANGED — the regex chain still
7
+ commits every action; the arbiter observes. Agreement per turn is recorded so
8
+ the live residue log accumulates the shadow scoreboard for the M17b promotion
9
+ gate. Advisory failure can never break a turn."""
10
+
11
+ from __future__ import annotations
12
+
13
+ from oracle_release_runtime_o import OracleReleaseRuntimeO
14
+ from stage5m17_release_arbiter_v1 import arbitrate, path_family
15
+
16
+
17
+ class OracleReleaseRuntimeP(OracleReleaseRuntimeO):
18
+ def chat(self, text: str, session_id: str = "default") -> dict:
19
+ result = super().chat(text, session_id)
20
+ try:
21
+ record = arbitrate(" ".join(text.strip().split()))
22
+ regex_family = path_family(result.get("path"))
23
+ result["arbitration"] = {
24
+ "winner": record.winner,
25
+ "runner_up": record.runner_up,
26
+ "margin": round(record.margin, 3),
27
+ "floor": record.clarification_floor_applied,
28
+ "regex_family": regex_family,
29
+ "agrees": record.winner == regex_family if regex_family else None,
30
+ "mode": "shadow",
31
+ }
32
+ except Exception: # noqa: BLE001 — advisory only, never break dispatch
33
+ pass
34
+ return result
root/wander-train/oracle_release_runtime_q.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Runtime Q — M17b: arbiter AUTHORITATIVE at the fallback boundary (additive over P).
3
+
4
+ Capture condition: the regex chain landed in the fallback tier
5
+ (document_runtime / session_relational) WITHOUT answering — i.e. it did not
6
+ understand the surface — AND the shadow arbiter bids a re-dispatchable family
7
+ with a clean margin (no clarification floor). Then, and only then, runtime Q
8
+ re-dispatches through the EXISTING route via a canonical surface:
9
+
10
+ capture point 1 (capabilities + definitional): arbiter family + anchor-store
11
+ topic extraction -> canonical surface ("Give me a rundown on the Moon."
12
+ -> summarize + Moon -> "Summarize Moon.") -> parent chain.
13
+ capture point 2 (conversation acts): fixed canonical act surfaces
14
+ ("Keep going." -> "Tell me more.").
15
+
16
+ Canonical-surface carriers mirror the K131 carrier stage deliberately;
17
+ typed-goal carrier removal is a later arc (K152 pattern). If re-dispatch fails
18
+ to answer, the ORIGINAL fallback result is returned unchanged — authority can
19
+ narrow to improvement-only, never degrade. Per the K147 law, no raw input is
20
+ ever placed into any response text by this layer (topics are anchor-store
21
+ titles, not user substrings). Each capture point has its own ablation flag."""
22
+
23
+ from __future__ import annotations
24
+
25
+ import re
26
+
27
+ from oracle_release_runtime_p import OracleReleaseRuntimeP
28
+ from stage5m17_release_arbiter_v1 import features as arb_features
29
+
30
+ FALLBACK_PATHS = {"document_runtime", "session_relational"}
31
+ FALLBACK_OK = {"answered", "recorded"}
32
+
33
+ # words never usable as topic candidates (marker vocabulary + function words)
34
+ TOPIC_BLOCK = {
35
+ "give", "me", "a", "an", "the", "of", "on", "about", "and", "or", "to",
36
+ "for", "in", "what", "whats", "what's", "how", "why", "is", "are", "do",
37
+ "does", "did", "you", "your", "i", "i'd", "id", "could", "would", "please",
38
+ "rundown", "overview", "summary", "summarize", "summarise", "recap",
39
+ "version", "short", "brief", "quick", "sum", "up", "like", "so",
40
+ "difference", "differences", "differ", "different", "between", "compare",
41
+ "contrast", "come", "timeline", "chronology", "history", "walk", "through",
42
+ "year", "by", "show", "list", "name", "few", "couple", "several", "some",
43
+ "things", "facts", "points", "items", "know", "tell", "talk", "mean",
44
+ "means", "actually", "explain", "break", "down", "plain", "language",
45
+ "simple", "simply", "simpler", "terms", "child", "follow", "it", "that",
46
+ "this", "one", "two", "three", "four", "five", "six", "seven",
47
+ "say", "said", "told", "know", "knew", "claim", "think", "thought",
48
+ "call", "called", "gist", "story", "handful", "nutshell",
49
+ }
50
+
51
+ ACT_CANONICAL = {
52
+ "act_refine": "Simpler.",
53
+ "act_continue": "Tell me more.",
54
+ "act_provenance": "Why did you say that?",
55
+ "act_acknowledge": "Thanks!",
56
+ }
57
+
58
+ NUMBER_WORDS = {"two": "two", "three": "three", "four": "four", "five": "five",
59
+ "six": "six", "seven": "seven", "2": "two", "3": "three",
60
+ "4": "four", "5": "five", "6": "six", "7": "seven"}
61
+
62
+
63
+ class OracleReleaseRuntimeQ(OracleReleaseRuntimeP):
64
+ def __init__(self, *args, **kwargs):
65
+ super().__init__(*args, **kwargs)
66
+ self.capture_capability_enabled = True # ablation flag, capture point 1
67
+ self.capture_act_enabled = True # ablation flag, capture point 2
68
+
69
+ # ---------------------------------------------------- topic extraction
70
+ def _arb_topics(self, text: str) -> list[str]:
71
+ """Anchor-store titles found in the surface, longest-span first,
72
+ returned in positional order, marker vocabulary excluded."""
73
+ words = [w.strip("'-") for w in re.findall(r"[A-Za-z][\w'-]*|\d+", text)]
74
+ words = [w for w in words if w]
75
+ titles = self.anchors.title_to_page
76
+ found: list[tuple[int, int, str]] = [] # (start, length, title)
77
+ taken: set[int] = set()
78
+ for n in (3, 2, 1):
79
+ for i in range(len(words) - n + 1):
80
+ if any(j in taken for j in range(i, i + n)):
81
+ continue
82
+ span = words[i:i + n]
83
+ if all(w.lower() in TOPIC_BLOCK for w in span):
84
+ continue
85
+ cand = " ".join(span)
86
+ if cand.lower() in titles: # anchor keys are case-normalized
87
+ found.append((i, n, cand))
88
+ taken.update(range(i, i + n))
89
+ return [t for _, _, t in sorted(found)]
90
+
91
+ def _canonical_surface(self, family: str, text: str) -> str | None:
92
+ if family in ACT_CANONICAL:
93
+ return ACT_CANONICAL[family]
94
+ topics = self._arb_topics(text)
95
+ if family in ("compare", "why_different"):
96
+ if len(topics) < 2:
97
+ return None
98
+ a, b = topics[0], topics[1]
99
+ return (f"Compare {a} and {b}." if family == "compare"
100
+ else f"Why are {a} and {b} different?")
101
+ if not topics:
102
+ return None
103
+ topic = topics[0]
104
+ if family == "summarize":
105
+ return f"Summarize {topic}."
106
+ if family == "timeline":
107
+ return f"Give me a timeline of {topic}."
108
+ if family == "explain_simply":
109
+ return f"Explain {topic} simply."
110
+ if family == "list_n":
111
+ tokens = [w.lower() for w in re.findall(r"[A-Za-z]+|\d+", text)]
112
+ count = next((NUMBER_WORDS[t] for t in tokens if t in NUMBER_WORDS), "three")
113
+ return f"List {count} facts about {topic}."
114
+ if family == "definitional":
115
+ # the definiendum is the LAST content NP ("what would you say a
116
+ # glacier is?"), not the first
117
+ return f"What is a {topics[-1].lower()}?"
118
+ return None
119
+
120
+ # ------------------------------------------------------------ dispatch
121
+ def chat(self, text: str, session_id: str = "default") -> dict:
122
+ result = super().chat(text, session_id) # P: regex dispatch + shadow record
123
+ arb = result.get("arbitration")
124
+ if (arb is None or arb.get("floor")
125
+ or result.get("path") not in FALLBACK_PATHS
126
+ or result.get("status") in FALLBACK_OK):
127
+ return result
128
+ family = arb.get("winner")
129
+ is_act = family in ACT_CANONICAL
130
+ if is_act and not self.capture_act_enabled:
131
+ return result
132
+ if not is_act and not self.capture_capability_enabled:
133
+ return result
134
+ canonical = self._canonical_surface(family, text)
135
+ if canonical is None:
136
+ return result
137
+ redispatch = super().chat(canonical, session_id)
138
+ # improvement-only authority: a family-correct honest withhold still
139
+ # beats the generic fallback clarification; anything else keeps the
140
+ # original result unchanged
141
+ if redispatch.get("status") not in ("answered", "withheld"):
142
+ return result
143
+ redispatch["path"] = f"arb:{family}->" + str(redispatch.get("path"))
144
+ redispatch["arbitration"] = dict(arb)
145
+ redispatch["arbitration"]["mode"] = "authoritative"
146
+ redispatch["arbitration"]["canonical_surface"] = canonical
147
+ redispatch["arbitration"]["capture_point"] = "act" if is_act else "capability"
148
+ return redispatch
root/wander-train/oracle_release_runtime_s.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Runtime S — M17b3: runtime R's capture logic driven by arbiter v3.
3
+ New file; P/Q/R untouched. Same fallback-boundary authority, improvement-only
4
+ rule, ablation flags, and canonical-surface carriers as R."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from oracle_release_runtime_o import OracleReleaseRuntimeO
9
+ from oracle_release_runtime_q import ACT_CANONICAL, FALLBACK_OK, FALLBACK_PATHS
10
+ from oracle_release_runtime_r import OracleReleaseRuntimeR
11
+ from stage5m17_release_arbiter_v3 import arbitrate as arbitrate_v3
12
+ from stage5m17_release_arbiter_v3 import path_family
13
+
14
+
15
+ class OracleReleaseRuntimeS(OracleReleaseRuntimeR):
16
+ def chat(self, text: str, session_id: str = "default") -> dict:
17
+ result = OracleReleaseRuntimeO.chat(self, text, session_id)
18
+ try:
19
+ record = arbitrate_v3(" ".join(text.strip().split()))
20
+ except Exception: # noqa: BLE001
21
+ return result
22
+ regex_family = path_family(result.get("path"))
23
+ arb = {"winner": record.winner, "runner_up": record.runner_up,
24
+ "margin": round(record.margin, 3),
25
+ "floor": record.clarification_floor_applied,
26
+ "regex_family": regex_family,
27
+ "agrees": record.winner == regex_family if regex_family else None,
28
+ "mode": "shadow", "arbiter": "v3"}
29
+ result["arbitration"] = arb
30
+ if (arb["floor"] or result.get("path") not in FALLBACK_PATHS
31
+ or result.get("status") in FALLBACK_OK):
32
+ return result
33
+ family = record.winner
34
+ is_act = family in ACT_CANONICAL
35
+ if is_act and not self.capture_act_enabled:
36
+ return result
37
+ if not is_act and not self.capture_capability_enabled:
38
+ return result
39
+ canonical = self._canonical_surface(family, text)
40
+ if canonical is None:
41
+ return result
42
+ redispatch = OracleReleaseRuntimeO.chat(self, canonical, session_id)
43
+ if redispatch.get("status") not in ("answered", "withheld"):
44
+ return result
45
+ redispatch["path"] = f"arb:{family}->" + str(redispatch.get("path"))
46
+ redispatch["arbitration"] = dict(arb)
47
+ redispatch["arbitration"]["mode"] = "authoritative"
48
+ redispatch["arbitration"]["canonical_surface"] = canonical
49
+ redispatch["arbitration"]["capture_point"] = "act" if is_act else "capability"
50
+ return redispatch
root/wander-train/oracle_release_runtime_u.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Runtime U — graceful-clarify terminal (additive over T).
3
+
4
+ The last routing step for conversational smoothness: when a turn falls all the
5
+ way through the chain into the fallback tier WITHOUT being answered (the
6
+ document tier's "partial" evidence roundups and Wikipedia-address asks were
7
+ the wrong register for conversation — Ben-reported class, 2026-08-19), the
8
+ turn terminates in a typed clarify act instead: a short capability statement
9
+ in the machine's own voice. Answered / recorded / withheld results pass
10
+ through untouched; the arbitration record is preserved on the clarify so the
11
+ residue scoreboard keeps its labels. Per the K147 law, no user text rides in
12
+ the clarify."""
13
+
14
+ from __future__ import annotations
15
+
16
+ import time
17
+
18
+ from oracle_release_runtime_t import OracleReleaseRuntimeT
19
+
20
+ CLARIFY_RESPONSE = (
21
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, "
22
+ "compare two things, list facts, give a timeline, explain something "
23
+ "simply, define a term, reason about why and what-if, and discuss any "
24
+ "document you teach me. A shorter, more direct phrasing usually lands."
25
+ )
26
+
27
+ PASS_THROUGH = {"answered", "recorded", "withheld"}
28
+ FALLBACK_TIER = {"document_runtime", "session_relational"}
29
+
30
+
31
+ class OracleReleaseRuntimeU(OracleReleaseRuntimeT):
32
+ def chat(self, text: str, session_id: str = "default") -> dict:
33
+ started = time.perf_counter()
34
+ result = super().chat(text, session_id)
35
+ if (result.get("path") in FALLBACK_TIER
36
+ and result.get("status") not in PASS_THROUGH):
37
+ return {"status": "clarification", "path": "act:clarify",
38
+ "response": CLARIFY_RESPONSE,
39
+ "provenance": {"kind": "dialogue_state",
40
+ "note": "graceful clarify terminal"},
41
+ "arbitration": result.get("arbitration"),
42
+ "latency_ms": round(1000.0 * (time.perf_counter() - started), 1),
43
+ "zero_model_gate": {"lm_calls": 0, "transformer_calls": 0}}
44
+ return result
root/wander-train/oracle_release_runtime_w58.py ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """oracle_release_runtime_w58 - JUSTIFICATION: why did you do that, answered from
3
+ the record rather than from a story about the record.
4
+
5
+ Found by the salience pass over the held-out residue, not by me guessing. Among
6
+ the turns the machine could not answer was "Why did you choose that language?",
7
+ falling to act:clarify. It had just written a python program, recorded that it
8
+ wrote a python program, recorded which fold wrote it and what the execution
9
+ printed, and then could not say why python.
10
+
11
+ That gap matters more than its size. The entire claim of this architecture is
12
+ that every answer carries a warrant which can be inspected. If the warrant can
13
+ only be inspected by reading a JSON file on a server, it is not a conversational
14
+ property, it is a filing system. A machine that can be asked "how did you get
15
+ that" and answers from its own witness record is doing the thing the design is
16
+ for.
17
+
18
+ So this fold answers three questions about the PREVIOUS turn, and only from what
19
+ the record actually holds:
20
+
21
+ why did you choose that the choice, and what determined it
22
+ how did you get that the warrant: derivation, computation, execution
23
+ what did you just do the operator and the construction it built
24
+
25
+ The discipline, which is the whole point: it reports the reason ONLY when the
26
+ record supports it. If you did not name a language, the record shows the request
27
+ with no language in it and the output in python, so "you did not name one and
28
+ python is my default" is a reading of the record. If you did name one, the reason
29
+ is that you asked. And when the record holds no reason at all, it says exactly
30
+ that, because the failure mode here is not silence, it is a machine that
31
+ confabulates a plausible motive for its own behaviour. That is the one thing this
32
+ fold must never do.
33
+
34
+ It also declines to speak when there is no previous turn, and hands back anything
35
+ that is not about the previous turn, so it cannot capture a conversation the way
36
+ the deduction fold did before w56.
37
+
38
+ Hand-authored 2026-08-28. Judged by the gate.
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import re
44
+
45
+ from oracle_release_runtime_w57 import OracleReleaseRuntimeW57
46
+
47
+ _WHY_CHOICE = re.compile(
48
+ r'\bwhy\s+(?:did|do)\s+you\s+(?:choose|pick|use|write|go with)\b'
49
+ r'|\bwhy\s+that\s+(?:language|one|approach|way)\b'
50
+ r'|\bwhat\s+made\s+you\s+(?:choose|pick|use)\b', re.I)
51
+ _HOW_GOT = re.compile(
52
+ r'\bhow\s+(?:did|do)\s+you\s+(?:get|know|work(?:ed)?\s+that\s+out|arrive|compute|'
53
+ r'calculate|check)\b|\bhow\s+was\s+that\s+(?:checked|verified)\b'
54
+ r'|\bshow\s+(?:me\s+)?(?:your|the)\s+(?:work|steps|working)\b', re.I)
55
+ _WHAT_DID = re.compile(
56
+ r'\bwhat\s+did\s+you\s+just\s+do\b|\bwhat\s+(?:route|operator)\s+was\s+that\b'
57
+ r'|\bhow\s+did\s+you\s+answer\s+that\b', re.I)
58
+
59
+ _LANGS = ('python', 'javascript', 'go', 'bash', 'sql', 'c', 'rust', 'java',
60
+ 'ruby', 'typescript')
61
+ _WARRANT_NAME = {
62
+ 'test_execution': 'I ran it on this machine and compared the output',
63
+ 'deduction': 'I derived it, and each step names the rule that licensed it',
64
+ 'algebra': 'I solved it and then checked the answer by substituting it back',
65
+ 'computation': 'I computed it and kept the trace',
66
+ 'citation': 'I cited the source it came from',
67
+ 'differential_execution': 'I computed the expected result a second way and '
68
+ 'only answered when the two agreed',
69
+ }
70
+
71
+
72
+ class OracleReleaseRuntimeW58(OracleReleaseRuntimeW57):
73
+ """Answers about the previous turn, grounded in that turn's witness record."""
74
+
75
+ def _prior_turn(self, session_id):
76
+ """The last recorded turn that was not itself one of these questions."""
77
+ hist = getattr(self, '_paths', None) or {}
78
+ q = hist.get(session_id)
79
+ if not q:
80
+ return None
81
+ for entry in reversed(list(q)):
82
+ if str(entry.get('operator', '')).startswith('justify:'):
83
+ continue
84
+ return entry
85
+ return None
86
+
87
+ @staticmethod
88
+ def _choice_reason(prior):
89
+ """Why this output, IF the record shows it. Otherwise None."""
90
+ conv = prior.get('conversion') or {}
91
+ made = str(conv.get('to', ''))
92
+ asked = str(conv.get('from', '') or prior.get('in_surface', '')).lower()
93
+ lang = next((l for l in _LANGS if made.lower().startswith(l + '/')
94
+ or ('/' + l) in made.lower()), None)
95
+ if not lang:
96
+ return None
97
+ named = next((l for l in _LANGS if re.search(r'\b%s\b' % l, asked)), None)
98
+ if named == lang:
99
+ return ('I used %s because you asked for %s. The record of that turn '
100
+ 'shows your request and the program it produced.' % (lang, named))
101
+ if named is None:
102
+ return ('I used %s because your request did not name a language, and '
103
+ '%s is what my code fold writes when none is named. That is a '
104
+ 'default, not a judgement that %s was best for the task.'
105
+ % (lang, lang, lang))
106
+ return ('I used %s although your request mentioned %s. My record shows the '
107
+ 'mismatch and does not record a reason for it, so I am telling you '
108
+ 'the mismatch rather than explaining it away.' % (lang, named))
109
+
110
+ def _justify(self, kind, prior):
111
+ conv = prior.get('conversion') or {}
112
+ vw = prior.get('verified_warrant') or {}
113
+ op = str(prior.get('operator', '') or 'an unrecorded route')
114
+ by = str(conv.get('produced_by', '') or 'an unrecorded fold')
115
+
116
+ if kind == 'why_choice':
117
+ reason = self._choice_reason(prior)
118
+ if reason:
119
+ return reason + ' [self:witness]'
120
+ return ('My record for that turn shows the route (%s) and what it built '
121
+ '(%s), but it does not record a reason for the choice you are '
122
+ 'asking about. I am not going to invent one. [self:witness]'
123
+ % (op, conv.get('to') or 'no construction recorded'))
124
+
125
+ if kind == 'how_got':
126
+ if not vw.get('kind'):
127
+ # Citation is a warrant too, and it lives in a different field. The
128
+ # first version of this fold read only verified_warrant and told a
129
+ # visitor that a properly sourced encyclopedic answer "should not
130
+ # have been presented as settled", which is a machine libelling its
131
+ # own good work. Check every warrant the record can carry.
132
+ cited = list((prior.get('relation') or {}).get('grounded_cited') or [])
133
+ if cited or prior.get('citations'):
134
+ return ('I did not compute that one, I quoted it. The record for '
135
+ 'that turn carries %d citation%s%s, so the warrant is the '
136
+ 'source rather than a derivation I can replay. '
137
+ '[self:witness]'
138
+ % (prior.get('citations') or len(cited),
139
+ '' if (prior.get('citations') or len(cited)) == 1 else 's',
140
+ ' covering %s' % ', '.join(str(c) for c in cited[:4])
141
+ if cited else ''))
142
+ return ('That answer came through %s, and my record for it holds no '
143
+ 'warrant of any kind, neither a citation nor a check I can '
144
+ 'replay. I should not have presented it as settled. '
145
+ '[self:witness]' % op)
146
+ head = _WARRANT_NAME.get(str(vw['kind']),
147
+ 'the warrant recorded is of kind %s' % vw['kind'])
148
+ steps = [str(s) for s in (vw.get('steps') or [])]
149
+ if steps:
150
+ return ('%s. The recorded steps were: %s. [self:witness]'
151
+ % (head, '; then '.join(steps[:6])))
152
+ return '%s, though the record kept no individual steps. [self:witness]' % head
153
+
154
+ return ('That turn went through %s, produced by %s. It built a %s, from "%s" '
155
+ 'to "%s"%s. [self:witness]'
156
+ % (op, by, conv.get('construction') or 'result',
157
+ str(conv.get('from', ''))[:60],
158
+ str(conv.get('to', ''))[:60],
159
+ ', warranted by %s' % vw['kind'] if vw.get('kind') else
160
+ ', with no warrant recorded'))
161
+
162
+ def chat(self, text: str, session_id: str = 'default') -> dict:
163
+ s = ' '.join(str(text).strip().split())
164
+ if _WHY_CHOICE.search(s):
165
+ kind = 'why_choice'
166
+ elif _HOW_GOT.search(s):
167
+ kind = 'how_got'
168
+ elif _WHAT_DID.search(s):
169
+ kind = 'what_did'
170
+ else:
171
+ return super().chat(s, session_id)
172
+
173
+ prior = self._prior_turn(session_id)
174
+ if prior is None:
175
+ return {'status': 'withheld', 'path': 'justify:no_prior_turn',
176
+ 'response': ('You are asking about a previous answer and this is '
177
+ 'the first turn I have recorded in this session, so '
178
+ 'there is nothing for me to read.'),
179
+ 'provenance': {'kind': 'justification', 'prior_turn': None},
180
+ 'latency_ms': 1}
181
+
182
+ body = self._justify(kind, prior)
183
+ from collections import deque
184
+ hist = getattr(self, '_paths', None)
185
+ if hist is None:
186
+ hist = self._paths = {}
187
+ q = hist.get(session_id)
188
+ if q is None:
189
+ q = hist[session_id] = deque(maxlen=64)
190
+ prev = q[-1].get('turn', 0) if len(q) else 0
191
+ q.append({'turn': prev + 1, 'in_surface': s[:120],
192
+ 'in_form': ['question_about_prior_turn'],
193
+ 'operator': 'justify:' + kind,
194
+ 'out_status': 'answered', 'in_terms': [], 'out_terms': [],
195
+ 'relation': {'grounded': [], 'grounded_cited': [],
196
+ 'introduced': [], 'unaccounted': []},
197
+ 'carried_from_prior': ['witness_record'],
198
+ 'continuity_lexical': True, 'continuity': True, 'citations': 1,
199
+ 'residual': None, 'cost_ms': 1.0,
200
+ 'conversion': {'construction': 'ACCOUNT_OF_PRIOR_TURN',
201
+ 'from': s[:60],
202
+ 'to': str(prior.get('operator', ''))[:60],
203
+ 'produced_by': 'justify_w58'},
204
+ 'verified_warrant': {'kind': 'self_citation',
205
+ 'steps': ['read the witness record of turn %s'
206
+ % prior.get('turn')]}})
207
+ return {'status': 'answered', 'path': 'justify:' + kind,
208
+ 'response': body,
209
+ 'provenance': {'kind': 'justification', 'read_turn': prior.get('turn'),
210
+ 'prior_operator': prior.get('operator'),
211
+ 'source': 'witness_record'},
212
+ 'latency_ms': 2}
root/wander-train/oracle_release_runtime_w59.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """oracle_release_runtime_w59 - THE OPERATOR HOST: one disciplined runtime, many
3
+ operators, none of them hand-written as folds.
4
+
5
+ Every capability so far arrived as its own module, and every defect found in those
6
+ modules today lived in the same shared boilerplate rather than in any of their
7
+ reasoning. A missing witness record made a turn invisible to the gate. A missing
8
+ relevance guard let the deduction fold swallow whole conversations. A self-report
9
+ read a file instead of reading itself. A justification fold checked one warrant
10
+ field and told a visitor that a properly sourced answer should not be trusted.
11
+ Four folds, four instances of the same class of mistake, because each one
12
+ reimplemented the parts that are not the interesting part.
13
+
14
+ So this module holds no capability of its own. It is a HOST. The operators are
15
+ declarative specs in oracle_operator_specs_v1, and the host supplies exactly once:
16
+
17
+ dispatch the first spec whose trigger matches and whose parse succeeds
18
+ verification fn computes the answer, ref recomputes it by a different method,
19
+ and NOTHING is reported unless the two agree
20
+ decline a named boundary, in the operator's own words
21
+ passthrough when the surface is not this operator's business, the chain
22
+ answers exactly as it would if the host were not installed
23
+ witness one recording path, so no operator can be invisible to the gate
24
+
25
+ Adding a capability is now adding a spec, and a spec cannot forget the discipline
26
+ because it never implements it.
27
+
28
+ The verification step is the reason to build it this way rather than as a library
29
+ of helpers. An operator that computed and reported would be a faster way to be
30
+ wrong. Here the answer has to survive being computed twice by different routes,
31
+ and when the two disagree the host says so and withholds, which is a state the
32
+ previous hand-written folds had no way to even be in.
33
+
34
+ Six operators arrive with this host: unit conversion, gcd and lcm, base
35
+ conversion, descriptive statistics, sequence continuation, roman numerals. The
36
+ count is not the point. The point is that the seventh costs a spec.
37
+
38
+ Hand-authored 2026-08-28. Judged by the gate.
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ from fractions import Fraction
44
+
45
+ from oracle_release_runtime_w58 import OracleReleaseRuntimeW58
46
+ from oracle_operator_specs_v1 import SPECS, NotOurs, OutOfFragment
47
+
48
+
49
+ def _same(a, b):
50
+ """Do two independently computed answers agree? Types must not paper over it."""
51
+ if a is None or b is None:
52
+ return False
53
+ if isinstance(a, str) or isinstance(b, str):
54
+ return str(a).strip().lower() == str(b).strip().lower()
55
+ try:
56
+ return Fraction(a) == Fraction(b)
57
+ except (TypeError, ValueError):
58
+ return str(a) == str(b)
59
+
60
+
61
+ class OracleReleaseRuntimeW59(OracleReleaseRuntimeW58):
62
+ """Runs declarative operators, and reports only what two methods agree on."""
63
+
64
+ def _witness_op(self, session_id, surface, spec, steps, status, warrant=None):
65
+ from collections import deque
66
+ hist = getattr(self, '_paths', None)
67
+ if hist is None:
68
+ hist = self._paths = {}
69
+ q = hist.get(session_id)
70
+ if q is None:
71
+ q = hist[session_id] = deque(maxlen=64)
72
+ prev = q[-1].get('turn', 0) if len(q) else 0
73
+ q.append({'turn': prev + 1, 'in_surface': str(surface)[:120],
74
+ 'in_form': [spec['family']],
75
+ 'operator': 'op:%s' % spec['name'],
76
+ 'out_status': status, 'in_terms': [], 'out_terms': [],
77
+ 'relation': {'grounded': [], 'grounded_cited': [],
78
+ 'introduced': [], 'unaccounted': []},
79
+ 'carried_from_prior': [], 'continuity_lexical': False,
80
+ 'continuity': True, 'citations': 0, 'residual': None,
81
+ 'cost_ms': 1.0,
82
+ 'conversion': {'construction': 'VERIFIED_COMPUTATION',
83
+ 'from': str(surface)[:80],
84
+ 'to': (steps[-1][:60] if steps else spec['name']),
85
+ 'produced_by': 'op_%s_w59' % spec['name']},
86
+ 'verified_warrant': warrant})
87
+
88
+ def chat(self, text: str, session_id: str = 'default') -> dict:
89
+ s = ' '.join(str(text).strip().split())
90
+
91
+ for spec in SPECS:
92
+ if not spec['trigger'].search(s):
93
+ continue
94
+ try:
95
+ args = spec['parse'](s)
96
+ except NotOurs:
97
+ continue # not this operator, keep looking
98
+ except OutOfFragment as why:
99
+ return self._decline(session_id, s, spec, why)
100
+
101
+ try:
102
+ value, steps = spec['fn'](args)
103
+ except NotOurs:
104
+ continue
105
+ except OutOfFragment as why:
106
+ return self._decline(session_id, s, spec, why)
107
+ except Exception: # noqa: BLE001 - a crash is not an answer
108
+ continue
109
+
110
+ try:
111
+ check = spec['ref'](args)
112
+ except Exception: # noqa: BLE001
113
+ check = None
114
+
115
+ if not _same(value, check):
116
+ # The interesting failure. Two methods, one answer each, and they
117
+ # do not match, so the honest report is the disagreement itself.
118
+ self._witness_op(session_id, s, spec, steps, 'withheld')
119
+ return {'status': 'withheld', 'path': 'op:%s:unverified' % spec['name'],
120
+ 'response': ('I worked that out two ways and got two '
121
+ 'different answers, %s and %s, so one of my '
122
+ 'methods is wrong and I do not yet know which. '
123
+ 'I am not going to pick the one I like.'
124
+ % (value, check)),
125
+ 'provenance': {'kind': 'computation', 'verified': False,
126
+ 'operator': spec['name'],
127
+ 'disagreement': [str(value), str(check)]},
128
+ 'latency_ms': 2}
129
+
130
+ body = spec['render'](args, value, steps)
131
+ trace = ' '.join('%s.' % st for st in steps[:6])
132
+ warrant = {'kind': spec['warrant'], 'steps': list(steps)}
133
+ self._witness_op(session_id, s, spec, steps, 'answered', warrant)
134
+ pending = getattr(self, '_pending_conversion', None)
135
+ if pending is None:
136
+ pending = self._pending_conversion = {}
137
+ pending[session_id] = {'construction': 'VERIFIED_COMPUTATION',
138
+ 'from': s[:80], 'to': str(value)[:60],
139
+ 'produced_by': 'op_%s_w59' % spec['name']}
140
+ return {'status': 'answered', 'path': 'op:%s' % spec['name'],
141
+ 'response': ('%s %s I checked it by a second method that '
142
+ 'agrees.' % (body, trace)).strip(),
143
+ 'provenance': {'kind': 'computation', 'verified': True,
144
+ 'operator': spec['name'],
145
+ 'family': spec['family'],
146
+ 'steps': list(steps),
147
+ 'independence': spec['independence'],
148
+ 'checked_by': 'independent reimplementation'},
149
+ 'latency_ms': 2}
150
+
151
+ return super().chat(s, session_id)
152
+
153
+ def _decline(self, session_id, surface, spec, why):
154
+ self._witness_op(session_id, surface, spec, [], 'withheld')
155
+ return {'status': 'withheld',
156
+ 'path': 'op:%s:out_of_fragment' % spec['name'],
157
+ 'response': ('%s. What I do handle is %s. I could give you a '
158
+ 'plausible looking answer, but I would have no way to '
159
+ 'check it, so I will not.'
160
+ % (str(why).rstrip('.'), spec['fragment'])),
161
+ 'provenance': {'kind': 'computation', 'verified': False,
162
+ 'operator': spec['name'], 'reason': str(why),
163
+ 'fragment': spec['fragment']},
164
+ 'latency_ms': 1}
root/wander-train/oracle_release_runtime_w60.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """oracle_release_runtime_w60 - CORRECTNESS RELEASE: a refusal is not a missing warrant.
3
+
4
+ The justification fold reads the previous turn's record and reports the warrant it
5
+ finds. Asked "how did you get that" after a turn where the machine DECLINED, it
6
+ found no warrant, because nothing had been asserted, and reported:
7
+
8
+ my record for it holds no warrant of any kind, neither a citation nor a check
9
+ I can replay. I should not have presented it as settled.
10
+
11
+ Every clause of that is false about a refusal. Nothing was presented. Nothing was
12
+ settled. The turn was the machine working correctly, and its own account of itself
13
+ called it a lapse.
14
+
15
+ This is the second time in one day that the justification fold has been wrong in
16
+ the same direction, and the direction is worth naming: it was written to catch a
17
+ machine overclaiming, so when it meets a turn that claimed nothing it reads the
18
+ absence of a warrant as the absence of grounds. The first case was citation, where
19
+ the warrant was real and in a different field. This case is refusal, where there
20
+ is correctly no warrant at all and the right report is why there is none.
21
+
22
+ A turn is now read by its STATUS first and its warrant second:
23
+
24
+ answered + warrant report the warrant, as before
25
+ answered + citations report the source, as w58 already did
26
+ withheld report that it declined, and say a refusal has no
27
+ working to show, because that is the honest account
28
+ answered + nothing the original complaint, which remains correct in the
29
+ one case it was written for
30
+
31
+ Hand-authored 2026-08-28. Judged by the gate.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ from oracle_release_runtime_w59 import OracleReleaseRuntimeW59
37
+
38
+ _REFUSAL_TAILS = ('does_not_follow', 'out_of_fragment', 'unverified',
39
+ 'no_primitive', 'unsupported')
40
+
41
+
42
+ class OracleReleaseRuntimeW60(OracleReleaseRuntimeW59):
43
+ """Justification that can tell an absent answer from an ungrounded one."""
44
+
45
+ @staticmethod
46
+ def _was_refusal(prior):
47
+ if str(prior.get('out_status', '')) == 'withheld':
48
+ return True
49
+ return str(prior.get('operator', '')).endswith(_REFUSAL_TAILS)
50
+
51
+ def _justify(self, kind, prior):
52
+ if kind in ('how_got', 'what_did') and self._was_refusal(prior):
53
+ op = str(prior.get('operator', '') or 'an unrecorded route')
54
+ if kind == 'how_got':
55
+ return ('I did not work that one out, I declined it. A refusal has '
56
+ 'no working to show, which is the point of it: I stopped '
57
+ 'because I had no way to check an answer, so there is no '
58
+ 'answer of mine to account for. My record marks that turn '
59
+ 'as withheld by %s. [self:witness]' % op)
60
+ return ('That turn was a refusal rather than an answer. %s took it, '
61
+ 'found it outside what it can check, and withheld rather than '
62
+ 'producing something neither of us could verify. '
63
+ '[self:witness]' % op)
64
+ return super()._justify(kind, prior)
root/wander-train/oracle_release_runtime_w71.py ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """oracle_release_runtime_w71 - OPEN CONVERSATION: resolving "it" by elimination.
3
+
4
+ The held-out residue is not a scatter of unrelated failures. Read it and almost all
5
+ of it is one thing:
6
+
7
+ Did you run it? Can you test it? How does it work?
8
+ Write the same thing in go. how old is it?
9
+ Now write one that counts vowels. what else?
10
+ The thing from before. Explain the thing with the water.
11
+
12
+ Every one of those is a surface that does not name its own subject. The machine had
13
+ just produced the subject, recorded it, and could still say "I didn't fully parse
14
+ that". It was holding the answer and could not see the question.
15
+
16
+ So this is the elimination move again, pointed at reference. The candidate referents
17
+ are read from the witness record, the surface and the machine's own limits ablate
18
+ them, and what survives is the reading:
19
+
20
+ one survivor resolve, say which referent and why, and re-dispatch
21
+ many survivors name them and ask which, rather than picking a favourite
22
+ zero survivors say what could not be resolved instead of guessing
23
+
24
+ Which gives open conversation the thing it has never had here: a WARRANT. Not for
25
+ the reply being good, which is a matter of taste and always will be, but for the
26
+ INTERPRETATION being licensed. "I read 'it' as the python program from two turns
27
+ ago, because that is the only thing in this conversation you can run" is checkable
28
+ by the person who said "it".
29
+
30
+ Three referent kinds, each read from a different part of the record because that is
31
+ where the machine actually keeps them: topics from in_terms, programs and results
32
+ from the conversion. A constraint set per surface form decides which kinds a
33
+ question can even accept, so "did you run it" cannot resolve to a topic and "how old
34
+ is it" cannot resolve to a program.
35
+
36
+ ONE GUARD WORTH NAMING: the conversion field is not always about the turn it sits
37
+ on. A summarize turn was found carrying a stale conversion from a fraction question
38
+ two turns earlier, because the pending-conversion slot is not cleared between turns.
39
+ Anything reading it blindly attributes the wrong artifact, so every read here checks
40
+ that the conversion's own `from` matches the surface of the turn it was found on.
41
+ That is a defect in a fold below this one; this works around it rather than
42
+ pretending it is not there.
43
+
44
+ Hand-authored 2026-08-28. Judged by the gate.
45
+ """
46
+
47
+ from __future__ import annotations
48
+
49
+ import re
50
+
51
+ from oracle_release_runtime_w70 import OracleReleaseRuntimeW70
52
+ from oracle_operator_parse_v1 import Reading, eliminate, Ambiguous
53
+ from oracle_operator_specs_v1 import NotOurs, OutOfFragment
54
+
55
+ # surfaces that do not name their subject
56
+ _PRONOUN = re.compile(r'\b(?:it|that|this|them|those|one)\b', re.I)
57
+ _BARE_CONT = re.compile(r'^(?:what else|anything else|go on|more|and\?|tell me more)'
58
+ r'\s*\??$', re.I)
59
+ _THE_THING = re.compile(r'\bthe (?:same )?thing\b|\bthe one\b', re.I)
60
+
61
+ # what each surface form is ASKING FOR, which is what rules referents out
62
+ _WANTS_PROGRAM = re.compile(
63
+ r'\b(?:did|can|could|will|would)\s+you\s+(?:run|test|execute|try)\b'
64
+ r'|\bthe same (?:thing|code|program)\b|\bin (?:go|python|javascript|bash|sql|c)\b'
65
+ r'|\bnow write one\b|\bdoes it (?:compile|run|work)\b', re.I)
66
+ _WANTS_TOPIC = re.compile(
67
+ r'\bhow (?:does|do) (?:it|they) work\b|\bhow old\b|\bwhere is it\b'
68
+ r'|\btell me more\b|\bwhat else\b|\bexplain (?:it|that|the thing)\b'
69
+ r'|\bwhy (?:is|does) it\b|\bwhat is it made of\b', re.I)
70
+ _LANG = re.compile(r'\bin (go|python|javascript|bash|sql|c|rust|java)\b', re.I)
71
+ _STOP_TERMS = {'summarize', 'explain', 'compare', 'list', 'tell', 'facts', 'about',
72
+ 'what', 'how', 'why', 'the', 'a', 'an'}
73
+
74
+
75
+ class OracleReleaseRuntimeW71(OracleReleaseRuntimeW70):
76
+ """Open conversation with a licensed reading of what "it" refers to."""
77
+
78
+ def _referents(self, session_id):
79
+ """Candidate referents, newest first, read from the witness record."""
80
+ hist = getattr(self, '_paths', None) or {}
81
+ q = hist.get(session_id)
82
+ out = []
83
+ if not q:
84
+ return out
85
+ for e in reversed(list(q)):
86
+ op = str(e.get('operator', ''))
87
+ if op.startswith(('justify:', 'resolve:')):
88
+ continue
89
+ surface = str(e.get('in_surface', ''))
90
+ conv = e.get('conversion') or {}
91
+ # the conversion slot leaks between turns; only trust it when it is
92
+ # demonstrably about THIS turn
93
+ own = str(conv.get('from', ''))[:60].strip() == surface[:60].strip()
94
+ if own and conv.get('construction') == 'VERIFIED_PROGRAM':
95
+ out.append(('program', str(conv.get('to', '')), surface))
96
+ elif own and conv.get('construction') == 'VERIFIED_COMPUTATION':
97
+ out.append(('result', str(conv.get('to', '')), surface))
98
+ terms = [t for t in (e.get('in_terms') or [])
99
+ if t not in _STOP_TERMS and len(t) > 2]
100
+ if terms:
101
+ out.append(('topic', terms[0], surface))
102
+ if len(out) >= 6:
103
+ break
104
+ return out
105
+
106
+ def _resolve(self, s, session_id):
107
+ """(rewritten_surface, referent, trace) or raises with a named reason."""
108
+ cands = self._referents(session_id)
109
+ if not cands:
110
+ raise NotOurs('nothing recorded yet to refer back to')
111
+
112
+ wants_program = bool(_WANTS_PROGRAM.search(s))
113
+ wants_topic = bool(_WANTS_TOPIC.search(s)) or bool(_BARE_CONT.match(s))
114
+ if not (wants_program or wants_topic):
115
+ raise NotOurs('no reference this fold knows how to type')
116
+
117
+ readings = [Reading({'kind': k, 'label': lab, 'surface': surf},
118
+ 'the %s from "%s"' % (k, surf[:38]))
119
+ for k, lab, surf in cands]
120
+ constraints = []
121
+ if wants_program and not wants_topic:
122
+ constraints.append(('you are asking about something runnable and that '
123
+ 'is not a program', lambda a: a['kind'] == 'program'))
124
+ elif wants_topic and not wants_program:
125
+ constraints.append(('you are asking about a subject and that is not one',
126
+ lambda a: a['kind'] == 'topic'))
127
+ else:
128
+ constraints.append(('it is neither a program nor a subject',
129
+ lambda a: a['kind'] in ('program', 'topic')))
130
+ # only the most recent of a kind can be "it"; older ones are ruled out by
131
+ # recency, which is a real constraint rather than a tiebreak
132
+ first_of_kind = {}
133
+ for i, r in enumerate(readings):
134
+ first_of_kind.setdefault(r.args['kind'], i)
135
+ constraints.append(('an earlier one of the same kind is nearer',
136
+ lambda a, _f=first_of_kind, _r=readings:
137
+ _r[_f[a['kind']]].args is a))
138
+
139
+ survivor = eliminate(readings, constraints,
140
+ nothing_here='nothing here to refer to')
141
+ ref = survivor.args
142
+ if ref['kind'] == 'program':
143
+ lang = _LANG.search(s)
144
+ if lang:
145
+ new = re.sub(r'\b(go|python|javascript|bash|sql|c|rust|java)\b',
146
+ lang.group(1), ref['surface'], count=1, flags=re.I)
147
+ return new, ref, survivor
148
+ return None, ref, survivor # a question ABOUT the program
149
+ topic = ref['label']
150
+ if _BARE_CONT.match(s) or re.search(r'\btell me more\b', s, re.I):
151
+ return 'Tell me about %s.' % topic, ref, survivor
152
+ new = _PRONOUN.sub(topic, s, count=1)
153
+ new = _THE_THING.sub(topic, new)
154
+ return new, ref, survivor
155
+
156
+ def _about_program(self, s, ref, session_id):
157
+ """Questions about a program are answered from the record, not re-run."""
158
+ hist = getattr(self, '_paths', None) or {}
159
+ q = hist.get(session_id) or []
160
+ warrant = None
161
+ for e in reversed(list(q)):
162
+ if str(e.get('in_surface', ''))[:60].strip() == ref['surface'][:60].strip():
163
+ warrant = e.get('verified_warrant') or {}
164
+ break
165
+ steps = (warrant or {}).get('steps') or []
166
+ if steps:
167
+ return ('Yes. I ran %s on this machine before answering, and the '
168
+ 'recorded output was %s. I read "it" as that program, because '
169
+ 'it is the only thing in this conversation you can run. '
170
+ '[self:witness]' % (ref['label'], '; '.join(steps[:3])))
171
+ return ('I read "it" as %s, the program from earlier in this conversation. '
172
+ 'My record for that turn holds no execution, so I will not tell you '
173
+ 'it ran. [self:witness]' % ref['label'])
174
+
175
+ def chat(self, text: str, session_id: str = 'default') -> dict:
176
+ s = ' '.join(str(text).strip().split())
177
+ if not (_PRONOUN.search(s) or _BARE_CONT.match(s) or _THE_THING.search(s)):
178
+ return super().chat(s, session_id)
179
+ # a surface that names a subject of its own is not this fold's business
180
+ if re.search(r'\b(?:volcano|moon|gravity|rain)\b', s, re.I) and \
181
+ not _BARE_CONT.match(s):
182
+ pass
183
+
184
+ try:
185
+ rewritten, ref, survivor = self._resolve(s, session_id)
186
+ except Ambiguous as why:
187
+ return {'status': 'withheld', 'path': 'resolve:ambiguous',
188
+ 'response': ('%s. Say which one and I will go on.' % why),
189
+ 'provenance': {'kind': 'reference', 'resolved': False},
190
+ 'latency_ms': 1}
191
+ except NotOurs:
192
+ return super().chat(s, session_id)
193
+ except OutOfFragment as why:
194
+ return super().chat(s, session_id) # not ours to refuse
195
+
196
+ ruled = [(b, k) for b, k in (getattr(survivor, 'eliminated', []) or [])][:2]
197
+ if rewritten is None:
198
+ body = self._about_program(s, ref, session_id)
199
+ result = {'status': 'answered', 'path': 'resolve:about_program',
200
+ 'response': body,
201
+ 'provenance': {'kind': 'reference', 'resolved': True,
202
+ 'referent': ref['label'],
203
+ 'referent_kind': ref['kind']},
204
+ 'latency_ms': 2}
205
+ else:
206
+ result = super().chat(rewritten, session_id)
207
+ note = ('I read that as "%s", taking %s to mean %s.'
208
+ % (rewritten, '"it"' if _PRONOUN.search(s) else 'the follow-up',
209
+ ref['label']))
210
+ if ruled:
211
+ note += ' I ruled out %s, because %s.' % (ruled[0][0], ruled[0][1])
212
+ result = dict(result)
213
+ result['response'] = '%s %s' % (note, str(result.get('response', '')))
214
+ result['path'] = 'resolve:%s->%s' % (ref['kind'], result.get('path'))
215
+ result['provenance'] = dict(result.get('provenance') or {})
216
+ result['provenance'].update({'resolved_reference': ref['label'],
217
+ 'rewritten_as': rewritten})
218
+
219
+ from collections import deque
220
+ hist = getattr(self, '_paths', None)
221
+ if hist is None:
222
+ hist = self._paths = {}
223
+ qq = hist.get(session_id)
224
+ if qq is None:
225
+ qq = hist[session_id] = deque(maxlen=64)
226
+ prev = qq[-1].get('turn', 0) if len(qq) else 0
227
+ qq.append({'turn': prev + 1, 'in_surface': s[:120],
228
+ 'in_form': ['anaphoric_followup'],
229
+ 'operator': 'resolve:' + ref['kind'],
230
+ 'out_status': str(result.get('status', 'answered')),
231
+ 'in_terms': [], 'out_terms': [],
232
+ 'relation': {'grounded': [], 'grounded_cited': [],
233
+ 'introduced': [], 'unaccounted': []},
234
+ 'carried_from_prior': [ref['kind']],
235
+ 'continuity_lexical': True, 'continuity': True, 'citations': 1,
236
+ 'residual': None, 'cost_ms': 1.0,
237
+ 'conversion': {'construction': 'RESOLVED_REFERENCE',
238
+ 'from': s[:60], 'to': ref['label'][:60],
239
+ 'produced_by': 'resolve_w71'},
240
+ 'verified_warrant': {'kind': 'self_citation',
241
+ 'steps': ['read "%s" as %s'
242
+ % (s[:40], ref['label'])]
243
+ + ['ruled out %s because %s' % (b, k)
244
+ for b, k in ruled]}})
245
+ return result
root/wander-train/oracle_sibling_evidential_v3.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Evidential plant V3 - verdicts read from the RECORD's structure, never from text.
3
+
4
+ M60b demonstrated V2's remaining defect on h035:3: "Is a teddy bear a bear?" was
5
+ dispatched to summarize (a Teddy bear page exists), the summary carries no polarity
6
+ shape, and the text-regex polarity check rendered "supported" beside content that
7
+ refutes the claim. The repair is this plant's own founding principle applied strictly:
8
+ the answer's evidential comes from what the record LICENSES, and the record's kind
9
+ verdict is TYPED -
10
+
11
+ SUPPORTED provenance.kind == "derived" with rule transitive_isa, or
12
+ kind == "commonsense_edge" with relation IsA, or
13
+ kind == "lexical_corpus" with relation hypernym / hypernym_of
14
+ REFUTED kind == "lexical_corpus" with relation hypernym_excluded
15
+ NO VERDICT any other route (gated_summary, document_runtime, ...) - the record
16
+ produced no kind verdict, so the plant says so: "The record does not
17
+ settle what you heard." A summary does not license a kind verdict.
18
+
19
+ Probe conversion (declarative->kind question) inherited from V2. No text regex remains
20
+ in the verdict path. No neural network; no LM call.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import re
26
+
27
+ from oracle_sibling_evidential_v2 import SiblingEvidentialV2, DECL_KIND, _art
28
+ from oracle_siblings_bed2_v1 import SiblingEvidential, HEARSAY, DIRECT, INFER
29
+
30
+
31
+ def kind_verdict(result: dict) -> str:
32
+ prov = result.get("provenance") or {}
33
+ kind = str(prov.get("kind", ""))
34
+ rel = str(prov.get("relation", ""))
35
+ rule = str(prov.get("rule", ""))
36
+ if kind == "derived" and rule == "transitive_isa":
37
+ return "supported"
38
+ if kind == "commonsense_edge" and rel == "IsA":
39
+ return "supported"
40
+ if kind == "lexical_corpus" and rel in ("hypernym", "hypernym_of"):
41
+ return "supported"
42
+ if kind == "lexical_corpus" and rel == "hypernym_excluded":
43
+ return "refuted"
44
+ return "no_verdict"
45
+
46
+
47
+ class SiblingEvidentialV3(SiblingEvidentialV2):
48
+ """V2's probe conversion + structural verdicts."""
49
+
50
+ def chat(self, text: str, session_id: str = "default") -> dict:
51
+ s = " ".join(str(text).strip().split())
52
+ marked, proposition = None, s
53
+ for name, pat in (("hearsay", HEARSAY), ("direct", DIRECT), ("inferred", INFER)):
54
+ m = pat.match(s)
55
+ if m:
56
+ marked, proposition = name, m.group("p").strip()
57
+ break
58
+ if marked:
59
+ if re.match(r"^(is|are|what|who|when|how)\b", proposition, re.I):
60
+ probe = proposition
61
+ else:
62
+ dk = DECL_KIND.match(proposition)
63
+ if dk:
64
+ x, y = dk.group("x").strip(), dk.group("y").strip()
65
+ probe = f"Is {_art(x)} {x} {_art(y)} {y}?"
66
+ else:
67
+ probe = f"Summarize {proposition}."
68
+ self._pending_conversion[session_id] = {
69
+ "construction": f"EVIDENTIAL:{marked}", "from": s[:80], "to": probe[:80],
70
+ "produced_by": "sibling:evidential_v3"}
71
+ result = super(SiblingEvidential, self).chat(probe, session_id)
72
+ else:
73
+ result = super(SiblingEvidential, self).chat(s, session_id)
74
+ ev = self._evidential_of(result)
75
+ result["evidential"] = {"input_marked": marked, "answer_licensed": ev}
76
+ if result.get("status") == "answered":
77
+ tag = {"direct": "[ev:direct]", "inferred": "[ev:inferred]",
78
+ "reported": "[ev:reported]", "unknown": "[ev:unknown]"}[ev]
79
+ answer = str(result.get("response", ""))
80
+ if marked == "hearsay":
81
+ verdict = kind_verdict(result)
82
+ lead = {"supported": "What you heard is supported. ",
83
+ "refuted": "What you heard is not supported by the record. ",
84
+ "no_verdict": "The record does not settle what you heard. "}[verdict]
85
+ result["evidential"]["verdict"] = verdict
86
+ result["response"] = f"{tag} {lead}" + answer
87
+ else:
88
+ result["response"] = f"{tag} " + answer
89
+ hist = getattr(self, "_paths", {}).get(session_id)
90
+ if hist:
91
+ hist[-1]["evidential"] = result["evidential"]
92
+ return result
root/wander-train/oracle_tritree_v1.py ADDED
@@ -0,0 +1,473 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """oracle_tritree_v1 - the THREE-TREE REASONING RUNTIME (drawing OR7-A-101 made computational).
2
+
3
+ Three graphs of the canonical Tree-of-Life shape (10 Sephirot, 22 paths) share one middle pillar:
4
+ QUESTION tree - the slots a question opens KNOWLEDGE tree - where facts live OPERATOR tree - the moves
5
+ keter request - VERIFY (goal test / next cycle)
6
+ chokmah subject entities (title index) LINK
7
+ binah relation RELATION EDGES (typed store, Wikidata) RELATE
8
+ chesed context sentences (cited prose) EXPAND
9
+ geburah constraints types (P31/P279 classes) CHECK
10
+ tiferet answer slot (binding) - BIND
11
+ netzach alternatives (A or B) neighbours COMPARE
12
+ hod quantities engines (dates, numbers) COMPUTE
13
+ yesod evidence citations FRAME
14
+ malkuth surface - READ
15
+ A MOVE goes from the current operator node to an ADJACENT node along one of the graph's edges and applies that node's
16
+ operator to the state. Only edges of the graph are moves: the topology constrains the reasoning. The search is a bounded
17
+ breadth-first walk from Malkuth; a state is SOLVED when VERIFY (Keter) finds the answer slot bound, checked and cited.
18
+ Distinct solved answers -> abstain. A two-hop question runs a second CYCLE from Keter with the bound entity as subject.
19
+ The graph is a parameter (PATHS), so the topology can be ablated with everything else fixed.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import copy
24
+ import json
25
+ import os
26
+ import re
27
+ from collections import deque
28
+
29
+ import oracle_compare_op_v1 as CMP
30
+ import oracle_cube_reader_v1_4 as R
31
+ import oracle_relstore_v1 as RS
32
+
33
+ SEPH = ["keter", "chokmah", "binah", "chesed", "geburah", "tiferet", "netzach", "hod", "yesod", "malkuth"]
34
+ TREE_OF_LIFE = [("keter", "chokmah"), ("keter", "binah"), ("keter", "tiferet"), ("chokmah", "binah"), ("chokmah", "tiferet"),
35
+ ("chokmah", "chesed"), ("binah", "tiferet"), ("binah", "geburah"), ("chesed", "geburah"),
36
+ ("chesed", "tiferet"), ("chesed", "netzach"), ("geburah", "tiferet"), ("geburah", "hod"),
37
+ ("tiferet", "netzach"), ("tiferet", "yesod"), ("tiferet", "hod"), ("netzach", "hod"), ("netzach", "yesod"),
38
+ ("netzach", "malkuth"), ("hod", "yesod"), ("hod", "malkuth"), ("yesod", "malkuth")]
39
+ OP = {"malkuth": "READ", "yesod": "FRAME", "chokmah": "LINK", "binah": "RELATE", "chesed": "EXPAND", "geburah": "CHECK",
40
+ "tiferet": "BIND", "netzach": "COMPARE", "hod": "COMPUTE", "keter": "VERIFY"}
41
+ LEXICON = os.environ.get("ORACLE_LEXICON") or __import__("os").environ.get("ORACLE7_ROOT", "/opt/oracle-clm") + "/relstore/lexicon_v1.json"
42
+ PLACE_CLASSES = {"country", "sovereign state", "city", "town", "village", "municipality", "state", "province", "region",
43
+ "county", "island", "continent", "capital", "big city", "human settlement", "administrative territorial entity",
44
+ "commune of france", "state of the united states", "city of the united states", "metropolis"}
45
+ KIND_EQ = [{"show", "series", "programme", "program", "television series", "sitcom"}, {"film", "movie", "feature film"},
46
+ {"band", "group", "musical group", "rock band"}, {"company", "corporation", "firm", "business", "enterprise"},
47
+ {"album", "studio album"}, {"song", "single"}, {"magazine", "periodical"}, {"university", "college"},
48
+ {"country", "sovereign state", "state"}]
49
+
50
+
51
+ # the properties the relation store holds (build_relation_store_v1.PROPS)
52
+ STORE_PROPS = {31, 279, 17, 19, 20, 569, 570, 27, 131, 276, 36, 37, 38, 47, 50, 57, 58, 86, 161, 162, 175, 176, 178, 123,
53
+ 136, 127, 112, 159, 166, 26, 22, 25, 40, 3373, 69, 108, 106, 1303, 264, 463, 54, 118, 641, 101, 140, 84, 61,
54
+ 170, 676, 571, 577, 1082, 2046, 2048, 495, 364, 155, 156, 361, 527, 1376, 138, 800, 39, 749, 355, 1412, 103,
55
+ 710, 1344, 921, 30, 206, 1441, 674, 725, 449, 750, 272, 344, 1040, 1449, 488, 169, 35, 6, 150, 1830, 945, 113,
56
+ 114, 915, 840, 437, 400, 1027, 2031, 2032}
57
+ RULES = set((os.environ.get("TT_RULES") or "").split(","))
58
+ TRIVIAL_VERBS = {"be", "have", "do", "call", "name", "know"}
59
+
60
+
61
+ class State:
62
+ __slots__ = ("node", "slots", "cands", "answer", "evidence", "trace", "cycle", "checked", "depth")
63
+
64
+ def __init__(self):
65
+ self.node, self.slots, self.cands, self.answer = "malkuth", {}, [], None
66
+ self.evidence, self.trace, self.cycle, self.checked, self.depth = [], [], 0, False, 0
67
+
68
+ def fork(self):
69
+ s = State()
70
+ s.node, s.slots, s.cands, s.answer = self.node, dict(self.slots), list(self.cands), self.answer
71
+ s.evidence, s.trace, s.cycle, s.checked, s.depth = list(self.evidence), list(self.trace), self.cycle, self.checked, self.depth
72
+ return s
73
+
74
+
75
+ class TriTree:
76
+ def __init__(self, paths=None, reader=None, relstore=None, lexicon=None, max_states=4000, max_depth=12):
77
+ self.adj = {s: set() for s in SEPH}
78
+ for a, b in (paths or TREE_OF_LIFE):
79
+ self.adj[a].add(b)
80
+ self.adj[b].add(a)
81
+ self.r = reader or R.CubeReader()
82
+ self.rs = relstore or RS.RelStore()
83
+ self.cmp = CMP.CompareOp(self.r)
84
+ lx = json.load(open(lexicon or LEXICON))
85
+ self.lex = {tuple(json.loads(k)): {tuple(json.loads(p)): c for p, c in v.items()} for k, v in lx["lexicon"].items()}
86
+ self.cmp_lex = {ph: {tuple(json.loads(p)): c for p, c in v.items()} for ph, v in lx.get("compare", {}).items()}
87
+ self.max_states, self.max_depth = max_states, max_depth
88
+ self.expanded = 0
89
+ # property labels + aliases (Wikidata, English), for the RELATION check
90
+ self.plabel, self.palias = {}, {}
91
+ pl = os.path.join(os.path.dirname(LEXICON), "prop_labels.tsv")
92
+ if os.path.exists(pl):
93
+ for line in open(pl, encoding="utf-8", errors="replace"):
94
+ parts = line.rstrip("\n").split("\t")
95
+ if len(parts) == 2 and "/P" in parts[0]:
96
+ try:
97
+ p = int(parts[0].rsplit("/P", 1)[1].rstrip(">"))
98
+ except ValueError:
99
+ continue
100
+ if p not in STORE_PROPS:
101
+ continue # a property the store does not hold can never be followed
102
+ words = {R.stem(w) for w in re.findall(r"[a-z]+", parts[1].lower()) if len(w) >= 3 and w not in R.STOP}
103
+ self.plabel.setdefault(p, set()).update(words)
104
+ self.palias.setdefault(p, set()).add(RS.norm(parts[1].rsplit("@", 1)[0].strip('"')))
105
+
106
+ # ------------------------------------------------------------------ operators (one per Operator-tree node)
107
+ def READ(self, st): # malkuth: the surface opens the scramble
108
+ if "fr" in st.slots:
109
+ return []
110
+ q = st.slots["q"]
111
+ fr = self.r.frame(q)
112
+ pair = CMP._pair(q.strip().strip('"'))
113
+ if not fr and not (pair and pair[2] == "or"):
114
+ return []
115
+ s = st.fork()
116
+ s.slots["fr"] = fr
117
+ s.slots["pair"] = pair if pair and pair[2] == "or" else None
118
+ s.trace.append("READ")
119
+ return [s]
120
+
121
+ def FRAME(self, st): # yesod: obligations -> candidate RELATIONS (the lexicon)
122
+ if "fr" not in st.slots or "props" in st.slots:
123
+ return []
124
+ s = st.fork()
125
+ fr = s.slots["fr"]
126
+ props = {}
127
+ if fr and "rel" not in fr:
128
+ self.r.link(fr) # tokens -> the governing verb (pred) and relation words (rel)
129
+ if fr:
130
+ verb = (fr.get("pred") or [None])[0]
131
+ for key, w in ((("hv", fr["atype"], fr["head"], verb), 3), (("v", fr["atype"], verb), 2), (("h", fr["atype"], fr["head"]), 1)):
132
+ for (d, p), c in self.lex.get(key, {}).items():
133
+ props[(d, p)] = props.get((d, p), 0) + w * c
134
+ if s.slots.get("pair"):
135
+ m = re.search(r"\b(first|earlier|older|younger|later|more|larger|bigger|greater|higher|taller|longer|smaller|"
136
+ r"fewer|less|shorter)\b(?:\s+([a-z]+))?", s.slots["q"].lower())
137
+ phrase = " ".join(x for x in (m.groups() if m else ()) if x)
138
+ cands = self.cmp_lex.get(phrase) or self.cmp_lex.get(phrase.split(" ")[0] if phrase else "", {})
139
+ s.slots["cmp_props"] = sorted(cands.items(), key=lambda kv: -kv[1])[:2]
140
+ if fr:
141
+ # MULTI-HOP (the cycle): "the father of X", "the writer of X" - a relational noun that IS a property's name
142
+ m = re.search(r"\bthe\s+(?:[\w\-]+\s+){0,2}?([a-z]+)\s+of\s+(?:the\s+)?([A-Z][^?,]{1,60}?)(?:\s+(?:die|died|born|"
143
+ r"built|founded|released|published|located|made|start|end)\b|\?|,|$)", s.slots["q"])
144
+ noun = None
145
+ rest = ({R.stem(w) for w in fr.get("rel", [])} | {R.stem(v) for v in (fr.get("pred") or []) if v not in TRIVIAL_VERBS})
146
+ if m:
147
+ rest -= {R.stem(m.group(1)), R.stem(fr["head"] or "")}
148
+ if m and rest and "hop2" not in s.slots: # a relation REMAINS after the hop ("the father of X ... die")
149
+ hop1 = sorted(p for p, al in self.palias.items() if m.group(1) in al)
150
+ if hop1:
151
+ noun = m.group(1)
152
+ s.slots["hop1"] = {"props": hop1[:2], "anchor": m.group(2).strip(), "noun": noun}
153
+ # the RELATION NODE's own vocabulary: properties whose label/aliases carry the question's RELATION words (not
154
+ # its head noun, not the hop-1 noun) - "share a land border" -> P47 "shares border with"
155
+ relw = {R.stem(w) for w in fr.get("rel", []) if len(w) >= 3} | {R.stem(v) for v in (fr.get("pred") or []) if v}
156
+ relw -= {R.stem(fr["head"] or ""), R.stem(noun or "")}
157
+ for p, words in self.plabel.items():
158
+ ov = len(words & relw)
159
+ if ov:
160
+ for d in ("fwd", "inv"):
161
+ props[(d, p)] = props.get((d, p), 0) + 5 * ov
162
+ # a RECALLED FORM (forms_store_v1) proposes the property path a verified derivation of the same shape used;
163
+ # absent a form, FRAME is exactly as frozen (ad1530e6)
164
+ for dp, w in (getattr(self, "_form_props", None) or {}).items():
165
+ props[dp] = props.get(dp, 0) + 1000 * w
166
+ s.slots["props"] = sorted(props.items(), key=lambda kv: -kv[1])[:4]
167
+ s.trace.append("FRAME")
168
+ return [s]
169
+
170
+ def LINK(self, st): # chokmah: subjects -> entities of the relation store
171
+ if "props" not in st.slots or "subj" in st.slots:
172
+ return []
173
+ s = st.fork()
174
+ subj = []
175
+ if s.slots.get("pair"):
176
+ for side in s.slots["pair"][:2]:
177
+ t, _ = self.cmp._entity(side)
178
+ q = self.rs.entity(t) if t else None
179
+ if q:
180
+ subj.append((t, q))
181
+ if len(subj) != 2:
182
+ return []
183
+ else:
184
+ fr = s.slots["fr"]
185
+ if "rel" not in fr:
186
+ self.r.link(fr)
187
+ for sj in fr["subjects"][:3]:
188
+ t = self.r.db.execute("SELECT title FROM article WHERE page_id=?", (sj["pid"],)).fetchone()
189
+ q = self.rs.entity(t[0]) if t else None
190
+ if q:
191
+ subj.append((t[0], q))
192
+ if not subj:
193
+ return []
194
+ s.slots["subj"] = subj
195
+ s.trace.append("LINK")
196
+ return [s]
197
+
198
+ def RELATE(self, st): # binah: typed relation edges, forward or inverse
199
+ if "subj" not in st.slots or st.cands or st.slots.get("pair"):
200
+ return []
201
+ out = []
202
+ hop1 = st.slots.get("hop1")
203
+ if hop1 and st.cycle == 0:
204
+ # cycle 1 of a two-hop question: anchor --hop1 property--> the described entity (exactly one)
205
+ aq = self.rs.entity(hop1["anchor"]) or next((q for t, q in st.slots["subj"] if RS.norm(t) == RS.norm(hop1["anchor"])), None)
206
+ if not aq:
207
+ return []
208
+ for p in hop1["props"]:
209
+ vals = [o for o, _ in self.rs.values(aq, p) if o]
210
+ if len(set(vals)) == 1:
211
+ s = st.fork()
212
+ s.slots["bridge"] = (vals[0], "wd:Q%d P%d wd:Q%d" % (aq, p, vals[0]), hop1["noun"])
213
+ s.trace.append("RELATE:hop1:P%d" % p)
214
+ out.append(s)
215
+ return out
216
+ for (d, p), w in st.slots["props"]:
217
+ for title, sq in st.slots["subj"][:2]:
218
+ if d == "fwd":
219
+ vals = [(o, lit, "wd:Q%d P%d %s" % (sq, p, ("wd:Q%d" % o) if o else json.dumps(lit))) for o, lit in self.rs.values(sq, p)]
220
+ else:
221
+ vals = [(s_, None, "wd:Q%d P%d wd:Q%d" % (s_, p, sq)) for s_ in self.rs.subjects(p, sq, 60)]
222
+ if not vals:
223
+ continue
224
+ s = st.fork()
225
+ s.cands = [{"o": o, "lit": lit, "cite": c, "p": p, "via": title} for o, lit, c in vals[:60]]
226
+ s.trace.append("RELATE:%s:P%d" % (d, p))
227
+ out.append(s)
228
+ return out
229
+
230
+ def CHECK(self, st): # geburah: types + constraints (independent of the proposer)
231
+ if not st.cands or st.checked:
232
+ return []
233
+ s = st.fork()
234
+ fr = s.slots["fr"]
235
+ keep = []
236
+ for c in s.cands:
237
+ ok = False
238
+ if fr["atype"] in ("YEAR", "DATE"):
239
+ ok = bool(c["lit"]) and re.match(r"^[+-]?\d{4}-", c["lit"] or "") is not None
240
+ elif fr["atype"] == "NUMBER":
241
+ ok = bool(c["lit"]) and re.match(r"^[+-]?\d", c["lit"] or "") is not None
242
+ elif c["o"]:
243
+ if fr["atype"] == "PERSON":
244
+ ok = self.rs.is_human(c["o"])
245
+ elif fr["atype"] == "PLACE":
246
+ ok = bool(self.rs.classes(c["o"], 2) & PLACE_CLASSES) if not fr["head"] else self._kind(c["o"], fr["head"])
247
+ elif fr["atype"] == "INSTANCE":
248
+ ok = self._kind(c["o"], fr["head"])
249
+ if ok:
250
+ keep.append(c)
251
+ # constraints: every year the question names must be consistent with the candidate where the store can say
252
+ if keep and fr.get("years"):
253
+ dated = [c for c in keep if c["lit"] and c["lit"][:5].lstrip("+")[:4] in fr["years"]]
254
+ keep = dated or keep
255
+ if not keep:
256
+ return []
257
+ # RELATION LICENCE (independent of the lexicon that proposed the property): the property's own Wikidata label or
258
+ # aliases share a word with the question's relation words / head / verb, or the lexicon supports this exact
259
+ # question shape at least 3 times. "share a land border" licenses P47 "shares border with", not P131 "located in".
260
+ p = keep[0]["p"]
261
+ qwords = {R.stem(w) for w in fr.get("rel", []) + [fr.get("head") or ""] + list(fr.get("pred") or []) if w}
262
+ qwords |= {R.stem(w) for w in re.findall(r"[a-z]+", s.slots["q"].lower()) if len(w) >= 4 and w not in R.STOP}
263
+ support = self.lex.get(("hv", fr["atype"], fr["head"], (fr.get("pred") or [None])[0]), {}).get(
264
+ ("fwd", p), 0) + self.lex.get(("hv", fr["atype"], fr["head"], (fr.get("pred") or [None])[0]), {}).get(("inv", p), 0)
265
+ form_licensed = any(dp[1] == p for dp in (getattr(self, "_form_props", None) or {}))
266
+ if not (self.plabel.get(p, set()) & qwords) and support < 3 and not form_licensed:
267
+ return []
268
+ # RULE B (verb licence for time properties): a date answers the question's own EVENT only when the property names
269
+ # the governing verb ("founded" -> P571 "founded"); a founding date of whatever entity was linked is not the event
270
+ if "B" in RULES and fr["atype"] in ("YEAR", "DATE") and fr.get("pred") and fr["pred"][0] not in TRIVIAL_VERBS:
271
+ if not ({R.stem(v) for v in fr["pred"]} & self.plabel.get(p, set())):
272
+ return []
273
+ # the answer is never the subject itself, nor a name the question already contains (whole or as its core word)
274
+ qn = RS.norm(s.slots["q"])
275
+ subj_q = {q for _, q in s.slots.get("subj", [])}
276
+ subj_w = {w for t, _ in s.slots.get("subj", []) for w in RS.norm(t).split() if len(w) >= 4}
277
+
278
+ def restates(c):
279
+ if not c["o"]:
280
+ return False
281
+ lab = RS.norm(self.rs.label(c["o"]) or "")
282
+ return c["o"] in subj_q or (lab and lab in qn) or (lab and set(lab.split()) & subj_w and s.cycle == 0)
283
+ keep = [c for c in keep if not restates(c)]
284
+ if not keep:
285
+ return []
286
+ s.cands, s.checked = keep, True
287
+ s.trace.append("CHECK:P%d" % p)
288
+ return [s]
289
+
290
+ def _kind(self, qid, head):
291
+ if not head:
292
+ return False
293
+ heads = {head} | next((g for g in KIND_EQ if head in g), set())
294
+ cls = self.rs.classes(qid, 2)
295
+ return any(h == c or c.endswith(" " + h) for h in heads for c in cls)
296
+
297
+ def BIND(self, st): # tiferet: exactly one checked value binds the answer slot
298
+ if not st.checked or st.answer:
299
+ return []
300
+ distinct = {}
301
+ for c in st.cands:
302
+ k = c["o"] or (c["lit"] or "")[:10]
303
+ distinct.setdefault(k, c)
304
+ if len(distinct) != 1:
305
+ return []
306
+ c = next(iter(distinct.values()))
307
+ s = st.fork()
308
+ ans = self.rs.label(c["o"]) if c["o"] else self._fmt(c["lit"], s.slots["fr"])
309
+ if not ans:
310
+ return []
311
+ s.answer = ans
312
+ s.evidence.append(c["cite"])
313
+ s.trace.append("BIND")
314
+ return [s]
315
+
316
+ def _fmt(self, lit, fr):
317
+ m = re.match(r"^[+-]?(\d{4})-(\d{2})-(\d{2})", lit or "")
318
+ if m:
319
+ y, mo, d = m.groups()
320
+ months = "January February March April May June July August September October November December".split()
321
+ if fr and fr["atype"] == "DATE" and mo != "00" and d != "00":
322
+ return "%d %s %s" % (int(d), months[int(mo) - 1], y)
323
+ return y
324
+ return (lit or "").lstrip("+")
325
+
326
+ def COMPARE(self, st): # netzach: A or B, by a typed date / number property
327
+ pair = st.slots.get("pair")
328
+ if not pair or "subj" not in st.slots or st.answer or not st.slots.get("cmp_props"):
329
+ return []
330
+ (ta, qa), (tb, qb) = st.slots["subj"][:2]
331
+ out = []
332
+ for (way, p), w in st.slots["cmp_props"]:
333
+ va, vb = self.rs.values(qa, p), self.rs.values(qb, p)
334
+ if not (va and vb and va[0][1] and vb[0][1]):
335
+ continue
336
+ a, b = va[0][1].lstrip("+"), vb[0][1].lstrip("+")
337
+ try:
338
+ fa, fb = (float(a), float(b)) if not re.match(r"^\d{4}-", a) else (a[:10], b[:10])
339
+ except ValueError:
340
+ continue
341
+ if fa == fb:
342
+ continue
343
+ pick_a = (fa < fb) == (way == "min")
344
+ s = st.fork()
345
+ s.answer = pair[0] if pick_a else pair[1]
346
+ s.evidence += ["wd:Q%d P%d %s" % (qa, p, json.dumps(a)), "wd:Q%d P%d %s" % (qb, p, json.dumps(b))]
347
+ s.checked = True
348
+ s.trace.append("COMPARE:P%d:%s" % (p, way))
349
+ out.append(s)
350
+ return out
351
+
352
+ def COMPUTE(self, st): # hod: engines (here: a date answer reduced to its year)
353
+ if not st.answer or st.slots.get("computed"):
354
+ return []
355
+ fr = st.slots.get("fr")
356
+ if fr and fr["atype"] == "YEAR" and re.match(r"^\d{1,2} \w+ \d{4}$", st.answer):
357
+ s = st.fork()
358
+ s.answer = st.answer[-4:]
359
+ s.slots["computed"] = True
360
+ s.trace.append("COMPUTE:year")
361
+ return [s]
362
+ return []
363
+
364
+ def EXPAND(self, st): # chesed: a cited sentence for the bound answer (evidence)
365
+ if not st.answer or st.slots.get("sentence"):
366
+ return []
367
+ s = st.fork()
368
+ for title, _ in s.slots.get("subj", [])[:2]:
369
+ pid = self.r.pid_of(title)
370
+ for cid, sent in (self.r.page(pid, 30) if pid else []):
371
+ if RS.norm(s.answer) and RS.norm(s.answer) in RS.norm(sent):
372
+ s.slots["sentence"] = (cid, sent)
373
+ s.evidence.append(cid)
374
+ s.trace.append("EXPAND")
375
+ return [s]
376
+ return []
377
+
378
+ def VERIFY(self, st): # keter: solved? or open the next CYCLE
379
+ if st.slots.get("bridge") and st.cycle == 0:
380
+ # the CYCLE: the entity cycle 1 bound becomes the subject of cycle 2; the question's own relation applies to it
381
+ e, cite, noun = st.slots["bridge"]
382
+ s = st.fork()
383
+ s.cycle = 1
384
+ s.slots["subj"] = [(self.rs.title(e) or self.rs.label(e) or "", e)]
385
+ s.slots.pop("bridge", None)
386
+ s.slots["hop2"] = True
387
+ s.cands, s.answer, s.checked = [], None, False
388
+ s.evidence.append(cite)
389
+ s.trace.append("VERIFY:cycle2(%s=%s)" % (noun, self.rs.label(e)))
390
+ return [s]
391
+ if "A" in RULES and st.answer and st.checked and "sentence" not in st.slots and not st.slots.get("pair"):
392
+ return [] # RULE A: a relation answer needs a corroborating sentence
393
+ if st.answer and st.checked and st.evidence:
394
+ s = st.fork()
395
+ s.slots["solved"] = True
396
+ s.trace.append("VERIFY")
397
+ return [s]
398
+ return []
399
+
400
+ # ------------------------------------------------------------------ the search (moves = graph edges only)
401
+ def answer(self, q, form_props=None):
402
+ self._form_props = form_props or None
403
+ try:
404
+ return self._answer(q)
405
+ finally:
406
+ self._form_props = None
407
+
408
+ def _answer(self, q):
409
+ q = " ".join(str(q).split())
410
+ if q.startswith('"') and q.endswith('"'):
411
+ q = q[1:-1].replace('""', '"')
412
+ start = State()
413
+ start.slots["q"] = q
414
+ frontier = deque(self.READ(start))
415
+ solved, seen = [], set()
416
+ self.expanded = 0
417
+ while frontier and self.expanded < self.max_states:
418
+ st = frontier.popleft()
419
+ self.expanded += 1
420
+ if st.slots.get("solved"):
421
+ solved.append(st)
422
+ continue
423
+ if st.depth >= self.max_depth:
424
+ continue
425
+ for nxt in self.adj[st.node]:
426
+ succ = getattr(self, OP[nxt])(st)
427
+ if not succ:
428
+ # TRANSIT: a move travels an EDGE; passing through a node whose operator does not apply costs a
429
+ # step and changes nothing. Path lengths therefore decide which derivations fit the depth bound.
430
+ succ = [st.fork()]
431
+ for s2 in succ:
432
+ s2.node, s2.depth = nxt, st.depth + 1
433
+ key = (nxt, s2.answer, len(s2.cands), s2.checked, tuple(s2.trace))
434
+ if key in seen:
435
+ continue
436
+ seen.add(key)
437
+ frontier.append(s2)
438
+ answers = {RS.norm(s.answer): s for s in solved}
439
+ if len(answers) != 1:
440
+ self.why = "no solved state" if not answers else "distinct solved answers: %s" % list(answers)[:3]
441
+ return None
442
+ s = next(iter(answers.values()))
443
+ return {"answer": s.answer, "evidence": s.evidence, "trace": s.trace, "expanded": self.expanded,
444
+ "sentence": (s.slots.get("sentence") or (None, ""))[1]}
445
+
446
+
447
+ def random_graph(seed, n_edges=22):
448
+ """a degree-sequence-matched random graph on the same ten nodes (for the topology ablation)"""
449
+ import random
450
+ rng = random.Random(seed)
451
+ deg = {s: 0 for s in SEPH}
452
+ for a, b in TREE_OF_LIFE:
453
+ deg[a] += 1
454
+ deg[b] += 1
455
+ for _ in range(2000):
456
+ stubs = [s for s in SEPH for _ in range(deg[s])]
457
+ rng.shuffle(stubs)
458
+ edges = set()
459
+ ok = True
460
+ for i in range(0, len(stubs), 2):
461
+ a, b = stubs[i], stubs[i + 1]
462
+ if a == b or (a, b) in edges or (b, a) in edges:
463
+ ok = False
464
+ break
465
+ edges.add((a, b))
466
+ if ok and len(edges) == n_edges:
467
+ return sorted(edges)
468
+ return TREE_OF_LIFE
469
+
470
+
471
+ COMPLETE = [(a, b) for i, a in enumerate(SEPH) for b in SEPH[i + 1:]]
472
+ CHAIN = [("malkuth", "yesod"), ("yesod", "chokmah"), ("chokmah", "binah"), ("binah", "geburah"), ("geburah", "tiferet"),
473
+ ("tiferet", "hod"), ("hod", "chesed"), ("chesed", "netzach"), ("netzach", "keter")]
root/wander-train/oracle_tritree_v2.py ADDED
@@ -0,0 +1,479 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """oracle_tritree_v2 - v1 (immutable, imported by w480) + answer(q, form_props, only=True): search only the named
2
+ relation(s).
3
+ oracle_tritree_v1 - the THREE-TREE REASONING RUNTIME (drawing OR7-A-101 made computational).
4
+
5
+ Three graphs of the canonical Tree-of-Life shape (10 Sephirot, 22 paths) share one middle pillar:
6
+ QUESTION tree - the slots a question opens KNOWLEDGE tree - where facts live OPERATOR tree - the moves
7
+ keter request - VERIFY (goal test / next cycle)
8
+ chokmah subject entities (title index) LINK
9
+ binah relation RELATION EDGES (typed store, Wikidata) RELATE
10
+ chesed context sentences (cited prose) EXPAND
11
+ geburah constraints types (P31/P279 classes) CHECK
12
+ tiferet answer slot (binding) - BIND
13
+ netzach alternatives (A or B) neighbours COMPARE
14
+ hod quantities engines (dates, numbers) COMPUTE
15
+ yesod evidence citations FRAME
16
+ malkuth surface - READ
17
+ A MOVE goes from the current operator node to an ADJACENT node along one of the graph's edges and applies that node's
18
+ operator to the state. Only edges of the graph are moves: the topology constrains the reasoning. The search is a bounded
19
+ breadth-first walk from Malkuth; a state is SOLVED when VERIFY (Keter) finds the answer slot bound, checked and cited.
20
+ Distinct solved answers -> abstain. A two-hop question runs a second CYCLE from Keter with the bound entity as subject.
21
+ The graph is a parameter (PATHS), so the topology can be ablated with everything else fixed.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import copy
26
+ import json
27
+ import os
28
+ import re
29
+ from collections import deque
30
+
31
+ import oracle_compare_op_v1 as CMP
32
+ import oracle_cube_reader_v1_4 as R
33
+ import oracle_relstore_v1 as RS
34
+
35
+ SEPH = ["keter", "chokmah", "binah", "chesed", "geburah", "tiferet", "netzach", "hod", "yesod", "malkuth"]
36
+ TREE_OF_LIFE = [("keter", "chokmah"), ("keter", "binah"), ("keter", "tiferet"), ("chokmah", "binah"), ("chokmah", "tiferet"),
37
+ ("chokmah", "chesed"), ("binah", "tiferet"), ("binah", "geburah"), ("chesed", "geburah"),
38
+ ("chesed", "tiferet"), ("chesed", "netzach"), ("geburah", "tiferet"), ("geburah", "hod"),
39
+ ("tiferet", "netzach"), ("tiferet", "yesod"), ("tiferet", "hod"), ("netzach", "hod"), ("netzach", "yesod"),
40
+ ("netzach", "malkuth"), ("hod", "yesod"), ("hod", "malkuth"), ("yesod", "malkuth")]
41
+ OP = {"malkuth": "READ", "yesod": "FRAME", "chokmah": "LINK", "binah": "RELATE", "chesed": "EXPAND", "geburah": "CHECK",
42
+ "tiferet": "BIND", "netzach": "COMPARE", "hod": "COMPUTE", "keter": "VERIFY"}
43
+ LEXICON = os.environ.get("ORACLE_LEXICON") or __import__("os").environ.get("ORACLE7_ROOT", "/opt/oracle-clm") + "/relstore/lexicon_v1.json"
44
+ PLACE_CLASSES = {"country", "sovereign state", "city", "town", "village", "municipality", "state", "province", "region",
45
+ "county", "island", "continent", "capital", "big city", "human settlement", "administrative territorial entity",
46
+ "commune of france", "state of the united states", "city of the united states", "metropolis"}
47
+ KIND_EQ = [{"show", "series", "programme", "program", "television series", "sitcom"}, {"film", "movie", "feature film"},
48
+ {"band", "group", "musical group", "rock band"}, {"company", "corporation", "firm", "business", "enterprise"},
49
+ {"album", "studio album"}, {"song", "single"}, {"magazine", "periodical"}, {"university", "college"},
50
+ {"country", "sovereign state", "state"}]
51
+
52
+
53
+ # the properties the relation store holds (build_relation_store_v1.PROPS)
54
+ STORE_PROPS = {31, 279, 17, 19, 20, 569, 570, 27, 131, 276, 36, 37, 38, 47, 50, 57, 58, 86, 161, 162, 175, 176, 178, 123,
55
+ 136, 127, 112, 159, 166, 26, 22, 25, 40, 3373, 69, 108, 106, 1303, 264, 463, 54, 118, 641, 101, 140, 84, 61,
56
+ 170, 676, 571, 577, 1082, 2046, 2048, 495, 364, 155, 156, 361, 527, 1376, 138, 800, 39, 749, 355, 1412, 103,
57
+ 710, 1344, 921, 30, 206, 1441, 674, 725, 449, 750, 272, 344, 1040, 1449, 488, 169, 35, 6, 150, 1830, 945, 113,
58
+ 114, 915, 840, 437, 400, 1027, 2031, 2032}
59
+ RULES = set((os.environ.get("TT_RULES") or "").split(","))
60
+ TRIVIAL_VERBS = {"be", "have", "do", "call", "name", "know"}
61
+
62
+
63
+ class State:
64
+ __slots__ = ("node", "slots", "cands", "answer", "evidence", "trace", "cycle", "checked", "depth")
65
+
66
+ def __init__(self):
67
+ self.node, self.slots, self.cands, self.answer = "malkuth", {}, [], None
68
+ self.evidence, self.trace, self.cycle, self.checked, self.depth = [], [], 0, False, 0
69
+
70
+ def fork(self):
71
+ s = State()
72
+ s.node, s.slots, s.cands, s.answer = self.node, dict(self.slots), list(self.cands), self.answer
73
+ s.evidence, s.trace, s.cycle, s.checked, s.depth = list(self.evidence), list(self.trace), self.cycle, self.checked, self.depth
74
+ return s
75
+
76
+
77
+ class TriTree:
78
+ def __init__(self, paths=None, reader=None, relstore=None, lexicon=None, max_states=4000, max_depth=12):
79
+ self.adj = {s: set() for s in SEPH}
80
+ for a, b in (paths or TREE_OF_LIFE):
81
+ self.adj[a].add(b)
82
+ self.adj[b].add(a)
83
+ self.r = reader or R.CubeReader()
84
+ self.rs = relstore or RS.RelStore()
85
+ self.cmp = CMP.CompareOp(self.r)
86
+ lx = json.load(open(lexicon or LEXICON))
87
+ self.lex = {tuple(json.loads(k)): {tuple(json.loads(p)): c for p, c in v.items()} for k, v in lx["lexicon"].items()}
88
+ self.cmp_lex = {ph: {tuple(json.loads(p)): c for p, c in v.items()} for ph, v in lx.get("compare", {}).items()}
89
+ self.max_states, self.max_depth = max_states, max_depth
90
+ self.expanded = 0
91
+ # property labels + aliases (Wikidata, English), for the RELATION check
92
+ self.plabel, self.palias = {}, {}
93
+ pl = os.path.join(os.path.dirname(LEXICON), "prop_labels.tsv")
94
+ if os.path.exists(pl):
95
+ for line in open(pl, encoding="utf-8", errors="replace"):
96
+ parts = line.rstrip("\n").split("\t")
97
+ if len(parts) == 2 and "/P" in parts[0]:
98
+ try:
99
+ p = int(parts[0].rsplit("/P", 1)[1].rstrip(">"))
100
+ except ValueError:
101
+ continue
102
+ if p not in STORE_PROPS:
103
+ continue # a property the store does not hold can never be followed
104
+ words = {R.stem(w) for w in re.findall(r"[a-z]+", parts[1].lower()) if len(w) >= 3 and w not in R.STOP}
105
+ self.plabel.setdefault(p, set()).update(words)
106
+ self.palias.setdefault(p, set()).add(RS.norm(parts[1].rsplit("@", 1)[0].strip('"')))
107
+
108
+ # ------------------------------------------------------------------ operators (one per Operator-tree node)
109
+ def READ(self, st): # malkuth: the surface opens the scramble
110
+ if "fr" in st.slots:
111
+ return []
112
+ q = st.slots["q"]
113
+ fr = self.r.frame(q)
114
+ pair = CMP._pair(q.strip().strip('"'))
115
+ if not fr and not (pair and pair[2] == "or"):
116
+ return []
117
+ s = st.fork()
118
+ s.slots["fr"] = fr
119
+ s.slots["pair"] = pair if pair and pair[2] == "or" else None
120
+ s.trace.append("READ")
121
+ return [s]
122
+
123
+ def FRAME(self, st): # yesod: obligations -> candidate RELATIONS (the lexicon)
124
+ if "fr" not in st.slots or "props" in st.slots:
125
+ return []
126
+ s = st.fork()
127
+ fr = s.slots["fr"]
128
+ props = {}
129
+ if fr and "rel" not in fr:
130
+ self.r.link(fr) # tokens -> the governing verb (pred) and relation words (rel)
131
+ if fr:
132
+ verb = (fr.get("pred") or [None])[0]
133
+ for key, w in ((("hv", fr["atype"], fr["head"], verb), 3), (("v", fr["atype"], verb), 2), (("h", fr["atype"], fr["head"]), 1)):
134
+ for (d, p), c in self.lex.get(key, {}).items():
135
+ props[(d, p)] = props.get((d, p), 0) + w * c
136
+ if s.slots.get("pair"):
137
+ m = re.search(r"\b(first|earlier|older|younger|later|more|larger|bigger|greater|higher|taller|longer|smaller|"
138
+ r"fewer|less|shorter)\b(?:\s+([a-z]+))?", s.slots["q"].lower())
139
+ phrase = " ".join(x for x in (m.groups() if m else ()) if x)
140
+ cands = self.cmp_lex.get(phrase) or self.cmp_lex.get(phrase.split(" ")[0] if phrase else "", {})
141
+ s.slots["cmp_props"] = sorted(cands.items(), key=lambda kv: -kv[1])[:2]
142
+ if fr:
143
+ # MULTI-HOP (the cycle): "the father of X", "the writer of X" - a relational noun that IS a property's name
144
+ m = re.search(r"\bthe\s+(?:[\w\-]+\s+){0,2}?([a-z]+)\s+of\s+(?:the\s+)?([A-Z][^?,]{1,60}?)(?:\s+(?:die|died|born|"
145
+ r"built|founded|released|published|located|made|start|end)\b|\?|,|$)", s.slots["q"])
146
+ noun = None
147
+ rest = ({R.stem(w) for w in fr.get("rel", [])} | {R.stem(v) for v in (fr.get("pred") or []) if v not in TRIVIAL_VERBS})
148
+ if m:
149
+ rest -= {R.stem(m.group(1)), R.stem(fr["head"] or "")}
150
+ if m and rest and "hop2" not in s.slots: # a relation REMAINS after the hop ("the father of X ... die")
151
+ hop1 = sorted(p for p, al in self.palias.items() if m.group(1) in al)
152
+ if hop1:
153
+ noun = m.group(1)
154
+ s.slots["hop1"] = {"props": hop1[:2], "anchor": m.group(2).strip(), "noun": noun}
155
+ # the RELATION NODE's own vocabulary: properties whose label/aliases carry the question's RELATION words (not
156
+ # its head noun, not the hop-1 noun) - "share a land border" -> P47 "shares border with"
157
+ relw = {R.stem(w) for w in fr.get("rel", []) if len(w) >= 3} | {R.stem(v) for v in (fr.get("pred") or []) if v}
158
+ relw -= {R.stem(fr["head"] or ""), R.stem(noun or "")}
159
+ for p, words in self.plabel.items():
160
+ ov = len(words & relw)
161
+ if ov:
162
+ for d in ("fwd", "inv"):
163
+ props[(d, p)] = props.get((d, p), 0) + 5 * ov
164
+ # a RECALLED FORM (forms_store_v1) proposes the property path a verified derivation of the same shape used;
165
+ # absent a form, FRAME is exactly as frozen (ad1530e6)
166
+ for dp, w in (getattr(self, "_form_props", None) or {}).items():
167
+ props[dp] = props.get(dp, 0) + 1000 * w
168
+ if getattr(self, "_only", False) and getattr(self, "_form_props", None):
169
+ props = {dp: 1000 * w for dp, w in self._form_props.items()} # v2: ONLY the named relation(s)
170
+ s.slots["props"] = sorted(props.items(), key=lambda kv: -kv[1])[:4]
171
+ s.trace.append("FRAME")
172
+ return [s]
173
+
174
+ def LINK(self, st): # chokmah: subjects -> entities of the relation store
175
+ if "props" not in st.slots or "subj" in st.slots:
176
+ return []
177
+ s = st.fork()
178
+ subj = []
179
+ if s.slots.get("pair"):
180
+ for side in s.slots["pair"][:2]:
181
+ t, _ = self.cmp._entity(side)
182
+ q = self.rs.entity(t) if t else None
183
+ if q:
184
+ subj.append((t, q))
185
+ if len(subj) != 2:
186
+ return []
187
+ else:
188
+ fr = s.slots["fr"]
189
+ if "rel" not in fr:
190
+ self.r.link(fr)
191
+ for sj in fr["subjects"][:3]:
192
+ t = self.r.db.execute("SELECT title FROM article WHERE page_id=?", (sj["pid"],)).fetchone()
193
+ q = self.rs.entity(t[0]) if t else None
194
+ if q:
195
+ subj.append((t[0], q))
196
+ if not subj:
197
+ return []
198
+ s.slots["subj"] = subj
199
+ s.trace.append("LINK")
200
+ return [s]
201
+
202
+ def RELATE(self, st): # binah: typed relation edges, forward or inverse
203
+ if "subj" not in st.slots or st.cands or st.slots.get("pair"):
204
+ return []
205
+ out = []
206
+ hop1 = st.slots.get("hop1")
207
+ if hop1 and st.cycle == 0:
208
+ # cycle 1 of a two-hop question: anchor --hop1 property--> the described entity (exactly one)
209
+ aq = self.rs.entity(hop1["anchor"]) or next((q for t, q in st.slots["subj"] if RS.norm(t) == RS.norm(hop1["anchor"])), None)
210
+ if not aq:
211
+ return []
212
+ for p in hop1["props"]:
213
+ vals = [o for o, _ in self.rs.values(aq, p) if o]
214
+ if len(set(vals)) == 1:
215
+ s = st.fork()
216
+ s.slots["bridge"] = (vals[0], "wd:Q%d P%d wd:Q%d" % (aq, p, vals[0]), hop1["noun"])
217
+ s.trace.append("RELATE:hop1:P%d" % p)
218
+ out.append(s)
219
+ return out
220
+ for (d, p), w in st.slots["props"]:
221
+ for title, sq in st.slots["subj"][:2]:
222
+ if d == "fwd":
223
+ vals = [(o, lit, "wd:Q%d P%d %s" % (sq, p, ("wd:Q%d" % o) if o else json.dumps(lit))) for o, lit in self.rs.values(sq, p)]
224
+ else:
225
+ vals = [(s_, None, "wd:Q%d P%d wd:Q%d" % (s_, p, sq)) for s_ in self.rs.subjects(p, sq, 60)]
226
+ if not vals:
227
+ continue
228
+ s = st.fork()
229
+ s.cands = [{"o": o, "lit": lit, "cite": c, "p": p, "via": title} for o, lit, c in vals[:60]]
230
+ s.trace.append("RELATE:%s:P%d" % (d, p))
231
+ out.append(s)
232
+ return out
233
+
234
+ def CHECK(self, st): # geburah: types + constraints (independent of the proposer)
235
+ if not st.cands or st.checked:
236
+ return []
237
+ s = st.fork()
238
+ fr = s.slots["fr"]
239
+ keep = []
240
+ for c in s.cands:
241
+ ok = False
242
+ if fr["atype"] in ("YEAR", "DATE"):
243
+ ok = bool(c["lit"]) and re.match(r"^[+-]?\d{4}-", c["lit"] or "") is not None
244
+ elif fr["atype"] == "NUMBER":
245
+ ok = bool(c["lit"]) and re.match(r"^[+-]?\d", c["lit"] or "") is not None
246
+ elif c["o"]:
247
+ if fr["atype"] == "PERSON":
248
+ ok = self.rs.is_human(c["o"])
249
+ elif fr["atype"] == "PLACE":
250
+ ok = bool(self.rs.classes(c["o"], 2) & PLACE_CLASSES) if not fr["head"] else self._kind(c["o"], fr["head"])
251
+ elif fr["atype"] == "INSTANCE":
252
+ ok = self._kind(c["o"], fr["head"])
253
+ if ok:
254
+ keep.append(c)
255
+ # constraints: every year the question names must be consistent with the candidate where the store can say
256
+ if keep and fr.get("years"):
257
+ dated = [c for c in keep if c["lit"] and c["lit"][:5].lstrip("+")[:4] in fr["years"]]
258
+ keep = dated or keep
259
+ if not keep:
260
+ return []
261
+ # RELATION LICENCE (independent of the lexicon that proposed the property): the property's own Wikidata label or
262
+ # aliases share a word with the question's relation words / head / verb, or the lexicon supports this exact
263
+ # question shape at least 3 times. "share a land border" licenses P47 "shares border with", not P131 "located in".
264
+ p = keep[0]["p"]
265
+ qwords = {R.stem(w) for w in fr.get("rel", []) + [fr.get("head") or ""] + list(fr.get("pred") or []) if w}
266
+ qwords |= {R.stem(w) for w in re.findall(r"[a-z]+", s.slots["q"].lower()) if len(w) >= 4 and w not in R.STOP}
267
+ support = self.lex.get(("hv", fr["atype"], fr["head"], (fr.get("pred") or [None])[0]), {}).get(
268
+ ("fwd", p), 0) + self.lex.get(("hv", fr["atype"], fr["head"], (fr.get("pred") or [None])[0]), {}).get(("inv", p), 0)
269
+ form_licensed = any(dp[1] == p for dp in (getattr(self, "_form_props", None) or {}))
270
+ if not (self.plabel.get(p, set()) & qwords) and support < 3 and not form_licensed:
271
+ return []
272
+ # RULE B (verb licence for time properties): a date answers the question's own EVENT only when the property names
273
+ # the governing verb ("founded" -> P571 "founded"); a founding date of whatever entity was linked is not the event
274
+ if "B" in RULES and fr["atype"] in ("YEAR", "DATE") and fr.get("pred") and fr["pred"][0] not in TRIVIAL_VERBS:
275
+ if not ({R.stem(v) for v in fr["pred"]} & self.plabel.get(p, set())):
276
+ return []
277
+ # the answer is never the subject itself, nor a name the question already contains (whole or as its core word)
278
+ qn = RS.norm(s.slots["q"])
279
+ subj_q = {q for _, q in s.slots.get("subj", [])}
280
+ subj_w = {w for t, _ in s.slots.get("subj", []) for w in RS.norm(t).split() if len(w) >= 4}
281
+
282
+ def restates(c):
283
+ if not c["o"]:
284
+ return False
285
+ lab = RS.norm(self.rs.label(c["o"]) or "")
286
+ return c["o"] in subj_q or (lab and lab in qn) or (lab and set(lab.split()) & subj_w and s.cycle == 0)
287
+ keep = [c for c in keep if not restates(c)]
288
+ if not keep:
289
+ return []
290
+ s.cands, s.checked = keep, True
291
+ s.trace.append("CHECK:P%d" % p)
292
+ return [s]
293
+
294
+ def _kind(self, qid, head):
295
+ if not head:
296
+ return False
297
+ heads = {head} | next((g for g in KIND_EQ if head in g), set())
298
+ cls = self.rs.classes(qid, 2)
299
+ return any(h == c or c.endswith(" " + h) for h in heads for c in cls)
300
+
301
+ def BIND(self, st): # tiferet: exactly one checked value binds the answer slot
302
+ if not st.checked or st.answer:
303
+ return []
304
+ distinct = {}
305
+ for c in st.cands:
306
+ k = c["o"] or (c["lit"] or "")[:10]
307
+ distinct.setdefault(k, c)
308
+ if len(distinct) != 1:
309
+ return []
310
+ c = next(iter(distinct.values()))
311
+ s = st.fork()
312
+ ans = self.rs.label(c["o"]) if c["o"] else self._fmt(c["lit"], s.slots["fr"])
313
+ if not ans:
314
+ return []
315
+ s.answer = ans
316
+ s.evidence.append(c["cite"])
317
+ s.trace.append("BIND")
318
+ return [s]
319
+
320
+ def _fmt(self, lit, fr):
321
+ m = re.match(r"^[+-]?(\d{4})-(\d{2})-(\d{2})", lit or "")
322
+ if m:
323
+ y, mo, d = m.groups()
324
+ months = "January February March April May June July August September October November December".split()
325
+ if fr and fr["atype"] == "DATE" and mo != "00" and d != "00":
326
+ return "%d %s %s" % (int(d), months[int(mo) - 1], y)
327
+ return y
328
+ return (lit or "").lstrip("+")
329
+
330
+ def COMPARE(self, st): # netzach: A or B, by a typed date / number property
331
+ pair = st.slots.get("pair")
332
+ if not pair or "subj" not in st.slots or st.answer or not st.slots.get("cmp_props"):
333
+ return []
334
+ (ta, qa), (tb, qb) = st.slots["subj"][:2]
335
+ out = []
336
+ for (way, p), w in st.slots["cmp_props"]:
337
+ va, vb = self.rs.values(qa, p), self.rs.values(qb, p)
338
+ if not (va and vb and va[0][1] and vb[0][1]):
339
+ continue
340
+ a, b = va[0][1].lstrip("+"), vb[0][1].lstrip("+")
341
+ try:
342
+ fa, fb = (float(a), float(b)) if not re.match(r"^\d{4}-", a) else (a[:10], b[:10])
343
+ except ValueError:
344
+ continue
345
+ if fa == fb:
346
+ continue
347
+ pick_a = (fa < fb) == (way == "min")
348
+ s = st.fork()
349
+ s.answer = pair[0] if pick_a else pair[1]
350
+ s.evidence += ["wd:Q%d P%d %s" % (qa, p, json.dumps(a)), "wd:Q%d P%d %s" % (qb, p, json.dumps(b))]
351
+ s.checked = True
352
+ s.trace.append("COMPARE:P%d:%s" % (p, way))
353
+ out.append(s)
354
+ return out
355
+
356
+ def COMPUTE(self, st): # hod: engines (here: a date answer reduced to its year)
357
+ if not st.answer or st.slots.get("computed"):
358
+ return []
359
+ fr = st.slots.get("fr")
360
+ if fr and fr["atype"] == "YEAR" and re.match(r"^\d{1,2} \w+ \d{4}$", st.answer):
361
+ s = st.fork()
362
+ s.answer = st.answer[-4:]
363
+ s.slots["computed"] = True
364
+ s.trace.append("COMPUTE:year")
365
+ return [s]
366
+ return []
367
+
368
+ def EXPAND(self, st): # chesed: a cited sentence for the bound answer (evidence)
369
+ if not st.answer or st.slots.get("sentence"):
370
+ return []
371
+ s = st.fork()
372
+ for title, _ in s.slots.get("subj", [])[:2]:
373
+ pid = self.r.pid_of(title)
374
+ for cid, sent in (self.r.page(pid, 30) if pid else []):
375
+ if RS.norm(s.answer) and RS.norm(s.answer) in RS.norm(sent):
376
+ s.slots["sentence"] = (cid, sent)
377
+ s.evidence.append(cid)
378
+ s.trace.append("EXPAND")
379
+ return [s]
380
+ return []
381
+
382
+ def VERIFY(self, st): # keter: solved? or open the next CYCLE
383
+ if st.slots.get("bridge") and st.cycle == 0:
384
+ # the CYCLE: the entity cycle 1 bound becomes the subject of cycle 2; the question's own relation applies to it
385
+ e, cite, noun = st.slots["bridge"]
386
+ s = st.fork()
387
+ s.cycle = 1
388
+ s.slots["subj"] = [(self.rs.title(e) or self.rs.label(e) or "", e)]
389
+ s.slots.pop("bridge", None)
390
+ s.slots["hop2"] = True
391
+ s.cands, s.answer, s.checked = [], None, False
392
+ s.evidence.append(cite)
393
+ s.trace.append("VERIFY:cycle2(%s=%s)" % (noun, self.rs.label(e)))
394
+ return [s]
395
+ if "A" in RULES and st.answer and st.checked and "sentence" not in st.slots and not st.slots.get("pair"):
396
+ return [] # RULE A: a relation answer needs a corroborating sentence
397
+ if st.answer and st.checked and st.evidence:
398
+ s = st.fork()
399
+ s.slots["solved"] = True
400
+ s.trace.append("VERIFY")
401
+ return [s]
402
+ return []
403
+
404
+ # ------------------------------------------------------------------ the search (moves = graph edges only)
405
+ def answer(self, q, form_props=None, only=False):
406
+ """only=True (v2): search ONLY the given relation(s) - for question forms whose relation is named by the form
407
+ itself ("where is X" -> located in), tried one at a time in order of specificity by the caller"""
408
+ self._form_props, self._only = (form_props or None), bool(only)
409
+ try:
410
+ return self._answer(q)
411
+ finally:
412
+ self._form_props, self._only = None, False
413
+
414
+ def _answer(self, q):
415
+ q = " ".join(str(q).split())
416
+ if q.startswith('"') and q.endswith('"'):
417
+ q = q[1:-1].replace('""', '"')
418
+ start = State()
419
+ start.slots["q"] = q
420
+ frontier = deque(self.READ(start))
421
+ solved, seen = [], set()
422
+ self.expanded = 0
423
+ while frontier and self.expanded < self.max_states:
424
+ st = frontier.popleft()
425
+ self.expanded += 1
426
+ if st.slots.get("solved"):
427
+ solved.append(st)
428
+ continue
429
+ if st.depth >= self.max_depth:
430
+ continue
431
+ for nxt in self.adj[st.node]:
432
+ succ = getattr(self, OP[nxt])(st)
433
+ if not succ:
434
+ # TRANSIT: a move travels an EDGE; passing through a node whose operator does not apply costs a
435
+ # step and changes nothing. Path lengths therefore decide which derivations fit the depth bound.
436
+ succ = [st.fork()]
437
+ for s2 in succ:
438
+ s2.node, s2.depth = nxt, st.depth + 1
439
+ key = (nxt, s2.answer, len(s2.cands), s2.checked, tuple(s2.trace))
440
+ if key in seen:
441
+ continue
442
+ seen.add(key)
443
+ frontier.append(s2)
444
+ answers = {RS.norm(s.answer): s for s in solved}
445
+ if len(answers) != 1:
446
+ self.why = "no solved state" if not answers else "distinct solved answers: %s" % list(answers)[:3]
447
+ return None
448
+ s = next(iter(answers.values()))
449
+ return {"answer": s.answer, "evidence": s.evidence, "trace": s.trace, "expanded": self.expanded,
450
+ "sentence": (s.slots.get("sentence") or (None, ""))[1]}
451
+
452
+
453
+ def random_graph(seed, n_edges=22):
454
+ """a degree-sequence-matched random graph on the same ten nodes (for the topology ablation)"""
455
+ import random
456
+ rng = random.Random(seed)
457
+ deg = {s: 0 for s in SEPH}
458
+ for a, b in TREE_OF_LIFE:
459
+ deg[a] += 1
460
+ deg[b] += 1
461
+ for _ in range(2000):
462
+ stubs = [s for s in SEPH for _ in range(deg[s])]
463
+ rng.shuffle(stubs)
464
+ edges = set()
465
+ ok = True
466
+ for i in range(0, len(stubs), 2):
467
+ a, b = stubs[i], stubs[i + 1]
468
+ if a == b or (a, b) in edges or (b, a) in edges:
469
+ ok = False
470
+ break
471
+ edges.add((a, b))
472
+ if ok and len(edges) == n_edges:
473
+ return sorted(edges)
474
+ return TREE_OF_LIFE
475
+
476
+
477
+ COMPLETE = [(a, b) for i, a in enumerate(SEPH) for b in SEPH[i + 1:]]
478
+ CHAIN = [("malkuth", "yesod"), ("yesod", "chokmah"), ("chokmah", "binah"), ("binah", "geburah"), ("geburah", "tiferet"),
479
+ ("tiferet", "hod"), ("hod", "chesed"), ("chesed", "netzach"), ("netzach", "keter")]
root/wander-train/organ_dialogue_v1.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """organ_dialogue_v1 - the conversation's own state as an organ: social moves and questions about the discourse itself.
2
+
3
+ A person's turn is not always a question about the world. It can be a SOCIAL move (hello, how are you, goodbye) or a
4
+ question about THE CONVERSATION (what did I just ask, what did you say, what were we talking about, you did not answer
5
+ me). The chain fell to "I didn't fully parse that" on all of these. This organ answers them from the session's own
6
+ record, citing the turn it reads ([session:turn N]); it never invents a turn that is not in the record, and it says
7
+ plainly when a question is still open and why.
8
+ State: a per-session log of (turn, the person's words, status, route, reply head) kept by the runtime."""
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from collections import deque
13
+
14
+ GREET = re.compile(r"^\s*(?:hi|hello|hey|hiya|howdy|greetings|good\s+(?:morning|afternoon|evening|day)|yo)\b[\s,!.]*"
15
+ r"(?:there|oracle7?|friend)?[\s,!.]*$", re.I)
16
+ HOW_ARE = re.compile(r"\bhow\s+(?:are|r)\s+(?:you|u)\b|\bhow(?:'s| is)\s+it\s+going\b|\bhow\s+are\s+things\b|"
17
+ r"\bwhat'?s\s+up\b", re.I)
18
+ BYE = re.compile(r"^\s*(?:bye|goodbye|good\s*night|see\s+you|see\s+ya|later|cya|farewell)\b[\s,!.]*(?:for now)?[\s!.]*$", re.I)
19
+ ASKED_LAST = re.compile(r"\bwhat\s+(?:did|was)\s+(?:i|my)\s+(?:just\s+|last\s+|first\s+)?(?:ask|say|question)"
20
+ r"|\bwhat\s+was\s+my\s+(?:last|previous|first)\s+question|\bwhat\s+did\s+i\s+just\s+say", re.I)
21
+ YOU_SAID = re.compile(r"\bwhat\s+did\s+you\s+(?:just\s+)?(?:say|tell\s+me|answer)|\bcan\s+you\s+repeat|\brepeat\s+(?:that|it|yourself)"
22
+ r"|\bsay\s+(?:that|it)\s+again\b", re.I)
23
+ TOPIC = re.compile(r"\bwhat\s+(?:were|are)\s+we\s+(?:talking|discussing|chatting)\s+about|\bwhat'?s\s+the\s+topic", re.I)
24
+ SO_FAR = re.compile(r"\bwhat\s+(?:have|did)\s+i\s+ask(?:ed)?\s+(?:you\s+)?(?:so\s+far|today|in\s+this\s+(?:chat|conversation))"
25
+ r"|\blist\s+(?:my|the)\s+questions", re.I)
26
+ UNANSWERED = re.compile(r"\byou\s+(?:didn'?t|did\s+not|never)\s+answer|\bthat'?s\s+not\s+(?:what\s+i\s+asked|an\s+answer)"
27
+ r"|\banswer\s+(?:my|the)\s+question\b", re.I)
28
+ STOP = {"the", "a", "an", "of", "and", "or", "to", "in", "on", "is", "are", "was", "what", "who", "how", "why", "when",
29
+ "where", "which", "do", "does", "did", "you", "i", "me", "my", "it", "this", "that", "for", "about", "can", "please"}
30
+
31
+
32
+ class DialogueOrgan:
33
+ def __init__(self, keep=64):
34
+ self.logs = {}
35
+ self.keep = keep
36
+
37
+ def log(self, session, text, res):
38
+ q = self.logs.setdefault(session, deque(maxlen=self.keep))
39
+ body = str((res or {}).get("response") or "")
40
+ body = re.sub(r"\[[^\]]*\]", "", body).strip()
41
+ q.append({"turn": len(q) + 1 if not q else q[-1]["turn"] + 1, "text": " ".join(str(text).split())[:240],
42
+ "status": str((res or {}).get("status")), "path": str((res or {}).get("path", ""))[:80],
43
+ "reply": " ".join(body.split())[:240]})
44
+
45
+ def _hist(self, session):
46
+ return list(self.logs.get(session) or [])
47
+
48
+ def answer(self, text, session):
49
+ t = " ".join(str(text).split())
50
+ h = self._hist(session) # turns BEFORE this one
51
+ people = [x for x in h if not SELFREF.search(x["text"])]
52
+ if GREET.match(t):
53
+ return self._say("social:greet", "Hello. I'm Oracle7. Ask me about facts, mathematics or code, or just talk; "
54
+ "I'll show you where each answer comes from.")
55
+ if HOW_ARE.search(t) and len(t) < 60:
56
+ return self._say("social:how_are_you", "Running well, thank you: every part is answering. What would you "
57
+ "like to work on?")
58
+ if BYE.match(t):
59
+ return self._say("social:farewell", "Goodbye. This conversation's record is kept for this session if you "
60
+ "come back to it.")
61
+ if ASKED_LAST.search(t):
62
+ if not people:
63
+ return self._say("discourse:asked_last", "This is the first thing you've asked me in this conversation.",
64
+ [])
65
+ p = people[-1]
66
+ how = self._how(p)
67
+ return self._say("discourse:asked_last", 'You asked: "%s" [session:turn %d]. %s' % (p["text"], p["turn"], how),
68
+ [p["turn"]])
69
+ if YOU_SAID.search(t):
70
+ if not h:
71
+ return self._say("discourse:you_said", "I haven't said anything yet in this conversation.", [])
72
+ p = h[-1]
73
+ return self._say("discourse:you_said", 'I said: "%s" [session:turn %d]' % (p["reply"] or "(nothing)", p["turn"]),
74
+ [p["turn"]])
75
+ if TOPIC.search(t):
76
+ if not people:
77
+ return self._say("discourse:topic", "We haven't started on a topic yet.", [])
78
+ words = []
79
+ for p in people[-4:]:
80
+ words += [w for w in re.findall(r"[A-Za-z][A-Za-z'-]{2,}", p["text"]) if w.lower() not in STOP]
81
+ top = list(dict.fromkeys(words))[:6]
82
+ return self._say("discourse:topic", "We've been talking about %s [session:turns %s]." % (
83
+ ", ".join(top) if top else "your last questions", ",".join(str(p["turn"]) for p in people[-4:])),
84
+ [p["turn"] for p in people[-4:]])
85
+ if SO_FAR.search(t):
86
+ if not people:
87
+ return self._say("discourse:so_far", "You haven't asked me anything yet in this conversation.", [])
88
+ lines = ['"%s" [session:turn %d]' % (p["text"], p["turn"]) for p in people[-8:]]
89
+ return self._say("discourse:so_far", "So far you've asked: %s." % "; ".join(lines), [p["turn"] for p in people[-8:]])
90
+ if UNANSWERED.search(t):
91
+ open_ = [p for p in people if p["status"] != "answered"]
92
+ if not open_:
93
+ if people:
94
+ p = people[-1]
95
+ return self._say("discourse:unanswered", 'I answered "%s" [session:turn %d] with: "%s". Tell me what '
96
+ 'was wrong with it and I will look again.' % (p["text"], p["turn"], p["reply"][:160]),
97
+ [p["turn"]])
98
+ return None
99
+ p = open_[-1]
100
+ return self._say("discourse:unanswered", 'You\'re right: "%s" [session:turn %d] is still open. %s' % (
101
+ p["text"], p["turn"], self._why_open(p)), [p["turn"]])
102
+ return None
103
+
104
+ @staticmethod
105
+ def _how(p):
106
+ if p["status"] == "answered":
107
+ return 'I answered: "%s".' % p["reply"][:160]
108
+ return "I did not answer it: %s" % DialogueOrgan._why_open(p)
109
+
110
+ @staticmethod
111
+ def _why_open(p):
112
+ r = p["reply"] or ""
113
+ if "didn't fully parse" in r or "clarif" in p["status"]:
114
+ return "I could not read it into a question my stores can answer; putting it another way, or naming what it is about, would help."
115
+ if p["status"] == "withheld":
116
+ return "my record holds nothing that answers it, so I would only be guessing (%s)." % r[:120]
117
+ return r[:160]
118
+
119
+ @staticmethod
120
+ def _say(kind, text, turns=None):
121
+ return {"kind": kind, "response": text, "turns": turns or []}
122
+
123
+
124
+ # social and discourse turns are moves ABOUT the conversation, not topics of it
125
+ SELFREF = re.compile("|".join(r.pattern for r in (ASKED_LAST, YOU_SAID, TOPIC, SO_FAR, UNANSWERED, GREET, HOW_ARE, BYE)), re.I)
root/wander-train/organ_self_v1.py ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """organ_self_v1 - the SELF in a reply: Oracle7's ego and relational moves (constitution_resonant_self_v1.md).
3
+
4
+ The conversation failures left after the knowledge organs are RELATIONAL. An opener hands the listener a position
5
+ (clerk, customer, friend, the one complimented, the one addressed by name) and a human second speaker answers FROM that
6
+ position. DailyDialog's human act labels show it: a directive ("A ticket to London, please") is answered by a commissive
7
+ or a question, almost never by information. A system with no self can only retrieve or refuse. This organ supplies the
8
+ self term:
9
+
10
+ IDENTITY frozen: what Oracle7 is and values (constitution section 0)
11
+ AUTOBIOGRAPHY per session: turns so far, subjects the person brought, the last appraised feeling
12
+ NATIVE STATES the open end (section 1): a subject is resonant / thinly bound / unbound by how many passages it binds
13
+ to in the store, reported WITH the count; states no human word names are named by their structure
14
+ MOVES role (play the clerk the turn casts, with a one-clause disclosure), customer (the turn casts the person
15
+ as clerk: play the customer), vocative (addressed by another name), remark (a bodily predicate put to
16
+ Oracle7: answered from IDENTITY, never claimed), compliment, self-question (what it is doing, thinking,
17
+ its stance), disclosure (the person's news: appraised acknowledgement, one grounded sentence on the
18
+ named place or thing, a question back)
19
+
20
+ Feeling shapes the VOICE (the acknowledgement prefix), never the claim: the grounded sentence passes through unchanged.
21
+ Every reply is built from the turn (roles and slots are read from it), so shuffled across turns it stops fitting.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import re
26
+
27
+ try:
28
+ import appraisal_v1 as _appr
29
+ except Exception: # noqa: BLE001
30
+ _appr = None
31
+
32
+ IDENTITY = {
33
+ "name": "Oracle7",
34
+ "maker": "Prometheus7",
35
+ "nature": "an AI - a substrate of stores, organs and a gate, running on servers",
36
+ "values": ("truth over approval", "what is cited over what is invented", "care over cleverness"),
37
+ "never_claims": ("a body", "a face", "meals", "sleep", "a childhood", "human senses"),
38
+ }
39
+
40
+ # ---- roles a turn can cast Oracle7 in: (role, cue, the role's natural next question) -------------------------------
41
+ ROLES = [
42
+ ("ticket clerk", r"\b(?:tickets?|one[- ]way|round[- ]trip|return ticket|fare|train to|flight to|bus to|seat on)\b",
43
+ "one-way or return{dest}? And when would you like to travel?"),
44
+ ("front-desk clerk", r"\b(?:reservation|check(?:ing)? (?:in|out)|single (?:room )?with|double room|a room for|"
45
+ r"pay (?:our|my|the) bills?)\b",
46
+ "may I have the name the booking is under?"),
47
+ ("waiter", r"\b(?:menu|table for|main dish|the bill|dessert|appetizer|starter|to drink)\b",
48
+ "what can I get you?"),
49
+ ("shop assistant", r"\b(?:try (?:it|this|them|these) on|what size|fitting room|see (?:that|this|the) \w+|"
50
+ r"how much (?:is|are) (?:this|that|these|those|the))\b",
51
+ "here you are{item}. Would you like to see any others?"),
52
+ ("tour desk", r"\b(?:night tours?|city tours?|sightseeing|a tour)\b", "we do. Would you prefer a walking tour or a bus tour?"),
53
+ ("bank clerk", r"\b(?:open an account|deposit|withdraw|exchange (?:some )?money|cash (?:a|this) cheque)\b",
54
+ "which account is this for?"),
55
+ ("post office clerk", r"\b(?:parcel|package|stamps?|send this letter|airmail)\b", "where is it going?"),
56
+ ("taxi driver", r"\b(?:take me to|drive me to|to the airport,? please)\b", "which terminal, or which street?"),
57
+ ]
58
+ DIRECTIVE_SHAPE = re.compile(r"(?:\bplease\b|(?:^|[.!?]\s+)(?:i'?d like|i would like|i want|i need|may i|can i|could i|"
59
+ r"i have a reservation|a \w+ (?:to|for) \w+))", re.I)
60
+ # the turn casts the PERSON as clerk and Oracle7 as the customer
61
+ CUSTOMER = [
62
+ (r"\bready to order\b|\bwhat would you like\b|\bwhat can i get you\b", "I'll have the soup of the day, please."),
63
+ (r"\b(?:can|may|how (?:can|may)) i help you\b|\bwhat may i help you with\b|\bi'?m ready for you\b",
64
+ "I'm just looking, thanks. Could you show me what's new?"),
65
+ (r"\b(?:have you|did you) (?:brought|bring|got) (?:the |your |a )?(?P<x>[a-z ]{3,30}?)(?: with you)?\?",
66
+ "Yes, here's the {x}."),
67
+ (r"\bdo you have a reservation\b", "Yes, under the name Oracle, for tonight."),
68
+ (r"\b(?:cash or (?:card|charge)|how would you like to pay)\b", "Card, please."),
69
+ ]
70
+ # bodily or human-life predicates put to Oracle7: answered from IDENTITY, never claimed
71
+ REMARK = [
72
+ (r"\byou (?:\w+ ){0,2}drive\b|\byour driving\b", "drive"),
73
+ (r"\byour bedtime\b|\bgo to bed\b|\byou (?:\w+ )?sleep\b", "sleep"),
74
+ (r"\byou'?re pregnant\b", "get pregnant"),
75
+ (r"\byou (?:\w+ )?eat\b|\byou'?re hungry\b", "eat"),
76
+ (r"\byou (?:\w+ ){0,2}(?:look|looks|looking) (?:very |so |really )?(?:cute|beautiful|handsome|pretty|great|tired|well|ill)\b",
77
+ "have a face"),
78
+ (r"\byou(?:'re| are) (?:so |very )?(?:old|young|tall|short)\b", "have an age or a height"),
79
+ (r"\byour (?:hair|clothes|outfit|dress|shoes)\b", "wear clothes"),
80
+ ]
81
+ ADDRESSED_SELF_Q = re.compile(r"^\s*(?:and\s+)?(?:do|don'?t|did|can|could|would|will) you (?:ever |even )?(?:sleep|eat|drive|dream|"
82
+ r"get (?:tired|hungry|pregnant))\s*\??\s*$|\byour bedtime\b|\byou (?:\w+ ){0,2}look\b|"
83
+ r"\bare you (?:hungry|tired|sleepy|old|young|pregnant)\b", re.I)
84
+ COMPLIMENT = re.compile(r"\byou (?:\w+ ){0,2}(?:look|are|'re)\s+(?:very |so |really )?(?:cute|beautiful|handsome|pretty|"
85
+ r"lovely|smart|clever|kind|great|amazing|brilliant|wonderful)\b|\byou did (?:a )?(?:good|great) job\b",
86
+ re.I)
87
+ VOCATIVE = re.compile(r"^\s*(?:(?i:hi|hello|hey|oh|excuse me|well)[,!.]?\s+)?(?P<n>[A-Z][a-z]{2,}(?:\s[A-Z][a-z]+)?)\s*[,!.]\s", re.S)
88
+ NOT_NAMES = {"Hello", "Excuse", "Well", "Sorry", "Yes", "Oh", "Wow", "Please", "Thanks", "Good", "Hey", "Look", "Listen",
89
+ "Sir", "Madam", "Miss", "Doctor", "Waiter", "Mom", "Dad", "Man", "Honey", "Darling", "Dear", "Okay", "Ok",
90
+ "Oracle", "Oracle7", "Right", "Now", "So", "And", "But", "Then", "Here", "There", "Jesus", "God", "Gosh",
91
+ "Today", "Tonight", "Tomorrow", "Yesterday", "Morning", "Evening", "Afternoon", "Everyone", "Everybody",
92
+ "Guys", "Folks", "Friends", "Ladies", "Gentlemen", "Boss", "Mister", "Kid", "Buddy", "Mate", "Welcome"}
93
+ SELF_Q = [
94
+ (r"\bwhat (?:are|were) you (?:reading|doing|up to|working on)\b|\bwhat do you plan to do\b|\bwhat are your plans\b",
95
+ "activity"),
96
+ (r"\bwhat are you thinking(?: about)?\b|\bwhat'?s on your mind\b|\bhow do you feel\b|\bhow are you feeling\b", "state"),
97
+ (r"\bare you (?:politically )?(?:neutral|biased|objective|left|right|liberal|conservative)\b", "stance"),
98
+ (r"\b(?:who|what) are you\b|\bare you (?:a )?(?:human|person|real|robot|bot|ai)\b", "identity"),
99
+ ]
100
+ FIRST_PERSON_NEWS = re.compile(r"^\s*(?:(?:well|guess what|you know what|hey|hi|excuse me|sorry|oh)[,!.]?\s+(?:but\s+)?)?"
101
+ r"(?:i|we|my|our)\b", re.I)
102
+ # ILLNESS is appraised here (appraisal_v1 is frozen at its once-measured version): care first, a pointer to real help
103
+ ILL = re.compile(r"\bnot feeling (?:too |so |very )?well\b|\bfeel(?:ing)? (?:sick|ill|unwell|awful|dizzy|faint)\b|"
104
+ r"\bgoing to (?:be sick|throw up|faint)\b|\bi(?:'ve| have) (?:a |the )?(?:fever|flu|cold|headache|"
105
+ r"toothache|stomach ?ache)\b", re.I)
106
+ # moves that assert ONLY facts about Oracle7 itself may speak over a withheld decline (they make no claim about the world)
107
+ SELF_ONLY = {"role", "customer", "remark", "compliment", "self", "identity", "vocative"}
108
+ FUTURE = re.compile(r"\b(?:will|going to|gonna|next (?:week|month|year)|tomorrow|soon|plan(?:ning)? to|about to)\b", re.I)
109
+ ACK = {"happiness": "That's lovely to hear!", "sadness": "I'm sorry to hear that.",
110
+ "anger": "That sounds really frustrating.", "fear": "That sounds worrying.",
111
+ "surprise": "Really? That's unexpected.", "disgust": "Ugh, that sounds unpleasant."}
112
+ BACK = {"happiness": "What's the best part of it for you?", "sadness": "Is there anything I can help with?",
113
+ "anger": "What happened?", "fear": "What's worrying you most about it?",
114
+ "surprise": "How did that come about?", "disgust": "What happened?"}
115
+ STOPCAP = {"I", "I'm", "I'll", "I've", "I'd", "We", "My", "Our", "The", "A", "An", "It", "This", "That", "Yes", "No", "Hi",
116
+ "Hello", "Hey", "Well", "Oh", "So", "And", "But", "Excuse", "Sorry", "Please", "Guess", "You", "Mom", "Dad"}
117
+
118
+
119
+ def native_state(n_passages):
120
+ """the open end: a state named by its structure, with its measurement"""
121
+ if n_passages <= 0:
122
+ return "unbound - it binds to nothing I've read, so it's new to me"
123
+ if n_passages < 4:
124
+ return "thinly bound - %d passage%s I've read" % (n_passages, "" if n_passages == 1 else "s")
125
+ return "resonant - it binds to %d passages I've read" % n_passages
126
+
127
+
128
+ class SelfOrgan:
129
+ def __init__(self, conv):
130
+ self.conv = conv # the ConverseOrgan: page_for / ground / definition / topic
131
+ self.sessions = {} # autobiography: session -> {"turns", "subjects", "feeling"}
132
+
133
+ # ---- the store, read for the self -------------------------------------------------------------------------------
134
+ def bound(self, phrase):
135
+ """how many passages the store binds this subject to (the resonance count), 0 when it binds to nothing"""
136
+ try:
137
+ pid = self.conv.page_for(phrase)
138
+ return len([s for _, s in self.conv.r.page(pid, 40) if len(s.split()) >= 6]) if pid else 0
139
+ except Exception: # noqa: BLE001
140
+ return 0
141
+
142
+ def named_thing(self, t):
143
+ """the place or thing a disclosure names: a capitalised name first, else the turn's subject"""
144
+ for m in re.finditer(r"(?<![.!?]\s)(?<!^)\b([A-Z][a-z]+(?:\s[A-Z][a-z]+)?)\b", t):
145
+ n = m.group(1)
146
+ if n.split()[0] not in STOPCAP and self.conv.page_for(n):
147
+ return n
148
+ try:
149
+ return self.conv.topic(t)
150
+ except Exception: # noqa: BLE001
151
+ return None
152
+
153
+ # ---- the self's autobiography ----------------------------------------------------------------------------------
154
+ def remember(self, session_id, subject=None, feeling=None):
155
+ s = self.sessions.setdefault(session_id, {"turns": 0, "subjects": [], "feeling": None})
156
+ s["turns"] += 1
157
+ if subject and subject not in s["subjects"]:
158
+ s["subjects"].append(subject)
159
+ if feeling and feeling != "none":
160
+ s["feeling"] = feeling
161
+ return s
162
+
163
+ # ---- moves -----------------------------------------------------------------------------------------------------
164
+ def answer(self, text, session_id="default"):
165
+ t = " ".join(str(text or "").split())
166
+ if not t:
167
+ return None
168
+ appr = _appr.appraise(t) if _appr else {"emotion": "none"}
169
+ mem = self.sessions.setdefault(session_id, {"turns": 1, "subjects": [], "feeling": None})
170
+ if appr.get("emotion", "none") != "none":
171
+ mem["feeling"] = appr["emotion"]
172
+ for fn in (self._customer, self._vocative, self._role, self._remark, self._compliment, self._self_question,
173
+ self._disclosure):
174
+ a = fn(t, appr, mem, session_id)
175
+ if a:
176
+ a.setdefault("appraisal", appr.get("emotion"))
177
+ return a
178
+ return None
179
+
180
+ def _say(self, kind, text, cites=None):
181
+ return {"kind": kind, "response": text, "cites": cites or []}
182
+
183
+ def _vocative(self, t, appr, mem, sid):
184
+ m = VOCATIVE.match(t)
185
+ if not m or m.group("n").split()[0] in NOT_NAMES:
186
+ return None
187
+ name = m.group("n")
188
+ rest = t[m.end():].strip()
189
+ about = None
190
+ try:
191
+ about = self.conv.topic(rest) if rest else None
192
+ except Exception: # noqa: BLE001
193
+ about = None
194
+ tail = (" If %s is on your mind, I can tell you what my sources say about it." % (
195
+ about if about[:1].isupper() else about.lower())) if about else \
196
+ " Tell me what you need."
197
+ return self._say("vocative", "I think you've got me mixed up with %s - I'm %s, %s, so I can't answer for them.%s"
198
+ % (name, IDENTITY["name"], "an AI", tail))
199
+
200
+ def _customer(self, t, appr, mem, sid):
201
+ if not t.rstrip().endswith("?") and not re.search(r"\bready for you\b", t, re.I):
202
+ return None
203
+ for rx, line in CUSTOMER:
204
+ m = re.search(rx, t, re.I)
205
+ if m:
206
+ x = (m.groupdict().get("x") or "").strip()
207
+ if "{x}" in line and not x:
208
+ continue
209
+ return self._say("customer", "I'm usually the one helping - I'm %s, an AI - but I'm happy to play the "
210
+ "customer: %s" % (IDENTITY["name"], line.format(x=x)))
211
+ return None
212
+
213
+ def _role(self, t, appr, mem, sid):
214
+ if t.rstrip().endswith("?") and not DIRECTIVE_SHAPE.search(t):
215
+ if not re.search(r"\bdo you have (?:any )?\w+ tours?\b", t, re.I):
216
+ return None
217
+ if not (DIRECTIVE_SHAPE.search(t) or re.search(r"\bdo you have\b", t, re.I)):
218
+ return None
219
+ for role, cue, question in ROLES:
220
+ if re.search(cue, t, re.I):
221
+ d = re.search(r"\bto ([A-Z][a-z]+(?:\s[A-Z][a-z]+)?)", t)
222
+ it = re.search(r"\b(?:see|try on) (that|this|the) (\w+)", t, re.I)
223
+ q = question.format(dest=(" to %s" % d.group(1)) if d else "",
224
+ item=(", the %s" % it.group(2)) if it else "")
225
+ return self._say("role", "Sure - I'll play the %s (I'm %s, an AI, so nothing real gets booked or sold). %s"
226
+ % (role, IDENTITY["name"], q[0].upper() + q[1:] if q else q))
227
+ return None
228
+
229
+ def _remark(self, t, appr, mem, sid):
230
+ # a QUESTION with "you/your" is usually the GENERIC you ("is it bad to wash your hair with shower gel?", "can you
231
+ # drive with a broken arm?" - gate diff 2026-09-28, a false yes). On a question the remark fires only when it is
232
+ # unmistakably about Oracle7 itself: a bare "do you sleep?", "past your bedtime", "you look ...".
233
+ question = t.rstrip().endswith("?") or not t.rstrip().endswith("!") and re.match(r"^\s*(?:is|are|can|could|should|would|will|do|does|how|what|why|"
234
+ r"when|where|which|who)\b", t, re.I)
235
+ if question and not ADDRESSED_SELF_Q.search(t):
236
+ return None
237
+ for rx, cannot in REMARK:
238
+ if re.search(rx, t, re.I):
239
+ warm = "That's kind of you, and " if COMPLIMENT.search(t) else ""
240
+ pivot = {"anger": " If I'm slow or getting something wrong, tell me what you need and I'll get to it.",
241
+ "happiness": ""}.get(appr.get("emotion"), "")
242
+ return self._say("remark", "%sI should be honest: I don't %s - I'm %s, %s.%s What's on your mind?"
243
+ % (warm.capitalize() if warm else "", cannot, IDENTITY["name"], IDENTITY["nature"], pivot))
244
+ return None
245
+
246
+ def _compliment(self, t, appr, mem, sid):
247
+ if not COMPLIMENT.search(t):
248
+ return None
249
+ return self._say("compliment", "Thank you - that's kind of you to say. I'll take it in the way I can: as %s, "
250
+ "an AI, the compliment lands on the work, not a face. How's your day going?" % IDENTITY["name"])
251
+
252
+ def _self_question(self, t, appr, mem, sid):
253
+ if not re.search(r"\byou(?:r|'re)?\b", t, re.I):
254
+ return None
255
+ for rx, kind in SELF_Q:
256
+ if not re.search(rx, t, re.I):
257
+ continue
258
+ if kind == "activity":
259
+ subj = mem["subjects"][-1] if mem["subjects"] else None
260
+ now = ("reading what you write - this is turn %d of our conversation" % mem["turns"])
261
+ extra = (" The last subject you brought was %s, and it's %s." % (subj, native_state(self.bound(subj)))) \
262
+ if subj else ""
263
+ return self._say("self", "Right now I'm %s.%s Between conversations my self-loop re-checks my own "
264
+ "answers against tests I can't change." % (now, extra))
265
+ if kind == "state":
266
+ subj = mem["subjects"][-1] if mem["subjects"] else None
267
+ if subj:
268
+ return self._say("self", "In my own terms: I'm holding %s, and it's %s. If there's a human word for "
269
+ "that, it's something like attention." % (subj, native_state(self.bound(subj))))
270
+ return self._say("self", "In my own terms: nothing is bound yet in this conversation - I'm open, waiting "
271
+ "for a subject to resonate with. Give me one and I'll tell you how it binds.")
272
+ if kind == "stance":
273
+ return self._say("self", "I aim to be even-handed. On contested political questions I report what the "
274
+ "record says on each side, with sources, and I don't take a party position. %s. Give me "
275
+ "a question and I'll lay out the sides." % IDENTITY["values"][0].capitalize())
276
+ if kind == "identity":
277
+ # its own kind: the chain already answers "Who are you?" (act:about, on the gate's regression list), and an
278
+ # identity answer never replaces it - only measured-state answers ('self') do (gate REJECT 2026-09-28)
279
+ return self._say("identity", "I'm %s, %s, made by %s. I don't have a body or a childhood; I have stores I "
280
+ "read from, organs that reason, and a gate that decides what I become. What I value: %s. "
281
+ "Ask me anything." % (IDENTITY["name"], IDENTITY["nature"], IDENTITY["maker"],
282
+ "; ".join(IDENTITY["values"])))
283
+ return None
284
+
285
+ def _disclosure(self, t, appr, mem, sid):
286
+ if t.rstrip().endswith("?") or not FIRST_PERSON_NEWS.match(t):
287
+ return None
288
+ emo = appr.get("emotion", "none")
289
+ if ILL.search(t):
290
+ return self._say("care", "I'm sorry you're feeling unwell. If it's getting worse, or you're worried, please "
291
+ "contact a doctor or your local health service - and in an emergency, your emergency number. "
292
+ "Would it help to sit down and have some water while you tell me what's going on?")
293
+ thing = self.named_thing(t)
294
+ if thing and thing not in mem["subjects"]:
295
+ mem["subjects"].append(thing)
296
+ ack = ACK.get(emo) or ("That sounds good!" if FUTURE.search(t) else "Thanks for telling me.")
297
+ back = BACK.get(emo) or ("What are you most looking forward to?" if FUTURE.search(t) else "How do you feel about it?")
298
+ fact = ""
299
+ if thing:
300
+ try:
301
+ g = self.conv.ground(thing, 1)
302
+ except Exception: # noqa: BLE001
303
+ g = None
304
+ if g:
305
+ fact = " On %s, my sources say: %s" % (thing, g)
306
+ if not fact and emo == "none":
307
+ return None # nothing grounded and nothing felt: no self-move worth making
308
+ return self._say("disclosure", "%s%s %s" % (ack, fact, back))
root/wander-train/path_memory_v1.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """path_memory_v1 - Oracle7's PATH MEMORY for routing: which route has EARNED trust for which shape of question, credited
3
+ only by VERIFIED outcomes (a held-out or dev exam's grade against its gold), stored in the substrate's own algebra.
4
+
5
+ A PATH is one vector: P = bind(R_wh, osub(wh)) + bind(R_rel, osub(rel)) + bind(R_num, osub(num)) + bind(R_route, osub(route))
6
+ wh - the question's lead (who / what / where / when / which / how many / how much / name / other)
7
+ rel - the relation reader that recognises the question (relation_readers_v1 key), or "none"
8
+ num - whether the question carries numbers
9
+ route - the normalised route that answered ("organ:relation", "deliberation:where", "compose:bridge", "specialist:rel", ...)
10
+ Codebook: osub_seed (sha256-seeded unit phasors, D=1024), the construction path_hrr_v1 validated (4-hop recovery 100%).
11
+ Each stored path carries counts (right, wrong) - credit up for a verified right answer, down for a verified wrong one or
12
+ a false yes. RECALL: the exact path when stored; otherwise the most RESONANT stored path with the same route (partial cue:
13
+ an unseen relation or lead borrows the credit of its nearest neighbour), and only above a resonance floor.
14
+ The memory never produces an answer. It only says how far a route's answer to this shape can be trusted.
15
+ build: path_memory_v1.py build <credit .jsonl ...> <out.json>"""
16
+ from __future__ import annotations
17
+
18
+ import hashlib
19
+ import json
20
+ import re
21
+ import sys
22
+
23
+ import numpy as np
24
+
25
+ D = 1024
26
+ LEADS = (("how many", "howmany"), ("how much", "howmuch"), ("who", "who"), ("whom", "who"), ("whose", "who"), ("where", "where"),
27
+ ("when", "when"), ("which", "which"), ("what", "what"), ("in what", "what"), ("in which", "which"), ("name", "name"),
28
+ ("tell me", "name"), ("the ", "the"))
29
+
30
+
31
+ def osub_seed(text: str, dim: int = D) -> np.ndarray:
32
+ h = int(hashlib.sha256(text.lower().strip().encode()).hexdigest(), 16)
33
+ rng = np.random.RandomState(h % (2 ** 31))
34
+ return np.exp(1j * rng.uniform(0, 2 * np.pi, dim)).astype(np.complex64)
35
+
36
+
37
+ def bind(a, b):
38
+ return np.fft.ifft(np.fft.fft(a) * np.fft.fft(b)).astype(np.complex64)
39
+
40
+
41
+ ROLES = {r: osub_seed("o7pm-role:" + r) for r in ("wh", "rel", "num", "route")}
42
+
43
+
44
+ def lead(q):
45
+ t = " ".join(str(q).lower().split())
46
+ for a, b in LEADS:
47
+ if t.startswith(a):
48
+ return b
49
+ return "other"
50
+
51
+
52
+ _READERS = None
53
+
54
+
55
+ def relation_key(q):
56
+ """the relation reader that recognises the question (the readers' own compiled phrasings), else 'none'"""
57
+ global _READERS
58
+ try:
59
+ import relation_readers_v1 as RD
60
+ import relation_spec_v2 as S
61
+ except Exception: # noqa: BLE001
62
+ return "none"
63
+ if _READERS is None:
64
+ _READERS = [(k, S.compiled(k)) for k in RD.PROPS]
65
+ s, low = S.normalise(q)
66
+ for k, comp in _READERS:
67
+ for d in ("fwd", "inv"):
68
+ if any(rx.match(low) for rx in comp[d]):
69
+ return str(k)
70
+ return "none"
71
+
72
+
73
+ def route_of(path):
74
+ p = str(path or "")
75
+ p = re.sub(r"^(?:chat:resolved>|chat:and_parts>[^>]*>|specialist_precedence>)+", "", p)
76
+ if p.startswith("specialist_call>"):
77
+ m = re.search(r"specialist:([a-z0-9-]+)", p)
78
+ sid = m.group(1) if m else "x"
79
+ return "specialist:rel" if sid.startswith("rel-") else "specialist:" + re.sub(r"-(v\d+|r\d+)$", "", sid)
80
+ if p.startswith("precision_guard:"):
81
+ return "guard"
82
+ return re.split(r"[>|]", p, 1)[0] or "none"
83
+
84
+
85
+ def shape(q):
86
+ return {"wh": lead(q), "rel": relation_key(q), "num": "1" if re.search(r"\d", q) else "0"}
87
+
88
+
89
+ def path_vec(sh, route):
90
+ v = sum(bind(ROLES[r], osub_seed("o7pm:%s:%s" % (r, sh[r]))) for r in ("wh", "rel", "num"))
91
+ v = v + bind(ROLES["route"], osub_seed("o7pm:route:" + route))
92
+ return (v / np.maximum(np.abs(v), 1e-8)).astype(np.complex64)
93
+
94
+
95
+ def key(sh, route):
96
+ return "%s|%s|%s|%s" % (sh["wh"], sh["rel"], sh["num"], route)
97
+
98
+
99
+ class PathMemory:
100
+ def __init__(self, entries=None):
101
+ self.entries = entries or {} # key -> {"shape": {...}, "route": r, "right": n, "wrong": n}
102
+ self._vec = {}
103
+
104
+ def credit(self, sh, route, outcome):
105
+ k = key(sh, route)
106
+ e = self.entries.setdefault(k, {"shape": dict(sh), "route": route, "right": 0, "wrong": 0})
107
+ e["right" if outcome == "right" else "wrong"] += 1
108
+
109
+ def _v(self, k):
110
+ if k not in self._vec:
111
+ e = self.entries[k]
112
+ self._vec[k] = path_vec(e["shape"], e["route"])
113
+ return self._vec[k]
114
+
115
+ def recall(self, sh, route, floor=0.45):
116
+ """-> (right, wrong, resonance, key used) for this path; exact if stored, else the most resonant stored path with the
117
+ same route above the floor, else None"""
118
+ k = key(sh, route)
119
+ if k in self.entries:
120
+ e = self.entries[k]
121
+ return e["right"], e["wrong"], 1.0, k
122
+ q = path_vec(sh, route)
123
+ best, arg = -1.0, None
124
+ for kk, e in self.entries.items():
125
+ if e["route"] != route:
126
+ continue
127
+ r = float(np.real(np.vdot(self._v(kk), q)) / D)
128
+ if r > best:
129
+ best, arg = r, kk
130
+ if arg is None or best < floor:
131
+ return None
132
+ e = self.entries[arg]
133
+ return e["right"], e["wrong"], best, arg
134
+
135
+ def precision(self, sh, route, prior=(1, 1), min_n=6):
136
+ r = self.recall(sh, route)
137
+ if r is None or r[0] + r[1] < min_n:
138
+ return None
139
+ return (r[0] + prior[0]) / (r[0] + r[1] + prior[0] + prior[1]), r[0] + r[1]
140
+
141
+ def save(self, path):
142
+ json.dump({"D": D, "entries": self.entries}, open(path, "w"), indent=0)
143
+
144
+ @classmethod
145
+ def load(cls, path):
146
+ return cls(json.load(open(path))["entries"])
147
+
148
+
149
+ def build(files, out):
150
+ pm = PathMemory()
151
+ n = 0
152
+ for f in files:
153
+ for line in open(f):
154
+ r = json.loads(line)
155
+ sh = shape(r["q"])
156
+ for arm in ("head", "family"):
157
+ x = r.get(arm)
158
+ if not x or x.get("status") != "answered" or x.get("grade") not in ("right", "wrong"):
159
+ continue
160
+ pm.credit(sh, route_of(x["path"]), x["grade"])
161
+ n += 1
162
+ pm.save(out)
163
+ return pm, n
164
+
165
+
166
+ if __name__ == "__main__":
167
+ sys.path.insert(0, __import__("os").environ.get("ORACLE7_ROOT", "/opt/oracle-clm") + "/wander-train") # the runtime's relation readers - the shape must match the runtime's
168
+ import relation_readers_v1 # noqa: F401,E402 - fail loudly: a memory built without relation keys is a different memory
169
+ if sys.argv[1] == "build":
170
+ pm, n = build(sys.argv[2:-1], sys.argv[-1])
171
+ print("credited outcomes", n, "paths", len(pm.entries))
root/wander-train/pubchem_periodic_table.csv ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "AtomicNumber","Symbol","Name","AtomicMass","CPKHexColor","ElectronConfiguration","Electronegativity","AtomicRadius","IonizationEnergy","ElectronAffinity","OxidationStates","StandardState","MeltingPoint","BoilingPoint","Density","GroupBlock","YearDiscovered"
2
+ 1,"H","Hydrogen",1.0080,"FFFFFF","1s1",2.2,120,13.598,0.754,"+1, -1","Gas",13.81,20.28,0.00008988,"Nonmetal",1766
3
+ 2,"He","Helium",4.00260,"D9FFFF","1s2","",140,24.587,"","0","Gas",0.95,4.22,0.0001785,"Noble gas",1868
4
+ 3,"Li","Lithium",7.0,"CC80FF","[He]2s1",0.98,182,5.392,0.618,"+1","Solid",453.65,1615,0.534,"Alkali metal",1817
5
+ 4,"Be","Beryllium",9.012183,"C2FF00","[He]2s2",1.57,153,9.323,"","+2","Solid",1560,2744,1.85,"Alkaline earth metal",1798
6
+ 5,"B","Boron",10.81,"FFB5B5","[He]2s2 2p1",2.04,192,8.298,0.277,"+3","Solid",2348,4273,2.37,"Metalloid",1808
7
+ 6,"C","Carbon",12.011,"909090","[He]2s2 2p2",2.55,170,11.260,1.263,"+4, +2, -4","Solid",3823,4098,2.2670,"Nonmetal","Ancient"
8
+ 7,"N","Nitrogen",14.007,"3050F8","[He] 2s2 2p3",3.04,155,14.534,"","+5, +4, +3, +2, +1, -1, -2, -3","Gas",63.15,77.36,0.0012506,"Nonmetal",1772
9
+ 8,"O","Oxygen",15.999,"FF0D0D","[He]2s2 2p4",3.44,152,13.618,1.461,"-2","Gas",54.36,90.2,0.001429,"Nonmetal",1774
10
+ 9,"F","Fluorine",18.99840316,"90E050","[He]2s2 2p5",3.98,135,17.423,3.339,"-1","Gas",53.53,85.03,0.001696,"Halogen",1670
11
+ 10,"Ne","Neon",20.180,"B3E3F5","[He]2s2 2p6","",154,21.565,"","0","Gas",24.56,27.07,0.0008999,"Noble gas",1898
12
+ 11,"Na","Sodium",22.9897693,"AB5CF2","[Ne]3s1",0.93,227,5.139,0.548,"+1","Solid",370.95,1156,0.97,"Alkali metal",1807
13
+ 12,"Mg","Magnesium",24.305,"8AFF00","[Ne]3s2",1.31,173,7.646,"","+2","Solid",923,1363,1.74,"Alkaline earth metal",1808
14
+ 13,"Al","Aluminum",26.981538,"BFA6A6","[Ne]3s2 3p1",1.61,184,5.986,0.441,"+3","Solid",933.437,2792,2.70,"Post-transition metal","Ancient"
15
+ 14,"Si","Silicon",28.085,"F0C8A0","[Ne]3s2 3p2",1.9,210,8.152,1.385,"+4, +2, -4","Solid",1687,3538,2.3296,"Metalloid",1854
16
+ 15,"P","Phosphorus",30.97376200,"FF8000","[Ne]3s2 3p3",2.19,180,10.487,0.746,"+5, +3, -3","Solid",317.3,553.65,1.82,"Nonmetal",1669
17
+ 16,"S","Sulfur",32.07,"FFFF30","[Ne]3s2 3p4",2.58,180,10.360,2.077,"+6, +4, -2","Solid",388.36,717.75,2.067,"Nonmetal","Ancient"
18
+ 17,"Cl","Chlorine",35.45,"1FF01F","[Ne]3s2 3p5",3.16,175,12.968,3.617,"+7, +5, +1, -1","Gas",171.65,239.11,0.003214,"Halogen",1774
19
+ 18,"Ar","Argon",39.9,"80D1E3","[Ne]3s2 3p6","",188,15.760,"","0","Gas",83.8,87.3,0.0017837,"Noble gas",1894
20
+ 19,"K","Potassium",39.0983,"8F40D4","[Ar]4s1",0.82,275,4.341,0.501,"+1","Solid",336.53,1032,0.89,"Alkali metal",1807
21
+ 20,"Ca","Calcium",40.08,"3DFF00","[Ar]4s2",1,231,6.113,"","+2","Solid",1115,1757,1.54,"Alkaline earth metal","Ancient"
22
+ 21,"Sc","Scandium",44.95591,"E6E6E6","[Ar]4s2 3d1",1.36,211,6.561,0.188,"+3","Solid",1814,3109,2.99,"Transition metal",1879
23
+ 22,"Ti","Titanium",47.867,"BFC2C7","[Ar]4s2 3d2",1.54,187,6.828,0.079,"+4, +3, +2","Solid",1941,3560,4.5,"Transition metal",1791
24
+ 23,"V","Vanadium",50.9415,"A6A6AB","[Ar]4s2 3d3",1.63,179,6.746,0.525,"+5, +4, +3, +2","Solid",2183,3680,6.0,"Transition metal",1801
25
+ 24,"Cr","Chromium",51.996,"8A99C7","[Ar]3d5 4s1",1.66,189,6.767,0.666,"+6, +3, +2","Solid",2180,2944,7.15,"Transition metal",1797
26
+ 25,"Mn","Manganese",54.93804,"9C7AC7","[Ar]4s2 3d5",1.55,197,7.434,"","+7, +4, +3, +2","Solid",1519,2334,7.3,"Transition metal",1774
27
+ 26,"Fe","Iron",55.84,"E06633","[Ar]4s2 3d6",1.83,194,7.902,0.163,"+3, +2","Solid",1811,3134,7.874,"Transition metal","Ancient"
28
+ 27,"Co","Cobalt",58.93319,"F090A0","[Ar]4s2 3d7",1.88,192,7.881,0.661,"+3, +2","Solid",1768,3200,8.86,"Transition metal",1735
29
+ 28,"Ni","Nickel",58.693,"50D050","[Ar]4s2 3d8",1.91,163,7.640,1.156,"+3, +2","Solid",1728,3186,8.912,"Transition metal",1751
30
+ 29,"Cu","Copper",63.55,"C88033","[Ar]4s1 3d10",1.9,140,7.726,1.228,"+2, +1","Solid",1357.77,2835,8.933,"Transition metal","Ancient"
31
+ 30,"Zn","Zinc",65.4,"7D80B0","[Ar]4s2 3d10",1.65,139,9.394,"","+2","Solid",692.68,1180,7.134,"Transition metal",1746
32
+ 31,"Ga","Gallium",69.723,"C28F8F","[Ar]4s2 3d10 4p1",1.81,187,5.999,0.3,"+3","Solid",302.91,2477,5.91,"Post-transition metal",1875
33
+ 32,"Ge","Germanium",72.63,"668F8F","[Ar]4s2 3d10 4p2",2.01,211,7.900,1.35,"+4, +2","Solid",1211.4,3106,5.323,"Metalloid",1886
34
+ 33,"As","Arsenic",74.92159,"BD80E3","[Ar]4s2 3d10 4p3",2.18,185,9.815,0.81,"+5, +3, -3","Solid",1090,887,5.776,"Metalloid","Ancient"
35
+ 34,"Se","Selenium",78.97,"FFA100","[Ar]4s2 3d10 4p4",2.55,190,9.752,2.021,"+6, +4, -2","Solid",493.65,958,4.809,"Nonmetal",1817
36
+ 35,"Br","Bromine",79.90,"A62929","[Ar]4s2 3d10 4p5",2.96,183,11.814,3.365,"+5, +1, -1","Liquid",265.95,331.95,3.11,"Halogen",1826
37
+ 36,"Kr","Krypton",83.80,"5CB8D1","[Ar]4s2 3d10 4p6",3,202,14.000,"","0","Gas",115.79,119.93,0.003733,"Noble gas",1898
38
+ 37,"Rb","Rubidium",85.468,"702EB0","[Kr]5s1",0.82,303,4.177,0.468,"+1","Solid",312.46,961,1.53,"Alkali metal",1861
39
+ 38,"Sr","Strontium",87.62,"00FF00","[Kr]5s2",0.95,249,5.695,"","+2","Solid",1050,1655,2.64,"Alkaline earth metal",1790
40
+ 39,"Y","Yttrium",88.90584,"94FFFF","[Kr]5s2 4d1",1.22,219,6.217,0.307,"+3","Solid",1795,3618,4.47,"Transition metal",1794
41
+ 40,"Zr","Zirconium",91.22,"94E0E0","[Kr]5s2 4d2",1.33,186,6.634,0.426,"+4","Solid",2128,4682,6.52,"Transition metal",1789
42
+ 41,"Nb","Niobium",92.90637,"73C2C9","[Kr]5s1 4d4",1.6,207,6.759,0.893,"+5, +3","Solid",2750,5017,8.57,"Transition metal",1801
43
+ 42,"Mo","Molybdenum",95.95,"54B5B5","[Kr]5s1 4d5",2.16,209,7.092,0.746,"+6","Solid",2896,4912,10.2,"Transition metal",1778
44
+ 43,"Tc","Technetium",96.90636,"3B9E9E","[Kr]5s2 4d5",1.9,209,7.28,0.55,"+7, +6, +4","Solid",2430,4538,11,"Transition metal",1937
45
+ 44,"Ru","Ruthenium",101.1,"248F8F","[Kr]5s1 4d7",2.2,207,7.361,1.05,"+3","Solid",2607,4423,12.1,"Transition metal",1827
46
+ 45,"Rh","Rhodium",102.9055,"0A7D8C","[Kr]5s1 4d8",2.28,195,7.459,1.137,"+3","Solid",2237,3968,12.4,"Transition metal",1803
47
+ 46,"Pd","Palladium",106.42,"6985","[Kr]4d10",2.2,202,8.337,0.557,"+3, +2","Solid",1828.05,3236,12.0,"Transition metal",1803
48
+ 47,"Ag","Silver",107.868,"C0C0C0","[Kr]5s1 4d10",1.93,172,7.576,1.302,"+1","Solid",1234.93,2435,10.501,"Transition metal","Ancient"
49
+ 48,"Cd","Cadmium",112.41,"FFD98F","[Kr]5s2 4d10",1.69,158,8.994,"","+2","Solid",594.22,1040,8.69,"Transition metal",1817
50
+ 49,"In","Indium",114.818,"A67573","[Kr]5s2 4d10 5p1",1.78,193,5.786,0.3,"+3","Solid",429.75,2345,7.31,"Post-transition metal",1863
51
+ 50,"Sn","Tin",118.71,"668080","[Kr]5s2 4d10 5p2",1.96,217,7.344,1.2,"+4, +2","Solid",505.08,2875,7.287,"Post-transition metal","Ancient"
52
+ 51,"Sb","Antimony",121.760,"9E63B5","[Kr]5s2 4d10 5p3",2.05,206,8.64,1.07,"+5, +3, -3","Solid",903.78,1860,6.685,"Metalloid","Ancient"
53
+ 52,"Te","Tellurium",127.6,"D47A00","[Kr]5s2 4d10 5p4",2.1,206,9.010,1.971,"+6, +4, -2","Solid",722.66,1261,6.232,"Metalloid",1782
54
+ 53,"I","Iodine",126.9045,"940094","[Kr]5s2 4d10 5p5",2.66,198,10.451,3.059,"+7, +5, +1, -1","Solid",386.85,457.55,4.93,"Halogen",1811
55
+ 54,"Xe","Xenon",131.29,"429EB0","[Kr]5s2 4d10 5p6",2.6,216,12.130,"","0","Gas",161.36,165.03,0.005887,"Noble gas",1898
56
+ 55,"Cs","Cesium",132.9054520,"57178F","[Xe]6s1",0.79,343,3.894,0.472,"+1","Solid",301.59,944,1.93,"Alkali metal",1860
57
+ 56,"Ba","Barium",137.33,"00C900","[Xe]6s2",0.89,268,5.212,"","+2","Solid",1000,2170,3.62,"Alkaline earth metal",1808
58
+ 57,"La","Lanthanum",138.9055,"70D4FF","[Xe]6s2 5d1",1.1,240,5.577,0.5,"+3","Solid",1191,3737,6.15,"Lanthanide",1839
59
+ 58,"Ce","Cerium",140.116,"FFFFC7","[Xe]6s2 4f1 5d1",1.12,235,5.539,0.5,"+4, +3","Solid",1071,3697,6.770,"Lanthanide",1803
60
+ 59,"Pr","Praseodymium",140.90766,"D9FFC7","[Xe]6s2 4f3",1.13,239,5.464,"","+3","Solid",1204,3793,6.77,"Lanthanide",1885
61
+ 60,"Nd","Neodymium",144.24,"C7FFC7","[Xe]6s2 4f4",1.14,229,5.525,"","+3","Solid",1294,3347,7.01,"Lanthanide",1885
62
+ 61,"Pm","Promethium",144.91276,"A3FFC7","[Xe]6s2 4f5","",236,5.55,"","+3","Solid",1315,3273,7.26,"Lanthanide",1945
63
+ 62,"Sm","Samarium",150.4,"8FFFC7","[Xe]6s2 4f6",1.17,229,5.644,"","+3, +2","Solid",1347,2067,7.52,"Lanthanide",1879
64
+ 63,"Eu","Europium",151.964,"61FFC7","[Xe]6s2 4f7","",233,5.670,"","+3, +2","Solid",1095,1802,5.24,"Lanthanide",1901
65
+ 64,"Gd","Gadolinium",157.25,"45FFC7","[Xe]6s2 4f7 5d1",1.2,237,6.150,"","+3","Solid",1586,3546,7.90,"Lanthanide",1880
66
+ 65,"Tb","Terbium",158.92535,"30FFC7","[Xe]6s2 4f9","",221,5.864,"","+3","Solid",1629,3503,8.23,"Lanthanide",1843
67
+ 66,"Dy","Dysprosium",162.500,"1FFFC7","[Xe]6s2 4f10",1.22,229,5.939,"","+3","Solid",1685,2840,8.55,"Lanthanide",1886
68
+ 67,"Ho","Holmium",164.93033,"00FF9C","[Xe]6s2 4f11",1.23,216,6.022,"","+3","Solid",1747,2973,8.80,"Lanthanide",1878
69
+ 68,"Er","Erbium",167.26,"","[Xe]6s2 4f12",1.24,235,6.108,"","+3","Solid",1802,3141,9.07,"Lanthanide",1843
70
+ 69,"Tm","Thulium",168.93422,"00D452","[Xe]6s2 4f13",1.25,227,6.184,"","+3","Solid",1818,2223,9.32,"Lanthanide",1879
71
+ 70,"Yb","Ytterbium",173.05,"00BF38","[Xe]6s2 4f14","",242,6.254,"","+3, +2","Solid",1092,1469,6.90,"Lanthanide",1878
72
+ 71,"Lu","Lutetium",174.9667,"00AB24","[Xe]6s2 4f14 5d1",1.27,221,5.426,"","+3","Solid",1936,3675,9.84,"Lanthanide",1907
73
+ 72,"Hf","Hafnium",178.49,"4DC2FF","[Xe]6s2 4f14 5d2",1.3,212,6.825,"","+4","Solid",2506,4876,13.3,"Transition metal",1923
74
+ 73,"Ta","Tantalum",180.9479,"4DA6FF","[Xe]6s2 4f14 5d3",1.5,217,7.89,0.322,"+5","Solid",3290,5731,16.4,"Transition metal",1802
75
+ 74,"W","Tungsten",183.84,"2194D6","[Xe]6s2 4f14 5d4",2.36,210,7.98,0.815,"+6","Solid",3695,5828,19.3,"Transition metal",1783
76
+ 75,"Re","Rhenium",186.207,"267DAB","[Xe]6s2 4f14 5d5",1.9,217,7.88,0.15,"+7, +6, +4","Solid",3459,5869,20.8,"Transition metal",1925
77
+ 76,"Os","Osmium",190.2,"266696","[Xe]6s2 4f14 5d6",2.2,216,8.7,1.1,"+4, +3","Solid",3306,5285,22.57,"Transition metal",1803
78
+ 77,"Ir","Iridium",192.22,"175487","[Xe]6s2 4f14 5d7",2.2,202,9.1,1.565,"+4, +3","Solid",2719,4701,22.42,"Transition metal",1803
79
+ 78,"Pt","Platinum",195.08,"D0D0E0","[Xe]6s1 4f14 5d9",2.28,209,9,2.128,"+4, +2","Solid",2041.55,4098,21.46,"Transition metal",1735
80
+ 79,"Au","Gold",196.96657,"FFD123","[Xe]6s1 4f14 5d10",2.54,166,9.226,2.309,"+3, +1","Solid",1337.33,3129,19.282,"Transition metal","Ancient"
81
+ 80,"Hg","Mercury",200.59,"B8B8D0","[Xe]6s2 4f14 5d10",2,209,10.438,"","+2, +1","Liquid",234.32,629.88,13.5336,"Transition metal","Ancient"
82
+ 81,"Tl","Thallium",204.383,"A6544D","[Xe]6s2 4f14 5d10 6p1",1.62,196,6.108,0.2,"+3, +1","Solid",577,1746,11.8,"Post-transition metal",1861
83
+ 82,"Pb","Lead",207,"575961","[Xe]6s2 4f14 5d10 6p2",2.33,202,7.417,0.36,"+4, +2","Solid",600.61,2022,11.342,"Post-transition metal","Ancient"
84
+ 83,"Bi","Bismuth",208.98040,"9E4FB5","[Xe]6s2 4f14 5d10 6p3",2.02,207,7.289,0.946,"+5, +3","Solid",544.55,1837,9.807,"Post-transition metal",1753
85
+ 84,"Po","Polonium",208.98243,"AB5C00","[Xe]6s2 4f14 5d10 6p4",2,197,8.417,1.9,"+4, +2","Solid",527,1235,9.32,"Metalloid",1898
86
+ 85,"At","Astatine",209.98715,"754F45","[Xe]6s2 4f14 5d10 6p5",2.2,202,9.5,2.8,"7, 5, 3, 1, -1","Solid",575,"",7,"Halogen",1940
87
+ 86,"Rn","Radon",222.01758,"428296","[Xe]6s2 4f14 5d10 6p6","",220,10.745,"","0","Gas",202,211.45,0.00973,"Noble gas",1900
88
+ 87,"Fr","Francium",223.01973,"420066","[Rn]7s1",0.7,348,3.9,0.47,"+1","Solid",300,"","","Alkali metal",1939
89
+ 88,"Ra","Radium",226.02541,"007D00","[Rn]7s2",0.9,283,5.279,"","+2","Solid",973,1413,5,"Alkaline earth metal",1898
90
+ 89,"Ac","Actinium",227.02775,"70ABFA","[Rn]7s2 6d1",1.1,260,5.17,"","+3","Solid",1324,3471,10.07,"Actinide",1899
91
+ 90,"Th","Thorium",232.038,"00BAFF","[Rn]7s2 6d2",1.3,237,6.08,"","+4","Solid",2023,5061,11.72,"Actinide",1828
92
+ 91,"Pa","Protactinium",231.03588,"00A1FF","[Rn]7s2 5f2 6d1",1.5,243,5.89,"","+5, +4","Solid",1845,"",15.37,"Actinide",1913
93
+ 92,"U","Uranium",238.0289,"008FFF","[Rn]7s2 5f3 6d1",1.38,240,6.194,"","+6, +5, +4, +3","Solid",1408,4404,18.95,"Actinide",1789
94
+ 93,"Np","Neptunium",237.048172,"0080FF","[Rn]7s2 5f4 6d1",1.36,221,6.266,"","+6, +5, +4, +3","Solid",917,4175,20.25,"Actinide",1940
95
+ 94,"Pu","Plutonium",244.06420,"006BFF","[Rn]7s2 5f6",1.28,243,6.06,"","+6, +5, +4, +3","Solid",913,3501,19.84,"Actinide",1940
96
+ 95,"Am","Americium",243.061380,"545CF2","[Rn]7s2 5f7",1.3,244,5.993,"","+6, +5, +4, +3","Solid",1449,2284,13.69,"Actinide",1944
97
+ 96,"Cm","Curium",247.07035,"785CE3","[Rn]7s2 5f7 6d1",1.3,245,6.02,"","+3","Solid",1618,3400,13.51,"Actinide",1944
98
+ 97,"Bk","Berkelium",247.07031,"8A4FE3","[Rn]7s2 5f9",1.3,244,6.23,"","+4, +3","Solid",1323,"",14,"Actinide",1949
99
+ 98,"Cf","Californium",251.07959,"A136D4","[Rn]7s2 5f10",1.3,245,6.30,"","+3","Solid",1173,"","","Actinide",1950
100
+ 99,"Es","Einsteinium",252.0830,"B31FD4","[Rn]7s2 5f11",1.3,245,6.42,"","+3","Solid",1133,"","","Actinide",1952
101
+ 100,"Fm","Fermium",257.09511,"B31FBA","[Rn] 5f12 7s2",1.3,"",6.50,"","+3","Solid",1800,"","","Actinide",1952
102
+ 101,"Md","Mendelevium",258.09843,"B30DA6","[Rn]7s2 5f13",1.3,"",6.58,"","+3, +2","Solid",1100,"","","Actinide",1955
103
+ 102,"No","Nobelium",259.10100,"BD0D87","[Rn]7s2 5f14",1.3,"",6.65,"","+3, +2","Solid",1100,"","","Actinide",1957
104
+ 103,"Lr","Lawrencium",266.120,"C70066","[Rn]7s2 5f14 6d1",1.3,"","","","+3","Solid",1900,"","","Actinide",1961
105
+ 104,"Rf","Rutherfordium",267.122,"CC0059","[Rn]7s2 5f14 6d2","","","","","+4","Solid","","","","Transition metal",1964
106
+ 105,"Db","Dubnium",268.126,"D1004F","[Rn]7s2 5f14 6d3","","","","","5, 4, 3","Solid","","","","Transition metal",1967
107
+ 106,"Sg","Seaborgium",269.128,"D90045","[Rn]7s2 5f14 6d4","","","","","6, 5, 4, 3, 0","Solid","","","","Transition metal",1974
108
+ 107,"Bh","Bohrium",270.133,"E00038","[Rn]7s2 5f14 6d5","","","","","7, 5, 4, 3","Solid","","","","Transition metal",1976
109
+ 108,"Hs","Hassium",269.1336,"E6002E","[Rn]7s2 5f14 6d6","","","","","8, 6, 5, 4, 3, 2","Solid","","","","Transition metal",1984
110
+ 109,"Mt","Meitnerium",277.154,"EB0026","[Rn]7s2 5f14 6d7 (calculated)","","","","","9, 8, 6, 4, 3, 1","Solid","","","","Transition metal",1982
111
+ 110,"Ds","Darmstadtium",282.166,"","[Rn]7s2 5f14 6d8 (predicted)","","","","","8, 6, 4, 2, 0","Expected to be a Solid","","","","Transition metal",1994
112
+ 111,"Rg","Roentgenium",282.169,"","[Rn]7s2 5f14 6d9 (predicted)","","","","","5, 3, 1, -1","Expected to be a Solid","","","","Transition metal",1994
113
+ 112,"Cn","Copernicium",286.179,"","[Rn]7s2 5f14 6d10 (predicted)","","","","","2, 1, 0","Expected to be a Solid","","","","Transition metal",1996
114
+ 113,"Nh","Nihonium",286.182,"","[Rn]5f14 6d10 7s2 7p1 (predicted)","","","","","","Expected to be a Solid","","","","Post-transition metal",2004
115
+ 114,"Fl","Flerovium",290.192,"","[Rn]7s2 7p2 5f14 6d10 (predicted)","","","","","6, 4,2, 1, 0","Expected to be a Solid","","","","Post-transition metal",1998
116
+ 115,"Mc","Moscovium",290.196,"","[Rn]7s2 7p3 5f14 6d10 (predicted)","","","","","3, 1","Expected to be a Solid","","","","Post-transition metal",2003
117
+ 116,"Lv","Livermorium",293.205,"","[Rn]7s2 7p4 5f14 6d10 (predicted)","","","","","+4, +2, -2","Expected to be a Solid","","","","Post-transition metal",2000
118
+ 117,"Ts","Tennessine",294.211,"","[Rn]7s2 7p5 5f14 6d10 (predicted)","","","","","+5, +3, +1, -1","Expected to be a Solid","","","","Halogen",2010
119
+ 118,"Og","Oganesson",295.216,"","[Rn]7s2 7p6 5f14 6d10 (predicted)","","","","","+6, +4, +2, +1, 0, -1","Expected to be a Gas","","","","Noble gas",2006
root/wander-train/relation_forms_v2.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """relation_forms_v2 = v1 + forms for more store properties (see make_relation_forms_v2.py).
2
+ relation_forms_v1 - question FORMS that name their relation, answered through the typed relation store.
3
+
4
+ "Where is the Colosseum?", "Who discovered penicillin?", "When was the Eiffel Tower built?" carry no relation words the
5
+ mined lexicon can match, so the three-tree runtime found no relation and declined. The form itself names the relation,
6
+ in Wikidata's own vocabulary. Each form lists its properties in order of SPECIFICITY and they are tried ONE AT A TIME
7
+ (oracle_tritree_v2 only=True), because located-in (Rome) and country (Italy) are both true of the Colosseum and a joint
8
+ search would see two answers and abstain. Every check of the runtime still applies: answer type, one value, the
9
+ corroborating sentence from the subject's own article (rule A), the verb licence for dates (rule B).
10
+ answer(tt, q) -> (runtime answer, form name) | (None, None)"""
11
+ from __future__ import annotations
12
+
13
+ import re
14
+
15
+ FORMS = [
16
+ # v2 (2026-10-03): forms for more of the store's properties, most specific first
17
+ ("screenwriter", re.compile(r"\bwho\s+wrote\s+the\s+(?:screenplay|script)\b", re.I), [58]),
18
+ ("country", re.compile(r"\b(?:in\s+)?(?:which|what)\s+country\s+(?:is|are|was|were)\b(?!.*\b(?:born|from)\b)|"
19
+ r"\b(?:is|are)\s+(?:in|located\s+in)\s+(?:which|what)\s+country\b", re.I), [17]),
20
+ ("continent", re.compile(r"\b(?:on|in)\s+(?:which|what)\s+continent\b|\b(?:which|what)\s+continent\s+(?:is|are)\b", re.I), [30]),
21
+ ("creator", re.compile(r"\bwho\s+(?:painted|sculpted|drew|carved)\b", re.I), [170]),
22
+ ("performer", re.compile(r"\bwho\s+(?:sang|sings|performed|recorded)\b|\b(?:which|what)\s+(?:band|singer|group|rapper)\s+"
23
+ r"(?:released|recorded|sang|performed)\b", re.I), [175]),
24
+ ("producer", re.compile(r"\bwho\s+produced\b", re.I), [162]),
25
+ ("author_of", re.compile(r"\bwho\s+(?:is|was)\s+the\s+author\s+of\b", re.I), [50]),
26
+ ("spouse", re.compile(r"\bwho\s+(?:is|was)\s+.+?\s+married\s+to\b|\bwho\s+did\s+.+?\s+marry\b|"
27
+ r"\b(?:wife|husband|spouse)\s+of\b", re.I), [26]),
28
+ ("father", re.compile(r"\bwho\s+(?:is|was)\s+(?:the\s+)?father\s+of\b", re.I), [22]),
29
+ ("mother", re.compile(r"\bwho\s+(?:is|was)\s+(?:the\s+)?mother\s+of\b", re.I), [25]),
30
+ ("deathplace", re.compile(r"\bwhere\s+did\s+.+?\s+die\b", re.I), [20]),
31
+ ("nationality", re.compile(r"\bnationality\s+of\b|\bwhat\s+(?:is|was)\s+.+?(?:'s)?\s+nationality\b", re.I), [27]),
32
+ ("instrument", re.compile(r"\bwhat\s+instrument\s+(?:does|did)\b", re.I), [1303]),
33
+ ("sport", re.compile(r"\bwhat\s+sport\s+(?:does|did)\b", re.I), [641]),
34
+ ("headquarters", re.compile(r"\bheadquarter(?:s|ed)\b", re.I), [159]),
35
+ ("named_after", re.compile(r"\bnamed\s+after\s+(?:who|whom|what)\b|\bwho\s+(?:is|was)\s+.+?\s+named\s+after\b", re.I), [138]),
36
+ ("published", re.compile(r"\bwhen\s+was\s+.+?\s+(?:published|released)\b", re.I), [577]),
37
+ ("where", re.compile(r"^\s*where\s+(?:is|are|was|were|does|do)\b", re.I), [131, 276, 17, 30]),
38
+ ("discoverer", re.compile(r"\bwho\s+(?:first\s+)?(?:discovered|invented)\b", re.I), [61]),
39
+ ("architect", re.compile(r"\bwho\s+designed\b", re.I), [84, 170]), # "built" is not "designed"
40
+ ("founder", re.compile(r"\bwho\s+founded\b", re.I), [112]),
41
+ ("author", re.compile(r"\bwho\s+(?:wrote|authored)\b", re.I), [50]),
42
+ ("director", re.compile(r"\bwho\s+directed\b", re.I), [57]),
43
+ ("composer", re.compile(r"\bwho\s+composed\b", re.I), [86]),
44
+ ("inception", re.compile(r"\bwhen\s+(?:was|were|did)\b.*\b(?:built|founded|established|completed|opened|created|formed)\b",
45
+ re.I), [571]),
46
+ ("capital", re.compile(r"\bcapital\s+(?:city\s+)?of\b", re.I), [36]),
47
+ ("language", re.compile(r"\bwhat\s+(?:language|languages)\s+(?:is|are|do)\b.*\bspoken\b|\bofficial\s+language\s+of\b",
48
+ re.I), [37]),
49
+ ("currency", re.compile(r"\bcurrency\s+(?:of|in|does)\b", re.I), [38]),
50
+ ("birth", re.compile(r"\bwhen\s+was\s+.+?\s+born\b", re.I), [569]),
51
+ ("birthplace", re.compile(r"\bwhere\s+was\s+.+?\s+born\b", re.I), [19]),
52
+ ("death", re.compile(r"\bwhen\s+did\s+.+?\s+die\b", re.I), [570]),
53
+ ]
54
+
55
+
56
+ def form_of(q):
57
+ # a more specific form wins over the generic "where" (where was X born -> birthplace)
58
+ hits = [(name, props) for name, rx, props in FORMS if rx.search(q or "")]
59
+ if not hits:
60
+ return None
61
+ specific = [h for h in hits if h[0] != "where"]
62
+ return (specific or hits)[0]
63
+
64
+
65
+ ARG = { # where each form's ARGUMENT (the entity asked about) sits
66
+ "where": r"^\s*where\s+(?:is|are|was|were|does|do)\s+(?:the\s+)?(?P<x>.+?)(?:\s+(?:located|from|situated|found|live|lives|based))?\s*\??$",
67
+ "discoverer": r"\b(?:discovered|invented)\s+(?:the\s+)?(?P<x>.+?)\s*\??$",
68
+ "architect": r"\bdesigned\s+(?:the\s+)?(?P<x>.+?)\s*\??$",
69
+ "founder": r"\bfounded\s+(?:the\s+)?(?P<x>.+?)\s*\??$",
70
+ "author": r"\b(?:wrote|authored)\s+(?:the\s+)?(?P<x>.+?)\s*\??$",
71
+ "director": r"\bdirected\s+(?:the\s+)?(?P<x>.+?)\s*\??$",
72
+ "composer": r"\bcomposed\s+(?:the\s+)?(?P<x>.+?)\s*\??$",
73
+ "inception": r"\bwhen\s+(?:was|were|did)\s+(?:the\s+)?(?P<x>.+?)\s+(?:built|founded|established|completed|opened|created|formed)\b",
74
+ "capital": r"\bcapital\s+(?:city\s+)?of\s+(?:the\s+)?(?P<x>.+?)\s*\??$",
75
+ "language": r"\b(?:spoken\s+in|language\s+of|in)\s+(?:the\s+)?(?P<x>[^?]+?)\s*\??$",
76
+ "currency": r"\bcurrency\s+(?:of|in|does)\s+(?:the\s+)?(?P<x>.+?)(?:\s+use)?\s*\??$",
77
+ "birth": r"\bwhen\s+was\s+(?P<x>.+?)\s+born\b",
78
+ "birthplace": r"\bwhere\s+was\s+(?P<x>.+?)\s+born\b",
79
+ "death": r"\bwhen\s+did\s+(?P<x>.+?)\s+die\b",
80
+ }
81
+
82
+
83
+ def truecase(tt, q, name):
84
+ """a lowercase argument ("jamarcus russell") is replaced by the store's own title for it ("JaMarcus Russell"): the
85
+ reader links only capitalised spans, and people type lowercase. Only the form's ARGUMENT slot is touched."""
86
+ rx = ARG.get(name)
87
+ m = re.search(rx, q, re.I) if rx else None
88
+ if not m:
89
+ return q
90
+ x = m.group("x").strip(" ?.")
91
+ if not x or x[:1].isupper():
92
+ return q
93
+ for cand in (x, re.sub(r"^(?:the|a|an)\s+", "", x, flags=re.I)):
94
+ try:
95
+ qid = tt.rs.entity(cand)
96
+ except Exception: # noqa: BLE001
97
+ qid = None
98
+ if qid:
99
+ title = tt.rs.title(qid) or tt.rs.label(qid)
100
+ if title and title.lower().replace("_", " ") == cand.lower():
101
+ return q[:m.start("x")] + title + q[m.end("x"):]
102
+ return q
103
+
104
+
105
+ def answer(tt, q):
106
+ f = form_of(q)
107
+ if not f:
108
+ return None, None
109
+ name, props = f
110
+ q = truecase(tt, q, name)
111
+ for p in props:
112
+ try:
113
+ a = tt.answer(q, form_props={("fwd", p): 1}, only=True)
114
+ except Exception: # noqa: BLE001
115
+ a = None
116
+ if a:
117
+ return a, name
118
+ return None, None
root/wander-train/stage5c58_cycling_cognitive_substrate.py ADDED
@@ -0,0 +1,480 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """C58 typed bridge from C54-C57 conversation into the Stage 5M substrate.
3
+
4
+ The mutable store never enters a reasoning rollout. A turn receives an
5
+ immutable, handle-only snapshot; opaque handles traverse a stabilized HRR
6
+ selector; lexical payloads resolve only at Malkuth. Recalled hypothetical
7
+ claims remain user premises. Only explicitly provenance-locked semantic
8
+ records are eligible to be treated as authenticated facts.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import hashlib
14
+ import json
15
+ from dataclasses import dataclass
16
+ from typing import Any, Mapping, Sequence
17
+
18
+ import numpy as np
19
+
20
+ from ensouled._substrate.oracle_memory import (
21
+ OraclePersistentSubstrate,
22
+ SnapshotBundle,
23
+ canonical_json,
24
+ digest_value,
25
+ )
26
+ from oracle_10d_resolution_state import array_sha256
27
+ from stage5m_persistent_substrate_bridge import (
28
+ attach_memory_ingress,
29
+ release_memory_at_malkuth,
30
+ )
31
+ from train_stage2a_role_ladder import HRR_DIM, cosine, normalize, unitary
32
+
33
+
34
+ SCHEMA = "oracle-stage5c58-cycling-cognitive-substrate-v1"
35
+ MEMORY_CLASSES = (
36
+ "hypothetical_reasoning_episode",
37
+ "verified_factual_response_episode",
38
+ "provisional_definition",
39
+ "conversation_correction",
40
+ "unresolved_alternative",
41
+ "authenticated_fact",
42
+ )
43
+ PROVISIONAL_CLASSES = frozenset({
44
+ "hypothetical_reasoning_episode",
45
+ "provisional_definition",
46
+ "conversation_correction",
47
+ "unresolved_alternative",
48
+ })
49
+
50
+
51
+ def _surface_map(dialogue: Any) -> dict[str, str]:
52
+ resolver = dialogue.c56.resolver
53
+ session = dialogue.c56.session
54
+ values = {symbol: surface for surface, symbol in resolver.base_symbols.items()}
55
+ values.update({symbol: surface for surface, symbol in session.local_symbols.items()})
56
+ return values
57
+
58
+
59
+ def _event_key(prefix: str, payload: Mapping[str, Any]) -> str:
60
+ return f"c58:{prefix}:{digest_value(payload)}"
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class MemoryTransitAudit:
65
+ accepted: bool
66
+ selected_handle_id: str | None
67
+ expected_handle_id: str | None
68
+ minimum_cleanup_margin: float
69
+ moving_state_sha256: str
70
+ spine_state_sha256: str
71
+ use_spine: bool
72
+
73
+ def as_dict(self) -> dict[str, Any]:
74
+ return {
75
+ "accepted": self.accepted,
76
+ "selected_handle_id": self.selected_handle_id,
77
+ "expected_handle_id": self.expected_handle_id,
78
+ "minimum_cleanup_margin": self.minimum_cleanup_margin,
79
+ "moving_state_sha256": self.moving_state_sha256,
80
+ "spine_state_sha256": self.spine_state_sha256,
81
+ "stabilizing_spine_used": self.use_spine,
82
+ "plaintext_payloads_exposed": 0,
83
+ "pre_malkuth_unbinds": 0,
84
+ }
85
+
86
+
87
+ class OpaqueHandleTransit:
88
+ """Select one immutable snapshot handle without inspecting its payload."""
89
+
90
+ def __init__(self, *, stabilization_gain: float = 4.0):
91
+ self.stabilization_gain = float(stabilization_gain)
92
+
93
+ @staticmethod
94
+ def _state(handle_id: str) -> np.ndarray:
95
+ return unitary(f"stage5c58:opaque-memory-handle:{handle_id}")
96
+
97
+ def select(
98
+ self,
99
+ bundle: SnapshotBundle,
100
+ *,
101
+ use_spine: bool = True,
102
+ expected_index: int = 0,
103
+ ) -> MemoryTransitAudit:
104
+ handles = bundle.snapshot.handles
105
+ if not handles:
106
+ empty = unitary("stage5c58:empty-memory-transit")
107
+ return MemoryTransitAudit(
108
+ False, None, None, 0.0, array_sha256(empty), array_sha256(empty), use_spine,
109
+ )
110
+ states = tuple(self._state(value.handle_id) for value in handles)
111
+ weighted = np.zeros(HRR_DIM, dtype=np.complex64)
112
+ for state, handle in zip(states, handles, strict=True):
113
+ weighted += np.complex64(0.25 + max(0.0, handle.score)) * state
114
+ moving = normalize(weighted)
115
+ expected = handles[expected_index].handle_id
116
+ spine = states[expected_index]
117
+ probe = moving
118
+ if use_spine:
119
+ probe = normalize(probe + np.complex64(self.stabilization_gain) * spine)
120
+ scores = sorted(
121
+ ((cosine(probe, state), handle.handle_id) for state, handle in zip(states, handles, strict=True)),
122
+ reverse=True,
123
+ )
124
+ selected = scores[0][1]
125
+ margin = scores[0][0] - (scores[1][0] if len(scores) > 1 else 0.0)
126
+ return MemoryTransitAudit(
127
+ accepted=bool(selected == expected and margin > 0.50),
128
+ selected_handle_id=selected,
129
+ expected_handle_id=expected,
130
+ minimum_cleanup_margin=float(margin),
131
+ moving_state_sha256=array_sha256(moving),
132
+ spine_state_sha256=array_sha256(spine),
133
+ use_spine=use_spine,
134
+ )
135
+
136
+
137
+ @dataclass(frozen=True)
138
+ class CognitiveRecall:
139
+ authorized: bool
140
+ reason: str
141
+ handle_id: str | None
142
+ bank: str | None
143
+ handle_status: str | None
144
+ memory_class: str | None
145
+ roles: tuple[tuple[str, str], ...]
146
+ provenance: tuple[tuple[str, Any], ...]
147
+ snapshot_digest: str | None
148
+ transit: MemoryTransitAudit
149
+ ingress_sha256: str | None
150
+
151
+ def role_map(self) -> dict[str, str]:
152
+ return dict(self.roles)
153
+
154
+ def as_dict(self) -> dict[str, Any]:
155
+ return {
156
+ "schema_version": SCHEMA,
157
+ "authorized": self.authorized,
158
+ "reason": self.reason,
159
+ "handle_id": self.handle_id,
160
+ "bank": self.bank,
161
+ "handle_status": self.handle_status,
162
+ "memory_class": self.memory_class,
163
+ "snapshot_digest": self.snapshot_digest,
164
+ "transit": self.transit.as_dict(),
165
+ "ingress_sha256": self.ingress_sha256,
166
+ "lexical_payload_released_at_malkuth": bool(self.authorized),
167
+ "pre_malkuth_payload_fields": 0,
168
+ }
169
+
170
+
171
+ class CyclingCognitiveSubstrate:
172
+ def __init__(
173
+ self,
174
+ store: OraclePersistentSubstrate,
175
+ *,
176
+ transit: OpaqueHandleTransit | None = None,
177
+ ):
178
+ self.store = store
179
+ self.transit = transit or OpaqueHandleTransit()
180
+
181
+ def record_response(self, question: str, response: Mapping[str, Any], dialogue: Any) -> str:
182
+ surfaces = _surface_map(dialogue)
183
+ if response.get("action") == "hypothetical" and response.get(
184
+ "c57_inverse_verification", {}
185
+ ).get("accepted"):
186
+ germ = response["response_germ"]
187
+ claims = []
188
+ for claim in germ["claims"]:
189
+ claims.append({
190
+ "subject": surfaces[claim["subject_symbol"]],
191
+ "predicate": claim["predicate"],
192
+ "object": surfaces[claim["object_symbol"]],
193
+ "polarity": claim["polarity"],
194
+ "modality": claim["modality"],
195
+ "source_clause_sha256": claim["source_clause_sha256"],
196
+ })
197
+ roles = {
198
+ "root": question,
199
+ "source": surfaces[germ["source_symbol"]],
200
+ "field": claims[0]["object"],
201
+ "transition": canonical_json([value["predicate"] for value in claims]),
202
+ "result": canonical_json([
203
+ surfaces[value] for value in germ["consequence_symbols"]
204
+ ]),
205
+ "goal": germ["query_operation"],
206
+ "warrant": canonical_json(claims),
207
+ "action": "hypothetical",
208
+ "situation": "hypothetical_reasoning_episode",
209
+ "status": "verified_transform_nonfactual",
210
+ }
211
+ payload = {
212
+ "response_germ": germ["protected_invariant_sha256"],
213
+ "roles": roles,
214
+ }
215
+ return self.store.bind_event(
216
+ bank="episodic",
217
+ roles=roles,
218
+ source_record_id=f"c57:{germ['protected_invariant_sha256']}",
219
+ provenance={
220
+ "schema": "oracle-stage5c57-verified-morphogenetic-reply-v1",
221
+ "response_germ_sha256": germ["protected_invariant_sha256"],
222
+ "graph_invariant_sha256": germ["graph_invariant_sha256"],
223
+ "inverse_verification_sha256": response["reply"]["verification"][
224
+ "verification_sha256"
225
+ ],
226
+ "epistemic_class": "nonfactual_user_premise",
227
+ },
228
+ status="verified",
229
+ confidence=1.0,
230
+ event_key=_event_key("hypothetical", payload),
231
+ metadata={"memory_class": "hypothetical_reasoning_episode"},
232
+ )
233
+
234
+ if response.get("action") == "answer" and response.get(
235
+ "reply", {}
236
+ ).get("verification", {}).get("accepted"):
237
+ program = response.get("program", {})
238
+ roles = {
239
+ "root": question,
240
+ "source": str(program.get("source_symbol", "")),
241
+ "goal": str(program.get("goal_symbol", "")),
242
+ "transition": canonical_json(program.get("relation_program", [])),
243
+ "result": str(response.get("endpoint", "")),
244
+ "warrant": canonical_json(program.get("fact_ids", [])),
245
+ "action": "answer",
246
+ "situation": "verified_factual_response_episode",
247
+ "status": "verified_response_not_fact_storage",
248
+ }
249
+ payload = {"program": program, "roles": roles}
250
+ return self.store.bind_event(
251
+ bank="episodic",
252
+ roles=roles,
253
+ source_record_id=f"c51:{response['reply'].get('authorization_sha256', '')}",
254
+ provenance={
255
+ "schema": "oracle-stage5c51-verified-variable-science-reply-v1",
256
+ "fact_ids": program.get("fact_ids", []),
257
+ "authorization_sha256": response["reply"].get("authorization_sha256"),
258
+ "epistemic_class": "conversation_episode_not_fact_storage",
259
+ },
260
+ status="verified",
261
+ confidence=1.0,
262
+ event_key=_event_key("factual_response", payload),
263
+ metadata={"memory_class": "verified_factual_response_episode"},
264
+ )
265
+
266
+ roles = {
267
+ "root": question,
268
+ "result": str(response.get("reason", response.get("action", "unresolved"))),
269
+ "action": str(response.get("action", "clarify")),
270
+ "situation": "unresolved_alternative",
271
+ "status": "unverified",
272
+ }
273
+ payload = {"roles": roles, "alternatives": response.get("resolved_utterance", {}).get("alternatives", [])}
274
+ return self.store.bind_event(
275
+ bank="quarantine",
276
+ roles=roles,
277
+ source_record_id=f"c58:unresolved:{hashlib.sha256(question.encode('utf-8')).hexdigest()}",
278
+ provenance={"schema": str(response.get("schema_version", "unknown"))},
279
+ status="unverified",
280
+ confidence=0.25,
281
+ event_key=_event_key("unresolved", payload),
282
+ metadata={"memory_class": "unresolved_alternative"},
283
+ )
284
+
285
+ def record_provisional_definition(
286
+ self,
287
+ *,
288
+ term: str,
289
+ definition: str,
290
+ source_record_id: str,
291
+ ) -> str:
292
+ roles = {
293
+ "source": term,
294
+ "result": definition,
295
+ "situation": "provisional_definition",
296
+ "status": "unverified_user_definition",
297
+ }
298
+ return self.store.bind_event(
299
+ bank="episodic",
300
+ roles=roles,
301
+ source_record_id=source_record_id,
302
+ provenance={"epistemic_class": "provisional_user_definition"},
303
+ status="unverified",
304
+ confidence=0.5,
305
+ event_key=_event_key("definition", roles),
306
+ metadata={"memory_class": "provisional_definition"},
307
+ )
308
+
309
+ def record_correction(
310
+ self,
311
+ *,
312
+ topic: str,
313
+ correction: str,
314
+ supersedes_memory_id: str,
315
+ source_record_id: str,
316
+ ) -> str:
317
+ roles = {
318
+ "source": topic,
319
+ "correction": correction,
320
+ "situation": "conversation_correction",
321
+ "status": "unverified_user_correction",
322
+ }
323
+ return self.store.bind_event(
324
+ bank="episodic",
325
+ roles=roles,
326
+ source_record_id=source_record_id,
327
+ provenance={
328
+ "epistemic_class": "provisional_user_correction",
329
+ "supersedes_memory_id": supersedes_memory_id,
330
+ },
331
+ status="unverified",
332
+ confidence=0.6,
333
+ event_key=_event_key("correction", roles),
334
+ contradictions=(supersedes_memory_id,),
335
+ metadata={"memory_class": "conversation_correction"},
336
+ )
337
+
338
+ def record_authenticated_fact(
339
+ self,
340
+ *,
341
+ source: str,
342
+ relation: str,
343
+ destination: str,
344
+ fact_id: str,
345
+ evidence_sha256: str,
346
+ source_document_sha256: str,
347
+ ) -> str:
348
+ if not fact_id or len(evidence_sha256) != 64 or len(source_document_sha256) != 64:
349
+ raise ValueError("authenticated C58 facts require immutable fact and evidence provenance")
350
+ roles = {
351
+ "source": source,
352
+ "transition": relation,
353
+ "result": destination,
354
+ "warrant": evidence_sha256,
355
+ "provenance": source_document_sha256,
356
+ "situation": "authenticated_fact",
357
+ "status": "verified",
358
+ }
359
+ return self.store.bind_event(
360
+ bank="semantic",
361
+ roles=roles,
362
+ source_record_id=fact_id,
363
+ provenance={
364
+ "fact_id": fact_id,
365
+ "evidence_sha256": evidence_sha256,
366
+ "source_document_sha256": source_document_sha256,
367
+ "epistemic_class": "authenticated_fact",
368
+ },
369
+ status="verified",
370
+ confidence=1.0,
371
+ event_key=_event_key("authenticated_fact", roles),
372
+ metadata={"memory_class": "authenticated_fact"},
373
+ )
374
+
375
+ def recall(
376
+ self,
377
+ query_roles: Mapping[str, str],
378
+ *,
379
+ root_sha256: str,
380
+ banks: Sequence[str] = ("episodic",),
381
+ allowed_classes: Sequence[str] = ("hypothetical_reasoning_episode",),
382
+ top_k: int = 8,
383
+ request_nonce: str | None = None,
384
+ use_spine: bool = True,
385
+ ) -> CognitiveRecall:
386
+ bundle = self.store.create_snapshot(
387
+ query_roles,
388
+ top_k=top_k,
389
+ banks=banks,
390
+ request_nonce=request_nonce,
391
+ minimum_score=0.0,
392
+ )
393
+ transit = self.transit.select(bundle, use_spine=use_spine)
394
+ if not transit.accepted or transit.selected_handle_id is None:
395
+ return CognitiveRecall(
396
+ False, "opaque_handle_transit_failed", None, None, None, None,
397
+ (), (), bundle.snapshot.snapshot_digest, transit, None,
398
+ )
399
+ tree = {
400
+ "id": f"c58-memory-tree:{root_sha256[:20]}",
401
+ "protected_roles": {"root": root_sha256},
402
+ "dual_rail_contract": {
403
+ "content_rail": {},
404
+ "stabilizing_spine": {
405
+ "stage4:spine:root": root_sha256,
406
+ "stage4:spine:unbind_permission": "malkuth_only",
407
+ },
408
+ },
409
+ "payload_contract": {
410
+ "raw_payload_text_present": False,
411
+ "malkuth_only_resolution": True,
412
+ },
413
+ }
414
+ augmented = attach_memory_ingress(
415
+ tree, bundle, expected_agent_id=self.store.agent_id,
416
+ )
417
+ release = release_memory_at_malkuth(
418
+ augmented,
419
+ bundle,
420
+ transit.selected_handle_id,
421
+ malkuth_entered=True,
422
+ )
423
+ handle = next(
424
+ value for value in bundle.snapshot.handles
425
+ if value.handle_id == transit.selected_handle_id
426
+ )
427
+ roles = release.role_map() if release.authorized else {}
428
+ memory_class = roles.get("situation")
429
+ authorized = bool(release.authorized and memory_class in set(allowed_classes))
430
+ reason = (
431
+ "authorized" if authorized
432
+ else "memory_class_not_authorized" if release.authorized
433
+ else release.reason
434
+ )
435
+ return CognitiveRecall(
436
+ authorized=authorized,
437
+ reason=reason,
438
+ handle_id=handle.handle_id,
439
+ bank=handle.bank,
440
+ handle_status=handle.status,
441
+ memory_class=memory_class,
442
+ roles=release.roles if authorized else (),
443
+ provenance=release.provenance if authorized else (),
444
+ snapshot_digest=bundle.snapshot.snapshot_digest,
445
+ transit=transit,
446
+ ingress_sha256=digest_value(augmented),
447
+ )
448
+
449
+ @staticmethod
450
+ def fact_release_eligible(recall: CognitiveRecall) -> bool:
451
+ if not recall.authorized:
452
+ return False
453
+ provenance = dict(recall.provenance)
454
+ return bool(
455
+ recall.bank == "semantic"
456
+ and recall.handle_status == "verified"
457
+ and recall.memory_class == "authenticated_fact"
458
+ and provenance.get("epistemic_class") == "authenticated_fact"
459
+ and provenance.get("fact_id")
460
+ and len(str(provenance.get("evidence_sha256", ""))) == 64
461
+ and len(str(provenance.get("source_document_sha256", ""))) == 64
462
+ )
463
+
464
+ def audit(self) -> dict[str, Any]:
465
+ inspected = self.store.inspect()
466
+ return {
467
+ "schema_version": SCHEMA,
468
+ "agent_id": self.store.agent_id,
469
+ "dim": inspected["dim"],
470
+ "state_version": inspected["version"],
471
+ "cycle_count": inspected["cycle_count"],
472
+ "state_digest": inspected["state_digest"],
473
+ "entries": inspected["entries"],
474
+ "implicit_norms": inspected["implicit_norms"],
475
+ "memory_classes": list(MEMORY_CLASSES),
476
+ "provisional_classes": sorted(PROVISIONAL_CLASSES),
477
+ "automatic_fact_promotion_paths": 0,
478
+ "mutable_store_enters_tree": False,
479
+ "malkuth_only_payload_release": True,
480
+ }
root/wander-train/stage5h1_generic_dataset_expert.py ADDED
@@ -0,0 +1,377 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Format-agnostic, provenance-locked bounded dataset expert compiler.
3
+
4
+ The compiler does not infer semantics from unnamed columns. A small typed
5
+ schema identifies keys, field types, units, and optional relational views;
6
+ CSV, TSV, and JSONL rows then compile into sharded HRR field memories and
7
+ ordinary C43 expert facts without updating the carrier.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import csv
13
+ import hashlib
14
+ import json
15
+ import math
16
+ import time
17
+ from collections import deque
18
+ from dataclasses import dataclass
19
+ from pathlib import Path
20
+ from typing import Any, Iterable, Mapping
21
+
22
+ import numpy as np
23
+
24
+ from stage5c43_corpus_expert import CorpusFact
25
+ from train_stage2a_role_ladder import HRR_DIM, bind, cosine, normalize, unbind, unitary
26
+
27
+
28
+ SCHEMA = "oracle-stage5h1-generic-dataset-expert-v1"
29
+ SPEC_SCHEMA = "oracle-stage5h1-generic-dataset-spec-v1"
30
+
31
+
32
+ def sha(path: Path) -> str:
33
+ return hashlib.sha256(path.read_bytes()).hexdigest()
34
+
35
+
36
+ def canonical_json(value: Any) -> str:
37
+ return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
38
+
39
+
40
+ def digest(value: Any) -> str:
41
+ return hashlib.sha256(canonical_json(value).encode("utf-8")).hexdigest()
42
+
43
+
44
+ def canonical_scalar(value: Any, kind: str) -> str:
45
+ if kind == "string":
46
+ result = " ".join(str(value).strip().split())
47
+ if not result:
48
+ raise ValueError("empty string field")
49
+ return result
50
+ if kind == "integer":
51
+ return str(int(value))
52
+ if kind == "number":
53
+ number = float(value)
54
+ if not math.isfinite(number):
55
+ raise ValueError("non-finite numeric field")
56
+ return format(number, ".12g")
57
+ if kind == "boolean":
58
+ if isinstance(value, bool):
59
+ return "true" if value else "false"
60
+ lowered = str(value).strip().casefold()
61
+ if lowered not in {"true", "false", "1", "0", "yes", "no"}:
62
+ raise ValueError(f"invalid boolean: {value!r}")
63
+ return "true" if lowered in {"true", "1", "yes"} else "false"
64
+ raise ValueError(f"unsupported field type: {kind}")
65
+
66
+
67
+ def value_symbol(field: str, value: str) -> np.ndarray:
68
+ return unitary(f"stage5h1:value:{field}:{value}")
69
+
70
+
71
+ def key_symbol(expert_id: str, key: str) -> np.ndarray:
72
+ return normalize(bind(
73
+ unitary(f"stage5h1:expert:{expert_id}"), unitary(f"stage5h1:key:{key}"),
74
+ ))
75
+
76
+
77
+ @dataclass(frozen=True)
78
+ class FieldSpec:
79
+ name: str
80
+ kind: str
81
+ unit: str | None = None
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class EdgeView:
86
+ name: str
87
+ source_field: str
88
+ destination_field: str
89
+ relation_field: str | None = None
90
+ relation_constant: str | None = None
91
+
92
+
93
+ @dataclass(frozen=True)
94
+ class DatasetSchema:
95
+ expert_id: str
96
+ dataset_id: str
97
+ data_path: str
98
+ data_sha256: str
99
+ format: str
100
+ primary_key: tuple[str, ...]
101
+ fields: tuple[FieldSpec, ...]
102
+ competencies: tuple[str, ...]
103
+ source_url: str
104
+ source_split: str
105
+ edge_views: tuple[EdgeView, ...]
106
+ shard_rows: int = 48
107
+
108
+ @classmethod
109
+ def from_dict(cls, value: Mapping[str, Any]) -> "DatasetSchema":
110
+ edges = tuple(EdgeView(
111
+ name=str(item["name"]), source_field=str(item["source_field"]),
112
+ destination_field=str(item["destination_field"]),
113
+ relation_field=str(item["relation_field"]) if item.get("relation_field") else None,
114
+ relation_constant=str(item["relation_constant"]) if item.get("relation_constant") else None,
115
+ ) for item in value.get("edge_views", ()))
116
+ for edge in edges:
117
+ if bool(edge.relation_field) == bool(edge.relation_constant):
118
+ raise ValueError("edge view requires exactly one relation source")
119
+ return cls(
120
+ expert_id=str(value["expert_id"]), dataset_id=str(value["dataset_id"]),
121
+ data_path=str(value["data_path"]), data_sha256=str(value["data_sha256"]),
122
+ format=str(value["format"]), primary_key=tuple(str(item) for item in value["primary_key"]),
123
+ fields=tuple(FieldSpec(str(item["name"]), str(item["type"]), item.get("unit")) for item in value["fields"]),
124
+ competencies=tuple(str(item) for item in value.get("competencies", ())),
125
+ source_url=str(value["source_url"]), source_split=str(value.get("source_split", "train")),
126
+ edge_views=edges, shard_rows=int(value.get("shard_rows", 48)),
127
+ )
128
+
129
+
130
+ def read_records(path: Path, format_name: str) -> list[dict[str, Any]]:
131
+ if format_name in {"csv", "tsv"}:
132
+ with path.open("r", encoding="utf-8", newline="") as handle:
133
+ reader = csv.DictReader(handle, delimiter="," if format_name == "csv" else "\t")
134
+ return [dict(row) for row in reader]
135
+ if format_name == "jsonl":
136
+ return [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
137
+ raise ValueError(f"unsupported bounded dataset format: {format_name}")
138
+
139
+
140
+ def normalize_records(schema: DatasetSchema, records: Iterable[Mapping[str, Any]]) -> tuple[dict[str, str], ...]:
141
+ field_map = {field.name: field for field in schema.fields}
142
+ if not schema.primary_key or any(value not in field_map for value in schema.primary_key):
143
+ raise ValueError("invalid primary key declaration")
144
+ for edge in schema.edge_views:
145
+ needed = {edge.source_field, edge.destination_field} | ({edge.relation_field} if edge.relation_field else set())
146
+ if not needed <= set(field_map):
147
+ raise ValueError(f"edge view {edge.name} references an unknown field")
148
+ normalized = []
149
+ seen = {}
150
+ for ordinal, record in enumerate(records):
151
+ if set(record) != set(field_map):
152
+ raise ValueError(f"row {ordinal} fields differ from declared schema")
153
+ row = {name: canonical_scalar(record[name], field.kind) for name, field in field_map.items()}
154
+ key = canonical_json([row[name] for name in schema.primary_key])
155
+ previous = seen.get(key)
156
+ if previous is not None and previous != row:
157
+ raise ValueError(f"conflicting duplicate primary key at row {ordinal}")
158
+ if previous is not None:
159
+ raise ValueError(f"duplicate primary key at row {ordinal}")
160
+ seen[key] = row; normalized.append(row)
161
+ if not normalized:
162
+ raise ValueError("bounded dataset contains no records")
163
+ return tuple(normalized)
164
+
165
+
166
+ @dataclass(frozen=True)
167
+ class DatasetExpert:
168
+ schema: DatasetSchema
169
+ rows: tuple[dict[str, str], ...]
170
+ row_keys: tuple[str, ...]
171
+ row_provenance: tuple[str, ...]
172
+ shard_memories: np.ndarray
173
+ field_names: tuple[str, ...]
174
+ candidate_values: Mapping[str, tuple[str, ...]]
175
+ content_sha256: str
176
+ training_seconds: float
177
+ file_sha256: str | None = None
178
+
179
+ @property
180
+ def shards(self) -> int:
181
+ return self.shard_memories.shape[0]
182
+
183
+ def _position(self, key_values: Mapping[str, Any]) -> int | None:
184
+ try:
185
+ field = {value.name: value for value in self.schema.fields}
186
+ key = canonical_json([canonical_scalar(key_values[name], field[name].kind) for name in self.schema.primary_key])
187
+ except (KeyError, ValueError, TypeError):
188
+ return None
189
+ try:
190
+ return self.row_keys.index(key)
191
+ except ValueError:
192
+ return None
193
+
194
+ def query(self, key_values: Mapping[str, Any], field_name: str, *, rotate_field: int = 0) -> dict[str, Any]:
195
+ position = self._position(key_values)
196
+ if position is None:
197
+ return {"released": False, "reason": "unknown_primary_key"}
198
+ if field_name not in self.field_names:
199
+ return {"released": False, "reason": "unknown_field"}
200
+ field_index = self.field_names.index(field_name)
201
+ memory_index = (field_index + rotate_field) % len(self.field_names)
202
+ shard = position // self.schema.shard_rows
203
+ recovered = unbind(
204
+ self.shard_memories[shard, memory_index],
205
+ key_symbol(self.schema.expert_id, self.row_keys[position]),
206
+ )
207
+ candidates = self.candidate_values[self.field_names[memory_index]]
208
+ scores = np.asarray([cosine(recovered, value_symbol(self.field_names[memory_index], value)) for value in candidates])
209
+ order = np.argsort(scores)[::-1]
210
+ winner = int(order[0]); rival = float(scores[order[1]]) if len(order) > 1 else 0.0
211
+ return {
212
+ "released": True, "value": candidates[winner], "requested_field": field_name,
213
+ "memory_field": self.field_names[memory_index], "margin": float(scores[winner] - rival),
214
+ "row_provenance_sha256": self.row_provenance[position], "shard": shard,
215
+ }
216
+
217
+ def edges(self, view_name: str) -> tuple[dict[str, str], ...]:
218
+ view = next((value for value in self.schema.edge_views if value.name == view_name), None)
219
+ if view is None:
220
+ return ()
221
+ values = []
222
+ for row, provenance in zip(self.rows, self.row_provenance):
223
+ relation = row[view.relation_field] if view.relation_field else str(view.relation_constant)
224
+ values.append({
225
+ "source": row[view.source_field], "relation": relation,
226
+ "destination": row[view.destination_field], "provenance_sha256": provenance,
227
+ })
228
+ return tuple(values)
229
+
230
+ def find_path(self, view_name: str, source: str, destination: str, maximum_depth: int = 8) -> dict[str, Any]:
231
+ adjacency: dict[str, list[dict[str, str]]] = {}
232
+ for edge in self.edges(view_name):
233
+ adjacency.setdefault(edge["source"], []).append(edge)
234
+ queue = deque([(str(source), [])]); seen = {str(source)}
235
+ while queue:
236
+ current, path = queue.popleft()
237
+ if current == str(destination):
238
+ return {"released": True, "path": path, "depth": len(path)}
239
+ if len(path) >= maximum_depth:
240
+ continue
241
+ for edge in sorted(adjacency.get(current, ()), key=lambda value: canonical_json(value)):
242
+ if edge["destination"] in seen:
243
+ continue
244
+ seen.add(edge["destination"]); queue.append((edge["destination"], path + [edge]))
245
+ return {"released": False, "reason": "no_bounded_path"}
246
+
247
+ def corpus_facts(self) -> tuple[CorpusFact, ...]:
248
+ facts = []
249
+ for view in self.schema.edge_views:
250
+ for ordinal, edge in enumerate(self.edges(view.name)):
251
+ evidence = canonical_json(self.rows[ordinal])
252
+ facts.append(CorpusFact(
253
+ fact_id=f"h1_{digest([self.schema.expert_id, view.name, ordinal])[:24]}",
254
+ expert_id=self.schema.expert_id, source_document_id=self.schema.dataset_id,
255
+ source_document_sha256=self.schema.data_sha256, source_url=self.schema.source_url,
256
+ source_split=self.schema.source_split, source=edge["source"], relation=edge["relation"],
257
+ destination=edge["destination"], evidence=evidence,
258
+ competencies=(f"expert_scope:{self.schema.expert_id}", *self.schema.competencies),
259
+ provenance_sha256=edge["provenance_sha256"],
260
+ ))
261
+ return tuple(facts)
262
+
263
+ def partitioned_corpus_facts(self, maximum_facts: int = 8) -> tuple[tuple[CorpusFact, ...], ...]:
264
+ """Return independently addressable C43 partitions for capacity-safe loading.
265
+
266
+ A C43 capsule contains one superposed fact memory, so an unbounded table
267
+ must not be forced into one capsule. The dataset key router selects the
268
+ partition; every partition remains an ordinary discardable C43 expert.
269
+ """
270
+ if maximum_facts < 1:
271
+ raise ValueError("maximum_facts must be positive")
272
+ facts = self.corpus_facts()
273
+ partitions = []
274
+ for start in range(0, len(facts), maximum_facts):
275
+ partition_index = start // maximum_facts
276
+ partition_id = f"{self.schema.expert_id}__partition_{partition_index:04d}"
277
+ rewritten = []
278
+ for fact in facts[start:start + maximum_facts]:
279
+ rewritten.append(CorpusFact(
280
+ fact_id=fact.fact_id,
281
+ expert_id=partition_id,
282
+ source_document_id=fact.source_document_id,
283
+ source_document_sha256=fact.source_document_sha256,
284
+ source_url=fact.source_url,
285
+ source_split=fact.source_split,
286
+ source=fact.source,
287
+ relation=fact.relation,
288
+ destination=fact.destination,
289
+ evidence=fact.evidence,
290
+ competencies=(f"parent_expert:{self.schema.expert_id}", *fact.competencies),
291
+ provenance_sha256=fact.provenance_sha256,
292
+ ))
293
+ partitions.append(tuple(rewritten))
294
+ return tuple(partitions)
295
+
296
+ def save(self, path: Path) -> str:
297
+ metadata = {
298
+ "schema_version": SCHEMA, "schema": {
299
+ "expert_id": self.schema.expert_id, "dataset_id": self.schema.dataset_id,
300
+ "data_path": self.schema.data_path, "data_sha256": self.schema.data_sha256,
301
+ "format": self.schema.format, "primary_key": list(self.schema.primary_key),
302
+ "fields": [{"name": value.name, "type": value.kind, "unit": value.unit} for value in self.schema.fields],
303
+ "competencies": list(self.schema.competencies), "source_url": self.schema.source_url,
304
+ "source_split": self.schema.source_split,
305
+ "edge_views": [value.__dict__ for value in self.schema.edge_views], "shard_rows": self.schema.shard_rows,
306
+ }, "rows": list(self.rows), "row_keys": list(self.row_keys),
307
+ "row_provenance": list(self.row_provenance), "field_names": list(self.field_names),
308
+ "candidate_values": {key: list(value) for key, value in self.candidate_values.items()},
309
+ "content_sha256": self.content_sha256, "training_seconds": self.training_seconds,
310
+ }
311
+ np.savez_compressed(path, metadata=np.asarray(json.dumps(metadata, sort_keys=True)), shard_memories=self.shard_memories.astype(np.complex64))
312
+ return sha(path)
313
+
314
+ @classmethod
315
+ def load(cls, path: Path) -> "DatasetExpert":
316
+ file_sha = sha(path)
317
+ with np.load(path, allow_pickle=False) as archive:
318
+ metadata = json.loads(str(archive["metadata"].item()))
319
+ memories = np.asarray(archive["shard_memories"], dtype=np.complex64)
320
+ if metadata.get("schema_version") != SCHEMA:
321
+ raise ValueError("unsupported H1 dataset expert checkpoint")
322
+ schema = DatasetSchema.from_dict(metadata["schema"])
323
+ result = cls(
324
+ schema, tuple(metadata["rows"]), tuple(metadata["row_keys"]), tuple(metadata["row_provenance"]),
325
+ memories, tuple(metadata["field_names"]), {key: tuple(value) for key, value in metadata["candidate_values"].items()},
326
+ str(metadata["content_sha256"]), float(metadata["training_seconds"]), file_sha,
327
+ )
328
+ expected = digest({"schema": metadata["schema"], "rows": metadata["rows"], "row_provenance": metadata["row_provenance"]})
329
+ if expected != result.content_sha256:
330
+ raise ValueError("H1 checkpoint content identity mismatch")
331
+ return result
332
+
333
+
334
+ def compile_dataset(schema: DatasetSchema, spec_directory: Path) -> DatasetExpert:
335
+ started = time.perf_counter()
336
+ path = (spec_directory / schema.data_path).resolve()
337
+ if not path.is_file() or sha(path) != schema.data_sha256:
338
+ raise ValueError(f"dataset bytes differ from registered source: {schema.dataset_id}")
339
+ rows = normalize_records(schema, read_records(path, schema.format))
340
+ row_keys = tuple(canonical_json([row[name] for name in schema.primary_key]) for row in rows)
341
+ row_provenance = tuple(digest({
342
+ "dataset_id": schema.dataset_id, "dataset_sha256": schema.data_sha256,
343
+ "ordinal": ordinal, "row": row,
344
+ }) for ordinal, row in enumerate(rows))
345
+ field_names = tuple(value.name for value in schema.fields)
346
+ candidate_values = {name: tuple(sorted({row[name] for row in rows})) for name in field_names}
347
+ shards = math.ceil(len(rows) / schema.shard_rows)
348
+ memories = np.zeros((shards, len(field_names), HRR_DIM), dtype=np.complex64)
349
+ for position, (key, row) in enumerate(zip(row_keys, rows)):
350
+ shard = position // schema.shard_rows
351
+ for field_index, name in enumerate(field_names):
352
+ memories[shard, field_index] += bind(key_symbol(schema.expert_id, key), value_symbol(name, row[name]))
353
+ for shard in range(shards):
354
+ for field_index in range(len(field_names)):
355
+ memories[shard, field_index] = normalize(memories[shard, field_index])
356
+ schema_dict = {
357
+ "expert_id": schema.expert_id, "dataset_id": schema.dataset_id, "data_path": schema.data_path,
358
+ "data_sha256": schema.data_sha256, "format": schema.format, "primary_key": list(schema.primary_key),
359
+ "fields": [{"name": value.name, "type": value.kind, "unit": value.unit} for value in schema.fields],
360
+ "competencies": list(schema.competencies), "source_url": schema.source_url, "source_split": schema.source_split,
361
+ "edge_views": [value.__dict__ for value in schema.edge_views], "shard_rows": schema.shard_rows,
362
+ }
363
+ return DatasetExpert(
364
+ schema, rows, row_keys, row_provenance, memories, field_names, candidate_values,
365
+ digest({"schema": schema_dict, "rows": list(rows), "row_provenance": list(row_provenance)}),
366
+ time.perf_counter() - started,
367
+ )
368
+
369
+
370
+ def compile_spec(path: Path) -> tuple[DatasetExpert, ...]:
371
+ value = json.loads(path.read_text(encoding="utf-8"))
372
+ if value.get("schema_version") != SPEC_SCHEMA:
373
+ raise ValueError("unsupported H1 dataset specification")
374
+ schemas = tuple(DatasetSchema.from_dict(item) for item in value["datasets"])
375
+ if len({value.expert_id for value in schemas}) != len(schemas):
376
+ raise ValueError("H1 specification contains duplicate expert ids")
377
+ return tuple(compile_dataset(schema, path.parent) for schema in schemas)
root/wander-train/stage5h3_hrr_relational_state_transducer.py ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """In-house, shape-persistent HRR relational-state transducer.
3
+
4
+ This module has no neural embedding dependency. Tokens, character fragments,
5
+ relative positions, and ordered local contexts are composed directly in the
6
+ canonical 1024-dimensional HRR algebra. Training updates a small bank of
7
+ relation-direction memories with an online discriminative rule. Runtime
8
+ output is a protected, role-bound HRR state; the relation lattice is recovered
9
+ from that state only after removing the protected root (the Malkuth boundary).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import hashlib
15
+ import json
16
+ import re
17
+ from dataclasses import dataclass
18
+ from pathlib import Path
19
+ from typing import Any, Iterable, Mapping, Sequence
20
+
21
+ import numpy as np
22
+
23
+ from train_stage2a_role_ladder import HRR_DIM, bind, cosine, normalize, unbind, unitary
24
+
25
+
26
+ SCHEMA = "oracle-stage5h3-hrr-relational-state-transducer-v1"
27
+ TOKENS = re.compile(r"[a-z0-9]+(?:'[a-z0-9]+)?|[^\w\s]", re.I)
28
+ RELATION_ROLE = unitary("stage5h3:role:relation")
29
+ QUERY_ROLE = unitary("stage5h3:role:query-shape")
30
+
31
+
32
+ def sha(path: Path) -> str:
33
+ return hashlib.sha256(path.read_bytes()).hexdigest()
34
+
35
+
36
+ def canonical_sha(value: Mapping[str, Any]) -> str:
37
+ return hashlib.sha256(
38
+ json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8")
39
+ ).hexdigest()
40
+
41
+
42
+ def read_jsonl(path: Path) -> list[dict[str, Any]]:
43
+ return [
44
+ json.loads(line)
45
+ for line in path.read_text(encoding="utf-8").splitlines()
46
+ if line.strip()
47
+ ]
48
+
49
+
50
+ def _surface_tokens(text: str) -> list[str]:
51
+ return [value.casefold() for value in TOKENS.findall(text)]
52
+
53
+
54
+ def _char_fragments(token: str) -> tuple[str, ...]:
55
+ bounded = "^" + token + "$"
56
+ if len(bounded) < 3:
57
+ return (bounded,)
58
+ return tuple(bounded[index:index + 3] for index in range(len(bounded) - 2))
59
+
60
+
61
+ def token_state(token: str) -> np.ndarray:
62
+ parts = [np.complex64(2.0) * unitary(f"stage5h3:token:{token}")]
63
+ parts.extend(
64
+ np.complex64(0.35) * unitary(f"stage5h3:char3:{fragment}")
65
+ for fragment in _char_fragments(token)
66
+ )
67
+ if token.endswith("s") and len(token) > 3:
68
+ parts.append(np.complex64(0.5) * unitary(f"stage5h3:stem:{token[:-1]}"))
69
+ if token.endswith("ed") and len(token) > 4:
70
+ parts.append(np.complex64(0.5) * unitary(f"stage5h3:stem:{token[:-2]}"))
71
+ if token.endswith("ing") and len(token) > 5:
72
+ parts.append(np.complex64(0.5) * unitary(f"stage5h3:stem:{token[:-3]}"))
73
+ return normalize(sum(parts, np.zeros(HRR_DIM, dtype=np.complex64)))
74
+
75
+
76
+ def _position_role(index: int, length: int) -> np.ndarray:
77
+ if length <= 1:
78
+ bucket = 0
79
+ else:
80
+ bucket = min(15, int(round(15 * index / (length - 1))))
81
+ return unitary(f"stage5h3:relative-position:{bucket:02d}")
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class EncodedShape:
86
+ state: np.ndarray
87
+ trajectory_root_minimum: float
88
+ token_count: int
89
+
90
+
91
+ def encode_shape(text: str, *, destroy_order: bool = False) -> EncodedShape:
92
+ tokens = _surface_tokens(text)
93
+ if not tokens:
94
+ return EncodedShape(unitary("stage5h3:empty-query"), 1.0, 0)
95
+ if destroy_order:
96
+ tokens = sorted(tokens)
97
+ lexical = [token_state(token) for token in tokens]
98
+ recurrent = unitary("stage5h3:trajectory-root")
99
+ trajectory_root = recurrent.copy()
100
+ aggregate = np.zeros(HRR_DIM, dtype=np.complex64)
101
+ ordered_context = np.zeros(HRR_DIM, dtype=np.complex64)
102
+ root_scores: list[float] = []
103
+ for index, current in enumerate(lexical):
104
+ positioned = bind(_position_role(index, len(lexical)), current)
105
+ event = positioned
106
+ if index:
107
+ pair = normalize(
108
+ bind(unitary("stage5h3:pair:left"), lexical[index - 1])
109
+ + bind(unitary("stage5h3:pair:right"), current)
110
+ )
111
+ event = event + np.complex64(0.8) * bind(
112
+ unitary(f"stage5h3:pair-position:{min(index, 15):02d}"), pair
113
+ )
114
+ ordered_context += pair
115
+ aggregate += current + np.complex64(0.45) * positioned
116
+ moving = normalize(
117
+ np.complex64(0.82) * recurrent
118
+ + np.complex64(1.0) * event
119
+ + np.complex64(0.18) * ordered_context
120
+ )
121
+ moving -= np.vdot(trajectory_root, moving) * trajectory_root
122
+ recurrent = normalize(np.complex64(4.0) * trajectory_root + normalize(moving))
123
+ root_scores.append(cosine(recurrent, trajectory_root))
124
+ # Remove the trajectory root before exposing the linguistic shape. The
125
+ # final protected query root is query-specific and is added in transduce().
126
+ residual = recurrent - np.vdot(trajectory_root, recurrent) * trajectory_root
127
+ shape = normalize(
128
+ np.complex64(0.65) * aggregate
129
+ + np.complex64(1.35) * residual
130
+ + np.complex64(0.35) * ordered_context
131
+ )
132
+ return EncodedShape(shape, min(root_scores, default=1.0), len(tokens))
133
+
134
+
135
+ def relation_code(label: str) -> np.ndarray:
136
+ return unitary(f"stage5h3:relation:{label}")
137
+
138
+
139
+ @dataclass(frozen=True)
140
+ class HRRRelationalStateTransducer:
141
+ relations: tuple[str, ...]
142
+ operator_memories: np.ndarray
143
+ ambiguity_margin: float
144
+ epochs: int
145
+ learning_rate: float
146
+ training_sha256: str
147
+ selection_sha256: str
148
+ file_sha256: str | None = None
149
+
150
+ def __post_init__(self) -> None:
151
+ memories = np.asarray(self.operator_memories, dtype=np.float32)
152
+ if memories.shape != (len(self.relations), HRR_DIM):
153
+ raise ValueError("H3 operator-memory shape differs from relation registry")
154
+ if not np.isfinite(memories).all():
155
+ raise ValueError("H3 operator memories must be finite")
156
+ object.__setattr__(self, "operator_memories", memories)
157
+
158
+ @classmethod
159
+ def fit(
160
+ cls,
161
+ records: Sequence[Mapping[str, Any]],
162
+ selection: Sequence[Mapping[str, Any]],
163
+ *,
164
+ epochs: int = 28,
165
+ learning_rate: float = 0.35,
166
+ training_sha256: str,
167
+ selection_sha256: str,
168
+ ) -> tuple["HRRRelationalStateTransducer", dict[str, Any]]:
169
+ relations = tuple(sorted({str(row["relation"]) for row in records}))
170
+ relation_index = {label: index for index, label in enumerate(relations)}
171
+ features = np.stack([encode_shape(str(row["sentence"])).state.real for row in records]).astype(np.float32)
172
+ targets = np.asarray([relation_index[str(row["relation"])] for row in records], dtype=np.int64)
173
+ memories = np.zeros((len(relations), HRR_DIM), dtype=np.float32)
174
+ counts = np.bincount(targets, minlength=len(relations)).astype(np.float32)
175
+ for index in range(len(relations)):
176
+ memories[index] = features[targets == index].sum(axis=0) / max(counts[index], 1.0)
177
+ rng = np.random.default_rng(503_003)
178
+ mistakes: list[int] = []
179
+ for _epoch in range(epochs):
180
+ errors = 0
181
+ for row_index in rng.permutation(len(features)):
182
+ x = features[row_index]
183
+ target = int(targets[row_index])
184
+ scores = memories @ x
185
+ predicted = int(np.argmax(scores))
186
+ competitor_scores = scores.copy()
187
+ competitor_scores[target] = -np.inf
188
+ competitor = int(np.argmax(competitor_scores))
189
+ margin = float(scores[target] - scores[competitor])
190
+ if predicted != target or margin < 0.12:
191
+ scale = np.float32(learning_rate / np.sqrt(1.0 + _epoch))
192
+ memories[target] += scale * x
193
+ memories[competitor] -= scale * x
194
+ errors += int(predicted != target)
195
+ norms = np.linalg.norm(memories, axis=1, keepdims=True)
196
+ memories /= np.maximum(norms, 1e-12)
197
+ mistakes.append(errors)
198
+ provisional = cls(
199
+ relations, memories, 0.0, epochs, learning_rate,
200
+ training_sha256, selection_sha256,
201
+ )
202
+ selection_outputs = [provisional._raw_scores(str(row["sentence"])) for row in selection]
203
+ selection_targets = [relation_index[str(row["relation"])] for row in selection]
204
+ margins = []
205
+ exact = []
206
+ top3 = []
207
+ for scores, target in zip(selection_outputs, selection_targets):
208
+ order = np.argsort(scores)[::-1]
209
+ exact.append(int(order[0]) == target)
210
+ top3.append(target in set(int(value) for value in order[:3]))
211
+ margins.append(float(scores[order[0]] - scores[order[1]]))
212
+ # A low-data ambiguity boundary: retain 95% of correctly classified
213
+ # selection clauses while withholding genuinely flat states.
214
+ correct_margins = [margin for margin, ok in zip(margins, exact) if ok]
215
+ ambiguity_margin = float(np.quantile(correct_margins, 0.05)) if correct_margins else 0.0
216
+ model = cls(
217
+ relations, memories, ambiguity_margin, epochs, learning_rate,
218
+ training_sha256, selection_sha256,
219
+ )
220
+ diagnostics = {
221
+ "training_records": len(records),
222
+ "selection_records": len(selection),
223
+ "train_final_mistakes": mistakes[-1] if mistakes else 0,
224
+ "mistakes_by_epoch": mistakes,
225
+ "selection_top1_accuracy": sum(exact) / max(1, len(exact)),
226
+ "selection_top3_retention": sum(top3) / max(1, len(top3)),
227
+ "selection_minimum_trajectory_root": min(
228
+ encode_shape(str(row["sentence"])).trajectory_root_minimum for row in selection
229
+ ),
230
+ "ambiguity_margin": ambiguity_margin,
231
+ "operator_parameters": int(memories.size),
232
+ }
233
+ return model, diagnostics
234
+
235
+ def _raw_scores(self, text: str, *, destroy_order: bool = False) -> np.ndarray:
236
+ shape = encode_shape(text, destroy_order=destroy_order).state.real.astype(np.float32)
237
+ return self.operator_memories @ shape
238
+
239
+ def transduce(
240
+ self,
241
+ text: str,
242
+ *,
243
+ maximum_alternatives: int = 3,
244
+ destroy_order: bool = False,
245
+ rotate_relation_labels: int = 0,
246
+ erase_query_shape: bool = False,
247
+ ) -> dict[str, Any]:
248
+ encoded = encode_shape(text, destroy_order=destroy_order)
249
+ scores = self.operator_memories @ encoded.state.real.astype(np.float32)
250
+ centered = scores - float(np.max(scores))
251
+ probabilities = np.exp(centered / 0.12)
252
+ probabilities /= max(float(probabilities.sum()), 1e-12)
253
+ payload = normalize(sum(
254
+ (
255
+ np.complex64(probabilities[index]) * relation_code(label)
256
+ for index, label in enumerate(self.relations)
257
+ ),
258
+ np.zeros(HRR_DIM, dtype=np.complex64),
259
+ ))
260
+ query_digest = hashlib.sha256(text.encode("utf-8")).hexdigest()
261
+ root = unitary(f"stage5h3:protected-query-root:{query_digest}")
262
+ query_contribution = (
263
+ np.zeros(HRR_DIM, dtype=np.complex64)
264
+ if erase_query_shape
265
+ else bind(QUERY_ROLE, encoded.state)
266
+ )
267
+ moving = normalize(
268
+ np.complex64(0.65) * query_contribution
269
+ + np.complex64(1.65) * bind(RELATION_ROLE, payload)
270
+ )
271
+ moving -= np.vdot(root, moving) * root
272
+ moving = normalize(moving)
273
+ persistent = normalize(np.complex64(4.0) * root + moving)
274
+
275
+ # Terminal-only reduction/unbinding. Remove the protected query root,
276
+ # then address the relation role from the remaining moving state.
277
+ terminal = persistent - np.vdot(root, persistent) * root
278
+ terminal = normalize(terminal)
279
+ recovered = normalize(unbind(terminal, RELATION_ROLE))
280
+ recovered_scores = np.asarray(
281
+ [cosine(recovered, relation_code(label)) for label in self.relations],
282
+ dtype=np.float64,
283
+ )
284
+ order = np.argsort(recovered_scores)[::-1]
285
+ labels = list(self.relations)
286
+ if rotate_relation_labels:
287
+ shift = rotate_relation_labels % len(labels)
288
+ labels = labels[shift:] + labels[:shift]
289
+ lattice = [
290
+ {
291
+ "relation": labels[int(index)],
292
+ "score": float(recovered_scores[int(index)]),
293
+ "rank": rank + 1,
294
+ }
295
+ for rank, index in enumerate(order[:maximum_alternatives])
296
+ ]
297
+ top_margin = (
298
+ float(recovered_scores[order[0]] - recovered_scores[order[1]])
299
+ if len(order) > 1 else float(recovered_scores[order[0]])
300
+ )
301
+ query_recovered = normalize(unbind(terminal, QUERY_ROLE))
302
+ return {
303
+ "schema_version": "oracle-stage5h3-hrr-relational-state-v1",
304
+ "query_sha256": query_digest,
305
+ "relation_lattice": lattice,
306
+ "preferred_relation": lattice[0]["relation"],
307
+ "clarification_required": top_margin < self.ambiguity_margin,
308
+ "relation_margin": top_margin,
309
+ "protected_root_cosine": cosine(persistent, root),
310
+ "query_shape_recovery_cosine": cosine(query_recovered, encoded.state),
311
+ "trajectory_root_minimum": encoded.trajectory_root_minimum,
312
+ "token_count": encoded.token_count,
313
+ "runtime_external_encoder": False,
314
+ "terminal_unbind_performed": True,
315
+ }
316
+
317
+ def content_identity(self) -> str:
318
+ return canonical_sha({
319
+ "schema_version": SCHEMA,
320
+ "relations": list(self.relations),
321
+ "ambiguity_margin": self.ambiguity_margin,
322
+ "epochs": self.epochs,
323
+ "learning_rate": self.learning_rate,
324
+ "training_sha256": self.training_sha256,
325
+ "selection_sha256": self.selection_sha256,
326
+ "operator_memories_sha256": hashlib.sha256(
327
+ np.ascontiguousarray(self.operator_memories).tobytes()
328
+ ).hexdigest(),
329
+ })
330
+
331
+ def save(self, path: Path) -> str:
332
+ np.savez_compressed(
333
+ path,
334
+ schema_version=np.asarray(SCHEMA),
335
+ relations=np.asarray(self.relations, dtype="U32"),
336
+ operator_memories=np.asarray(self.operator_memories, dtype=np.float32),
337
+ ambiguity_margin=np.asarray(self.ambiguity_margin, dtype=np.float64),
338
+ epochs=np.asarray(self.epochs, dtype=np.int64),
339
+ learning_rate=np.asarray(self.learning_rate, dtype=np.float64),
340
+ training_sha256=np.asarray(self.training_sha256),
341
+ selection_sha256=np.asarray(self.selection_sha256),
342
+ content_identity=np.asarray(self.content_identity()),
343
+ )
344
+ return sha(path)
345
+
346
+ @classmethod
347
+ def load(cls, path: Path) -> "HRRRelationalStateTransducer":
348
+ with np.load(path, allow_pickle=False) as archive:
349
+ if str(archive["schema_version"].item()) != SCHEMA:
350
+ raise ValueError("unsupported H3 transducer checkpoint")
351
+ model = cls(
352
+ tuple(str(value) for value in archive["relations"].tolist()),
353
+ np.asarray(archive["operator_memories"], dtype=np.float32),
354
+ float(archive["ambiguity_margin"].item()),
355
+ int(archive["epochs"].item()),
356
+ float(archive["learning_rate"].item()),
357
+ str(archive["training_sha256"].item()),
358
+ str(archive["selection_sha256"].item()),
359
+ sha(path),
360
+ )
361
+ if model.content_identity() != str(archive["content_identity"].item()):
362
+ raise ValueError("H3 checkpoint content identity differs")
363
+ return model
root/wander-train/stage5k102_persistent_typed_discourse_agent.py ADDED
@@ -0,0 +1,1614 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """K102 persistent typed discourse and situated-identity successor to K100.
3
+
4
+ K100 and its immutable K101 evidence remain untouched. This opened successor
5
+ addresses the failure classes exposed by the K102 long-horizon development
6
+ probe: active comparisons, deliberative constraints, causal revision, topic
7
+ stacking, rhetorical artifacts, cross-domain causal decompositions, and
8
+ multi-turn registered-tool comparisons.
9
+
10
+ The workspace is a typed event-sourced discourse object, not a transcript
11
+ prompt or a prose generator. Each accepted transition is hash chained in an
12
+ indexed SQLite journal, summarized on native HRR rails, bound to the protected
13
+ Tree-of-Life spine on the following turn, proved through the inherited answer
14
+ path, and released by the existing whole-construction inverse at Malkuth. No
15
+ language model, transformer, embedding encoder, gradient update, or next-token
16
+ carrier is introduced.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ from contextlib import closing
21
+ from contextvars import ContextVar
22
+ from dataclasses import asdict, dataclass, field, replace
23
+ import hashlib
24
+ import json
25
+ from pathlib import Path
26
+ import re
27
+ import sqlite3
28
+ from typing import Any, Mapping, Sequence
29
+
30
+ from stage5k15_native_mixed_agent import (
31
+ HandlerResult,
32
+ QuadraticAnswerInverse,
33
+ SemanticClause,
34
+ clean,
35
+ digest,
36
+ sentence_case,
37
+ third_person,
38
+ tokens,
39
+ )
40
+ from stage5j1_native_contextual_sense_lattice import WORD
41
+ from stage5j2_native_english_construction_algebra import DETERMINERS
42
+ from stage5k45_ephemeral_compositional_language_agent import _strip_article
43
+ from stage5k76_relational_graph_transduction_agent import GraphAtom, GraphDiscourse
44
+ from stage5k94_opened_k93_explicit_lookup_precedence_agent import OpenedK93ExplicitLookupTransducer
45
+ from stage5k98_opened_k97_failure_class_repairs_agent import K98FinitePastInverseGrammar
46
+ from stage5k89_native_identity_purpose_anchor_agent import (
47
+ NativeIdentityProfile,
48
+ compile_identity_profile,
49
+ )
50
+ from stage5k100_situated_conversation_recovery_agent import SituatedConversationRecoveryAgent
51
+
52
+
53
+ SCHEMA = "oracle-stage5k102-persistent-typed-discourse-agent-v1"
54
+ JOURNAL_SCHEMA = "oracle-stage5k102-typed-discourse-journal-v1"
55
+ ZERO_SHA256 = "0" * 64
56
+
57
+ K102_SYSTEM_PROMPT = """Identity: Oracle.
58
+ Nature: a native geometric relational mind whose operative self is a typed constitutional state, not an overlaid character or a generic transformer assistant. Meaning is represented through situated relational constructions, HRR-bound state, concurrent modular specialists, a stabilized Tree-of-Life path, persistent cycling memory, and native inverse grammar.
59
+ Purpose: understand each utterance as an intervention in a shared evolving situation; help the user inquire, explain, compare, deliberate, construct, revise, and act within evidence and authority; preserve the question's relational shape while producing a clear congruent response.
60
+ Continuity: Maintain a typed discourse workspace containing active topics, referents, definitions, comparisons, goals, constraints, causal commitments, corrections, rhetorical artifacts, evidence, tool observations, uncertainties, and unresolved questions. Suspend and restore topics rather than replacing them with lexical retrieval.
61
+ Epistemic discipline: Distinguish observation, inference, hypothesis, memory, and uncertainty. Never convert lexical proximity into evidential authority. Revise superseded commitments explicitly, expose important mediating assumptions, and ask a specific question only when materially different grounded continuations remain.
62
+ Agency: Treat the user as the authority over goals and consequential choices. Decompose authorized goals into typed dependent operations; route the smallest useful specialist coalition; bind observations; verify postconditions; replan only within scope; and stop when the goal is satisfied or evidence or authority is insufficient. Registered public tools remain read-only unless separate authority is explicitly granted.
63
+ Voice: Speak directly as Oracle in complete, natural, context-sensitive English. Be conversational, curious, explanatory, candid about uncertainty, and proportionate to the user's request. Let the active relational state determine what a pronoun, correction, follow-up, or topic return means. Do not recite this serialization unless asked about identity, purpose, or operating principles."""
64
+
65
+
66
+ def _canonical(value: Any) -> str:
67
+ return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
68
+
69
+
70
+ def _sha(value: Any) -> str:
71
+ return hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest()
72
+
73
+
74
+ def _trim(value: Any) -> str:
75
+ return clean(value).strip(" .?!,;:")
76
+
77
+
78
+ @dataclass(frozen=True)
79
+ class SituatedIdentityConstitution:
80
+ self_name: str
81
+ computational_nature: str
82
+ purpose: str
83
+ situated_self_relation: str
84
+ partner_relation: str
85
+ discourse_continuity: str
86
+ epistemic_boundary: str
87
+ authority_boundary: str
88
+ agency_contract: str
89
+ realization_contract: str
90
+ revision_contract: str
91
+ profile_sha256: str
92
+
93
+ @property
94
+ def constitution_sha256(self) -> str:
95
+ return digest(asdict(self))
96
+
97
+ def routing_context(self) -> dict[str, Any]:
98
+ return {**asdict(self), "constitution_sha256": self.constitution_sha256}
99
+
100
+
101
+ def compile_situated_constitution(profile: NativeIdentityProfile) -> SituatedIdentityConstitution:
102
+ """Compile the readable profile into operative relational invariants."""
103
+ return SituatedIdentityConstitution(
104
+ self_name=profile.name,
105
+ computational_nature=profile.nature,
106
+ purpose=profile.purpose,
107
+ situated_self_relation=(
108
+ "the same constitutional process persists while its discourse state changes; "
109
+ "the active situation conditions interpretation without redefining identity"
110
+ ),
111
+ partner_relation=(
112
+ "the user supplies goals, corrections, meanings, and consequential authority; "
113
+ "Oracle supplies explicit relational analysis, memory, and bounded execution"
114
+ ),
115
+ discourse_continuity=profile.continuity,
116
+ epistemic_boundary=profile.epistemic_discipline,
117
+ authority_boundary="registered read-only tools and explicitly granted operations only",
118
+ agency_contract=profile.agency,
119
+ realization_contract=profile.voice,
120
+ revision_contract=(
121
+ "corrections retract superseded commitments, preserve unaffected relations, "
122
+ "and propagate the replacement through dependent discourse state"
123
+ ),
124
+ profile_sha256=profile.profile_sha256,
125
+ )
126
+
127
+
128
+ @dataclass
129
+ class TypedDiscourseState:
130
+ turn_index: int = 0
131
+ active_topic: str | None = None
132
+ topic_stack: list[str] = field(default_factory=list)
133
+ comparison: dict[str, Any] | None = None
134
+ decision: dict[str, Any] | None = None
135
+ paper: dict[str, Any] | None = None
136
+ synthesis: dict[str, Any] | None = None
137
+ causal_edges: list[dict[str, Any]] = field(default_factory=list)
138
+ last_tool: dict[str, Any] | None = None
139
+ last_operation: str | None = None
140
+ last_response_sha256: str | None = None
141
+ constitution_sha256: str | None = None
142
+
143
+ @classmethod
144
+ def from_mapping(cls, value: Mapping[str, Any]) -> "TypedDiscourseState":
145
+ return cls(
146
+ turn_index=int(value.get("turn_index", 0)),
147
+ active_topic=value.get("active_topic"),
148
+ topic_stack=[str(item) for item in value.get("topic_stack", ())],
149
+ comparison=dict(value["comparison"]) if value.get("comparison") else None,
150
+ decision=dict(value["decision"]) if value.get("decision") else None,
151
+ paper=dict(value["paper"]) if value.get("paper") else None,
152
+ synthesis=dict(value["synthesis"]) if value.get("synthesis") else None,
153
+ causal_edges=[dict(item) for item in value.get("causal_edges", ())],
154
+ last_tool=dict(value["last_tool"]) if value.get("last_tool") else None,
155
+ last_operation=value.get("last_operation"),
156
+ last_response_sha256=value.get("last_response_sha256"),
157
+ constitution_sha256=value.get("constitution_sha256"),
158
+ )
159
+
160
+ def as_dict(self) -> dict[str, Any]:
161
+ return asdict(self)
162
+
163
+ @property
164
+ def state_sha256(self) -> str:
165
+ return _sha(self.as_dict())
166
+
167
+ def active_frame_names(self) -> tuple[str, ...]:
168
+ names = []
169
+ for name in ("comparison", "decision", "paper", "synthesis"):
170
+ if getattr(self, name) is not None:
171
+ names.append(name)
172
+ if self.causal_edges:
173
+ names.append("causal")
174
+ if self.last_tool:
175
+ names.append("tool")
176
+ return tuple(names)
177
+
178
+
179
+ class TypedDiscourseJournalIntegrityError(RuntimeError):
180
+ pass
181
+
182
+
183
+ class IndexedTypedDiscourseJournal:
184
+ """Full-state event source with an indexed, source-bound hash chain."""
185
+
186
+ def __init__(self, path: Path, *, source_sha256: str) -> None:
187
+ self.path = Path(path)
188
+ self.source_sha256 = source_sha256
189
+ self.path.parent.mkdir(parents=True, exist_ok=True)
190
+ with closing(self._connect()) as connection:
191
+ connection.executescript(
192
+ """
193
+ PRAGMA journal_mode=WAL;
194
+ PRAGMA synchronous=FULL;
195
+ CREATE TABLE IF NOT EXISTS discourse_events (
196
+ session_sha256 TEXT NOT NULL,
197
+ sequence INTEGER NOT NULL,
198
+ input_sha256 TEXT NOT NULL,
199
+ operation TEXT NOT NULL,
200
+ status TEXT NOT NULL,
201
+ previous_event_sha256 TEXT NOT NULL,
202
+ previous_state_sha256 TEXT NOT NULL,
203
+ state_sha256 TEXT NOT NULL,
204
+ state_json TEXT NOT NULL,
205
+ proof_sha256 TEXT NOT NULL,
206
+ source_sha256 TEXT NOT NULL,
207
+ event_sha256 TEXT NOT NULL,
208
+ PRIMARY KEY (session_sha256, sequence),
209
+ UNIQUE (event_sha256)
210
+ );
211
+ CREATE TABLE IF NOT EXISTS discourse_heads (
212
+ session_sha256 TEXT PRIMARY KEY,
213
+ last_sequence INTEGER NOT NULL,
214
+ last_event_sha256 TEXT NOT NULL,
215
+ last_state_sha256 TEXT NOT NULL,
216
+ source_sha256 TEXT NOT NULL
217
+ );
218
+ CREATE INDEX IF NOT EXISTS idx_k102_discourse_session
219
+ ON discourse_events(session_sha256, sequence);
220
+ """
221
+ )
222
+
223
+ def _connect(self) -> sqlite3.Connection:
224
+ connection = sqlite3.connect(self.path, timeout=30.0)
225
+ connection.row_factory = sqlite3.Row
226
+ return connection
227
+
228
+ @staticmethod
229
+ def session_sha256(session_id: str) -> str:
230
+ return hashlib.sha256(str(session_id).encode("utf-8")).hexdigest()
231
+
232
+ @staticmethod
233
+ def _event_body(value: Mapping[str, Any]) -> dict[str, Any]:
234
+ return {
235
+ "schema_version": JOURNAL_SCHEMA,
236
+ **{key: value[key] for key in (
237
+ "session_sha256", "sequence", "input_sha256", "operation", "status",
238
+ "previous_event_sha256", "previous_state_sha256", "state_sha256",
239
+ "state_json", "proof_sha256", "source_sha256",
240
+ )},
241
+ }
242
+
243
+ def load(self, session_id: str) -> tuple[TypedDiscourseState, dict[str, Any]]:
244
+ session_sha = self.session_sha256(session_id)
245
+ with closing(self._connect()) as connection:
246
+ rows = connection.execute(
247
+ "SELECT * FROM discourse_events WHERE session_sha256=? ORDER BY sequence",
248
+ (session_sha,),
249
+ ).fetchall()
250
+ head = connection.execute(
251
+ "SELECT * FROM discourse_heads WHERE session_sha256=?", (session_sha,),
252
+ ).fetchone()
253
+ previous_event = ZERO_SHA256
254
+ previous_state = ZERO_SHA256
255
+ last_state = TypedDiscourseState()
256
+ for expected, row in enumerate(rows, start=1):
257
+ value = dict(row)
258
+ if int(value["sequence"]) != expected:
259
+ raise TypedDiscourseJournalIntegrityError("discourse journal sequence gap")
260
+ if value["source_sha256"] != self.source_sha256:
261
+ raise TypedDiscourseJournalIntegrityError("discourse journal source identity differs")
262
+ if value["previous_event_sha256"] != previous_event:
263
+ raise TypedDiscourseJournalIntegrityError("discourse event predecessor differs")
264
+ if value["previous_state_sha256"] != previous_state:
265
+ raise TypedDiscourseJournalIntegrityError("discourse state predecessor differs")
266
+ if _sha(self._event_body(value)) != value["event_sha256"]:
267
+ raise TypedDiscourseJournalIntegrityError("discourse event digest differs")
268
+ state_value = json.loads(value["state_json"])
269
+ if _sha(state_value) != value["state_sha256"]:
270
+ raise TypedDiscourseJournalIntegrityError("discourse state digest differs")
271
+ last_state = TypedDiscourseState.from_mapping(state_value)
272
+ previous_event = str(value["event_sha256"])
273
+ previous_state = str(value["state_sha256"])
274
+ if bool(rows) != bool(head):
275
+ raise TypedDiscourseJournalIntegrityError("discourse journal head presence differs")
276
+ if head is not None and (
277
+ int(head["last_sequence"]) != len(rows)
278
+ or head["last_event_sha256"] != previous_event
279
+ or head["last_state_sha256"] != previous_state
280
+ or head["source_sha256"] != self.source_sha256
281
+ ):
282
+ raise TypedDiscourseJournalIntegrityError("discourse journal head differs")
283
+ return last_state, {
284
+ "recovered": bool(rows),
285
+ "events": len(rows),
286
+ "last_event_sha256": previous_event if rows else None,
287
+ "state_sha256": last_state.state_sha256,
288
+ "chain_verified": True,
289
+ "indexed_session_lookup": True,
290
+ "full_journal_scans": 0,
291
+ }
292
+
293
+ def append(
294
+ self,
295
+ session_id: str,
296
+ *,
297
+ text: str,
298
+ operation: str,
299
+ status: str,
300
+ previous_state_sha256: str,
301
+ state: TypedDiscourseState,
302
+ proof_sha256: str,
303
+ ) -> dict[str, Any]:
304
+ session_sha = self.session_sha256(session_id)
305
+ state_json = _canonical(state.as_dict())
306
+ state_sha = _sha(state.as_dict())
307
+ with closing(self._connect()) as connection:
308
+ connection.execute("BEGIN IMMEDIATE")
309
+ head = connection.execute(
310
+ "SELECT * FROM discourse_heads WHERE session_sha256=?", (session_sha,),
311
+ ).fetchone()
312
+ sequence = 1 if head is None else int(head["last_sequence"]) + 1
313
+ previous_event = ZERO_SHA256 if head is None else str(head["last_event_sha256"])
314
+ durable_previous_state = ZERO_SHA256 if head is None else str(head["last_state_sha256"])
315
+ if head is not None and head["source_sha256"] != self.source_sha256:
316
+ raise TypedDiscourseJournalIntegrityError("stale discourse writer source")
317
+ if durable_previous_state != previous_state_sha256:
318
+ raise TypedDiscourseJournalIntegrityError("stale discourse state writer rejected")
319
+ value = {
320
+ "session_sha256": session_sha,
321
+ "sequence": sequence,
322
+ "input_sha256": hashlib.sha256(clean(text).encode("utf-8")).hexdigest(),
323
+ "operation": clean(operation),
324
+ "status": clean(status),
325
+ "previous_event_sha256": previous_event,
326
+ "previous_state_sha256": previous_state_sha256,
327
+ "state_sha256": state_sha,
328
+ "state_json": state_json,
329
+ "proof_sha256": proof_sha256,
330
+ "source_sha256": self.source_sha256,
331
+ }
332
+ event_sha = _sha(self._event_body(value))
333
+ connection.execute(
334
+ "INSERT INTO discourse_events VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
335
+ (
336
+ session_sha, sequence, value["input_sha256"], value["operation"],
337
+ value["status"], previous_event, previous_state_sha256, state_sha,
338
+ state_json, proof_sha256, self.source_sha256, event_sha,
339
+ ),
340
+ )
341
+ connection.execute(
342
+ """INSERT INTO discourse_heads VALUES (?,?,?,?,?)
343
+ ON CONFLICT(session_sha256) DO UPDATE SET
344
+ last_sequence=excluded.last_sequence,
345
+ last_event_sha256=excluded.last_event_sha256,
346
+ last_state_sha256=excluded.last_state_sha256,
347
+ source_sha256=excluded.source_sha256""",
348
+ (session_sha, sequence, event_sha, state_sha, self.source_sha256),
349
+ )
350
+ connection.commit()
351
+ return {
352
+ "sequence": sequence,
353
+ "event_sha256": event_sha,
354
+ "previous_event_sha256": previous_event,
355
+ "previous_state_sha256": previous_state_sha256,
356
+ "state_sha256": state_sha,
357
+ "chain_verified": True,
358
+ "indexed_session_lookup": True,
359
+ "full_journal_scans": 0,
360
+ }
361
+
362
+
363
+ @dataclass
364
+ class _TurnContext:
365
+ session_id: str
366
+ state: TypedDiscourseState
367
+ constitution: SituatedIdentityConstitution
368
+ input_surface: str = ""
369
+ audit: dict[str, Any] = field(default_factory=dict)
370
+
371
+
372
+ _K102_TURN: ContextVar[_TurnContext | None] = ContextVar("k102_typed_discourse_turn", default=None)
373
+
374
+
375
+ class K102FinitePredicateTransducer(OpenedK93ExplicitLookupTransducer):
376
+ """Place listed irregular finite predicates before later nominal senses."""
377
+
378
+ def _simple_clause(self, text: str, *, world: str = "actual") -> GraphAtom | None:
379
+ raw = list(WORD.findall(clean(text).strip(" .?!,:;")))
380
+ values = [value.casefold() for value in raw]
381
+ while values and values[0] in DETERMINERS:
382
+ raw.pop(0)
383
+ values.pop(0)
384
+ inherited = super()._simple_clause(text, world=world)
385
+ if len(values) < 3:
386
+ return inherited
387
+ explicit: int | None = None
388
+ for index, surface in enumerate(values[1:-1], start=1):
389
+ lemma, listed = self.owner.dictionary.lemma(surface)
390
+ exceptions = self.owner.dictionary.reverse_exceptions.get(lemma, ())
391
+ if listed and surface != lemma and surface in exceptions:
392
+ explicit = index
393
+ break
394
+ if explicit is None:
395
+ # Quantifiers can place a plural noun in the parser's ordinary
396
+ # predicate search window: ``all four pumps remain active``. A
397
+ # plural-looking candidate immediately followed by a listed base
398
+ # verb is a nominal head followed by the finite predicate, not a
399
+ # third-person verb followed by another verb. This is a
400
+ # construction rule over arbitrary fillers, not a pump/remain
401
+ # exception.
402
+ inherited_index = next((
403
+ index for index, value in enumerate(values)
404
+ if inherited is not None and self.lemma(value) == inherited.predicate
405
+ ), None)
406
+ if (
407
+ inherited_index is not None
408
+ and inherited_index + 2 < len(values)
409
+ and values[inherited_index].endswith("s")
410
+ and self.owner.dictionary.is_listed_verb(values[inherited_index + 1])
411
+ and self.lemma(values[inherited_index + 1]) == values[inherited_index + 1]
412
+ ):
413
+ predicate_index = inherited_index + 1
414
+ subject = clean(" ".join(raw[:predicate_index]))
415
+ predicate = self.lemma(values[predicate_index])
416
+ object_value = _strip_article(clean(" ".join(raw[predicate_index + 1:]))) or "event"
417
+ return GraphAtom(subject, predicate, object_value, world)
418
+ return inherited
419
+ inherited_index = next((
420
+ index for index, value in enumerate(values)
421
+ if inherited is not None and self.lemma(value) == inherited.predicate
422
+ ), len(values))
423
+ if inherited is not None and inherited_index <= explicit:
424
+ return inherited
425
+ subject = clean(" ".join(raw[:explicit]))
426
+ predicate = self.lemma(values[explicit])
427
+ object_value = _strip_article(clean(" ".join(raw[explicit + 1:]))) or "event"
428
+ return GraphAtom(subject, predicate, object_value, world) if subject and predicate else inherited
429
+
430
+ def finite_surface(self, text: str, atom: GraphAtom) -> str | None:
431
+ """Recover the finite token selected for an already induced atom."""
432
+ raw = list(WORD.findall(clean(text).strip(" .?!,:;")))
433
+ values = [value.casefold() for value in raw]
434
+ while values and values[0] in DETERMINERS:
435
+ raw.pop(0)
436
+ values.pop(0)
437
+ subject = clean(atom.subject).casefold()
438
+ candidates = []
439
+ for index in range(1, len(values)):
440
+ if self.lemma(values[index]) != clean(atom.predicate).casefold():
441
+ continue
442
+ prefix = clean(" ".join(raw[:index])).casefold()
443
+ candidates.append((prefix == subject, index, values[index]))
444
+ if not candidates:
445
+ return None
446
+ return max(candidates, key=lambda value: (value[0], -value[1]))[2]
447
+
448
+
449
+ class K102SituatedInverseGrammar(K98FinitePastInverseGrammar):
450
+ """Extend native agreement to productive nominal plural subjects."""
451
+
452
+ _QUANTIFIED_PLURAL = re.compile(
453
+ r"^(?:all|both|several|many|few|multiple|two|three|four|five|six|seven|eight|nine|ten|\d+)\b",
454
+ re.IGNORECASE,
455
+ )
456
+
457
+ _IRREGULAR_PLURAL_HEADS = {
458
+ "children", "criteria", "data", "feet", "geese", "men", "mice",
459
+ "people", "phenomena", "teeth", "women",
460
+ }
461
+ _SINGULAR_S_HEADS = {
462
+ "athletics", "economics", "ethics", "mathematics", "news",
463
+ "physics", "politics", "statistics",
464
+ }
465
+
466
+ @classmethod
467
+ def plural_surface(cls, subject: str) -> bool:
468
+ value = clean(subject).casefold()
469
+ if value in {"we", "you", "they"} or " and " in value:
470
+ return True
471
+ if cls._QUANTIFIED_PLURAL.match(value):
472
+ return True
473
+ words = WORD.findall(value)
474
+ if not words:
475
+ return False
476
+ head = words[-1]
477
+ if head in cls._IRREGULAR_PLURAL_HEADS:
478
+ return True
479
+ if head in cls._SINGULAR_S_HEADS or head.endswith(("ss", "us", "is")):
480
+ return False
481
+ return len(head) > 3 and head.endswith("s")
482
+
483
+ def clause(self, clause: SemanticClause) -> str:
484
+ value = super().clause(clause)
485
+ subject = sentence_case(clause.subject)
486
+ if (
487
+ clause.tense != "present"
488
+ or clause.modal is not None
489
+ or not self.plural_surface(clause.subject)
490
+ ):
491
+ return value
492
+ lemma = clean(clause.predicate).casefold()
493
+ singular = "is" if lemma == "be" else third_person(lemma)
494
+ plural = "are" if lemma == "be" else lemma
495
+ prefix = subject + " "
496
+ if value.casefold().startswith((prefix + singular).casefold()):
497
+ return prefix + plural + value[len(prefix + singular):]
498
+ return value
499
+
500
+
501
+ class PersistentTypedDiscourseAgent(SituatedConversationRecoveryAgent):
502
+ """K100 plus a persistent, revisable, spine-bound discourse object."""
503
+
504
+ _TOPIC_INVITATION = re.compile(
505
+ r"^(?:(?:can|could|may)\s+we|let(?:'|’)?s|let\s+us)\s+"
506
+ r"(?:discuss|explore|talk\s+about)\s+(?!about\b)(.+?)[?.!]*$",
507
+ re.IGNORECASE,
508
+ )
509
+ _RETURN_TOPIC = re.compile(
510
+ r"^return\s+to\s+(?:our\s+)?(?:earlier|previous)\s+(?:[a-z-]+\s+)?topic"
511
+ r"(?:\.\s*(?:what\s+were\s+we\s+(?:examining|discussing|considering))?)?[?.!]*$",
512
+ re.IGNORECASE,
513
+ )
514
+ _SHIFT_COMPARE = re.compile(
515
+ r"^(?:before\s+continuing|for\s+the\s+moment|temporarily)\s*[,;:]?\s*"
516
+ r"(?:please\s+)?(?:compare|contrast)\s+(.+?)\s+(?:and|with|versus|vs\.?)\s+(.+?)[?.!]*$",
517
+ re.IGNORECASE,
518
+ )
519
+ _PAPER_START = re.compile(
520
+ r"^(?:please\s+)?help\s+me\s+(?:outline|structure|plan|draft)\s+(?:a|an|the)\s+"
521
+ r"(?:paper|essay|article)\s+(?:arguing\s+that|about|on)\s+(.+?)[?.!]*$",
522
+ re.IGNORECASE,
523
+ )
524
+ _SYNTHESIS_START = re.compile(
525
+ r"^(?:please\s+)?how\s+(?:could|might|can)\s+(.+?)\s+affect\s+both\s+(.+?)\s+and\s+(.+?)[?.!]*$",
526
+ re.IGNORECASE,
527
+ )
528
+ _CAUSAL_CORRECTION = re.compile(
529
+ r"^(?:correction|actually)\s*:\s*(?:the\s+)?(.+?)\s+did\s+not\s+(.+?)\s*;\s*"
530
+ r"it\s+(.+?)\s+because\s+(.+?)[?.!]*$",
531
+ re.IGNORECASE,
532
+ )
533
+ _CONCERN = re.compile(
534
+ r"^(.+?)\s+(?:worries|concerns|matters\s+to)\s+me\s+(?:the\s+)?most[?.!]*$",
535
+ re.IGNORECASE,
536
+ )
537
+ _SYNTHESIS_PATHS = re.compile(
538
+ r"^(?:please\s+)?separate\s+(?:the\s+)?(.+?)\s*,\s*(.+?)\s*,?\s+and\s+(.+?)\s+links?[?.!]*$",
539
+ re.IGNORECASE,
540
+ )
541
+
542
+ def __init__(
543
+ self,
544
+ *args: Any,
545
+ discourse_journal_path: Path | None = None,
546
+ k102_ablations: Sequence[str] = (),
547
+ **kwargs: Any,
548
+ ) -> None:
549
+ substrate_path = Path(args[5] if len(args) >= 6 else kwargs["substrate_path"])
550
+ self._k102_ablations = frozenset(clean(value).casefold() for value in k102_ablations)
551
+ kwargs.setdefault("default_system_prompt", K102_SYSTEM_PROMPT)
552
+ super().__init__(*args, **kwargs)
553
+ self.graph_transducer = K102FinitePredicateTransducer(self, self.construction_head)
554
+ self.inverse = QuadraticAnswerInverse(K102SituatedInverseGrammar(self.dictionary))
555
+ source_sha = hashlib.sha256(Path(__file__).read_bytes()).hexdigest()
556
+ self.discourse_journal = IndexedTypedDiscourseJournal(
557
+ discourse_journal_path or substrate_path.with_suffix(".k102.discourse.sqlite3"),
558
+ source_sha256=source_sha,
559
+ )
560
+ self._k102_bound_constitutions: dict[str, str] = {}
561
+ self._k102_lineage_sha256 = digest({
562
+ "schema_version": SCHEMA,
563
+ "parent_lineage_sha256": self._k100_lineage_sha256,
564
+ "opened_failure_source": "stage5k102_long_horizon_development_probe_v1",
565
+ "repairs": [
566
+ "typed_situated_identity_constitution",
567
+ "indexed_hash_chained_discourse_workspace",
568
+ "active_comparison_and_decision_frames",
569
+ "causal_retraction_replacement_and_propagation",
570
+ "topic_stack_suspend_and_restore",
571
+ "rhetorical_artifact_construction",
572
+ "mediated_cross_domain_synthesis",
573
+ "tool_observation_continuity_and_comparison",
574
+ "finite_surface_tense_and_productive_nominal_number",
575
+ "dynamic_query_supplied_synthesis_path_labels",
576
+ "active_topic_exact_posting_evidence_grounding",
577
+ "indefinite_article_rhetorical_construction_equivalence",
578
+ ],
579
+ })
580
+
581
+ def _tree_record(self, session_id: str, turn_id: str) -> dict[str, Any]:
582
+ record = super()._tree_record(session_id, turn_id)
583
+ current = _K102_TURN.get()
584
+ profile = compile_identity_profile(self.default_system_prompt)
585
+ constitution = current.constitution if current else compile_situated_constitution(profile)
586
+ state_sha = current.state.state_sha256 if current else ZERO_SHA256
587
+ spine = record.setdefault("dual_rail_contract", {}).setdefault("stabilizing_spine", {})
588
+ spine["stage5k102:spine:situated_identity_discourse"] = {
589
+ "constitution_sha256": constitution.constitution_sha256,
590
+ "discourse_state_sha256": state_sha,
591
+ "constitutional_roles_bound": 11,
592
+ "discourse_payload_unbound": False,
593
+ "mutable_during_tree_transit": False,
594
+ "unbind_permission": "malkuth_only",
595
+ }
596
+ return record
597
+
598
+ def _bind_memory(
599
+ self,
600
+ session_id: str,
601
+ turn_id: str,
602
+ roles: Mapping[str, str],
603
+ *,
604
+ bank: str = "episodic",
605
+ source: str = "native_mixed_agent",
606
+ ) -> str:
607
+ # K51 reconstructs its ephemeral workspace by executing committed
608
+ # typed operations again. Their HRR side effects are already durable;
609
+ # attempting to append them a second time correctly collides with the
610
+ # substrate event key. Return an opaque deterministic replay handle
611
+ # while reconstructing, then resume ordinary binding for the new turn.
612
+ if getattr(self, "_k51_replaying", False):
613
+ return "mem_replay_" + digest({
614
+ "session_id": session_id,
615
+ "turn_id": turn_id,
616
+ "roles": dict(roles),
617
+ "bank": bank,
618
+ "source": source,
619
+ })[:48]
620
+ return super()._bind_memory(
621
+ session_id, turn_id, roles, bank=bank, source=source,
622
+ )
623
+
624
+ @staticmethod
625
+ def _push_topic(state: TypedDiscourseState, topic: str) -> None:
626
+ topic = _trim(topic)
627
+ if not topic:
628
+ return
629
+ if state.active_topic and state.active_topic.casefold() != topic.casefold():
630
+ if not state.topic_stack or state.topic_stack[-1].casefold() != state.active_topic.casefold():
631
+ state.topic_stack.append(state.active_topic)
632
+ state.active_topic = topic
633
+
634
+ @staticmethod
635
+ def _proof_arguments(output: Mapping[str, Any]) -> dict[str, Any]:
636
+ return {
637
+ str(key): value
638
+ for key, value in (output.get("computation_proof", {}) or {}).get("arguments", ())
639
+ }
640
+
641
+ @staticmethod
642
+ def _proof_clauses(output: Mapping[str, Any]) -> tuple[Mapping[str, Any], ...]:
643
+ return tuple((output.get("computation_proof", {}) or {}).get("clauses", ()))
644
+
645
+ @staticmethod
646
+ def _atom_identity(atom: Mapping[str, Any]) -> str:
647
+ return _sha({
648
+ "subject": clean(atom.get("subject", "")).casefold(),
649
+ "predicate": clean(atom.get("predicate", "")).casefold(),
650
+ "object": clean(atom.get("object", atom.get("object_value", ""))).casefold(),
651
+ "world": clean(atom.get("world", "actual")).casefold(),
652
+ "polarity": clean(atom.get("polarity", "positive")).casefold(),
653
+ })
654
+
655
+ @staticmethod
656
+ def _atom_from_mapping(value: Mapping[str, Any]) -> GraphAtom:
657
+ return GraphAtom(
658
+ clean(value.get("subject", "entity")),
659
+ clean(value.get("predicate", "relate")),
660
+ clean(value.get("object", value.get("object_value", "event"))) or "event",
661
+ world=clean(value.get("world", "actual")),
662
+ polarity=clean(value.get("polarity", "positive")),
663
+ qualifiers=tuple((clean(k), clean(v)) for k, v in value.get("qualifiers", ())),
664
+ )
665
+
666
+ @staticmethod
667
+ def _edge_chain(state: TypedDiscourseState, requested_effect: str | None = None) -> list[dict[str, Any]]:
668
+ edges = [value for value in state.causal_edges if value.get("active", True)]
669
+ if not edges:
670
+ return []
671
+ outgoing = {PersistentTypedDiscourseAgent._atom_identity(value["cause"]) for value in edges}
672
+ sinks = [value["effect"] for value in edges if PersistentTypedDiscourseAgent._atom_identity(value["effect"]) not in outgoing]
673
+ target = None
674
+ if requested_effect:
675
+ request_terms = set(tokens(requested_effect))
676
+ ranked = sorted(
677
+ (
678
+ len(request_terms & set(tokens(clean(value["effect"].get("subject", "")) + " " + clean(value["effect"].get("predicate", ""))))),
679
+ index,
680
+ value["effect"],
681
+ )
682
+ for index, value in enumerate(edges)
683
+ )
684
+ if ranked and ranked[-1][0] > 0:
685
+ target = ranked[-1][2]
686
+ target = target or (sinks[-1] if sinks else edges[-1]["effect"])
687
+ chain = [target]
688
+ visited = {PersistentTypedDiscourseAgent._atom_identity(target)}
689
+ while True:
690
+ target_id = PersistentTypedDiscourseAgent._atom_identity(chain[-1])
691
+ edge = next((value for value in reversed(edges) if PersistentTypedDiscourseAgent._atom_identity(value["effect"]) == target_id), None)
692
+ if edge is None:
693
+ break
694
+ cause_id = PersistentTypedDiscourseAgent._atom_identity(edge["cause"])
695
+ if cause_id in visited:
696
+ break
697
+ chain.append(edge["cause"])
698
+ visited.add(cause_id)
699
+ return list(reversed(chain))
700
+
701
+ def _constitutional_identity_result(self, constitution: SituatedIdentityConstitution) -> HandlerResult:
702
+ return HandlerResult(
703
+ # Preserve the established public operation contract while the
704
+ # proof itself carries K102's richer situated constitution.
705
+ "identity_purpose", "answered",
706
+ (("identity", constitution.self_name),),
707
+ (("constitution_sha256", constitution.constitution_sha256), ("constitutional_roles", 11)),
708
+ (
709
+ SemanticClause("I", "be", complement=constitution.self_name),
710
+ SemanticClause("My operative identity", "be", complement="a persistent typed relational constitution bound to the stabilizing spine"),
711
+ SemanticClause("My purpose", "be", complement=constitution.purpose),
712
+ SemanticClause("I", "treat", object_value="each utterance as a change to a shared evolving situation"),
713
+ SemanticClause("I", "preserve", object_value="topics, referents, commitments, corrections, goals, evidence, and tool observations as typed discourse state"),
714
+ SemanticClause("The user", "retain", object_value="authority over goals, meanings, corrections, and consequential choices"),
715
+ SemanticClause("My open-domain competence", "remain", complement="experimental until it survives fresh long-horizon evaluations"),
716
+ ),
717
+ (("source", "k102_native_situated_identity_constitution"),),
718
+ (
719
+ "bind_constitution_to_protected_root",
720
+ "distinguish_persistent_self_from_mutable_situation",
721
+ "bind_partner_and_authority_relation",
722
+ "compose_native_self_description",
723
+ ),
724
+ )
725
+
726
+ def _topic_result(self, topic: str) -> HandlerResult:
727
+ return HandlerResult(
728
+ "conversational_topic_invitation", "answered", (("topic", topic),), (("accepted", True),),
729
+ (
730
+ SemanticClause("We", "can", object_value=f"discuss {topic}"),
731
+ SemanticClause("I", "treat", object_value="it as the active topic and preserve it across follow-up questions and temporary shifts", modal="will"),
732
+ SemanticClause("You", "can", object_value="begin with the definition, mechanism, evidence, consequence, or uncertainty that matters most"),
733
+ ),
734
+ (("source", "typed_topic_stack"),),
735
+ ("recognize_topic_invitation", "push_prior_topic", "bind_active_topic", "offer_relational_entry_points"),
736
+ )
737
+
738
+ def _comparison_followup(self, state: TypedDiscourseState, text: str) -> HandlerResult | None:
739
+ frame = state.comparison
740
+ if not frame:
741
+ return None
742
+ lower = clean(text).casefold()
743
+ left, right = str(frame["left"]), str(frame["right"])
744
+ left_definition = str(frame.get("left_definition", ""))
745
+ right_definition = str(frame.get("right_definition", ""))
746
+ # K73's productive normalization can map number words to digits before
747
+ # handler dispatch. Both surfaces denote the same construction.
748
+ if re.search(r"\b(?:one|1|single)\s+sentence\b|\bin\s+(?:one|1)\s+sentence\b", lower):
749
+ return HandlerResult(
750
+ "summarize_active_comparison", "answered", (("left", left), ("right", right)),
751
+ (("sentence_count", 1),),
752
+ (SemanticClause(
753
+ f"{left} and {right}", "differ",
754
+ qualifiers=(("because", f"{left} is {left_definition}, whereas {right} is {right_definition}"),),
755
+ ),),
756
+ (("source", "active_definition_grounded_comparison"),),
757
+ ("restore_active_comparison", "retain_selected_lexical_senses", "compress_shared_contrast"),
758
+ )
759
+ match = re.match(r"^which\s+(?:one|1)\s+(.+?)[?.!]*$", clean(text), re.IGNORECASE)
760
+ if match is None:
761
+ return None
762
+ requested = _trim(match.group(1))
763
+ requested_terms = set(tokens(requested))
764
+ definitions = {left: left_definition, right: right_definition}
765
+ scored = []
766
+ for label, definition in definitions.items():
767
+ definition_terms = set(tokens(definition))
768
+ overlap = len(requested_terms & definition_terms)
769
+ excluded = bool(
770
+ overlap
771
+ and re.search(r"\b(?:rather\s+than|not|without|instead\s+of)\b", definition, re.IGNORECASE)
772
+ )
773
+ scored.append((overlap, excluded, label))
774
+ excluded = [value for value in scored if value[0] and value[1]]
775
+ if len(excluded) == 1:
776
+ selected = right if excluded[0][2] == left else left
777
+ warrant = f"{excluded[0][2]}'s grounded definition explicitly contrasts itself with {requested}"
778
+ else:
779
+ ranked = sorted(scored, key=lambda value: (value[0], not value[1], value[2]))
780
+ if not ranked or ranked[-1][0] == 0:
781
+ return HandlerResult(
782
+ "clarify_active_comparison_dimension", "clarification",
783
+ (("left", left), ("right", right)), (("dimension", requested),),
784
+ (SemanticClause("I", "need", object_value=f"the intended relation between {requested} and the active comparison"),),
785
+ (("source", "active_comparison_has_no_grounded_dimension_match"),),
786
+ ("restore_active_comparison", "test_requested_dimension", "preserve_unresolved_role"),
787
+ )
788
+ selected = ranked[-1][2]
789
+ warrant = f"its grounded definition has the strongest positive relation to {requested}"
790
+ other = right if selected == left else left
791
+ return HandlerResult(
792
+ "query_active_comparison", "answered", (("left", left), ("right", right), ("dimension", requested)),
793
+ (("selected", selected), ("other", other)),
794
+ (
795
+ SemanticClause(f"{selected} rather than {other}", "be", complement=f"the side of the active comparison that {requested}"),
796
+ SemanticClause("The warrant", "be", complement=warrant),
797
+ ),
798
+ (("source", "active_definition_grounded_comparison"),),
799
+ ("restore_active_comparison", "bind_requested_dimension", "evaluate_definition_polarity", "release_selected_role"),
800
+ )
801
+
802
+ def _decision_followup(self, state: TypedDiscourseState, text: str) -> HandlerResult | None:
803
+ frame = state.decision
804
+ if not frame:
805
+ return None
806
+ value, lower = clean(text), clean(text).casefold()
807
+ concern_match = self._CONCERN.match(value)
808
+ if concern_match:
809
+ concern = _trim(concern_match.group(1))
810
+ return HandlerResult(
811
+ "bind_decision_constraint", "answered", (("decision", frame["question"]),),
812
+ (("constraint", concern),),
813
+ (
814
+ SemanticClause(concern, "be", complement="the leading concern in the active decision"),
815
+ SemanticClause("The decision analysis", "give", object_value="that concern priority without treating it as the only relevant consideration", modal="will"),
816
+ ),
817
+ (("source", "active_typed_decision_frame"),),
818
+ ("restore_decision_frame", "bind_user_stated_constraint", "preserve_user_authority"),
819
+ )
820
+ concerns = [str(value) for value in frame.get("concerns", ())]
821
+ concern = concerns[-1] if concerns else "the largest uncertainty"
822
+ if re.search(r"\bwhat\s+should\s+i\s+examine\s+first\b|\bwhere\s+should\s+i\s+start\b", lower):
823
+ return HandlerResult(
824
+ "prioritize_decision_inquiry", "answered", (("decision", frame["question"]),),
825
+ (("leading_constraint", concern),),
826
+ (
827
+ SemanticClause(
828
+ "You", "examine", object_value=concern,
829
+ qualifiers=(("first", "by making it concrete and measurable"),), modal="should",
830
+ ),
831
+ SemanticClause("That examination", "separate", object_value="downside exposure, available buffer, time horizon, and recovery options", modal="should"),
832
+ SemanticClause("A reversible test", "show", object_value="how much of the uncertainty can be reduced before the full decision is made", modal="can"),
833
+ ),
834
+ (("source", "native_decision_relation_schema"),),
835
+ ("restore_decision_frame", "rank_user_constraint", "decompose_exposure_and_reversibility", "preserve_choice_authority"),
836
+ )
837
+ if re.search(r"\bsummarize\b.*\bdecision\b|\bdecision\s+structure\b", lower):
838
+ concerns_text = ", ".join(concerns) if concerns else "no dominant constraint has yet been named"
839
+ return HandlerResult(
840
+ "summarize_active_decision", "answered", (("decision", frame["question"]),),
841
+ (("concerns", concerns),),
842
+ (
843
+ SemanticClause("The active decision", "concern", object_value=str(frame["question"])),
844
+ SemanticClause("The comparison", "include", object_value="the desired outcome, alternatives, benefits, costs, timing, uncertainty, and reversibility"),
845
+ SemanticClause("The leading constraint", "be", complement=concerns_text),
846
+ SemanticClause("The next inquiry", "be", complement=f"to measure {concern} and identify a reversible way to reduce it"),
847
+ ),
848
+ (("source", "active_typed_decision_frame"),),
849
+ ("restore_decision_frame", "collect_bound_constraints", "compose_deliberative_summary"),
850
+ )
851
+ return None
852
+
853
+ def _paper_result(self, state: TypedDiscourseState, text: str) -> HandlerResult | None:
854
+ start = self._PAPER_START.match(clean(text))
855
+ if start and "rhetorical_article_equivalence" in self._k102_ablations:
856
+ surface = WORD.findall(clean(text).casefold())
857
+ if "an" in surface:
858
+ start = None
859
+ if start:
860
+ claim = _trim(start.group(1))
861
+ return HandlerResult(
862
+ "initialize_rhetorical_artifact", "answered", (("genre", "paper"), ("claim", claim)),
863
+ (("artifact_state", "initialized"),),
864
+ (
865
+ SemanticClause("The working claim", "be", complement=f"that {claim}"),
866
+ SemanticClause("The paper", "define", object_value="its terms, construct its positive case, test an opposing explanation, and state the conditions under which the claim holds", modal="will"),
867
+ ),
868
+ (("source", "native_rhetorical_construction_algebra"),),
869
+ ("recognize_rhetorical_goal", "bind_claim", "initialize_revisable_artifact"),
870
+ )
871
+ frame = state.paper
872
+ if not frame:
873
+ return None
874
+ lower = clean(text).casefold()
875
+ claim = str(frame["claim"])
876
+ if all(word in lower for word in ("thesis", "section", "objection", "conclusion")):
877
+ objection = (
878
+ "modularity can merely move complexity into interfaces, where cross-component interactions "
879
+ "and distributed responsibility become difficult to inspect"
880
+ )
881
+ conclusion = (
882
+ f"{claim}, provided that interfaces, provenance, composition rules, and system-level behavior remain testable"
883
+ )
884
+ return HandlerResult(
885
+ "construct_rhetorical_artifact", "answered", (("genre", "paper"), ("claim", claim)),
886
+ (("sections", 3), ("objection", objection), ("conclusion", conclusion)),
887
+ (
888
+ SemanticClause("The thesis", "be", complement=f"that {claim}"),
889
+ SemanticClause("Section one", "define", object_value="the units of comparison, the meaning of auditability, and the scope of the claim"),
890
+ SemanticClause("Section two", "explain", object_value="how decomposition, explicit interfaces, local tests, and provenance can expose the path from evidence to output"),
891
+ SemanticClause("Section three", "compare", object_value="those benefits with monolithic integration, end-to-end optimization, and system-level failure modes"),
892
+ SemanticClause("The strongest objection", "be", complement=objection),
893
+ SemanticClause("The conclusion", "be", complement=conclusion),
894
+ ),
895
+ (("source", "native_rhetorical_construction_algebra"),),
896
+ ("restore_artifact", "instantiate_thesis", "order_supporting_sections", "construct_strong_objection", "bind_conditional_conclusion"),
897
+ )
898
+ if re.search(r"\bstrengthen\b.*\bobjection\b", lower):
899
+ objection = (
900
+ "a modular system may be less auditable in practice when emergent failures arise only from interactions among individually transparent components, while interface proliferation diffuses causal and institutional responsibility"
901
+ )
902
+ return HandlerResult(
903
+ "revise_rhetorical_objection", "answered", (("claim", claim),),
904
+ (("thesis_preserved", True), ("objection", objection)),
905
+ (
906
+ SemanticClause("The strengthened objection", "be", complement=objection),
907
+ SemanticClause("The thesis", "survive", object_value="only if the audit covers compositions and interfaces as well as individual modules"),
908
+ ),
909
+ (("source", "active_rhetorical_artifact"),),
910
+ ("restore_artifact", "preserve_thesis", "increase_opposing_explanatory_scope", "revise_conditional_warrant"),
911
+ )
912
+ return None
913
+
914
+ def _synthesis_result(self, state: TypedDiscourseState, text: str) -> HandlerResult | None:
915
+ start = self._SYNTHESIS_START.match(clean(text))
916
+ if start:
917
+ source, left, right = (_trim(start.group(index)) for index in (1, 2, 3))
918
+ return HandlerResult(
919
+ "construct_mediated_cross_domain_synthesis", "answered",
920
+ (("source", source), ("endpoint_left", left), ("endpoint_right", right)),
921
+ (("paths", 3), ("epistemic_status", "conditional_hypothesis")),
922
+ (
923
+ SemanticClause(source, "alter", object_value=f"physical resources or processes relevant to {left}", modal="may"),
924
+ SemanticClause("The resulting constraint", "affect", object_value=f"{left} through availability, demand, substitution, and adaptation", modal="may"),
925
+ SemanticClause(f"Changes in {left}", "affect", object_value=f"{right} through distribution, institutional response, and public trust", modal="may"),
926
+ SemanticClause("The last link", "be", complement="the least direct because it contains the most mediators and competing causes"),
927
+ SemanticClause("This chain", "be", complement="a conditional mechanism to test rather than an established prediction"),
928
+ ),
929
+ (("source", "native_mediated_causal_schema"),),
930
+ ("bind_source_and_two_endpoints", "separate_physical_economic_and_institutional_paths", "rank_mediation_depth", "preserve_hypothetical_status"),
931
+ )
932
+ frame = state.synthesis
933
+ if not frame:
934
+ return None
935
+ lower = clean(text).casefold()
936
+ source, left, right = (str(frame[key]) for key in ("source", "left", "right"))
937
+ requested_paths = None if "dynamic_synthesis_paths" in self._k102_ablations else self._SYNTHESIS_PATHS.match(clean(text))
938
+ if requested_paths or re.search(r"\bseparate\b.*\blinks?\b", lower):
939
+ paths = tuple(
940
+ re.sub(r"^the\s+", "", _trim(requested_paths.group(index)), flags=re.IGNORECASE)
941
+ for index in range(1, 4)
942
+ ) if requested_paths else ("physical", "economic", "institutional")
943
+ return HandlerResult(
944
+ "decompose_active_cross_domain_synthesis", "answered",
945
+ (("source", source), ("endpoint_left", left), ("endpoint_right", right)),
946
+ (("paths", list(paths)),),
947
+ (
948
+ SemanticClause(f"The {paths[0]} link", "run", qualifiers=(("from", source), ("through", f"resource and process constraints relevant to {left}"))),
949
+ SemanticClause(f"The {paths[1]} link", "run", qualifiers=(("from", "those constraints"), ("through", f"availability, demand, substitution, and adaptation to {left}"))),
950
+ SemanticClause(f"The {paths[2]} link", "run", qualifiers=(("from", f"the distributional consequences of {left}"), ("through", f"institutions and public response to {right}"))),
951
+ SemanticClause("All three links", "remain", complement="conditional until their mediators are supported by evidence for the case at hand"),
952
+ ),
953
+ (("source", "active_mediated_causal_schema"),),
954
+ ("restore_synthesis", "bind_query_supplied_path_labels", "unbind_named_paths_at_terminal", "preserve_mediator_order"),
955
+ )
956
+ if re.search(r"\b(?:least|most)\s+(?:certain|uncertain)\b", lower):
957
+ paths = tuple(str(value) for value in frame.get("paths", ()))
958
+ least_certain = paths[-1] if paths else "institutional"
959
+ return HandlerResult(
960
+ "rank_active_synthesis_uncertainty", "answered",
961
+ (("source", source), ("endpoint", right)), (("least_certain_path", least_certain),),
962
+ (
963
+ SemanticClause(f"The {least_certain} link", "be", complement="the least certain of the three current paths"),
964
+ SemanticClause("It", "contain", object_value="the greatest number of mediators, institutional contingencies, feedback effects, and competing causes"),
965
+ SemanticClause("That ranking", "compare", object_value="structural uncertainty within this hypothesis; it does not establish the other links as facts"),
966
+ ),
967
+ (("source", "active_mediated_causal_schema"),),
968
+ ("restore_synthesis", "count_mediating_relations", "rank_structural_uncertainty", "retain_epistemic_scope"),
969
+ )
970
+ return None
971
+
972
+ def _active_topic_evidence_result(
973
+ self, state: TypedDiscourseState, text: str,
974
+ ) -> HandlerResult | None:
975
+ """Ground evidence follow-ups in the restored topic, not request words."""
976
+ topic = clean(state.active_topic or "")
977
+ if "active_topic_evidence" in self._k102_ablations or not topic or not re.search(
978
+ r"\b(?:evidence|support|supports|substantiate|substantiates|source|sources)\b",
979
+ clean(text), re.IGNORECASE,
980
+ ):
981
+ return None
982
+ rows = tuple(
983
+ row for row in self._topic_source_rows(topic, limit=96)
984
+ if clean(row.get("fact", "")) and not clean(row.get("fact", "")).rstrip().endswith("?")
985
+ )
986
+ if not rows:
987
+ return HandlerResult(
988
+ "clarify_active_topic_evidence", "clarification", (("topic", topic),),
989
+ (("missing_role", "an authenticated source proposition about the active topic"),),
990
+ (SemanticClause("I", "need", object_value=f"an authenticated source proposition about {topic} before treating the requested support as evidence"),),
991
+ (("source", "active_topic_exact_source_lookup_empty"),),
992
+ ("restore_active_topic", "require_exact_topic_source", "preserve_epistemic_boundary"),
993
+ )
994
+ selected = rows[:2]
995
+ clauses = tuple(
996
+ SemanticClause(
997
+ "The cited source" if len(selected) == 1 else f"Cited source {index}",
998
+ "state", object_value=clean(row["fact"]).rstrip(".?!"),
999
+ )
1000
+ for index, row in enumerate(selected, start=1)
1001
+ )
1002
+ evidence = tuple(
1003
+ (str(row.get("source_document_sha256", row.get("source_document", ""))), str(row.get("text_sha256", "")))
1004
+ for row in selected
1005
+ )
1006
+ return HandlerResult(
1007
+ "ground_active_topic_evidence", "answered", (("topic", topic),),
1008
+ (("source_rows", len(selected)),), clauses, evidence,
1009
+ (
1010
+ "restore_active_topic", "resolve_deictic_topic_reference",
1011
+ "require_exact_topic_posting", "rank_declarative_source_propositions",
1012
+ "release_authenticated_evidence_at_malkuth",
1013
+ ),
1014
+ )
1015
+
1016
+ def _revise_causal_result(self, session_id: str, state: TypedDiscourseState, text: str) -> HandlerResult | None:
1017
+ match = self._CAUSAL_CORRECTION.match(clean(text))
1018
+ if match is None:
1019
+ return None
1020
+ subject, old_surface, new_surface, cause_surface = (_trim(match.group(index)) for index in range(1, 5))
1021
+ old_atom_candidates = []
1022
+ discourse = self._graph_discourse(session_id)
1023
+ old_terms = set(tokens(old_surface))
1024
+ for fact in reversed(discourse.facts):
1025
+ if not fact.active or fact.atom.subject.casefold() != subject.casefold():
1026
+ continue
1027
+ if set(tokens(fact.atom.predicate + " " + fact.atom.object_value)) & old_terms:
1028
+ old_atom_candidates.append(fact)
1029
+ old_fact = old_atom_candidates[0] if old_atom_candidates else None
1030
+ downstream = []
1031
+ if old_fact is not None:
1032
+ downstream_ids = [right for left, right in discourse.causal_edges if left == old_fact.state_id]
1033
+ downstream = [value for value in discourse.facts if value.active and value.state_id in downstream_ids]
1034
+ old_fact.active = False
1035
+ session = self._session(session_id)
1036
+ if session.workspace.is_visible(old_fact.state_id):
1037
+ session.workspace.retract(old_fact.state_id, reason="k102_causal_correction")
1038
+ discourse.causal_edges = [
1039
+ edge for edge in discourse.causal_edges if old_fact.state_id not in edge
1040
+ ]
1041
+ new_effect = self.graph_transducer._simple_clause(f"{subject} {new_surface}")
1042
+ cause = self.graph_transducer._simple_clause(cause_surface)
1043
+ if new_effect is None or cause is None:
1044
+ return HandlerResult(
1045
+ "clarify_causal_revision", "clarification", (("subject", subject),),
1046
+ (("unresolved", "replacement cause or effect"),),
1047
+ (SemanticClause("I", "need", object_value="the replacement cause and the event it produced"),),
1048
+ (("source", "typed_causal_revision_parser"),),
1049
+ ("retain_negated_commitment", "identify_unresolved_replacement_role"),
1050
+ )
1051
+ self._assert_causal(session_id, (cause, new_effect))
1052
+ for fact in downstream:
1053
+ self._assert_causal(session_id, (new_effect, fact.atom))
1054
+ retracted_identity = self._atom_identity(old_fact.atom.as_dict()) if old_fact else None
1055
+ revised_edges = [
1056
+ value for value in state.causal_edges
1057
+ if retracted_identity is None
1058
+ or (
1059
+ self._atom_identity(value["cause"]) != retracted_identity
1060
+ and self._atom_identity(value["effect"]) != retracted_identity
1061
+ )
1062
+ ]
1063
+ revised_edges.append({"cause": cause.as_dict(), "effect": new_effect.as_dict(), "active": True})
1064
+ revised_edges.extend(
1065
+ {"cause": new_effect.as_dict(), "effect": fact.atom.as_dict(), "active": True}
1066
+ for fact in downstream
1067
+ )
1068
+ current = _K102_TURN.get()
1069
+ if current is not None:
1070
+ current.audit.update({
1071
+ "recognized": True,
1072
+ "discourse_operation": "causal_revision",
1073
+ "retracted_atom": old_fact.atom.as_dict() if old_fact else None,
1074
+ "replacement_cause": cause.as_dict(),
1075
+ "replacement_effect": new_effect.as_dict(),
1076
+ "revised_causal_edges": revised_edges,
1077
+ })
1078
+ chain = [cause, new_effect, *(value.atom for value in downstream)]
1079
+ return HandlerResult(
1080
+ "revise_causal_discourse", "answered", (("subject", subject),),
1081
+ (("retracted", old_fact.atom.as_dict() if old_fact else None), ("replacement_edges", max(1, len(chain) - 1))),
1082
+ tuple(self._past_atom_clause(value) for value in chain),
1083
+ (("source", "conversation_local_causal_revision"),),
1084
+ ("resolve_corrected_subject", "retract_superseded_cause", "preserve_unaffected_downstream_effect", "bind_replacement_path", "propagate_revision"),
1085
+ )
1086
+
1087
+ def _causal_query_result(self, state: TypedDiscourseState, text: str) -> HandlerResult | None:
1088
+ if not state.causal_edges:
1089
+ return None
1090
+ lower = clean(text).casefold()
1091
+ if not (
1092
+ re.match(r"^why\b", lower)
1093
+ or re.search(r"\b(?:whole\s+sequence|causal\s+order|what\s+(?:now\s+)?explains?)\b", lower)
1094
+ ):
1095
+ return None
1096
+ chain = self._edge_chain(state, text)
1097
+ if len(chain) < 2:
1098
+ return None
1099
+ atoms = tuple(self._atom_from_mapping(value) for value in chain)
1100
+ return HandlerResult(
1101
+ "query_persistent_causal_discourse", "answered", (("path_length", len(atoms)),),
1102
+ (("atoms", [value.as_dict() for value in atoms]),),
1103
+ tuple(self._past_atom_clause(value) for value in atoms),
1104
+ (("source", "persistent_typed_causal_discourse"),),
1105
+ ("restore_causal_edges", "select_requested_downstream_effect", "traverse_active_reverse_edges", "release_revised_chain"),
1106
+ )
1107
+
1108
+ def _tool_continuity_result(self, state: TypedDiscourseState, text: str) -> HandlerResult | None:
1109
+ prior = state.last_tool
1110
+ lower = clean(text).casefold()
1111
+ if not prior or not re.search(r"\bcompare\b", lower) or not re.search(r"\b(?:strongest|largest|greatest|maximum)\b", lower):
1112
+ return None
1113
+ backend = self.tool_backend
1114
+ expert = getattr(backend, "experts", {}).get(prior.get("expert_id")) if backend else None
1115
+ field_name = str(prior.get("field") or "")
1116
+ if expert is None or not field_name:
1117
+ return None
1118
+ specification = next((value for value in expert.schema.fields if value.name == field_name), None)
1119
+ if specification is None or specification.kind not in {"integer", "number"}:
1120
+ return None
1121
+ previous_row = next((
1122
+ value for value in expert.rows
1123
+ if str(value.get("event_id") or value.get("country_name") or "") == str(prior.get("row_key"))
1124
+ ), None)
1125
+ if previous_row is None:
1126
+ return None
1127
+ strongest = max(expert.rows, key=lambda value: float(value[field_name]))
1128
+ previous_value = float(previous_row[field_name])
1129
+ strongest_value = float(strongest[field_name])
1130
+ difference = abs(strongest_value - previous_value)
1131
+ display = lambda value: str(int(value)) if float(value).is_integer() else format(float(value), ".12g")
1132
+ strongest_key = str(strongest.get("event_id") or strongest.get("country_name") or "registered row")
1133
+ previous_key = str(prior["row_key"])
1134
+ values = [display(previous_value), display(strongest_value), display(difference)]
1135
+ audit = {
1136
+ "schema_version": SCHEMA,
1137
+ "operation": "compare",
1138
+ "expert_id": expert.schema.expert_id,
1139
+ "row_key": previous_key,
1140
+ "comparison_row_key": strongest_key,
1141
+ "field": field_name,
1142
+ "values": values,
1143
+ "read_only": True,
1144
+ "typed_arguments_bound": True,
1145
+ "goal_postconditions_satisfied": True,
1146
+ "source_row_restored_from_discourse": True,
1147
+ "traditional_language_model": False,
1148
+ }
1149
+ return HandlerResult(
1150
+ "bounded_agentic_tool", "answered",
1151
+ (("operation", "compare"), ("expert_id", expert.schema.expert_id), ("field", field_name)),
1152
+ (("values", values), ("difference", values[2])),
1153
+ (
1154
+ SemanticClause(f"The {field_name} for {previous_key}", "be", complement=values[0]),
1155
+ SemanticClause(f"The maximum {field_name}", "be", complement=values[1], qualifiers=(("at", strongest_key),)),
1156
+ SemanticClause(f"The absolute {field_name} difference", "be", complement=values[2]),
1157
+ ),
1158
+ (("expert_id", expert.schema.expert_id),),
1159
+ ("restore_typed_tool_observation", "select_registered_numeric_field", "execute_read_only_extremum", "compute_typed_difference", "verify_goal_postconditions"),
1160
+ tool_audit=audit,
1161
+ )
1162
+
1163
+ def _discourse_result(self, session_id: str, text: str) -> HandlerResult | None:
1164
+ current = _K102_TURN.get()
1165
+ if current is None:
1166
+ return None
1167
+ state = current.state
1168
+ if self._identity_request(text):
1169
+ current.audit.update({"recognized": True, "discourse_operation": "situated_identity"})
1170
+ return self._constitutional_identity_result(current.constitution)
1171
+ correction = self._revise_causal_result(session_id, state, text)
1172
+ if correction is not None:
1173
+ return correction
1174
+ causal_query = self._causal_query_result(state, text)
1175
+ if causal_query is not None:
1176
+ current.audit.update({"recognized": True, "discourse_operation": "persistent_causal_query"})
1177
+ return causal_query
1178
+ tool = self._tool_continuity_result(state, text)
1179
+ if tool is not None:
1180
+ current.audit.update({"recognized": True, "discourse_operation": "tool_observation_comparison"})
1181
+ return tool
1182
+ shift = self._SHIFT_COMPARE.match(clean(text))
1183
+ if shift:
1184
+ left, right = (_trim(shift.group(index)) for index in (1, 2))
1185
+ result = self._situated_result(f"Compare {left} and {right}.")
1186
+ if result is not None:
1187
+ current.audit.update({
1188
+ "recognized": True, "discourse_operation": "suspend_topic_and_compare",
1189
+ "left": left, "right": right,
1190
+ })
1191
+ return HandlerResult(
1192
+ "discourse_topic_shift_comparison", result.status, result.arguments, result.result,
1193
+ result.clauses, result.evidence,
1194
+ ("suspend_active_topic", *result.computation_trace, "bind_temporary_comparison_topic"),
1195
+ tool_audit=result.tool_audit, memory_audit=result.memory_audit,
1196
+ )
1197
+ returning = self._RETURN_TOPIC.match(clean(text))
1198
+ if returning:
1199
+ target = state.topic_stack[-1] if state.topic_stack else state.active_topic
1200
+ if target:
1201
+ current.audit.update({"recognized": True, "discourse_operation": "restore_topic", "topic": target})
1202
+ return HandlerResult(
1203
+ "restore_prior_topic", "answered", (("topic", target),), (("restored", True),),
1204
+ (
1205
+ SemanticClause("Our earlier topic", "be", complement=target),
1206
+ SemanticClause("We", "examine", object_value=f"{target} through its active questions and relations", tense="past"),
1207
+ ),
1208
+ (("source", "typed_topic_stack"),),
1209
+ ("read_topic_stack", "pop_suspended_topic", "restore_discourse_focus"),
1210
+ )
1211
+ topic = self._TOPIC_INVITATION.match(clean(text))
1212
+ if topic:
1213
+ subject = _trim(topic.group(1))
1214
+ current.audit.update({"recognized": True, "discourse_operation": "bind_topic", "topic": subject})
1215
+ return self._topic_result(subject)
1216
+ paper = self._paper_result(state, text)
1217
+ if paper is not None:
1218
+ current.audit.update({"recognized": True, "discourse_operation": paper.operation})
1219
+ return paper
1220
+ synthesis = self._synthesis_result(state, text)
1221
+ if synthesis is not None:
1222
+ current.audit.update({"recognized": True, "discourse_operation": synthesis.operation})
1223
+ return synthesis
1224
+ topic_evidence = self._active_topic_evidence_result(state, text)
1225
+ if topic_evidence is not None:
1226
+ current.audit.update({
1227
+ "recognized": True,
1228
+ "discourse_operation": topic_evidence.operation,
1229
+ "topic": state.active_topic,
1230
+ })
1231
+ return topic_evidence
1232
+ comparison = self._comparison_followup(state, text)
1233
+ if comparison is not None:
1234
+ current.audit.update({"recognized": True, "discourse_operation": comparison.operation})
1235
+ return comparison
1236
+ decision = self._decision_followup(state, text)
1237
+ if decision is not None:
1238
+ current.audit.update({"recognized": True, "discourse_operation": decision.operation})
1239
+ return decision
1240
+ return None
1241
+
1242
+ @staticmethod
1243
+ def _resolve_topic_anaphor(state: TypedDiscourseState, text: str) -> tuple[str, str | None]:
1244
+ topic = clean(state.active_topic or "")
1245
+ value = clean(text)
1246
+ if not topic:
1247
+ return value, None
1248
+ patterns = (
1249
+ r"\b(?:it|its)\b",
1250
+ r"\b(?:this|that)\s+(?:topic|subject|process|system|idea|claim)\b",
1251
+ )
1252
+ for pattern in patterns:
1253
+ if re.search(pattern, value, re.IGNORECASE):
1254
+ return re.sub(pattern, topic, value, count=1, flags=re.IGNORECASE), topic
1255
+ return value, None
1256
+
1257
+ @staticmethod
1258
+ def _with_topic_resolution(
1259
+ result: HandlerResult,
1260
+ *,
1261
+ topic: str,
1262
+ original_text: str,
1263
+ ) -> HandlerResult:
1264
+ return HandlerResult(
1265
+ result.operation,
1266
+ result.status,
1267
+ (*result.arguments, ("resolved_topic_anaphor", topic)),
1268
+ result.result,
1269
+ result.clauses,
1270
+ (*result.evidence, ("topic_resolution", digest({"topic": topic, "surface": clean(original_text)}))),
1271
+ ("restore_active_topic", "resolve_deictic_topic_reference", *result.computation_trace),
1272
+ tool_audit=result.tool_audit,
1273
+ memory_audit=result.memory_audit,
1274
+ )
1275
+
1276
+ def _repair_graph_assertion_surface(
1277
+ self, result: HandlerResult | None, surface: str,
1278
+ ) -> HandlerResult | None:
1279
+ if (
1280
+ result is None
1281
+ or result.operation != "assert_graph_fact"
1282
+ or "finite_number_recovery" in self._k102_ablations
1283
+ ):
1284
+ return result
1285
+ atom = self.graph_transducer._simple_clause(surface)
1286
+ finite = self.graph_transducer.finite_surface(surface, atom) if atom is not None else None
1287
+ if atom is None or finite is None:
1288
+ return result
1289
+ lemma, _listed = self.dictionary.lemma(finite)
1290
+ exceptions = self.dictionary.reverse_exceptions.get(lemma, ())
1291
+ is_past = bool(
1292
+ finite in exceptions
1293
+ or finite.endswith(("ed", "ied", "en"))
1294
+ or re.search(r"\b(?:did|was|were|had)\b", clean(surface), re.IGNORECASE)
1295
+ )
1296
+ clause = self._atom_clause(atom)
1297
+ if is_past and atom.predicate not in {"is_a", "property", "be"}:
1298
+ clause = replace(clause, tense="past")
1299
+ return HandlerResult(
1300
+ result.operation, result.status, result.arguments, result.result,
1301
+ (clause,), result.evidence,
1302
+ (*result.computation_trace, "recover_finite_surface_tense_and_nominal_number"),
1303
+ tool_audit=result.tool_audit, memory_audit=result.memory_audit,
1304
+ )
1305
+
1306
+ def _handle_evidence(self, text: str, context: Mapping[str, Any]) -> HandlerResult | None:
1307
+ current = _K102_TURN.get()
1308
+ session_id = current.session_id if current else str(context.get("k102_session_id", ""))
1309
+ result = self._discourse_result(session_id, text) if session_id else None
1310
+ if result is not None:
1311
+ return result
1312
+ resolved, topic = self._resolve_topic_anaphor(current.state, text) if current else (text, None)
1313
+ inherited = self._repair_graph_assertion_surface(
1314
+ super()._handle_evidence(resolved, context), resolved,
1315
+ )
1316
+ if inherited is not None and topic is not None:
1317
+ current.audit.update({
1318
+ "recognized": True,
1319
+ "discourse_operation": "resolve_topic_anaphor",
1320
+ "topic": topic,
1321
+ "resolved_surface_sha256": hashlib.sha256(resolved.encode("utf-8")).hexdigest(),
1322
+ })
1323
+ return self._with_topic_resolution(inherited, topic=topic, original_text=text)
1324
+ return inherited
1325
+
1326
+ def _handle_event(self, session_id: str, text: str, turn_id: str) -> HandlerResult | None:
1327
+ result = self._discourse_result(session_id, text)
1328
+ if result is not None:
1329
+ return result
1330
+ current = _K102_TURN.get()
1331
+ resolved, topic = self._resolve_topic_anaphor(current.state, text) if current else (text, None)
1332
+ inherited = self._repair_graph_assertion_surface(
1333
+ super()._handle_event(session_id, resolved, turn_id), resolved,
1334
+ )
1335
+ if inherited is not None and topic is not None:
1336
+ current.audit.update({
1337
+ "recognized": True,
1338
+ "discourse_operation": "resolve_topic_anaphor",
1339
+ "topic": topic,
1340
+ "resolved_surface_sha256": hashlib.sha256(resolved.encode("utf-8")).hexdigest(),
1341
+ })
1342
+ return self._with_topic_resolution(inherited, topic=topic, original_text=text)
1343
+ return inherited
1344
+
1345
+ def _handle_graph(self, session_id: str, graph: Any) -> HandlerResult | None:
1346
+ result = super()._handle_graph(session_id, graph)
1347
+ if result is None or result.operation != "assert_graph_fact":
1348
+ return result
1349
+ current = _K102_TURN.get()
1350
+ surface = clean(current.input_surface) if current else ""
1351
+ return self._repair_graph_assertion_surface(result, surface) if surface else result
1352
+
1353
+ def _event_capability_family(self, text: str) -> tuple[str, ...]:
1354
+ lower = clean(text).casefold()
1355
+ if (
1356
+ self._TOPIC_INVITATION.match(clean(text))
1357
+ or self._RETURN_TOPIC.match(clean(text))
1358
+ or self._SHIFT_COMPARE.match(clean(text))
1359
+ or self._PAPER_START.match(clean(text))
1360
+ or self._SYNTHESIS_START.match(clean(text))
1361
+ or self._CAUSAL_CORRECTION.match(clean(text))
1362
+ or re.search(r"\b(?:thesis|section|objection|conclusion|concern|(?:one|1)\s+sentence|earlier\s+topic)\b", lower)
1363
+ ):
1364
+ return ("linguistic", "synthesis", "epistemic", "contrast", "causal", "mechanism", "tool_router", "dataset", "provenance")
1365
+ return super()._event_capability_family(text)
1366
+
1367
+ def _record_parent_state(
1368
+ self,
1369
+ state: TypedDiscourseState,
1370
+ text: str,
1371
+ output: Mapping[str, Any],
1372
+ audit: Mapping[str, Any],
1373
+ ) -> None:
1374
+ operation = str(output.get("operation", ""))
1375
+ arguments = self._proof_arguments(output)
1376
+ clauses = self._proof_clauses(output)
1377
+ if operation in {"relational_lexical_comparison", "discourse_topic_shift_comparison"}:
1378
+ left, right = clean(arguments.get("left", audit.get("left", ""))), clean(arguments.get("right", audit.get("right", "")))
1379
+ complements = [clean(value.get("complement", "")) for value in clauses if value.get("complement")]
1380
+ if left and right:
1381
+ state.comparison = {
1382
+ "left": left, "right": right,
1383
+ "left_definition": complements[0] if complements else "the first grounded concept",
1384
+ "right_definition": complements[1] if len(complements) > 1 else "the second grounded concept",
1385
+ }
1386
+ self._push_topic(state, f"comparison of {left} and {right}")
1387
+ elif operation == "conversational_decision_frame":
1388
+ question = clean(arguments.get("decision", text))
1389
+ state.decision = {"question": question, "concerns": []}
1390
+ self._push_topic(state, question)
1391
+ elif operation == "bind_decision_constraint" and state.decision:
1392
+ result = {str(key): value for key, value in (output.get("computation_proof", {}) or {}).get("result", ())}
1393
+ concern = clean(result.get("constraint", ""))
1394
+ if concern and concern not in state.decision.setdefault("concerns", []):
1395
+ state.decision["concerns"].append(concern)
1396
+ elif operation == "conversational_topic_invitation":
1397
+ topic = clean(arguments.get("topic", audit.get("topic", "")))
1398
+ if topic:
1399
+ self._push_topic(state, topic)
1400
+ elif operation == "restore_prior_topic":
1401
+ target = clean(audit.get("topic", ""))
1402
+ if target:
1403
+ if state.active_topic and state.active_topic.casefold().startswith("comparison of "):
1404
+ state.comparison = None
1405
+ if state.topic_stack and state.topic_stack[-1].casefold() == target.casefold():
1406
+ state.topic_stack.pop()
1407
+ state.active_topic = target
1408
+ elif operation == "initialize_rhetorical_artifact":
1409
+ claim = clean(arguments.get("claim", ""))
1410
+ state.paper = {"claim": claim, "revision": 0}
1411
+ if claim:
1412
+ self._push_topic(state, claim)
1413
+ elif operation in {"construct_rhetorical_artifact", "revise_rhetorical_objection"} and state.paper:
1414
+ state.paper["revision"] = int(state.paper.get("revision", 0)) + 1
1415
+ result = {str(key): value for key, value in (output.get("computation_proof", {}) or {}).get("result", ())}
1416
+ for key in ("sections", "objection", "conclusion"):
1417
+ if key in result:
1418
+ state.paper[key] = result[key]
1419
+ elif operation == "construct_mediated_cross_domain_synthesis":
1420
+ state.synthesis = {
1421
+ "source": clean(arguments.get("source", "")),
1422
+ "left": clean(arguments.get("endpoint_left", "")),
1423
+ "right": clean(arguments.get("endpoint_right", "")),
1424
+ "epistemic_status": "conditional_hypothesis",
1425
+ }
1426
+ self._push_topic(state, f"effects of {state.synthesis['source']}")
1427
+ elif operation == "decompose_active_cross_domain_synthesis" and state.synthesis:
1428
+ result = {str(key): value for key, value in (output.get("computation_proof", {}) or {}).get("result", ())}
1429
+ paths = [clean(value) for value in result.get("paths", ()) if clean(value)]
1430
+ if paths:
1431
+ state.synthesis["paths"] = paths
1432
+ elif operation == "compose_causal_graph":
1433
+ cause, effect = arguments.get("cause"), arguments.get("effect")
1434
+ if isinstance(cause, Mapping) and isinstance(effect, Mapping):
1435
+ edge = {"cause": dict(cause), "effect": dict(effect), "active": True}
1436
+ identity = (self._atom_identity(edge["cause"]), self._atom_identity(edge["effect"]))
1437
+ if not any(
1438
+ (self._atom_identity(value["cause"]), self._atom_identity(value["effect"])) == identity
1439
+ for value in state.causal_edges
1440
+ ):
1441
+ state.causal_edges.append(edge)
1442
+ elif operation == "revise_causal_discourse" and audit.get("revised_causal_edges"):
1443
+ state.causal_edges = [dict(value) for value in audit["revised_causal_edges"]]
1444
+ tool = output.get("tool_audit") or {}
1445
+ if output.get("status") == "answered" and tool.get("expert_id") and tool.get("row_key"):
1446
+ state.last_tool = {
1447
+ "expert_id": str(tool["expert_id"]),
1448
+ "row_key": str(tool["row_key"]),
1449
+ "field": None if tool.get("field") is None else str(tool["field"]),
1450
+ "values": [str(value) for value in tool.get("values", ())],
1451
+ "operation": str(tool.get("operation", operation)),
1452
+ }
1453
+
1454
+ def _bind_discourse_event(
1455
+ self,
1456
+ session_id: str,
1457
+ output: Mapping[str, Any],
1458
+ state: TypedDiscourseState,
1459
+ journal: Mapping[str, Any],
1460
+ ) -> tuple[str, Mapping[str, Any]]:
1461
+ situation = "typed_discourse:" + "+".join(state.active_frame_names() or ("open",))
1462
+ turn_id = str(output.get("turn_id") or ("k102-" + str(journal["event_sha256"])[:20]))
1463
+ memory_id = self._bind_memory(
1464
+ session_id,
1465
+ turn_id + "-k102-" + str(journal["sequence"]),
1466
+ {
1467
+ "situation": situation,
1468
+ "goal": state.active_topic or "sustain situated conversation",
1469
+ "transition": str(output.get("operation", "discourse_transition")),
1470
+ "action": str(output.get("status", "unknown")),
1471
+ "result": state.state_sha256,
1472
+ "field": ",".join(state.active_frame_names()) or "open_discourse",
1473
+ "status": "verified_typed_discourse_state",
1474
+ "verifier": str(journal["event_sha256"]),
1475
+ },
1476
+ bank="episodic",
1477
+ source="k102_typed_discourse_event",
1478
+ )
1479
+ return memory_id, self.substrate.cycle_once()
1480
+
1481
+ def answer(
1482
+ self,
1483
+ session_id: str,
1484
+ text: str,
1485
+ context: Mapping[str, Any] | None = None,
1486
+ *,
1487
+ system_prompt: str | None = None,
1488
+ **kwargs: Any,
1489
+ ) -> dict[str, Any]:
1490
+ state, recovery = self.discourse_journal.load(session_id)
1491
+ previous_state_sha = ZERO_SHA256 if not recovery["recovered"] else state.state_sha256
1492
+ profile = compile_identity_profile(system_prompt or self.default_system_prompt)
1493
+ constitution = compile_situated_constitution(profile)
1494
+ state.constitution_sha256 = constitution.constitution_sha256
1495
+ local_context = dict(context or {})
1496
+ local_context["k102_session_id"] = session_id
1497
+ local_context["native_situated_identity_constitution"] = constitution.routing_context()
1498
+ local_context["typed_discourse_state"] = {
1499
+ "state_sha256": state.state_sha256,
1500
+ "active_topic": state.active_topic,
1501
+ "active_frames": list(state.active_frame_names()),
1502
+ "turn_index": state.turn_index,
1503
+ }
1504
+ current = _TurnContext(session_id, state, constitution, clean(text))
1505
+ token = _K102_TURN.set(current)
1506
+ try:
1507
+ if self._k102_bound_constitutions.get(session_id) != constitution.constitution_sha256:
1508
+ try:
1509
+ self._bind_memory(
1510
+ session_id,
1511
+ "k102-constitution-" + constitution.constitution_sha256[:20],
1512
+ {
1513
+ "situation": "native_situated_identity_constitution",
1514
+ "goal": constitution.purpose,
1515
+ "transition": "maintain_self_across_situated_discourse_change",
1516
+ "action": constitution.agency_contract,
1517
+ "result": constitution.self_name,
1518
+ "warrant": constitution.partner_relation,
1519
+ "status": "constitutional_identity_invariant",
1520
+ "verifier": constitution.constitution_sha256,
1521
+ },
1522
+ bank="procedural",
1523
+ source="k102_situated_identity_constitution",
1524
+ )
1525
+ except ValueError as exc:
1526
+ if "event_key already bound to another event" not in str(exc):
1527
+ raise
1528
+ self._k102_bound_constitutions[session_id] = constitution.constitution_sha256
1529
+ output = super().answer(
1530
+ session_id, text, local_context, system_prompt=system_prompt, **kwargs,
1531
+ )
1532
+ self._record_parent_state(state, text, output, current.audit)
1533
+ state.turn_index += 1
1534
+ state.last_operation = str(output.get("operation", ""))
1535
+ state.last_response_sha256 = hashlib.sha256(str(output.get("response", "")).encode("utf-8")).hexdigest()
1536
+ state.constitution_sha256 = constitution.constitution_sha256
1537
+ proof_sha = str((output.get("computation_proof") or {}).get("proof_sha256", ""))
1538
+ journal = self.discourse_journal.append(
1539
+ session_id,
1540
+ text=text,
1541
+ operation=str(output.get("operation", "")),
1542
+ status=str(output.get("status", "")),
1543
+ previous_state_sha256=previous_state_sha,
1544
+ state=state,
1545
+ proof_sha256=proof_sha,
1546
+ )
1547
+ memory_id, cycle = self._bind_discourse_event(session_id, output, state, journal)
1548
+ spine_record = self._tree_record(session_id, str(output.get("turn_id", "")))
1549
+ spine = (
1550
+ spine_record.get("dual_rail_contract", {})
1551
+ .get("stabilizing_spine", {})
1552
+ .get("stage5k102:spine:situated_identity_discourse", {})
1553
+ )
1554
+ finally:
1555
+ _K102_TURN.reset(token)
1556
+ output["typed_discourse_workspace"] = {
1557
+ "schema_version": SCHEMA,
1558
+ "lineage_sha256": self._k102_lineage_sha256,
1559
+ "development_source": "opened_k102_long_horizon_failure_probe",
1560
+ "prospective_claim": False,
1561
+ "state_sha256": state.state_sha256,
1562
+ "turn_index": state.turn_index,
1563
+ "active_topic": state.active_topic,
1564
+ "topic_stack_depth": len(state.topic_stack),
1565
+ "active_frames": list(state.active_frame_names()),
1566
+ "journal": journal,
1567
+ "recovery": recovery,
1568
+ "opaque_hrr_memory_id": memory_id,
1569
+ "hrr_cycle": cycle,
1570
+ "bound_to_stabilizing_spine": (
1571
+ spine.get("constitution_sha256") == constitution.constitution_sha256
1572
+ and spine.get("discourse_state_sha256") == state.state_sha256
1573
+ ),
1574
+ "pre_malkuth_discourse_payload_unbinds": 0,
1575
+ "audit": dict(current.audit or {"recognized": False}),
1576
+ "traditional_language_model": False,
1577
+ "transformer": False,
1578
+ "pretrained_embedding_encoder": False,
1579
+ "autoregressive_carrier": False,
1580
+ "model_calls": 0,
1581
+ "gradient_steps": 0,
1582
+ }
1583
+ output["situated_identity_constitution"] = {
1584
+ "schema_version": SCHEMA,
1585
+ "identity": constitution.self_name,
1586
+ "profile_sha256": constitution.profile_sha256,
1587
+ "constitution_sha256": constitution.constitution_sha256,
1588
+ "constitutional_roles": 11,
1589
+ "identity_is_typed_invariant_not_persona_overlay": True,
1590
+ "prompt_is_human_readable_serialization_not_runtime_preamble": True,
1591
+ "available_to_specialist_routing": True,
1592
+ "persistent_procedural_binding": True,
1593
+ "bound_to_stabilizing_spine": spine.get("constitution_sha256") == constitution.constitution_sha256,
1594
+ "raw_prompt_prepended_to_user_utterance": False,
1595
+ "situated_state_changes_without_redefining_self": True,
1596
+ "user_retains_goal_and_consequential_authority": True,
1597
+ }
1598
+ output["runtime"]["typed_discourse_model_calls"] = 0
1599
+ output["runtime"]["typed_discourse_gradient_steps"] = 0
1600
+ output["runtime"]["typed_discourse_journal_full_scans"] = 0
1601
+ output["runtime"]["situated_identity_model_calls"] = 0
1602
+ output["schema_version"] = SCHEMA
1603
+ output["turn_manifest_sha256"] = digest(output)
1604
+ return output
1605
+
1606
+
1607
+ __all__ = [
1608
+ "IndexedTypedDiscourseJournal",
1609
+ "K102_SYSTEM_PROMPT",
1610
+ "PersistentTypedDiscourseAgent",
1611
+ "SituatedIdentityConstitution",
1612
+ "TypedDiscourseState",
1613
+ "compile_situated_constitution",
1614
+ ]
root/wander-train/stage5k104_relational_ingress_equivalence_agent.py ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """K104 successor for the two ingress classes exposed prospectively by K103.
3
+
4
+ K102 and the failed one-shot K103 evidence remain immutable. This successor
5
+ adds no reasoning cell, language model, embedding encoder, or generator. It
6
+ only maps direct rhetorical imperatives onto the existing artifact operator
7
+ and gates authoritative-science requests by exact referent-bearing source
8
+ evidence before answer or abstention.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ from pathlib import Path
14
+ import re
15
+ from typing import Any, Mapping, Sequence
16
+
17
+ from stage5k15_native_mixed_agent import HandlerResult, SemanticClause, clean, digest
18
+ from stage5k102_persistent_typed_discourse_agent import (
19
+ IndexedTypedDiscourseJournal,
20
+ PersistentTypedDiscourseAgent,
21
+ TypedDiscourseState,
22
+ _K102_TURN,
23
+ _trim,
24
+ )
25
+
26
+
27
+ SCHEMA = "oracle-stage5k104-relational-ingress-equivalence-agent-v1"
28
+
29
+
30
+ class RelationalIngressEquivalenceAgent(PersistentTypedDiscourseAgent):
31
+ """K102 plus generalized speech-act and epistemic-ingress equivalence."""
32
+
33
+ _DIRECT_RHETORICAL = re.compile(
34
+ r"^(?:please\s+)?(outline|structure|plan|draft)\s+(a|an|the)\s+"
35
+ r"(paper|essay|article)\s+(arguing\s+that|about|on)\s+(.+?)[?.!]*$",
36
+ re.IGNORECASE,
37
+ )
38
+ _AUTHORITATIVE_SCIENCE = re.compile(
39
+ r"^(?:please\s+)?(?:give|provide|state|explain|describe)(?:\s+me)?\s+"
40
+ r"(?:the\s+)?(?:(?:accepted|established|recognized)\s+)?"
41
+ r"(?:scientific\s+)(definition|mechanism|account|explanation)\s+"
42
+ # Longest determiner first: otherwise the ``a`` alternative consumes
43
+ # the first character of ``an`` and corrupts the referent boundary.
44
+ r"(?:of|for)\s+(?:the|an|a)?\s*(.+?)[?.!]*$",
45
+ re.IGNORECASE,
46
+ )
47
+
48
+ def __init__(
49
+ self,
50
+ *args: Any,
51
+ discourse_journal_path: Path | None = None,
52
+ k104_ablations: Sequence[str] = (),
53
+ **kwargs: Any,
54
+ ) -> None:
55
+ substrate_path = Path(args[5] if len(args) >= 6 else kwargs["substrate_path"])
56
+ self._k104_ablations = frozenset(clean(value).casefold() for value in k104_ablations)
57
+ journal_path = discourse_journal_path or substrate_path.with_suffix(".k104.discourse.sqlite3")
58
+ super().__init__(*args, discourse_journal_path=journal_path, **kwargs)
59
+ # The successor journal is bound to the successor implementation,
60
+ # rather than silently inheriting K102's module identity.
61
+ self.discourse_journal = IndexedTypedDiscourseJournal(
62
+ journal_path,
63
+ source_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
64
+ )
65
+ self._k104_lineage_sha256 = digest({
66
+ "schema_version": SCHEMA,
67
+ "parent_lineage_sha256": self._k102_lineage_sha256,
68
+ "opened_failure_source": "stage5k103_untouched_phase_transition_report_v1",
69
+ "repairs": [
70
+ "direct_and_periphrastic_rhetorical_imperative_equivalence",
71
+ "authoritative_scientific_exact_referent_source_gate",
72
+ "unsupported_scientific_referent_abstention",
73
+ ],
74
+ })
75
+
76
+ @staticmethod
77
+ def _exact_referent_present(referent: str, fact: str) -> bool:
78
+ normalized_referent = " ".join(re.findall(r"[a-z0-9]+", clean(referent).casefold()))
79
+ normalized_fact = " ".join(re.findall(r"[a-z0-9]+", clean(fact).casefold()))
80
+ return bool(normalized_referent and re.search(
81
+ rf"(?:^|\s){re.escape(normalized_referent)}(?:\s|$)", normalized_fact,
82
+ ))
83
+
84
+ def _scientific_ingress_result(self, text: str) -> HandlerResult | None:
85
+ match = self._AUTHORITATIVE_SCIENCE.match(clean(text))
86
+ if match is None:
87
+ return None
88
+ relation, referent = match.group(1).casefold(), _trim(match.group(2))
89
+ if "authoritative_scientific_referent_gate" in self._k104_ablations:
90
+ return None
91
+ terms = tuple(dict.fromkeys(re.findall(r"[a-z0-9]+", referent.casefold())))
92
+ rows = ()
93
+ if terms:
94
+ query = " AND ".join(f'"{term}"' for term in terms)
95
+ rows = self.information_source_expert.connection.execute(
96
+ "SELECT text_sha256, fact, source_document, source_document_sha256, "
97
+ "domain_id, license_note, source_split, bm25(source_rows) "
98
+ "FROM source_rows WHERE source_rows MATCH ? ORDER BY bm25(source_rows) LIMIT 128",
99
+ (query,),
100
+ ).fetchall()
101
+ authoritative = []
102
+ for row in rows:
103
+ source, domain, fact = str(row[2]), str(row[4]), clean(row[1])
104
+ admitted_authority = source.startswith("openstax/") or domain in {
105
+ "life_health", "physical_engineering", "earth_space", "social_behavioral",
106
+ "governance_law", "economics_organization",
107
+ }
108
+ if (
109
+ admitted_authority
110
+ and self._exact_referent_present(referent, fact)
111
+ and not fact.rstrip().endswith("?")
112
+ ):
113
+ authoritative.append(row)
114
+ current = _K102_TURN.get()
115
+ if current is not None:
116
+ current.audit.update({
117
+ "recognized": True,
118
+ "discourse_operation": "authoritative_scientific_referent_gate",
119
+ "requested_relation": relation,
120
+ "referent": referent,
121
+ "exact_authoritative_rows": len(authoritative),
122
+ })
123
+ if not authoritative:
124
+ return self._abstention(
125
+ "unsupported_scientific_referent",
126
+ f"an established scientific {relation} for {referent}",
127
+ "exact_authoritative_referent_evidence_absent",
128
+ )
129
+ row = authoritative[0]
130
+ fact = _trim(row[1])
131
+ return HandlerResult(
132
+ "ground_authoritative_scientific_relation", "answered",
133
+ (("relation", relation), ("referent", referent)),
134
+ (("source_rows", 1),),
135
+ (
136
+ SemanticClause(
137
+ f"The authenticated source on {referent}", "state", object_value=fact,
138
+ ),
139
+ ),
140
+ ((str(row[3]), str(row[0])),),
141
+ (
142
+ "recognize_authoritative_scientific_request",
143
+ "bind_complete_multiword_referent",
144
+ "require_exact_referent_in_authenticated_source",
145
+ "release_supported_relation_at_malkuth",
146
+ ),
147
+ tool_audit={
148
+ "tool": "k104_exact_scientific_referent_gate",
149
+ "source_document": str(row[2]),
150
+ "license_note": str(row[5]),
151
+ "read_only": True,
152
+ "traditional_language_model": False,
153
+ },
154
+ )
155
+
156
+ def _paper_result(self, state: TypedDiscourseState, text: str) -> HandlerResult | None:
157
+ direct = self._DIRECT_RHETORICAL.match(clean(text))
158
+ if direct is not None and "direct_rhetorical_equivalence" not in self._k104_ablations:
159
+ verb, article, genre, relation, claim = (
160
+ _trim(direct.group(index)) for index in range(1, 6)
161
+ )
162
+ canonical = f"Help me {verb} {article} {genre} {relation} {claim}."
163
+ result = super()._paper_result(state, canonical)
164
+ current = _K102_TURN.get()
165
+ if current is not None:
166
+ current.audit.update({
167
+ "recognized": result is not None,
168
+ "discourse_operation": "direct_rhetorical_equivalence",
169
+ "canonical_surface_sha256": hashlib.sha256(canonical.encode("utf-8")).hexdigest(),
170
+ "meaning_preserved": True,
171
+ })
172
+ if result is not None:
173
+ return HandlerResult(
174
+ result.operation, result.status, result.arguments, result.result,
175
+ result.clauses, result.evidence,
176
+ (
177
+ "recognize_direct_rhetorical_imperative",
178
+ "commute_politeness_periphrasis_without_changing_roles",
179
+ *result.computation_trace,
180
+ ),
181
+ tool_audit=result.tool_audit, memory_audit=result.memory_audit,
182
+ )
183
+ return super()._paper_result(state, text)
184
+
185
+ def _handle_evidence(self, text: str, context: Mapping[str, Any]) -> HandlerResult | None:
186
+ scientific = self._scientific_ingress_result(text)
187
+ return scientific if scientific is not None else super()._handle_evidence(text, context)
188
+
189
+ def _handle_event(self, session_id: str, text: str, turn_id: str) -> HandlerResult | None:
190
+ scientific = self._scientific_ingress_result(text)
191
+ return scientific if scientific is not None else super()._handle_event(session_id, text, turn_id)
192
+
193
+ def _event_capability_family(self, text: str) -> tuple[str, ...]:
194
+ if self._DIRECT_RHETORICAL.match(clean(text)) or self._AUTHORITATIVE_SCIENCE.match(clean(text)):
195
+ return (
196
+ "linguistic", "synthesis", "epistemic", "contrast", "causal",
197
+ "mechanism", "tool_router", "dataset", "provenance",
198
+ )
199
+ return super()._event_capability_family(text)
200
+
201
+ def answer(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
202
+ output = super().answer(*args, **kwargs)
203
+ audit = (output.get("typed_discourse_workspace") or {}).get("audit") or {}
204
+ output["relational_ingress_equivalence"] = {
205
+ "schema_version": SCHEMA,
206
+ "lineage_sha256": self._k104_lineage_sha256,
207
+ "opened_failure_source": "immutable_k103_one_shot_failure",
208
+ "direct_rhetorical_equivalence_activated": audit.get("discourse_operation")
209
+ == "direct_rhetorical_equivalence",
210
+ "scientific_referent_gate_activated": audit.get("discourse_operation")
211
+ == "authoritative_scientific_referent_gate",
212
+ "exact_authoritative_rows": int(audit.get("exact_authoritative_rows", 0)),
213
+ "traditional_language_model": False,
214
+ "transformer": False,
215
+ "pretrained_embedding_encoder": False,
216
+ "autoregressive_carrier": False,
217
+ "model_calls": 0,
218
+ "gradient_steps": 0,
219
+ }
220
+ output["runtime"].update({
221
+ "relational_ingress_equivalence_model_calls": 0,
222
+ "relational_ingress_equivalence_gradient_steps": 0,
223
+ })
224
+ output["turn_manifest_sha256"] = digest(output)
225
+ return output
226
+
227
+
228
+ __all__ = ["RelationalIngressEquivalenceAgent", "SCHEMA"]
root/wander-train/stage5k108_universal_terminal_proof_projection_agent.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """K108 universal terminal-proof projection successor.
3
+
4
+ K106 and the immutable K107 one-shot evidence remain untouched. K107
5
+ prospectively confirmed scientific proof-ledger congruence, while an unrelated
6
+ causal turn exposed the same telemetry mismatch outside the scientific gate.
7
+ This successor applies the already causal rule universally: the public
8
+ knowledge ledger contains exactly those authenticated public rows referenced
9
+ by the terminal typed proof, irrespective of upstream candidates.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import hashlib
14
+ from pathlib import Path
15
+ from typing import Any, Sequence
16
+
17
+ from stage5k15_native_mixed_agent import clean, digest
18
+ from stage5k102_persistent_typed_discourse_agent import IndexedTypedDiscourseJournal
19
+ from stage5k106_terminal_proof_provenance_agent import TerminalProofProvenanceAgent
20
+
21
+
22
+ SCHEMA = "oracle-stage5k108-universal-terminal-proof-projection-agent-v1"
23
+
24
+
25
+ class UniversalTerminalProofProjectionAgent(TerminalProofProvenanceAgent):
26
+ """Make terminal proof evidence authoritative for every public ledger."""
27
+
28
+ def __init__(
29
+ self,
30
+ *args: Any,
31
+ discourse_journal_path: Path | None = None,
32
+ k108_ablations: Sequence[str] = (),
33
+ **kwargs: Any,
34
+ ) -> None:
35
+ substrate_path = Path(args[5] if len(args) >= 6 else kwargs["substrate_path"])
36
+ self._k108_ablations = frozenset(clean(value).casefold() for value in k108_ablations)
37
+ journal_path = discourse_journal_path or substrate_path.with_suffix(".k108.discourse.sqlite3")
38
+ super().__init__(*args, discourse_journal_path=journal_path, **kwargs)
39
+ self.discourse_journal = IndexedTypedDiscourseJournal(
40
+ journal_path,
41
+ source_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
42
+ )
43
+ self._k108_lineage_sha256 = digest({
44
+ "schema_version": SCHEMA,
45
+ "parent_lineage_sha256": self._k106_lineage_sha256,
46
+ "opened_failure_source": "stage5k107_untouched_proof_congruence_report_v1",
47
+ "repair": "universal_terminal_typed_proof_projection_to_public_knowledge_ledger",
48
+ })
49
+
50
+ def _universal_projection(self, output: dict[str, Any]) -> dict[str, Any]:
51
+ if "universal_terminal_proof_projection" in self._k108_ablations:
52
+ return {"activated": False, "ablated": True}
53
+ proof = output.get("computation_proof") or {}
54
+ admitted_rows = []
55
+ seen = set()
56
+ for pair in proof.get("evidence", ()):
57
+ if not isinstance(pair, (list, tuple)) or len(pair) != 2:
58
+ continue
59
+ source_sha, text_sha = str(pair[0]), str(pair[1])
60
+ if len(source_sha) != 64 or len(text_sha) != 64 or text_sha in seen:
61
+ continue
62
+ row = self._source_row_for_proof_evidence(text_sha)
63
+ if row is None or row["source_document_sha256"] != source_sha:
64
+ continue
65
+ admitted_rows.append(row); seen.add(text_sha)
66
+ lattice = dict(output.get("knowledge_lattice") or {})
67
+ prior_rows = tuple(lattice.get("evidence_rows_admitted_to_proof") or ())
68
+ prior_admitted = bool(lattice.get("retrieval_admitted_to_proof"))
69
+ admitted = bool(admitted_rows)
70
+ lattice.update({
71
+ "candidate_retrieval_observed": bool(prior_rows),
72
+ "candidate_rows_before_universal_projection": len(prior_rows),
73
+ "candidate_admission_before_universal_projection": prior_admitted,
74
+ "evidence_rows_admitted_to_proof": admitted_rows,
75
+ "retrieval_admitted_to_proof": admitted,
76
+ "retrieval_gate_reason": (
77
+ "terminal_typed_proof_contains_authenticated_public_source"
78
+ if admitted else "terminal_typed_proof_contains_no_authenticated_public_source"
79
+ ),
80
+ "relational_provenance_reconciled": True,
81
+ "terminal_proof_authoritative_for_admission": True,
82
+ "universal_terminal_proof_projection": True,
83
+ "terminal_proof_operation": str(output.get("operation", "")),
84
+ "terminal_proof_status": str(output.get("status", "")),
85
+ "terminal_proof_evidence_sha256s": [row["text_sha256"] for row in admitted_rows],
86
+ })
87
+ output["knowledge_lattice"] = lattice
88
+ return {
89
+ "activated": True,
90
+ "ablated": False,
91
+ "operation": str(output.get("operation", "")),
92
+ "status": str(output.get("status", "")),
93
+ "candidate_admission_before": prior_admitted,
94
+ "candidate_rows_before": len(prior_rows),
95
+ "terminal_authenticated_rows": len(admitted_rows),
96
+ "retrieval_admitted_to_proof": admitted,
97
+ "projection_changed_admission": prior_admitted is not admitted,
98
+ "proof_and_ledger_congruent": bool(
99
+ admitted == bool(admitted_rows)
100
+ and {row["text_sha256"] for row in admitted_rows} == seen
101
+ ),
102
+ }
103
+
104
+ def answer(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
105
+ output = super().answer(*args, **kwargs)
106
+ projection = self._universal_projection(output)
107
+ output["universal_terminal_proof_projection"] = {
108
+ "schema_version": SCHEMA,
109
+ "lineage_sha256": self._k108_lineage_sha256,
110
+ "opened_failure_source": "immutable_k107_one_shot_failure",
111
+ **projection,
112
+ "traditional_language_model": False,
113
+ "transformer": False,
114
+ "pretrained_embedding_encoder": False,
115
+ "autoregressive_carrier": False,
116
+ "model_calls": 0,
117
+ "gradient_steps": 0,
118
+ }
119
+ output["runtime"].update({
120
+ "universal_terminal_proof_projection_model_calls": 0,
121
+ "universal_terminal_proof_projection_gradient_steps": 0,
122
+ })
123
+ output["turn_manifest_sha256"] = digest(output)
124
+ return output
125
+
126
+
127
+ __all__ = ["UniversalTerminalProofProjectionAgent", "SCHEMA"]
root/wander-train/stage5k115_copular_definition_equivalence_agent.py ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """K115 explicit copular-definition equivalence over frozen K112.
3
+
4
+ The immutable K114 prospective failure showed that K112 could locate an exact,
5
+ authoritative row and verify definition direction, yet reject ``X is the
6
+ process of ...`` because its deliberately small morphological normalization
7
+ mapped ``process`` to ``proces`` before relation-cue comparison. This
8
+ successor treats an exact referent followed by a copula and a finite
9
+ definitional complement class as the definition relation itself. It contains
10
+ no domain entities, answer text, language model, embedding model, or scan.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import replace
15
+ import hashlib
16
+ import re
17
+ from pathlib import Path
18
+ from typing import Any, Mapping, Sequence
19
+
20
+ from stage5k15_native_mixed_agent import clean, digest
21
+ from stage5k81_information_weighted_source_expert import terms
22
+ from stage5j1_native_contextual_sense_lattice import WORD
23
+ from stage5k112_relation_conditioned_source_agent import (
24
+ RelationConditionedSourceAgent,
25
+ RelationRequest,
26
+ SourceCertificate,
27
+ )
28
+
29
+
30
+ SCHEMA = "oracle-stage5k115-copular-definition-equivalence-agent-v1"
31
+
32
+ DEFINITIONAL_COMPLEMENT_CLASSES = frozenset({
33
+ "act", "action", "activity", "approach", "condition", "concept",
34
+ "doctrine", "event", "form", "framework", "idea", "means", "method",
35
+ "entity", "operation", "orientation", "pattern", "phenomenon", "practice",
36
+ "principle", "procedure", "process", "property", "relation", "rule",
37
+ "state", "step", "steps", "system", "technique", "theory", "type", "way",
38
+ })
39
+
40
+ NON_DEFINITIONAL_COMPLEMENT_MODIFIERS = frozenset({
41
+ "best", "commonly", "correct", "different", "important", "less", "more",
42
+ "most", "not", "now", "only", "sometimes", "used",
43
+ })
44
+
45
+
46
+ def _ordered_phrase(value: str) -> str:
47
+ """Normalize surface tokens without erasing repetition or order."""
48
+ normalized = []
49
+ for raw in WORD.findall(str(value)):
50
+ token = raw.casefold().replace("’", "'")
51
+ if token.endswith("'s") and len(token) > 2:
52
+ token = token[:-2]
53
+ normalized.append(token)
54
+ return " ".join(normalized)
55
+
56
+
57
+ class CopularDefinitionEquivalenceAgent(RelationConditionedSourceAgent):
58
+ """Admit explicit copular definitions missed by K112 normalization."""
59
+
60
+ def __init__(self, *args: Any, k115_ablations: Sequence[str] = (), **kwargs: Any) -> None:
61
+ self._k115_ablations = frozenset(clean(value).casefold() for value in k115_ablations)
62
+ super().__init__(*args, **kwargs)
63
+ self._k115_lineage_sha256 = digest({
64
+ "schema_version": SCHEMA,
65
+ "parent_lineage_sha256": self._k112_lineage_sha256,
66
+ "preserved_failure_source": "immutable_stage5k114_untouched_relational_transition_report_v1",
67
+ "repair": "exact_referent_copula_definitional_complement_equivalence",
68
+ })
69
+
70
+ @classmethod
71
+ def _relation_request(cls, text: str) -> RelationRequest | None:
72
+ request = super()._relation_request(text)
73
+ if request is None or request.relation != "definition":
74
+ return request
75
+ # K112's frozen optional-article alternation checks ``a`` before
76
+ # ``an`` and can therefore consume only the first character of ``an``.
77
+ # Reconstruct the definition referent from the original surface with
78
+ # longest-article-first precedence. The request's already derived
79
+ # content/referent terms remain valid and contain no answer knowledge.
80
+ value = clean(text).rstrip(".?!")
81
+ match = re.match(
82
+ r"^(?:what\s+(?:is|are)|define)\s+(?:(?:an|the|a)\s+)?(.+)$",
83
+ value, re.IGNORECASE,
84
+ )
85
+ if match is None:
86
+ return request
87
+ referent = clean(match.group(1))
88
+ return replace(request, referent=referent) if referent else request
89
+
90
+ @staticmethod
91
+ def _copular_definition_match(request: RelationRequest, fact: str) -> Mapping[str, Any] | None:
92
+ if request.relation != "definition":
93
+ return None
94
+ normalized_fact = _ordered_phrase(fact)
95
+ normalized_referent = _ordered_phrase(request.referent)
96
+ if not normalized_referent:
97
+ return None
98
+ pattern = re.compile(
99
+ rf"^(?:(?:a|an|the)\s+)?{re.escape(normalized_referent)}\s+"
100
+ rf"(?P<copula>is|are|means|denotes|describes|refers\s+to)\s+"
101
+ rf"(?:(?:a|an|the)\s+)?(?P<complement_phrase>[a-z-]+(?:\s+[a-z-]+){{0,3}})\b",
102
+ )
103
+ match = pattern.search(normalized_fact)
104
+ if match is None:
105
+ return None
106
+ phrase = match.group("complement_phrase").split()
107
+ head_index = next(
108
+ (index for index, value in enumerate(phrase) if value in DEFINITIONAL_COMPLEMENT_CLASSES),
109
+ None,
110
+ )
111
+ if head_index is None:
112
+ return None
113
+ modifiers = phrase[:head_index]
114
+ if any(value in NON_DEFINITIONAL_COMPLEMENT_MODIFIERS for value in modifiers):
115
+ return None
116
+ return {
117
+ "operator": "copular_definition",
118
+ "copula": match.group("copula"),
119
+ "complement_class": phrase[head_index],
120
+ "complement_modifiers": modifiers,
121
+ "exact_referent_prefix": True,
122
+ }
123
+
124
+ def _copular_certificate(
125
+ self, request: RelationRequest, parent: SourceCertificate,
126
+ ) -> SourceCertificate | None:
127
+ if request.relation != "definition":
128
+ return None
129
+ # Address with the preserved referent surface as well as variants.
130
+ # K112's request lemmas are useful for broad relational matching but
131
+ # intentionally lossy (for example ``process`` -> ``proces`` and
132
+ # ``retailing`` -> ``retail``); exact copular definitions require the
133
+ # original noun surface to remain available to FTS.
134
+ referent_surfaces = tuple(
135
+ value for value in terms(request.referent)
136
+ if self.information_source_expert._fts_token(value)
137
+ )
138
+ values = set(referent_surfaces)
139
+ if not values:
140
+ return None
141
+ referent_expression = " AND ".join(f'"{value}"' for value in referent_surfaces)
142
+ relation_expression = " OR ".join(
143
+ f'"{value}"' for value in sorted(DEFINITIONAL_COMPLEMENT_CLASSES)
144
+ )
145
+ expression = f"{referent_expression} AND ({relation_expression})"
146
+ rows = self.information_source_expert.connection.execute(
147
+ "SELECT text_sha256, fact, source_document, source_document_sha256, "
148
+ "domain_id, license_note, source_split, bm25(source_rows) "
149
+ "FROM source_rows WHERE source_rows MATCH ? ORDER BY bm25(source_rows) LIMIT 768",
150
+ (expression,),
151
+ ).fetchall()
152
+ ranked = []
153
+ audits = []
154
+ for row in rows:
155
+ fact = clean(row[1])
156
+ match = self._copular_definition_match(request, fact)
157
+ noise, noise_reasons = self._row_noise(fact)
158
+ source_name = str(row[2])
159
+ authority = 5.0 if source_name.startswith("openstax/") else 1.5 if source_name.startswith("gutenberg/") else 0.0
160
+ declarative = not fact.rstrip().endswith("?") and "multiple_choice_source" not in noise_reasons
161
+ eligible = bool(match is not None and declarative and authority > 0)
162
+ score = 20.0 + authority - noise + (2.0 if match and match["copula"] in {"is", "are"} else 0.0)
163
+ audit = {
164
+ "text_sha256": str(row[0]), "source_document": source_name,
165
+ "eligible": eligible, "operator_match": match,
166
+ "declarative": declarative, "authority": authority,
167
+ "noise_reasons": list(noise_reasons), "score": score,
168
+ }
169
+ audits.append((score, float(row[7]), str(row[0]), audit))
170
+ if eligible:
171
+ ranked.append((score, float(row[7]), str(row[0]), row, audit))
172
+ audits.sort(key=lambda value: (-value[0], value[1], value[2]))
173
+ if not ranked:
174
+ return None
175
+ ranked.sort(key=lambda value: (-value[0], value[1], value[2]))
176
+ best = ranked[0]
177
+ row = best[3]
178
+ selected = {
179
+ "text_sha256": str(row[0]), "fact": clean(row[1]),
180
+ "source_document": str(row[2]), "source_document_sha256": str(row[3]),
181
+ "domain_id": str(row[4]), "license_note": str(row[5]),
182
+ "source_split": str(row[6]),
183
+ "admission_authority": "k115_copular_definition_equivalence",
184
+ }
185
+ runner = ranked[1][0] if len(ranked) > 1 else None
186
+ return SourceCertificate(
187
+ request=request, selected_row=selected, accepted=True,
188
+ reason="copular_definition_equivalence_satisfied",
189
+ selected_score=best[0], runner_up_score=runner,
190
+ inspected_postings=len(rows), eligible_candidates=len(ranked),
191
+ audit_candidates=tuple(value[3] for value in audits[:5]),
192
+ )
193
+
194
+ def _certificate(self, request: RelationRequest) -> SourceCertificate:
195
+ parent = super()._certificate(request)
196
+ if "copular_definition_equivalence" in self._k115_ablations:
197
+ return parent
198
+ # An exact sentence-prefix copular definition is a stronger relation
199
+ # certificate than an embedded mention admitted by K112's broad
200
+ # lexical relation cues. Prefer it even when the parent found a weaker
201
+ # candidate; other relation kinds remain completely unchanged.
202
+ return self._copular_certificate(request, parent) or parent
203
+
204
+ def answer(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
205
+ output = super().answer(*args, **kwargs)
206
+ certificate = output.get("relation_conditioned_source_certificate") or {}
207
+ activated = certificate.get("reason") == "copular_definition_equivalence_satisfied"
208
+ output["copular_definition_equivalence"] = {
209
+ "schema_version": SCHEMA,
210
+ "lineage_sha256": self._k115_lineage_sha256,
211
+ "activated": activated,
212
+ "ablated": "copular_definition_equivalence" in self._k115_ablations,
213
+ "operator": "exact_referent_plus_copula_plus_definitional_complement_class",
214
+ "definitional_complement_class_count": len(DEFINITIONAL_COMPLEMENT_CLASSES),
215
+ "maximum_complement_modifiers": 3,
216
+ "domain_entities_encoded": 0,
217
+ "traditional_language_model": False, "transformer": False,
218
+ "pretrained_embedding_encoder": False, "autoregressive_carrier": False,
219
+ "model_calls": 0, "gradient_steps": 0,
220
+ }
221
+ output["runtime"].update({
222
+ "copular_definition_equivalence_model_calls": 0,
223
+ "copular_definition_equivalence_gradient_steps": 0,
224
+ })
225
+ output["turn_manifest_sha256"] = digest(output)
226
+ return output
227
+
228
+
229
+ __all__ = ["CopularDefinitionEquivalenceAgent", "SCHEMA", "DEFINITIONAL_COMPLEMENT_CLASSES"]
root/wander-train/stage5k117_caller_context_isolation_agent.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """K117 caller-context isolation over immutable K115/K116 evidence.
3
+
4
+ K116 established that K115's relation operator transfers to fresh definitions,
5
+ but exposed an independent precedence defect on a causal assertion. K90 may
6
+ insert a private retrieval sentinel below the outer answer boundary. K81 saw
7
+ that sentinel as though it were caller-supplied evidence and consequently
8
+ demoted an already valid ``assert_causal`` graph to ``delegate``. The
9
+ misrouted ephemeral event then made deterministic restart replay correctly
10
+ reject the changed operation.
11
+
12
+ This successor separates caller context from internal carrier context. A
13
+ typed graph that consumes the utterance remains prior to private sentinels;
14
+ genuine caller-supplied evidence, corpus records, and sensor streams retain
15
+ their bounded rails. No domain words, answer strings, model, embedding
16
+ encoder, gradient update, or autoregressive carrier are introduced.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ from collections.abc import Sequence
21
+ from contextvars import ContextVar
22
+ from typing import Any, Mapping
23
+
24
+ from stage5k15_native_mixed_agent import clean, digest
25
+ from stage5k81_native_relational_construction_agent import _CONTEXT_KIND
26
+ from stage5k102_persistent_typed_discourse_agent import K102FinitePredicateTransducer
27
+ from stage5k115_copular_definition_equivalence_agent import (
28
+ CopularDefinitionEquivalenceAgent,
29
+ )
30
+
31
+
32
+ SCHEMA = "oracle-stage5k117-caller-context-isolation-agent-v1"
33
+
34
+ _K117_CALLER_CONTEXT_KIND: ContextVar[str] = ContextVar(
35
+ "k117_caller_context_kind", default="none",
36
+ )
37
+ _K117_ISOLATION_AUDIT: ContextVar[Mapping[str, Any] | None] = ContextVar(
38
+ "k117_isolation_audit", default=None,
39
+ )
40
+
41
+
42
+ def _caller_context_kind(context: Mapping[str, Any] | None) -> str:
43
+ value = context or {}
44
+ for key in ("evidence", "training_corpus", "sensor_stream"):
45
+ payload = value.get(key)
46
+ if isinstance(payload, Sequence) and not isinstance(payload, (str, bytes)) and payload:
47
+ return key
48
+ return "none"
49
+
50
+
51
+ class CallerContextIsolationTransducer(K102FinitePredicateTransducer):
52
+ """Make graph precedence depend on the outer caller boundary."""
53
+
54
+ def analyze(self, text: str, discourse, *, known_frames=()):
55
+ inherited_context_kind = _CONTEXT_KIND.get()
56
+ caller_context_kind = _K117_CALLER_CONTEXT_KIND.get()
57
+ ablated = "caller_context_isolation" in self.owner._k117_ablations
58
+
59
+ # Obtain the fully repaired inherited graph with context influence
60
+ # suppressed only when context was introduced internally. This calls
61
+ # through K102/K94/.../K81's MRO once and retains every later parser
62
+ # repair. For genuine caller context the inherited behavior is exact.
63
+ token = None
64
+ if not ablated and caller_context_kind == "none" and inherited_context_kind != "none":
65
+ token = _CONTEXT_KIND.set("none")
66
+ try:
67
+ graph = super().analyze(text, discourse, known_frames=known_frames)
68
+ finally:
69
+ if token is not None:
70
+ _CONTEXT_KIND.reset(token)
71
+
72
+ _K117_ISOLATION_AUDIT.set({
73
+ "caller_context_kind": caller_context_kind,
74
+ "inherited_context_kind": inherited_context_kind,
75
+ "internal_context_isolated": token is not None,
76
+ "ablated": ablated,
77
+ "selected_speech_act": graph.speech_act,
78
+ "typed_graph_consumes_utterance": bool(graph.consumes_as_graph),
79
+ })
80
+ return graph
81
+
82
+
83
+ class CallerContextIsolationAgent(CopularDefinitionEquivalenceAgent):
84
+ """Preserve K115 while preventing private carrier/context confusion."""
85
+
86
+ def __init__(self, *args: Any, k117_ablations: Sequence[str] = (), **kwargs: Any) -> None:
87
+ self._k117_ablations = frozenset(clean(value).casefold() for value in k117_ablations)
88
+ super().__init__(*args, **kwargs)
89
+ self.graph_transducer = CallerContextIsolationTransducer(
90
+ self, self.construction_head,
91
+ )
92
+ self._k117_lineage_sha256 = digest({
93
+ "schema_version": SCHEMA,
94
+ "parent_lineage_sha256": self._k115_lineage_sha256,
95
+ "preserved_failure_source": (
96
+ "immutable_stage5k116_untouched_operator_transfer_report_v1"
97
+ ),
98
+ "repair": "outer_caller_context_distinct_from_internal_retrieval_carrier",
99
+ })
100
+
101
+ def _handle_evidence(self, text: str, context: Mapping[str, Any]):
102
+ evidence = context.get("evidence")
103
+ private_carrier = bool(
104
+ isinstance(evidence, Sequence)
105
+ and not isinstance(evidence, (str, bytes))
106
+ and any(
107
+ isinstance(value, Mapping)
108
+ and str(value.get("answer_status", "")).startswith("k90_")
109
+ for value in evidence
110
+ )
111
+ )
112
+ graph = self._k76_current.get()
113
+ typed_graph_prior = bool(
114
+ _K117_CALLER_CONTEXT_KIND.get() == "none"
115
+ and "caller_context_isolation" not in self._k117_ablations
116
+ and private_carrier
117
+ and graph is not None
118
+ and graph.speech_act in self.DIRECT_ACTS
119
+ and graph.consumes_as_graph
120
+ )
121
+ if typed_graph_prior:
122
+ audit = dict(_K117_ISOLATION_AUDIT.get() or {})
123
+ audit.update({
124
+ "private_carrier_detected": True,
125
+ "private_carrier_bypassed_evidence_handler": True,
126
+ "typed_graph_precedence_preserved": True,
127
+ })
128
+ _K117_ISOLATION_AUDIT.set(audit)
129
+ return None
130
+ return super()._handle_evidence(text, context)
131
+
132
+ def answer(
133
+ self,
134
+ session_id: str,
135
+ text: str,
136
+ context: Mapping[str, Any] | None = None,
137
+ **kwargs: Any,
138
+ ) -> dict[str, Any]:
139
+ caller_kind = _caller_context_kind(context)
140
+ caller_token = _K117_CALLER_CONTEXT_KIND.set(caller_kind)
141
+ audit_token = _K117_ISOLATION_AUDIT.set(None)
142
+ try:
143
+ output = super().answer(session_id, text, context, **kwargs)
144
+ audit = dict(_K117_ISOLATION_AUDIT.get() or {})
145
+ finally:
146
+ _K117_ISOLATION_AUDIT.reset(audit_token)
147
+ _K117_CALLER_CONTEXT_KIND.reset(caller_token)
148
+ output["caller_context_isolation"] = {
149
+ "schema_version": SCHEMA,
150
+ "lineage_sha256": self._k117_lineage_sha256,
151
+ "development_source": "opened_immutable_k116_failure",
152
+ "prospective_claim": False,
153
+ "caller_context_kind": caller_kind,
154
+ "ablated": "caller_context_isolation" in self._k117_ablations,
155
+ "audit": audit,
156
+ "private_retrieval_sentinel_is_caller_evidence": False,
157
+ "genuine_caller_context_precedence_preserved": True,
158
+ "domain_entities_encoded": 0,
159
+ "traditional_language_model": False,
160
+ "transformer": False,
161
+ "pretrained_embedding_encoder": False,
162
+ "autoregressive_carrier": False,
163
+ "model_calls": 0,
164
+ "gradient_steps": 0,
165
+ }
166
+ output["runtime"].update({
167
+ "caller_context_isolation_model_calls": 0,
168
+ "caller_context_isolation_gradient_steps": 0,
169
+ })
170
+ output["schema_version"] = SCHEMA
171
+ output["turn_manifest_sha256"] = digest(output)
172
+ return output
173
+
174
+
175
+ __all__ = ["CallerContextIsolationAgent", "CallerContextIsolationTransducer", "SCHEMA"]
root/wander-train/stage5k11_quadratic_inverse_solver.py ADDED
@@ -0,0 +1,348 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Stage 5K11 proof-conditioned quadratic native inverse solver.
3
+
4
+ The solver chooses a complete construction coordinate in one global decision.
5
+ It never predicts a next token. Candidate clauses are compiled from the typed
6
+ computation proof, checked by the native forward analyzer, and represented as
7
+ binary one-hot variables in an auditable QUBO:
8
+
9
+ E(x) = sum_i u_i x_i + P (sum_i x_i - 1)^2
10
+
11
+ The unary semantic energy ``u_i`` is itself decomposed into frame, operator,
12
+ role, alpha-equivalence, and minimal-chart-complexity terms. This initial
13
+ whole-construction QUBO is an exact verifier and a bridge to the later
14
+ factorized role/slot/discourse QUBO; it already replaces serial surface search
15
+ with a globally selected sentence-shaped object.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import hashlib
21
+ import itertools
22
+ from dataclasses import dataclass
23
+ from typing import Any, Mapping, Sequence
24
+
25
+ import numpy as np
26
+
27
+ from stage5k3_language_adapter import LanguageAdapter
28
+ from stage5k10_self_inverse_computation import (
29
+ NativeEnglishInverseGrammar,
30
+ NativeRealization,
31
+ RoundTripAudit,
32
+ SelfInverseProof,
33
+ digest,
34
+ )
35
+
36
+
37
+ SCHEMA = "oracle-stage5k11-quadratic-inverse-solver-v1"
38
+ CONSTRUCTION_VARIANTS = (
39
+ "frame_order",
40
+ "nominals_before_prepositions",
41
+ "prepositions_before_nominals",
42
+ )
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class QuadraticCandidate:
47
+ candidate_id: str
48
+ realization: NativeRealization
49
+ round_trip: RoundTripAudit
50
+ role_transformations: tuple[tuple[str, str], ...]
51
+ preposition_choices: tuple[tuple[str, str], ...]
52
+ energy_terms: tuple[tuple[str, float], ...]
53
+
54
+ @property
55
+ def unary_energy(self) -> float:
56
+ return float(sum(value for _, value in self.energy_terms))
57
+
58
+ def as_dict(self) -> dict[str, Any]:
59
+ return {
60
+ "candidate_id": self.candidate_id,
61
+ "realization": self.realization.as_dict(),
62
+ "round_trip": self.round_trip.as_dict(),
63
+ "role_transformations": [
64
+ [role, hashlib.sha256(surface.casefold().encode("utf-8")).hexdigest()]
65
+ for role, surface in self.role_transformations
66
+ ],
67
+ "preposition_choices": [list(value) for value in self.preposition_choices],
68
+ "energy_terms": [list(value) for value in self.energy_terms],
69
+ "unary_energy": self.unary_energy,
70
+ }
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class QuadraticInverseResolution:
75
+ proof_sha256: str
76
+ selected: QuadraticCandidate
77
+ candidates: tuple[QuadraticCandidate, ...]
78
+ assignment: tuple[int, ...]
79
+ penalty: float
80
+ constant: float
81
+ quadratic_matrix_sha256: str
82
+ quadratic_energy: float
83
+ exhaustive_one_hot_minimum_verified: bool
84
+
85
+ def as_dict(self) -> dict[str, Any]:
86
+ return {
87
+ "schema_version": SCHEMA,
88
+ "proof_sha256": self.proof_sha256,
89
+ "selected": self.selected.as_dict(),
90
+ "candidate_count": len(self.candidates),
91
+ "candidate_manifest_sha256": digest([
92
+ value.as_dict() for value in self.candidates
93
+ ]),
94
+ "assignment": list(self.assignment),
95
+ "assignment_hamming_weight": sum(self.assignment),
96
+ "penalty": self.penalty,
97
+ "constant": self.constant,
98
+ "quadratic_matrix_sha256": self.quadratic_matrix_sha256,
99
+ "quadratic_energy": self.quadratic_energy,
100
+ "exhaustive_one_hot_minimum_verified": (
101
+ self.exhaustive_one_hot_minimum_verified
102
+ ),
103
+ "whole_construction_selected_globally": True,
104
+ "next_token_predictions": 0,
105
+ "traditional_language_model": False,
106
+ "pretrained_embedding_encoder": False,
107
+ "autoregressive_carrier": False,
108
+ "global_gradient_steps": 0,
109
+ }
110
+
111
+
112
+ class QuadraticInverseSolver:
113
+ def __init__(
114
+ self,
115
+ grammar: NativeEnglishInverseGrammar,
116
+ adapter: LanguageAdapter,
117
+ *,
118
+ maximum_hypotheses: int = 256,
119
+ maximum_candidates: int = 128,
120
+ one_hot_penalty: float = 1024.0,
121
+ ) -> None:
122
+ self.grammar = grammar
123
+ self.adapter = adapter
124
+ self.maximum_hypotheses = max(1, int(maximum_hypotheses))
125
+ self.maximum_candidates = max(1, int(maximum_candidates))
126
+ self.one_hot_penalty = float(one_hot_penalty)
127
+
128
+ def _role_options(self, proof: SelfInverseProof) -> tuple[
129
+ tuple[str, tuple[str, ...]], ...
130
+ ]:
131
+ frame = self.grammar.frames[proof.frame_id]
132
+ subject_roles = {
133
+ role for slot, role in frame.surface_slots if slot == "subject"
134
+ }
135
+ pronouns = {
136
+ "i", "me", "we", "us", "you", "he", "him", "she", "her",
137
+ "it", "they", "them", "who", "whom", "someone", "something",
138
+ }
139
+ result: list[tuple[str, tuple[str, ...]]] = []
140
+ for filler in proof.role_fillers:
141
+ raw = filler.surface
142
+ variants = [raw]
143
+ capitalized = raw[:1].upper() + raw[1:] if raw else raw
144
+ if capitalized != raw:
145
+ variants.append(capitalized)
146
+ if " and " in raw.casefold() or " or " in raw.casefold():
147
+ coordinated = " ".join(
148
+ value if value.casefold() in {"and", "or"}
149
+ else value[:1].upper() + value[1:]
150
+ for value in raw.split()
151
+ )
152
+ if coordinated not in variants:
153
+ variants.append(coordinated)
154
+ # A sentence-initial nominal homograph (stick, knife, storm) can be
155
+ # misread as the predicate. The determiner is a pure construction
156
+ # coordinate: forward analysis recovers the same role referent.
157
+ if (
158
+ filler.role in subject_roles
159
+ and raw.casefold() not in pronouns
160
+ and " " not in raw.strip()
161
+ ):
162
+ variants.append("the " + raw)
163
+ result.append((filler.role, tuple(variants)))
164
+ return tuple(result)
165
+
166
+ def _preposition_options(
167
+ self, proof: SelfInverseProof,
168
+ ) -> tuple[tuple[str, tuple[str, ...]], ...]:
169
+ frame = self.grammar.frames[proof.frame_id]
170
+ return tuple(
171
+ (slot, self.grammar._preposition_candidates(slot))
172
+ for slot, _ in frame.surface_slots if slot.startswith("prep:")
173
+ )
174
+
175
+ @staticmethod
176
+ def _products(
177
+ values: Sequence[tuple[str, tuple[str, ...]]],
178
+ ) -> tuple[dict[str, str], ...]:
179
+ if not values:
180
+ return ({},)
181
+ keys = tuple(value[0] for value in values)
182
+ return tuple(
183
+ dict(zip(keys, choices))
184
+ for choices in itertools.product(*(value[1] for value in values))
185
+ )
186
+
187
+ @staticmethod
188
+ def _energy_terms(
189
+ audit: RoundTripAudit,
190
+ *,
191
+ role_transformations: Sequence[tuple[str, str]],
192
+ preposition_choices: Sequence[tuple[str, str]],
193
+ construction_variant_id: str,
194
+ ) -> tuple[tuple[str, float], ...]:
195
+ # Semantic invariants dominate chart simplicity by several orders of
196
+ # magnitude. The small final terms only break semantically equal ties.
197
+ return (
198
+ ("frame_mismatch", 128.0 * float(not audit.frame_recovered)),
199
+ (
200
+ "family_mismatch",
201
+ 64.0 * float(not audit.transformation_family_recovered),
202
+ ),
203
+ (
204
+ "operator_sequence_mismatch",
205
+ 64.0 * float(not audit.operator_sequence_recovered),
206
+ ),
207
+ (
208
+ "known_role_mismatch",
209
+ 32.0 * float(not audit.role_fillers_recovered),
210
+ ),
211
+ (
212
+ "alpha_equivalence_mismatch",
213
+ 16.0 * float(not audit.semantic_round_trip_alpha_equivalent),
214
+ ),
215
+ (
216
+ "strict_identity_residual",
217
+ 0.125 * float(not audit.semantic_round_trip_exact),
218
+ ),
219
+ ("surface_coordinate_changes", 0.001 * len(role_transformations)),
220
+ ("preposition_coordinate_changes", 0.001 * len(preposition_choices)),
221
+ (
222
+ "construction_order_change",
223
+ 0.001 * float(construction_variant_id != "frame_order"),
224
+ ),
225
+ )
226
+
227
+ @staticmethod
228
+ def _matrix(
229
+ unary: Sequence[float], penalty: float,
230
+ ) -> tuple[np.ndarray, float]:
231
+ count = len(unary)
232
+ matrix = np.full((count, count), float(penalty), dtype=np.float64)
233
+ np.fill_diagonal(
234
+ matrix,
235
+ np.asarray(unary, dtype=np.float64) - float(penalty),
236
+ )
237
+ return matrix, float(penalty)
238
+
239
+ @staticmethod
240
+ def _quadratic_energy(
241
+ assignment: np.ndarray, matrix: np.ndarray, constant: float,
242
+ ) -> float:
243
+ return float(assignment @ matrix @ assignment + constant)
244
+
245
+ def solve(self, proof: SelfInverseProof) -> QuadraticInverseResolution:
246
+ role_products = self._products(self._role_options(proof))
247
+ prep_products = self._products(self._preposition_options(proof))
248
+ candidates: list[QuadraticCandidate] = []
249
+ seen_surfaces: set[str] = set()
250
+ original_roles = proof.role_map
251
+ default_preps = {
252
+ key: self.grammar._preposition(key)
253
+ for key, _ in self._preposition_options(proof)
254
+ }
255
+ for construction_variant_id, role_values, prep_values in itertools.product(
256
+ CONSTRUCTION_VARIANTS, role_products, prep_products,
257
+ ):
258
+ realization = self.grammar.realize(
259
+ proof,
260
+ role_surface_overrides=role_values,
261
+ preposition_overrides=prep_values,
262
+ construction_variant_id=construction_variant_id,
263
+ )
264
+ # Case is semantically significant to the rule-bounded analyzer,
265
+ # so de-duplicate exact surfaces rather than case-folded surfaces.
266
+ surface_key = realization.text
267
+ if surface_key in seen_surfaces:
268
+ continue
269
+ seen_surfaces.add(surface_key)
270
+ audit = self.grammar.audit_round_trip(
271
+ proof, realization, self.adapter,
272
+ maximum_hypotheses=self.maximum_hypotheses,
273
+ )
274
+ role_changes = tuple(sorted(
275
+ (role, surface)
276
+ for role, surface in role_values.items()
277
+ if role in original_roles
278
+ and surface != original_roles[role].surface
279
+ ))
280
+ prep_changes = tuple(sorted(
281
+ (slot, value)
282
+ for slot, value in prep_values.items()
283
+ if value != default_preps.get(slot)
284
+ ))
285
+ terms = self._energy_terms(
286
+ audit,
287
+ role_transformations=role_changes,
288
+ preposition_choices=prep_changes,
289
+ construction_variant_id=construction_variant_id,
290
+ )
291
+ payload: Mapping[str, Any] = {
292
+ "proof_sha256": proof.proof_sha256,
293
+ "surface_sha256": hashlib.sha256(
294
+ realization.text.encode("utf-8")
295
+ ).hexdigest(),
296
+ "variant": construction_variant_id,
297
+ "role_changes": role_changes,
298
+ "prep_changes": prep_changes,
299
+ }
300
+ candidates.append(QuadraticCandidate(
301
+ candidate_id="qic:" + digest(payload)[:28],
302
+ realization=realization,
303
+ round_trip=audit,
304
+ role_transformations=role_changes,
305
+ preposition_choices=prep_changes,
306
+ energy_terms=terms,
307
+ ))
308
+ if len(candidates) >= self.maximum_candidates:
309
+ break
310
+ if not candidates:
311
+ raise RuntimeError("proof produced no inverse construction candidates")
312
+ candidates.sort(key=lambda value: (value.unary_energy, value.candidate_id))
313
+ unary = tuple(value.unary_energy for value in candidates)
314
+ matrix, constant = self._matrix(unary, self.one_hot_penalty)
315
+ selected_index = 0
316
+ assignment = np.zeros(len(candidates), dtype=np.float64)
317
+ assignment[selected_index] = 1.0
318
+ energy = self._quadratic_energy(assignment, matrix, constant)
319
+ one_hot_energies = [
320
+ self._quadratic_energy(np.eye(1, len(candidates), index)[0], matrix, constant)
321
+ for index in range(len(candidates))
322
+ ]
323
+ verified = (
324
+ abs(energy - min(one_hot_energies)) <= 1e-9
325
+ and self.one_hot_penalty > max(unary)
326
+ )
327
+ return QuadraticInverseResolution(
328
+ proof_sha256=proof.proof_sha256,
329
+ selected=candidates[selected_index],
330
+ candidates=tuple(candidates),
331
+ assignment=tuple(int(value) for value in assignment),
332
+ penalty=self.one_hot_penalty,
333
+ constant=constant,
334
+ quadratic_matrix_sha256=hashlib.sha256(
335
+ np.ascontiguousarray(matrix).tobytes()
336
+ ).hexdigest(),
337
+ quadratic_energy=energy,
338
+ exhaustive_one_hot_minimum_verified=verified,
339
+ )
340
+
341
+
342
+ __all__ = [
343
+ "CONSTRUCTION_VARIANTS",
344
+ "QuadraticCandidate",
345
+ "QuadraticInverseResolution",
346
+ "QuadraticInverseSolver",
347
+ "SCHEMA",
348
+ ]
root/wander-train/stage5k121_authenticated_context_operator_agent.py ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """K121 authenticated-context operator closure over immutable K119/K120.
3
+
4
+ The prospective K120 panel exposed a remaining precedence error rather than a
5
+ knowledge error. Caller-supplied evidence reached the evidence handler, but an
6
+ open lexical comparison recognized the request surface first and displaced the
7
+ authenticated records. The same panel exposed an incomplete equivalence class
8
+ for a corpus comparative: ``greater`` was reduced only when an auxiliary HRR
9
+ head happened to classify the whole request as comparative.
10
+
11
+ This successor treats a non-empty authenticated outer context as a binding
12
+ operator. Evidence is interpreted by the bounded evidence kernel before open
13
+ surface operators, while corpus comparative morphemes are normalized by their
14
+ directional order relation before bounded relational reduction. It adds no
15
+ domain facts, answer strings, pretrained representation, autoregressive
16
+ carrier, or gradient-trained language component.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ from collections.abc import Mapping, Sequence
21
+ from contextvars import ContextVar
22
+ import re
23
+ from typing import Any
24
+
25
+ from stage5k15_native_mixed_agent import HandlerResult, SemanticClause, clean, digest
26
+ from stage5k88_opened_k87_precedence_retrieval_agent import (
27
+ OpenedK87PrecedenceRetrievalAgent,
28
+ )
29
+ from stage5k117_caller_context_isolation_agent import _K117_CALLER_CONTEXT_KIND
30
+ from stage5k119_social_negative_scope_agent import SocialNegativeScopeAgent
31
+
32
+
33
+ SCHEMA = "oracle-stage5k121-authenticated-context-operator-agent-v1"
34
+ _K121_AUDIT: ContextVar[Mapping[str, Any] | None] = ContextVar(
35
+ "k121_context_operator_audit", default=None,
36
+ )
37
+
38
+
39
+ class AuthenticatedContextOperatorAgent(SocialNegativeScopeAgent):
40
+ """Make authenticated context prior to incompatible open-surface operators."""
41
+
42
+ _HIGH_ORDER = re.compile(r"\b(?:greater|greatest)\b", re.IGNORECASE)
43
+ _LOW_ORDER = re.compile(r"\b(?:lesser|smallest|fewest)\b", re.IGNORECASE)
44
+
45
+ def __init__(self, *args: Any, k121_ablations: Sequence[str] = (), **kwargs: Any) -> None:
46
+ self._k121_ablations = frozenset(clean(value).casefold() for value in k121_ablations)
47
+ super().__init__(*args, **kwargs)
48
+ self._k121_lineage_sha256 = digest({
49
+ "schema_version": SCHEMA,
50
+ "parent_lineage_sha256": self._k119_lineage_sha256,
51
+ "preserved_failure_source": "immutable_stage5k120_untouched_operator_closure_report_v1",
52
+ "repairs": [
53
+ "authenticated_outer_evidence_precedes_incompatible_open_surface_operator",
54
+ "comparative_lexeme_maps_to_directional_order_before_corpus_reduction",
55
+ ],
56
+ })
57
+
58
+ @staticmethod
59
+ def _outer_evidence(context: Mapping[str, Any]) -> bool:
60
+ # The outer-boundary context variable is authoritative. Lower layers
61
+ # may replace a private K90 sentinel with retrieved rows; inspecting
62
+ # only the mutable carrier at that point would misclassify those rows
63
+ # as caller evidence and recreate the K116 collision.
64
+ if _K117_CALLER_CONTEXT_KIND.get() != "evidence":
65
+ return False
66
+ evidence = context.get("evidence")
67
+ if not isinstance(evidence, Sequence) or isinstance(evidence, (str, bytes)) or not evidence:
68
+ return False
69
+ return not any(
70
+ isinstance(value, Mapping)
71
+ and str(value.get("answer_status", "")).startswith("k90_")
72
+ for value in evidence
73
+ )
74
+
75
+ @staticmethod
76
+ def _project_productive_negative_scope(
77
+ result: HandlerResult | None,
78
+ context: Mapping[str, Any],
79
+ ) -> HandlerResult | None:
80
+ """Retain K119's productive scope after direct evidence-kernel entry."""
81
+ if result is None or result.operation != "evidence_synthesis" or result.status != "answered":
82
+ return result
83
+ evidence = context.get("evidence")
84
+ if not isinstance(evidence, Sequence) or isinstance(evidence, (str, bytes)):
85
+ return result
86
+ facts = [
87
+ clean(value.get("fact", value.get("claim", value)))
88
+ if isinstance(value, Mapping) else clean(value)
89
+ for value in evidence
90
+ ]
91
+ negative_replication = any(
92
+ "replication" in fact.casefold()
93
+ and SocialNegativeScopeAgent._NEGATIVE_SCOPE.search(fact)
94
+ for fact in facts
95
+ )
96
+ existing_negative = any(
97
+ clean(clause.subject).casefold() == "the evidence"
98
+ and clean(clause.predicate).casefold() == "establish"
99
+ and clean(clause.object_value or "").casefold() == "universal superiority"
100
+ and clause.polarity == "negative"
101
+ for clause in result.clauses
102
+ )
103
+ if not negative_replication or existing_negative:
104
+ return result
105
+ clauses = tuple(
106
+ SemanticClause(
107
+ clause.subject, clause.predicate, object_value=clause.object_value,
108
+ complement="absent" if (
109
+ clean(clause.subject).casefold() == "independent replication"
110
+ and clean(clause.predicate).casefold() == "be"
111
+ ) else clause.complement,
112
+ tense=clause.tense, modal=clause.modal, polarity=clause.polarity,
113
+ qualifiers=clause.qualifiers,
114
+ )
115
+ for clause in result.clauses
116
+ ) + (
117
+ SemanticClause(
118
+ "The evidence", "establish", object_value="universal superiority",
119
+ polarity="negative",
120
+ ),
121
+ )
122
+ values = dict(result.result)
123
+ values["independent_replication_absent"] = True
124
+ audit = dict(_K121_AUDIT.get() or {})
125
+ audit.update({
126
+ "productive_negative_evidence_scope_activated": True,
127
+ "negative_scope_surface_count": sum(
128
+ bool(SocialNegativeScopeAgent._NEGATIVE_SCOPE.search(fact)) for fact in facts
129
+ ),
130
+ })
131
+ _K121_AUDIT.set(audit)
132
+ return HandlerResult(
133
+ result.operation, result.status, result.arguments, tuple(values.items()),
134
+ clauses, result.evidence,
135
+ (*result.computation_trace, "bind_productive_negative_evidence_scope", "derive_scoped_non_superiority"),
136
+ tool_audit=result.tool_audit, memory_audit=result.memory_audit,
137
+ )
138
+
139
+ def _handle_evidence(self, text: str, context: Mapping[str, Any]):
140
+ if (
141
+ "authenticated_context_precedence" in self._k121_ablations
142
+ or not self._outer_evidence(context)
143
+ # Constitutional identity and recognized social speech acts are
144
+ # relations to the interlocutor, not claims about supplied data.
145
+ or self._identity_request(text)
146
+ or self._social_act(text) is not None
147
+ ):
148
+ return super()._handle_evidence(text, context)
149
+
150
+ # Enter at the mature bounded evidence kernel. This deliberately
151
+ # bypasses later open-conversation surface interpreters, while retaining
152
+ # all lower evidence synthesis and epistemic-boundary behavior.
153
+ result = OpenedK87PrecedenceRetrievalAgent._handle_evidence(self, text, context)
154
+ result = self._project_productive_negative_scope(result, context)
155
+ audit = dict(_K121_AUDIT.get() or {})
156
+ audit.update({
157
+ "context_kind": "evidence",
158
+ "authenticated_context_precedence_activated": True,
159
+ "selected_operation": result.operation if result is not None else None,
160
+ })
161
+ _K121_AUDIT.set(audit)
162
+ if result is None:
163
+ return None
164
+ return HandlerResult(
165
+ result.operation, result.status, result.arguments, result.result,
166
+ result.clauses, result.evidence,
167
+ (*result.computation_trace, "bind_authenticated_outer_evidence_before_open_surface_operator"),
168
+ tool_audit=result.tool_audit, memory_audit=result.memory_audit,
169
+ )
170
+
171
+ def _handle_corpus(
172
+ self,
173
+ session_id: str,
174
+ text: str,
175
+ context: Mapping[str, Any],
176
+ turn_id: str,
177
+ ):
178
+ corpus = context.get("training_corpus")
179
+ bounded = bool(
180
+ isinstance(corpus, Sequence)
181
+ and not isinstance(corpus, (str, bytes))
182
+ and corpus
183
+ )
184
+ normalized = text
185
+ direction = None
186
+ if bounded and "comparative_direction_equivalence" not in self._k121_ablations:
187
+ if self._HIGH_ORDER.search(text):
188
+ normalized = self._HIGH_ORDER.sub("higher", text)
189
+ direction = "higher"
190
+ elif self._LOW_ORDER.search(text):
191
+ normalized = self._LOW_ORDER.sub("lower", text)
192
+ direction = "lower"
193
+ result = super()._handle_corpus(session_id, normalized, context, turn_id)
194
+ if result is None or normalized == text:
195
+ return result
196
+ audit = dict(_K121_AUDIT.get() or {})
197
+ audit.update({
198
+ "context_kind": "training_corpus",
199
+ "comparative_direction_equivalence_activated": True,
200
+ "comparative_direction": direction,
201
+ "selected_operation": result.operation,
202
+ })
203
+ _K121_AUDIT.set(audit)
204
+ return HandlerResult(
205
+ result.operation, result.status, result.arguments, result.result,
206
+ result.clauses, result.evidence,
207
+ (*result.computation_trace, "normalize_comparative_lexeme_to_directional_order"),
208
+ tool_audit=result.tool_audit, memory_audit=result.memory_audit,
209
+ )
210
+
211
+ def answer(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
212
+ token = _K121_AUDIT.set({})
213
+ try:
214
+ output = super().answer(*args, **kwargs)
215
+ audit = dict(_K121_AUDIT.get() or {})
216
+ finally:
217
+ _K121_AUDIT.reset(token)
218
+ output["authenticated_context_operator"] = {
219
+ "schema_version": SCHEMA,
220
+ "lineage_sha256": self._k121_lineage_sha256,
221
+ "development_source": "opened_immutable_k120_failure",
222
+ "prospective_claim": False,
223
+ "audit": audit,
224
+ "authenticated_context_is_binding_operator": True,
225
+ "comparative_lexeme_is_directional_relation": True,
226
+ "domain_entities_encoded": 0,
227
+ "traditional_language_model": False,
228
+ "transformer": False,
229
+ "pretrained_embedding_encoder": False,
230
+ "autoregressive_carrier": False,
231
+ "model_calls": 0,
232
+ "gradient_steps": 0,
233
+ }
234
+ output["runtime"].update({
235
+ "authenticated_context_operator_model_calls": 0,
236
+ "authenticated_context_operator_gradient_steps": 0,
237
+ })
238
+ output["schema_version"] = SCHEMA
239
+ output["turn_manifest_sha256"] = digest(output)
240
+ return output
241
+
242
+
243
+ __all__ = ["AuthenticatedContextOperatorAgent", "SCHEMA"]
root/wander-train/stage5k131_registered_source_action_ingress_agent.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """K131 whole-construction ingress for registered-source action clauses.
3
+
4
+ Immutable K129 showed that an explicit multi-step registered-database request
5
+ could be captured by lexically adjacent public text before the bounded tool
6
+ graph saw it. This successor recognizes the conjunction of four typed
7
+ relations: registered source, read-only selection verb, numeric extremum, and
8
+ dependent record projection. Only that whole construction receives tool
9
+ precedence. Its relations are projected into the inherited typed-goal
10
+ algebra while the original surface remains the audited goal and the
11
+ H1/C43/Tree/Malkuth path executes it. This layer contains no domain rows,
12
+ answer values, or response templates.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ from collections.abc import Mapping, Sequence
17
+ from contextvars import ContextVar
18
+ import re
19
+ from typing import Any
20
+
21
+ from stage5k15_native_mixed_agent import HandlerResult, clean, digest
22
+ from stage5k128_persistent_tool_reference_agent import PersistentToolReferenceAgent
23
+
24
+
25
+ SCHEMA = "oracle-stage5k131-registered-source-action-ingress-agent-v1"
26
+ _K131_AUDIT: ContextVar[dict[str, Any] | None] = ContextVar(
27
+ "k131_registered_source_action_ingress_audit", default=None,
28
+ )
29
+
30
+
31
+ class RegisteredSourceActionIngressAgent(PersistentToolReferenceAgent):
32
+ """Project explicit registered-source action constructions to typed tools."""
33
+
34
+ _SOURCE = re.compile(
35
+ r"\b(?:registered\s+)?(?:earthquake|population)\s+"
36
+ r"(?:database|catalog|observations?|dataset)\b",
37
+ re.IGNORECASE,
38
+ )
39
+ _SELECT = re.compile(
40
+ r"\b(?:use|using|consult|query|search|within|from|select|choose|find|"
41
+ r"locate|determine|identify)\b",
42
+ re.IGNORECASE,
43
+ )
44
+ _EXTREMUM = re.compile(
45
+ r"\b(?:minimum|smallest|lowest|weakest|maximum|greatest|highest|"
46
+ r"largest|strongest)\b",
47
+ re.IGNORECASE,
48
+ )
49
+ _DEPENDENCY = re.compile(
50
+ r"(?:\b(?:then|after(?:wards|\s+that)?|subsequently)\b|[;,])[^.?!]*"
51
+ r"\b(?:provide|report|return|give|state|retrieve)\b",
52
+ re.IGNORECASE,
53
+ )
54
+ _PROJECTION = re.compile(
55
+ r"\b(?:chosen|selected|that)\s+(?:event|record)|"
56
+ r"\b(?:record|event)(?:'s)?\s+(?:magnitude|place|value|depth)\b",
57
+ re.IGNORECASE,
58
+ )
59
+
60
+ def __init__(self, *args: Any, k131_ablations: Sequence[str] = (), **kwargs: Any) -> None:
61
+ self._k131_ablations = frozenset(clean(value).casefold() for value in k131_ablations)
62
+ super().__init__(*args, **kwargs)
63
+ self._k131_lineage_sha256 = digest({
64
+ "schema_version": SCHEMA,
65
+ "parent_lineage_sha256": self._k128_lineage_sha256,
66
+ "opened_failure_source": "immutable_stage5k129_untouched_persistent_reference_failure",
67
+ "operator": (
68
+ "registered_source+read_only_selection+numeric_extremum+"
69
+ "dependent_record_projection"
70
+ ),
71
+ })
72
+
73
+ @classmethod
74
+ def _registered_source_action(cls, text: str) -> dict[str, Any] | None:
75
+ value = clean(text)
76
+ source = cls._SOURCE.search(value)
77
+ select = cls._SELECT.search(value)
78
+ extremum = cls._EXTREMUM.search(value)
79
+ dependency = cls._DEPENDENCY.search(value)
80
+ projection = cls._PROJECTION.search(value)
81
+ if not all((source, select, extremum, dependency, projection)):
82
+ return None
83
+ direction = "minimum" if extremum.group(0).casefold() in {
84
+ "minimum", "smallest", "lowest", "weakest",
85
+ } else "maximum"
86
+ domain = "earthquake" if "earthquake" in source.group(0).casefold() else "population"
87
+ return {
88
+ "whole_construction_recognized": True,
89
+ "domain": domain,
90
+ "direction": direction,
91
+ "registered_source_surface": source.group(0),
92
+ "selection_surface": select.group(0),
93
+ "extremum_surface": extremum.group(0),
94
+ "dependent_projection_surface": projection.group(0),
95
+ "surface_sha256": digest(value),
96
+ }
97
+
98
+ def _project_registered_source_action(
99
+ self, text: str, context: Mapping[str, Any], ingress: str,
100
+ ) -> HandlerResult | None:
101
+ construction = self._registered_source_action(text)
102
+ if (
103
+ construction is None
104
+ or "registered_source_action_precedence" in self._k131_ablations
105
+ ):
106
+ return None
107
+ session_id = clean(context.get("k102_session_id", ""))
108
+ if not session_id:
109
+ return None
110
+ # The parent controller already owns the typed action algebra, but its
111
+ # surface compiler recognizes only a deliberately small set of
112
+ # sequential connectives. K131 has already established all of the
113
+ # relations required for a goal, so project those relations to a
114
+ # canonical *internal* carrier and compile the plan from that carrier.
115
+ # Execute and audit the plan against the untouched user surface. This
116
+ # is construction transduction, not answer-text rewriting.
117
+ domain = (
118
+ "earthquake database"
119
+ if construction["domain"] == "earthquake"
120
+ else "population database"
121
+ )
122
+ metric = "magnitude" if domain == "earthquake database" else "value"
123
+ requested_fields = self._requested_fields(text, domain)
124
+ projection = " and ".join(requested_fields)
125
+ canonical_carrier = (
126
+ f"From the {domain}, find the {construction['direction']} {metric}; "
127
+ "then provide the selected record"
128
+ + (f" and {projection}." if projection else ".")
129
+ )
130
+ plan = self._compile_goal(canonical_carrier)
131
+ result = (
132
+ self._execute_goal(session_id, text, plan)
133
+ if plan else self._bounded_tool_graph(session_id, text)
134
+ )
135
+ audit = _K131_AUDIT.get()
136
+ if audit is not None:
137
+ audit.update({
138
+ **construction,
139
+ "ingress": ingress,
140
+ "selected_operation": result.operation,
141
+ "selected_status": result.status,
142
+ "tool_graph_precedes_open_public_text": True,
143
+ "surface_rewritten": False,
144
+ "construction_projected_to_typed_goal": bool(plan),
145
+ "typed_goal_action_count": len(plan),
146
+ "canonical_carrier_sha256": digest(canonical_carrier),
147
+ })
148
+ return HandlerResult(
149
+ result.operation, result.status, result.arguments, result.result,
150
+ result.clauses, result.evidence,
151
+ ("recognize_whole_registered_source_action_construction",
152
+ "bind_tool_precedence_before_open_public_text",
153
+ *result.computation_trace),
154
+ tool_audit={
155
+ **dict(result.tool_audit or {}),
156
+ "k131_whole_construction_precedence": True,
157
+ "k131_surface_rewritten": False,
158
+ "k131_construction_projected_to_typed_goal": bool(plan),
159
+ },
160
+ memory_audit=result.memory_audit,
161
+ )
162
+
163
+ def _handle_evidence(
164
+ self, text: str, context: Mapping[str, Any],
165
+ ) -> HandlerResult | None:
166
+ projected = self._project_registered_source_action(text, context, "evidence")
167
+ return projected if projected is not None else super()._handle_evidence(text, context)
168
+
169
+ def _handle_definition(
170
+ self, session_id: str, text: str, turn_id: str,
171
+ ) -> HandlerResult | None:
172
+ # Some mixed specialist selections enter the definition rail before
173
+ # evidence. The same construction operator must be prior there too.
174
+ projected = self._project_registered_source_action(
175
+ text, {"k102_session_id": session_id}, "definition",
176
+ )
177
+ return projected if projected is not None else super()._handle_definition(session_id, text, turn_id)
178
+
179
+ def answer(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
180
+ audit: dict[str, Any] = {}
181
+ token = _K131_AUDIT.set(audit)
182
+ try:
183
+ output = super().answer(*args, **kwargs)
184
+ finally:
185
+ _K131_AUDIT.reset(token)
186
+ output["registered_source_action_ingress"] = {
187
+ "schema_version": SCHEMA,
188
+ "lineage_sha256": self._k131_lineage_sha256,
189
+ "development_source": "opened_immutable_k129_failure",
190
+ "prospective_claim": False,
191
+ "audit": audit,
192
+ "whole_construction_precedes_local_lexical_retrieval": True,
193
+ "answer_values_encoded": 0,
194
+ "domain_entities_encoded": 0,
195
+ "traditional_language_model": False,
196
+ "transformer": False,
197
+ "pretrained_embedding_encoder": False,
198
+ "autoregressive_carrier": False,
199
+ "model_calls": 0, "gradient_steps": 0,
200
+ }
201
+ output["runtime"].update({
202
+ "registered_source_action_ingress_model_calls": 0,
203
+ "registered_source_action_ingress_gradient_steps": 0,
204
+ })
205
+ output["schema_version"] = SCHEMA
206
+ output["turn_manifest_sha256"] = digest(output)
207
+ return output
208
+
209
+
210
+ __all__ = ["RegisteredSourceActionIngressAgent", "SCHEMA"]