Preserve quantity, relation and conversation meaning in legacy runtime paths

#2
by emberian - opened
Files changed (43) hide show
  1. root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_operator_specs_v1.py +57 -21
  2. root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_ao.py +24 -2
  3. root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w442.py +20 -0
  4. root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w468.py +28 -8
  5. root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w488.py +15 -2
  6. root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w534.py +46 -3
  7. root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w557.py +1 -1
  8. root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/runtime_conversation_state.py +98 -0
  9. root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/runtime_final_outcome.py +4 -0
  10. root/specialists/_gate/combo_runtime_v1.py +67 -6
  11. root/specialists/energy-v3/organ_energy_v1.py +215 -21
  12. root/specialists/geometry-v4/manifest.json +2 -2
  13. root/specialists/geometry-v4/organ_geometry_v4.py +175 -2
  14. root/wander-train/math_organ_v1.py +25 -6
  15. root/wander-train/math_organ_v2.py +7 -6
  16. root/wander-train/math_organ_v3.py +7 -6
  17. root/wander-train/oracle_operator_specs_v1.py +57 -21
  18. root/wander-train/oracle_release_runtime_ao.py +24 -2
  19. root/wander-train/reach_router_v1.py +5 -3
  20. root/wander-train/reach_router_v2.py +5 -3
  21. root/wander-train/reach_router_v3.py +60 -11
  22. root/wander-train/relation_spec_v1.py +62 -0
  23. root/wander-train/word_problems_v1.py +132 -10
  24. tests/conversation_grammar/test_m53_dummy_it.py +126 -0
  25. tests/conversation_grammar_r468/test_door_dummy_it.py +142 -0
  26. tests/conversation_guard/test_last_question.py +152 -0
  27. tests/usage/README.md +26 -0
  28. tests/usage/composition/test_energy_repair.py +131 -0
  29. tests/usage/computing/test_base_conversion.py +88 -0
  30. tests/usage/conversation/birth-replies.json +69 -0
  31. tests/usage/conversation/test_state.py +126 -0
  32. tests/usage/documents/test_conflict_repair.py +82 -0
  33. tests/usage/engineering/test_condition_attachment.py +142 -0
  34. tests/usage/evidence/test_math_finite.py +71 -0
  35. tests/usage/fixtures/composition/cases.json +62 -0
  36. tests/usage/fixtures/engineering/cases.json +16 -0
  37. tests/usage/fixtures/engineering/followup_cases.json +22 -0
  38. tests/usage/fixtures/reliability/cases.json +77 -0
  39. tests/usage/fixtures/reliability/preserved_answers.json +202 -0
  40. tests/usage/reliability/test_geometry_consumer.py +189 -0
  41. tests/usage/reliability/test_geometry_units.py +113 -0
  42. tests/usage/reliability_route/test_geometry_route.py +188 -0
  43. tests/usage/research/test_relation_semantics.py +83 -0
root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_operator_specs_v1.py CHANGED
@@ -198,33 +198,63 @@ def _gcd_ref(a):
198
 
199
 
200
  # ── 3. base conversion ────────────────────────────────────────────────────────
201
- # fn is repeated divmod; ref is positional expansion of the digits it produced.
202
- _BASE_RX = re.compile(
203
- r'\b(?:convert|write|express)?\s*(-?\d+)\s*(?:from\s*)?(?:base\s*(\d+))?\s*'
204
- r'(?:in|to|into)\s*(?:base\s*)?(\d+|binary|hex|hexadecimal|octal|decimal)\b', re.I)
205
  _BASE_NAME = {'binary': 2, 'hex': 16, 'hexadecimal': 16, 'octal': 8, 'decimal': 10}
 
 
 
 
 
 
 
 
206
  _DIGITS = '0123456789abcdefghijklmnopqrstuvwxyz'
207
 
208
 
209
  def _base_parse(s):
210
- m = _BASE_RX.search(s)
211
  if not m:
212
- raise NotOurs('no number and target base here')
213
- n = int(m.group(1))
214
- frm = int(m.group(2)) if m.group(2) else 10
215
- tgt = m.group(3).lower()
216
- to = _BASE_NAME.get(tgt, None)
217
- if to is None:
218
- to = int(tgt)
219
- if not (2 <= to <= 36) or not (2 <= frm <= 36):
220
- raise OutOfFragment('I convert between base 2 and base 36 only')
221
- if n < 0:
 
 
 
 
 
 
 
222
  raise OutOfFragment('I convert non-negative whole numbers only')
223
- return {'n': n, 'frm': frm, 'to': to}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
 
225
 
226
  def _base_fn(a):
227
- n, steps, out = a['n'], [], ''
228
  if n == 0:
229
  return '0', ['0 in any base is 0']
230
  while n:
@@ -235,8 +265,13 @@ def _base_fn(a):
235
 
236
 
237
  def _base_ref(a):
238
- """Independent: build with a positional table, then check by expanding it."""
239
- n = a['n']
 
 
 
 
 
240
  if n == 0:
241
  return '0'
242
  digits, powers = [], []
@@ -418,8 +453,9 @@ SPECS = [
418
  'parse': _base_parse, 'fn': _base_fn, 'ref': _base_ref,
419
  'render': lambda a, v, s: '%d in base %d is %s.' % (a['n'], a['to'], v),
420
  'warrant': 'computation',
421
- 'independence': 'moderate: repeated division against positional expansion; '
422
- 'both walk the same digits, in opposite directions',
 
423
  'fragment': 'non-negative whole numbers, bases 2 to 36'},
424
 
425
  {'name': 'statistics', 'family': 'measurement',
 
198
 
199
 
200
  # ── 3. base conversion ────────────────────────────────────────────────────────
201
+ # The parser binds the original digits and source base. The function decodes
202
+ # with int(); the reference independently expands source digits positionally.
 
 
203
  _BASE_NAME = {'binary': 2, 'hex': 16, 'hexadecimal': 16, 'octal': 8, 'decimal': 10}
204
+ _BASE_ATOM = r'(?:base\s*\d+|binary|hexadecimal|hex|octal|decimal)'
205
+ _BASE_RX = re.compile(
206
+ r'^\s*(?:(?:convert|write|express|what\s+is)\s+)?'
207
+ r'(?:(?P<prefix>' + _BASE_ATOM + r')\s+)?'
208
+ r'(?P<digits>[+-]?[0-9a-z]+)'
209
+ r'(?:\s+(?:(?:from|in)\s+)?(?P<source>' + _BASE_ATOM + r'))?'
210
+ r'\s+(?:in|to|into|as)\s+(?P<target>' + _BASE_ATOM + r'|\d+)'
211
+ r'\s*[.?!]?\s*$', re.I | re.ASCII)
212
  _DIGITS = '0123456789abcdefghijklmnopqrstuvwxyz'
213
 
214
 
215
  def _base_parse(s):
216
+ m = _BASE_RX.fullmatch(s)
217
  if not m:
218
+ raise NotOurs('no complete integer base conversion here')
219
+
220
+ def base(token):
221
+ token = token.lower()
222
+ value = _BASE_NAME.get(token)
223
+ if value is None:
224
+ numeral = re.sub(r'^base\s*', '', token)
225
+ if len(numeral) > 3:
226
+ raise OutOfFragment('I convert between base 2 and base 36 only')
227
+ value = int(numeral)
228
+ if not 2 <= value <= 36:
229
+ raise OutOfFragment('I convert between base 2 and base 36 only')
230
+ return value
231
+
232
+ sources = [base(token) for token in (m['prefix'], m['source']) if token]
233
+ raw = m['digits'].lower()
234
+ if raw.startswith('-'):
235
  raise OutOfFragment('I convert non-negative whole numbers only')
236
+ raw = raw.lstrip('+')
237
+ # A conventional literal prefix is source evidence, not a digit to discard.
238
+ # Explicit bases above 16 may use x as a digit; only infer prefixes when no
239
+ # explicit source is supplied, or that source matches the prefix convention.
240
+ literal = {'0b': 2, '0o': 8, '0x': 16}.get(raw[:2])
241
+ if literal is not None and (not sources or all(b == literal for b in sources)):
242
+ sources.append(literal)
243
+ raw = raw[2:]
244
+ elif literal is not None and sources and sources[0] in (2, 8, 10, 16):
245
+ raise OutOfFragment('the literal prefix conflicts with the stated source base')
246
+ if len(set(sources)) > 1:
247
+ raise OutOfFragment('the stated source bases disagree')
248
+ frm = sources[0] if sources else 10
249
+ to = base(m['target'])
250
+ if not raw or len(raw) > 128 or any(_DIGITS.index(c) >= frm for c in raw):
251
+ raise OutOfFragment('I need 1 to 128 valid digits in the stated source base')
252
+ n = int(raw, frm)
253
+ return {'n': n, 'digits': raw, 'frm': frm, 'to': to}
254
 
255
 
256
  def _base_fn(a):
257
+ n, steps, out = int(a['digits'], a['frm']), [], ''
258
  if n == 0:
259
  return '0', ['0 in any base is 0']
260
  while n:
 
265
 
266
 
267
  def _base_ref(a):
268
+ """Expand original source digits, then encode using a positional table.
269
+
270
+ This does not share int() source decoding or repeated output division with
271
+ the primary function. Both still depend on the same untrusted parse.
272
+ """
273
+ n = sum(_DIGITS.index(digit) * a['frm'] ** power
274
+ for power, digit in enumerate(reversed(a['digits'])))
275
  if n == 0:
276
  return '0'
277
  digits, powers = [], []
 
453
  'parse': _base_parse, 'fn': _base_fn, 'ref': _base_ref,
454
  'render': lambda a, v, s: '%d in base %d is %s.' % (a['n'], a['to'], v),
455
  'warrant': 'computation',
456
+ 'independence': 'moderate: int source decoding and repeated output division '
457
+ 'against positional source expansion and output encoding; '
458
+ 'both still depend on the same untrusted parse',
459
  'fragment': 'non-negative whole numbers, bases 2 to 36'},
460
 
461
  {'name': 'statistics', 'family': 'measurement',
root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_ao.py CHANGED
@@ -22,6 +22,16 @@ import re
22
 
23
  from oracle_release_runtime_an import OracleReleaseRuntimeAN
24
 
 
 
 
 
 
 
 
 
 
 
25
  DEICTIC = re.compile(r"\b(it|there|that|this|its|they|them|those|these|the same)\b", re.I)
26
  ELLIPSIS = re.compile(r"^(?:and|what about|the same for)\s+(?P<t>.+?)\??$", re.I)
27
  BARE = re.compile(r"^(?:the\s+)?[a-z][a-z \-']{2,40}$", re.I)
@@ -41,6 +51,17 @@ PREDICATE_OF = {
41
  }
42
 
43
 
 
 
 
 
 
 
 
 
 
 
 
44
  class OracleReleaseRuntimeAO(OracleReleaseRuntimeAN):
45
  """Conversions are applied ONLY when the unchanged chain would not answer."""
46
 
@@ -103,10 +124,11 @@ class OracleReleaseRuntimeAO(OracleReleaseRuntimeAN):
103
  if not CONTINUATION.match(s):
104
  return f"Summarize {s.strip()}.", "ELLIPSIS_COMPLETION"
105
  topic = self._live_topic(session_id)
106
- if topic and DEICTIC.search(s):
 
107
  if low in PREDICATE_OF:
108
  return PREDICATE_OF[low].format(t=topic), "REFERENT_SUBSTITUTION"
109
- return DEICTIC.sub(topic, s, count=1), "REFERENT_SUBSTITUTION"
110
  return None, None
111
 
112
  def chat(self, text: str, session_id: str = "default") -> dict:
 
22
 
23
  from oracle_release_runtime_an import OracleReleaseRuntimeAN
24
 
25
+ # Untrusted grammar guard: in "does it take/cost ... to <action>", it
26
+ # may be a grammatical subject rather than an entity from the conversation.
27
+ # Decline substitution for that token; other references remain eligible. The
28
+ # match stays within a clause and accepts decimal amounts before the infinitive.
29
+ DUMMY_IT = re.compile(
30
+ r"\b(?:does|did|will|would|can|could|should|has|had)\s+(?P<it>it)\s+"
31
+ r"(?:take|taken|cost)\s+"
32
+ r"(?:(?:\d+(?:\.\d+)?\s+(?:milliseconds?|seconds?|minutes?|hours?|days?|weeks?|"
33
+ r"months?|years?|dollars?|euros?|pounds?)|[$€£]\d+(?:\.\d+)?)\s+)?"
34
+ r"to\s+[a-z]", re.I)
35
  DEICTIC = re.compile(r"\b(it|there|that|this|its|they|them|those|these|the same)\b", re.I)
36
  ELLIPSIS = re.compile(r"^(?:and|what about|the same for)\s+(?P<t>.+?)\??$", re.I)
37
  BARE = re.compile(r"^(?:the\s+)?[a-z][a-z \-']{2,40}$", re.I)
 
51
  }
52
 
53
 
54
+ def referential_deictic(text):
55
+ """First eligible reference, excluding only take/cost-infinitive dummy it.
56
+
57
+ This does not claim to parse all English expletives. Skipping an ambiguous
58
+ token leaves the original question available to the unchanged handler.
59
+ """
60
+ dummy_spans = {match.span("it") for match in DUMMY_IT.finditer(text)}
61
+ return next((match for match in DEICTIC.finditer(text)
62
+ if match.span() not in dummy_spans), None)
63
+
64
+
65
  class OracleReleaseRuntimeAO(OracleReleaseRuntimeAN):
66
  """Conversions are applied ONLY when the unchanged chain would not answer."""
67
 
 
124
  if not CONTINUATION.match(s):
125
  return f"Summarize {s.strip()}.", "ELLIPSIS_COMPLETION"
126
  topic = self._live_topic(session_id)
127
+ reference = referential_deictic(s)
128
+ if topic and reference:
129
  if low in PREDICATE_OF:
130
  return PREDICATE_OF[low].format(t=topic), "REFERENT_SUBSTITUTION"
131
+ return s[:reference.start()] + topic + s[reference.end():], "REFERENT_SUBSTITUTION"
132
  return None, None
133
 
134
  def chat(self, text: str, session_id: str = "default") -> dict:
root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w442.py CHANGED
@@ -130,14 +130,34 @@ class OracleReleaseRuntimeW442(_Base):
130
  numbers = {t for t in terms if t.isdigit()}
131
  terms_stem = [_stem(t) for t in terms]
132
  best = None
 
133
  for sent in _sentences(passage):
134
  pool = set(_stem(t) for t in _toks(sent))
135
  ok, cov, via = self._r436_covered(terms, terms_stem, numbers, pool)
136
  if ok:
 
 
 
 
 
 
137
  hits = sum(1 for t in terms_stem if t in pool)
138
  if best is None or hits > best[0]:
139
  best = (hits, sent, cov, via)
140
  out = dict(res)
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  if best is None:
142
  return self._r434_receipt(out, res, 'I read the passage and the question but found no sentence that covers the question in full, so I will not guess.', 'no_covering_sentence')
143
  _, sent, cov, via = best
 
130
  numbers = {t for t in terms if t.isdigit()}
131
  terms_stem = [_stem(t) for t in terms]
132
  best = None
133
+ covered_verdicts = []
134
  for sent in _sentences(passage):
135
  pool = set(_stem(t) for t in _toks(sent))
136
  ok, cov, via = self._r436_covered(terms, terms_stem, numbers, pool)
137
  if ok:
138
+ # Every covering sentence matters: selecting the first tied
139
+ # sentence can silently choose one of conflicting versions.
140
+ vt = set(terms) | set(terms_stem) | {_stem(a) for _, a in via} | {a for _, a in via}
141
+ candidate_verdict, _ = self._r434_verdict(sent, vt, terms_stem, q)
142
+ if candidate_verdict is not None:
143
+ covered_verdicts.append((cov, candidate_verdict, sent))
144
  hits = sum(1 for t in terms_stem if t in pool)
145
  if best is None or hits > best[0]:
146
  best = (hits, sent, cov, via)
147
  out = dict(res)
148
+ # A partial lexical match may concern another subject (e.g. Cedar
149
+ # Archive rather than Lumen Archive). It must not veto a fully
150
+ # covered answer. Aliases count as covered, as in the existing reader.
151
+ best_coverage = best[2] if best is not None else 0.0
152
+ conflicting_candidates = [(v, sentence) for coverage, v, sentence in covered_verdicts if coverage >= best_coverage]
153
+ settled_verdicts = {v for v, _ in conflicting_candidates}
154
+ if len(settled_verdicts) > 1:
155
+ conflict = self._r434_receipt(out, res,
156
+ 'The passage contains covering sentences with conflicting answers, so I will not choose a version.',
157
+ 'conflicting_covering_sentences')
158
+ conflict['references'] = [{'id': 'passage', 'text': sentence}
159
+ for _, sentence in conflicting_candidates]
160
+ return conflict
161
  if best is None:
162
  return self._r434_receipt(out, res, 'I read the passage and the question but found no sentence that covers the question in full, so I will not guess.', 'no_covering_sentence')
163
  _, sent, cov, via = best
root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w468.py CHANGED
@@ -33,6 +33,14 @@ try:
33
  except Exception: # noqa: BLE001
34
  _ir = None
35
 
 
 
 
 
 
 
 
 
36
  POINTER = re.compile(r"\b(it|its|that|this|they|them|those|these)\b", re.I)
37
  DEICTIC = re.compile(r"\b(?:the|that|this)\s+(?:thing|one|place|stuff|topic|subject|idea|person|guy|animal|city|country)"
38
  r"(?:\s+(?:with|from|about|of|in|on)\s+(?:the\s+)?[a-z][\w\-]*(?:\s+[a-z][\w\-]*)?)?\b", re.I)
@@ -96,6 +104,20 @@ def from_thread(question, subject, replies, year_now):
96
  return None
97
 
98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  class OracleReleaseRuntimeW468(_Base):
100
 
101
  def _r468_thread(self, sid):
@@ -148,19 +170,17 @@ class OracleReleaseRuntimeW468(_Base):
148
  del th[:-MAX_THREAD]
149
 
150
  def _r468_bind(self, text, subject):
151
- m = DEICTIC.search(text)
152
- if m:
153
- return text[:m.start()] + subject + text[m.end():]
154
- m = POINTER.search(text)
155
  if m:
156
- word = m.group(1).lower()
157
  rep = subject + "'s" if word == "its" else subject
158
  return text[:m.start()] + rep + text[m.end():]
159
  return None
160
 
161
  def _r468_door(self, text, sid, res):
162
  thread = self._r468_thread(sid)
163
- points = bool(DEICTIC.search(text) or POINTER.search(text))
 
164
  named = self._r468_subjects(text)
165
  if points and thread and not named:
166
  subject = thread[-1]
@@ -197,13 +217,13 @@ class OracleReleaseRuntimeW468(_Base):
197
  out = dict(res)
198
  out.update({"status": "withheld", "path": "door:bound|vessel:receipt",
199
  "response": "I took \"%s\" to mean %s, but I have no attested answer to that about %s."
200
- % ((DEICTIC.search(text) or POINTER.search(text)).group(0), subject, subject),
201
  "provenance": {"kind": "dialogue_state",
202
  "r468_door": {"act": "bound_no_answer", "subject": subject,
203
  "rewritten": rewritten}}})
204
  return out
205
  # ASK: a specific question, naming what could not be resolved and what this conversation has raised
206
- unresolved = (DEICTIC.search(text) or POINTER.search(text))
207
  phrase = unresolved.group(0) if unresolved else None
208
  offers = list(reversed(thread[-3:]))
209
  if phrase and offers:
 
33
  except Exception: # noqa: BLE001
34
  _ir = None
35
 
36
+ # Untrusted bounded grammar guard, kept equal to AO without importing its
37
+ # runtime chain. Both binding and clarification exclude the same dummy token.
38
+ DUMMY_IT = re.compile(
39
+ r"\b(?:does|did|will|would|can|could|should|has|had)\s+(?P<it>it)\s+"
40
+ r"(?:take|taken|cost)\s+"
41
+ r"(?:(?:\d+(?:\.\d+)?\s+(?:milliseconds?|seconds?|minutes?|hours?|days?|weeks?|"
42
+ r"months?|years?|dollars?|euros?|pounds?)|[$€£]\d+(?:\.\d+)?)\s+)?"
43
+ r"to\s+[a-z]", re.I)
44
  POINTER = re.compile(r"\b(it|its|that|this|they|them|those|these)\b", re.I)
45
  DEICTIC = re.compile(r"\b(?:the|that|this)\s+(?:thing|one|place|stuff|topic|subject|idea|person|guy|animal|city|country)"
46
  r"(?:\s+(?:with|from|about|of|in|on)\s+(?:the\s+)?[a-z][\w\-]*(?:\s+[a-z][\w\-]*)?)?\b", re.I)
 
104
  return None
105
 
106
 
107
+ def _referential_pointer(text):
108
+ """One genuine pointing phrase/token, preserving later refs after dummy it.
109
+
110
+ Take/cost-infinitive grammar may use it without any prior entity. Leave that
111
+ token to the original reader; this is not a general English pronoun parser.
112
+ """
113
+ phrase = DEICTIC.search(text)
114
+ if phrase:
115
+ return phrase
116
+ dummy_spans = {match.span("it") for match in DUMMY_IT.finditer(text)}
117
+ return next((match for match in POINTER.finditer(text)
118
+ if match.span() not in dummy_spans), None)
119
+
120
+
121
  class OracleReleaseRuntimeW468(_Base):
122
 
123
  def _r468_thread(self, sid):
 
170
  del th[:-MAX_THREAD]
171
 
172
  def _r468_bind(self, text, subject):
173
+ m = _referential_pointer(text)
 
 
 
174
  if m:
175
+ word = m.group(0).lower()
176
  rep = subject + "'s" if word == "its" else subject
177
  return text[:m.start()] + rep + text[m.end():]
178
  return None
179
 
180
  def _r468_door(self, text, sid, res):
181
  thread = self._r468_thread(sid)
182
+ reference = _referential_pointer(text)
183
+ points = reference is not None
184
  named = self._r468_subjects(text)
185
  if points and thread and not named:
186
  subject = thread[-1]
 
217
  out = dict(res)
218
  out.update({"status": "withheld", "path": "door:bound|vessel:receipt",
219
  "response": "I took \"%s\" to mean %s, but I have no attested answer to that about %s."
220
+ % (reference.group(0), subject, subject),
221
  "provenance": {"kind": "dialogue_state",
222
  "r468_door": {"act": "bound_no_answer", "subject": subject,
223
  "rewritten": rewritten}}})
224
  return out
225
  # ASK: a specific question, naming what could not be resolved and what this conversation has raised
226
+ unresolved = reference
227
  phrase = unresolved.group(0) if unresolved else None
228
  offers = list(reversed(thread[-3:]))
229
  if phrase and offers:
root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w488.py CHANGED
@@ -1,6 +1,8 @@
1
  #!/usr/bin/env python3
2
  """oracle_release_runtime_w488 - WORD PROBLEMS: single-step prose read into an equation by a stored schema.
3
 
 
 
4
  The math organ (v1/v2) declines a word problem ("the quantities must be translated into equations first"). This fold
5
  connects word_problems_v1 (sha 4c0c60b1): each SCHEMA is a stored form - trigger words, the TYPED quantities it needs
6
  (money, percent, time, length, mass, volume; read with their units), and a formula - and it fires only when every
@@ -34,6 +36,7 @@ def _fmt(v, unit):
34
  return "$%s" % ("{:,.2f}".format(v) if v % 1 else "{:,.0f}".format(v))
35
  if unit == "%":
36
  return "%s%%" % ("%g" % round(v, 4))
 
37
  return ("%g" % round(v, 6)) + ((" " + unit) if unit and unit not in ("sq", "cu") else
38
  (" square units" if unit == "sq" else (" cubic units" if unit == "cu" else "")))
39
 
@@ -46,13 +49,23 @@ class OracleReleaseRuntimeW488(_Base):
46
  return out
47
  a = _run(_wp.answer, text)
48
  if not a:
49
- return out
 
 
 
 
 
 
 
 
 
 
50
  body = ("[ev:derived] %s. Read as %s: %s. Checked: every quantity the formula needs was found with its unit, "
51
  "and the question asks for what the formula gives." % (_fmt(a["value"], a["unit"]),
52
  a["schema"].replace("_", " "),
53
  "; ".join(a["steps"])))
54
  new = dict(out)
55
  new.update({'status': 'answered', 'path': 'organ:math>word:%s' % a['schema'], 'response': body,
56
- 'provenance': {'kind': 'organ_word_problem', 'schema': a['schema'], 'value': a['value'],
57
  'parent_path': str(out.get('path', '')), 'parent_status': out.get('status')}})
58
  return new
 
1
  #!/usr/bin/env python3
2
  """oracle_release_runtime_w488 - WORD PROBLEMS: single-step prose read into an equation by a stored schema.
3
 
4
+ This Python routing shim is untrusted; its checks are not proof or admission authority.
5
+
6
  The math organ (v1/v2) declines a word problem ("the quantities must be translated into equations first"). This fold
7
  connects word_problems_v1 (sha 4c0c60b1): each SCHEMA is a stored form - trigger words, the TYPED quantities it needs
8
  (money, percent, time, length, mass, volume; read with their units), and a formula - and it fires only when every
 
36
  return "$%s" % ("{:,.2f}".format(v) if v % 1 else "{:,.0f}".format(v))
37
  if unit == "%":
38
  return "%s%%" % ("%g" % round(v, 4))
39
+ unit = unit.replace("^2", "²").replace("^3", "³")
40
  return ("%g" % round(v, 6)) + ((" " + unit) if unit and unit not in ("sq", "cu") else
41
  (" square units" if unit == "sq" else (" cubic units" if unit == "cu" else "")))
42
 
 
49
  return out
50
  a = _run(_wp.answer, text)
51
  if not a:
52
+ explain = getattr(_wp, 'explain_refusal', None)
53
+ reason = _run(explain, text) if callable(explain) else None
54
+ if not reason:
55
+ return out
56
+ new = dict(out)
57
+ new.update({'status': 'clarification', 'path': 'organ:math>word:%s' % reason['reason'],
58
+ 'response': reason['response'],
59
+ 'provenance': {'kind': 'word_problem_refusal', 'schema': reason['schema'],
60
+ 'reason': reason['reason'], 'conflict': reason['conflict'],
61
+ 'parent_path': str(out.get('path', '')), 'parent_status': out.get('status')}})
62
+ return new
63
  body = ("[ev:derived] %s. Read as %s: %s. Checked: every quantity the formula needs was found with its unit, "
64
  "and the question asks for what the formula gives." % (_fmt(a["value"], a["unit"]),
65
  a["schema"].replace("_", " "),
66
  "; ".join(a["steps"])))
67
  new = dict(out)
68
  new.update({'status': 'answered', 'path': 'organ:math>word:%s' % a['schema'], 'response': body,
69
+ 'provenance': {'kind': 'organ_word_problem', 'schema': a['schema'], 'value': a['value'], 'unit': a['unit'],
70
  'parent_path': str(out.get('path', '')), 'parent_status': out.get('status')}})
71
  return new
root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w534.py CHANGED
@@ -16,6 +16,9 @@ in both variants - bar <= 2) and CONVERSATION TEST 2 (run once, head w533). Two
16
  to. Here the turn is resolved FIRST (the same engine, chat_context_v2.rewrite), the whole stack is asked the
17
  standalone question, and the conversation state is updated from the FINAL answer (its receipt names the entity).
18
  A turn the engine splits into parts, or cannot resolve, goes down exactly as typed.
 
 
 
19
  Every guard WITHHOLDS with its reason; nothing here invents an answer. The fold imports no runtime other than its parent;
20
  the conversation engine is called through its own public functions (rewrite, update, split_parts)."""
21
  from __future__ import annotations
@@ -64,6 +67,37 @@ def private_state(q):
64
  return bool(PRIVATE.search(q)) and not QUANTITY.search(q)
65
 
66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  def premise_years(q):
68
  m = PREMISE.match(q)
69
  if not m or _EL is None:
@@ -98,7 +132,7 @@ def _withhold(out, rule, reason):
98
  return new
99
 
100
 
101
- def final_guards(q, out):
102
  """the w532 guards, on the FINAL answer of any route"""
103
  if not isinstance(out, dict) or str(out.get("status", "")) != "answered":
104
  return out
@@ -107,7 +141,7 @@ def final_guards(q, out):
107
  return out
108
  if "specialist" in path and sign_dropped(q, reply):
109
  return _withhold(out, "G1_sign_kept", REASON["G1_sign_kept"])
110
- if private_state(q):
111
  return _withhold(out, "G2_private_state", REASON["G2_private_state"])
112
  try:
113
  pm = premise_years(q)
@@ -131,10 +165,19 @@ class OracleReleaseRuntimeW534(_Base):
131
  standalone = s
132
  except Exception: # noqa: BLE001 - an engine failure leaves the turn exactly as typed
133
  standalone = None
 
 
 
 
 
 
 
 
 
134
  out = super().chat(standalone or q, session_id=session_id)
135
  if not isinstance(out, dict):
136
  return out
137
- out = final_guards(standalone or q, out)
138
  if standalone:
139
  out = dict(out)
140
  if not str(out.get("path", "")).startswith("chat:"):
 
16
  to. Here the turn is resolved FIRST (the same engine, chat_context_v2.rewrite), the whole stack is asked the
17
  standalone question, and the conversation state is updated from the FINAL answer (its receipt names the entity).
18
  A turn the engine splits into parts, or cannot resolve, goes down exactly as typed.
19
+ A last-question recall may pass G2 only when its complete reply and turn references match
20
+ the same session's dialogue record captured before this turn. This does not authorize
21
+ guessing personal facts or answering an additional private question.
22
  Every guard WITHHOLDS with its reason; nothing here invents an answer. The fold imports no runtime other than its parent;
23
  the conversation engine is called through its own public functions (rewrite, update, split_parts)."""
24
  from __future__ import annotations
 
67
  return bool(PRIVATE.search(q)) and not QUANTITY.search(q)
68
 
69
 
70
+ # A whole request for the last recorded question/utterance, not a substring in
71
+ # a compound private-state request. First-question/history-list semantics are
72
+ # deliberately outside this exception: the dialogue organ selects the last turn.
73
+ LAST_TURN = re.compile(r"\s*(?:what\s+did\s+i\s+(?:(?:just|last)\s+)?(?:ask|say)(?:\s+you)?|"
74
+ r"what\s+was\s+my\s+(?:last|previous)\s+question)\s*[?.!]*\s*", re.I)
75
+
76
+
77
+ def recorded_history_reply(q, out, expected):
78
+ """An exact match to a same-session reply captured before the parent ran.
79
+
80
+ Path/citation strings alone are not authority. The caller obtains expected
81
+ from the dialogue organ's pure read of its current, bounded session history.
82
+ This Python check does not prove the authenticity of that in-memory history.
83
+ """
84
+ if not LAST_TURN.fullmatch(q) or not isinstance(expected, dict):
85
+ return False
86
+ if expected.get("kind") != "discourse:asked_last":
87
+ return False
88
+ turns = expected.get("turns") or []
89
+ provenance = out.get("provenance")
90
+ references = [{"id": "session:turn %d" % turn, "text": "this conversation, turn %d" % turn,
91
+ "role": "discourse"} for turn in turns]
92
+ return (out.get("path") == "organ:dialogue>discourse:asked_last"
93
+ and out.get("response") == expected.get("response")
94
+ and out.get("references") == references
95
+ and isinstance(provenance, dict)
96
+ and provenance.get("kind") == "organ_dialogue"
97
+ and provenance.get("move") == expected["kind"]
98
+ and provenance.get("turns") == turns)
99
+
100
+
101
  def premise_years(q):
102
  m = PREMISE.match(q)
103
  if not m or _EL is None:
 
132
  return new
133
 
134
 
135
+ def final_guards(q, out, history_reply=None):
136
  """the w532 guards, on the FINAL answer of any route"""
137
  if not isinstance(out, dict) or str(out.get("status", "")) != "answered":
138
  return out
 
141
  return out
142
  if "specialist" in path and sign_dropped(q, reply):
143
  return _withhold(out, "G1_sign_kept", REASON["G1_sign_kept"])
144
+ if private_state(q) and not recorded_history_reply(q, out, history_reply):
145
  return _withhold(out, "G2_private_state", REASON["G2_private_state"])
146
  try:
147
  pm = premise_years(q)
 
165
  standalone = s
166
  except Exception: # noqa: BLE001 - an engine failure leaves the turn exactly as typed
167
  standalone = None
168
+ history_reply = None
169
+ if LAST_TURN.fullmatch(standalone or q):
170
+ try:
171
+ # DialogueOrgan.answer only reads its bounded session log. Read
172
+ # before the parent appends this turn, including on an empty log.
173
+ dialogue = self._c480_dialogue()
174
+ history_reply = dialogue.answer(standalone or q, session_id) if dialogue else None
175
+ except Exception: # noqa: BLE001 - missing history keeps the existing private-state guard
176
+ pass
177
  out = super().chat(standalone or q, session_id=session_id)
178
  if not isinstance(out, dict):
179
  return out
180
+ out = final_guards(standalone or q, out, history_reply)
181
  if standalone:
182
  out = dict(out)
183
  if not str(out.get("path", "")).startswith("chat:"):
root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w557.py CHANGED
@@ -73,7 +73,7 @@ ROUTE_SPELLING = [
73
  # what an answer may stand on and still become state: a citation or source tag, a proof, or a checked route
74
  CITED = re.compile(r"\[[^\]\s]{2,}\]")
75
  CHECKED = ("specialist_call", "specialist_after_guard", "owner_first", "family_precedence", "network", "compute:", "organ:math",
76
- "op:", "verified:", "algebra:", "deduce:")
77
  SESSION_STORES = ("dialogue", "transitions", "_pending_conversion", "_paths")
78
  _ADJ = {s: [] for s in SEPH}
79
  for _i, (_a, _b) in enumerate(EDGES):
 
73
  # what an answer may stand on and still become state: a citation or source tag, a proof, or a checked route
74
  CITED = re.compile(r"\[[^\]\s]{2,}\]")
75
  CHECKED = ("specialist_call", "specialist_after_guard", "owner_first", "family_precedence", "network", "compute:", "organ:math",
76
+ "op:", "verified:", "algebra:", "deduce:", "organ:relation>")
77
  SESSION_STORES = ("dialogue", "transitions", "_pending_conversion", "_paths")
78
  _ADJ = {s: [] for s in SEPH}
79
  for _i, (_a, _b) in enumerate(EDGES):
root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/runtime_conversation_state.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Untrusted reconciliation of legacy conversation caches with delivered replies.
2
+
3
+ This is Python bookkeeping, not a proof or an answer authority. The runtime
4
+ serializes chat calls. Module-global v2 context is borrowed only for one turn;
5
+ committed contexts belong to a runtime instance and a conversation ID.
6
+ """
7
+ from copy import deepcopy
8
+ import math
9
+ import re
10
+ import sys
11
+
12
+ _TOPIC = re.compile(r"^\s*(?:new topic|switching topics|change of topic)\s*:", re.I)
13
+ _SCALAR = re.compile(r"^\s*\[ev:derived\]\s+([-+]?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)\. In steps:")
14
+
15
+
16
+ def begin(runtime, text, sid):
17
+ cc = sys.modules.get("chat_context_v2")
18
+ stores = getattr(runtime, "_conversation_contexts", None)
19
+ if stores is None:
20
+ stores = runtime._conversation_contexts = {}
21
+ prior = deepcopy(stores.get(sid))
22
+ frame = {"cc": cc, "prior": prior, "sid": sid}
23
+ if cc is not None:
24
+ frame["ambient"] = (sid in cc._STATES, cc._STATES.get(sid))
25
+ cc._STATES[sid] = deepcopy(prior) if prior is not None else cc.Turn()
26
+ values = getattr(runtime, "_last_value", {})
27
+ frame["value"] = (sid in values, values.get(sid))
28
+ if _TOPIC.match(str(text)):
29
+ if cc is not None:
30
+ cc._STATES[sid] = cc.Turn()
31
+ frame["prior"] = None
32
+ values.pop(sid, None)
33
+ getattr(runtime, "_v4_disc", {}).pop(sid, None)
34
+ return frame
35
+
36
+
37
+ def scalar(reply, speculative, previous):
38
+ """Only the two known scalar arithmetic routes can populate w29's cache."""
39
+ prov = reply.get("provenance")
40
+ prov = prov if isinstance(prov, dict) else {}
41
+ path = str(reply.get("path", ""))
42
+ if prov.get("kind") == "computation" and prov.get("verified") is True and path in ("compute:verified", "chat:resolved>compute:verified"):
43
+ # The final closed arithmetic route wrote this value itself.
44
+ return speculative
45
+ if path == "organ:math>evaluate" and prov.get("kind") == "organ_math" and prov.get("directive") == "evaluate":
46
+ match = _SCALAR.match(str(reply.get("response", "")))
47
+ if match:
48
+ value = float(match.group(1))
49
+ return value if math.isfinite(value) else None
50
+ if path.startswith("act:"):
51
+ return previous
52
+ return None
53
+
54
+
55
+ def finish(runtime, text, reply, frame, supported):
56
+ sid, cc = frame["sid"], frame["cc"]
57
+ values = getattr(runtime, "_last_value", None)
58
+ if values is None:
59
+ values = runtime._last_value = {}
60
+ previous = frame["value"][1] if frame["value"][0] else None
61
+ value = scalar(reply, values.get(sid), previous) if supported else None
62
+ values.pop(sid, None)
63
+ if value is not None:
64
+ values[sid] = value
65
+ if cc is None:
66
+ return
67
+ prior = frame["prior"]
68
+ cc._STATES[sid] = deepcopy(prior) if prior is not None else cc.Turn()
69
+ if not supported:
70
+ cc._STATES[sid] = cc.Turn()
71
+ elif not str(reply.get("path", "")).startswith("act:"):
72
+ # Direct questions legitimately carry chat_resolved: null. Only a
73
+ # typed rewrite record with a string question overrides the input.
74
+ prov = reply.get("provenance")
75
+ resolved = prov.get("chat_resolved") if isinstance(prov, dict) else None
76
+ question = resolved.get("read_as") if isinstance(resolved, dict) else None
77
+ question = question if isinstance(question, str) and question.strip() else text
78
+ # Clear numerical slots before recognition so an unrecognized final
79
+ # correction cannot silently retain an older number or solved variable.
80
+ cc._STATES[sid].result = None
81
+ cc._STATES[sid].solved = {}
82
+ cc.update(sid, question, reply)
83
+ if value is not None:
84
+ cc._STATES[sid].result = (value, "")
85
+ stores = runtime._conversation_contexts
86
+ if sid not in stores and len(stores) >= cc.STATE_CAP:
87
+ stores.pop(next(iter(stores)))
88
+ stores[sid] = deepcopy(cc._STATES[sid])
89
+
90
+
91
+ def release(frame):
92
+ cc = frame["cc"]
93
+ if cc is not None:
94
+ had, value = frame["ambient"]
95
+ if had:
96
+ cc._STATES[frame["sid"]] = value
97
+ else:
98
+ cc._STATES.pop(frame["sid"], None)
root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/runtime_final_outcome.py CHANGED
@@ -10,6 +10,7 @@ from functools import wraps
10
  import os
11
  import re
12
  import time
 
13
 
14
  _FRAMES = ContextVar("oracle_final_turns", default=())
15
  _CITE = re.compile(r"\[[a-z0-9_]+:?[a-z0-9_.\-]*\]")
@@ -45,6 +46,7 @@ def final_turn(method):
45
  "snapshot": self._v4_snapshot(session_id),
46
  "started": time.perf_counter(), "first": None,
47
  "hits": sum(rc.hits for rc in self._v4_recall())}
 
48
  token = _FRAMES.set(frames + (frame,))
49
  try:
50
  result = method(self, text, session_id)
@@ -65,6 +67,7 @@ def final_turn(method):
65
  raise
66
  finally:
67
  if outer:
 
68
  _FRAMES.reset(token)
69
 
70
  wrapped._oracle_final_turn = True
@@ -101,6 +104,7 @@ def finish_turn(runtime, text, session_id, result, frame, stands, append, entry,
101
  # AN's recorder derives all terms and metrics again from the final reply.
102
  runtime._record_turn(text, session_id, reply, frame["started"], prior)
103
  getattr(runtime, "_pending_conversion", {}).pop(session_id, None)
 
104
  commit_disc = getattr(runtime, "_v4_commit_disc", None)
105
  if commit_disc:
106
  commit_disc(text, session_id, reply)
 
10
  import os
11
  import re
12
  import time
13
+ import runtime_conversation_state as conversation
14
 
15
  _FRAMES = ContextVar("oracle_final_turns", default=())
16
  _CITE = re.compile(r"\[[a-z0-9_]+:?[a-z0-9_.\-]*\]")
 
46
  "snapshot": self._v4_snapshot(session_id),
47
  "started": time.perf_counter(), "first": None,
48
  "hits": sum(rc.hits for rc in self._v4_recall())}
49
+ frame["conversation"] = conversation.begin(self, text, session_id)
50
  token = _FRAMES.set(frames + (frame,))
51
  try:
52
  result = method(self, text, session_id)
 
67
  raise
68
  finally:
69
  if outer:
70
+ conversation.release(frame["conversation"])
71
  _FRAMES.reset(token)
72
 
73
  wrapped._oracle_final_turn = True
 
104
  # AN's recorder derives all terms and metrics again from the final reply.
105
  runtime._record_turn(text, session_id, reply, frame["started"], prior)
106
  getattr(runtime, "_pending_conversion", {}).pop(session_id, None)
107
+ conversation.finish(runtime, text, reply, frame["conversation"], answered and not unsupported)
108
  commit_disc = getattr(runtime, "_v4_commit_disc", None)
109
  if commit_disc:
110
  commit_disc(text, session_id, reply)
root/specialists/_gate/combo_runtime_v1.py CHANGED
@@ -238,6 +238,58 @@ QWORDS = set("what what's how who whom whose which where when why is are was wer
238
  "please tell give name".split())
239
 
240
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  def _split_parts(text: str) -> list[str]:
242
  """split a turn at its question marks; never inside `code` or quoted spans (SQL, JSON, strings).
243
  Semicolons never split: "10 of 40 deer were tagged; 50 had been tagged ..." is one question."""
@@ -251,7 +303,16 @@ def _split_parts(text: str) -> list[str]:
251
  out.append(t[prev:c].strip())
252
  prev = c
253
  out.append(t[prev:].strip())
254
- return [p for p in out if p]
 
 
 
 
 
 
 
 
 
255
 
256
 
257
  class Combo:
@@ -340,9 +401,9 @@ class Combo:
340
  return self._arbitrate(text, self._ask_all(text, session_id), session_id)
341
 
342
  def _segment(self, parts, session_id):
343
- """left to right: a SHORT part (<= 6 words) that no organ takes alone rejoins its neighbour when the joined
344
- text is taken ("... in 5-card poker? How many?", "Is Basque related to ...? What family is it?").
345
- A full question nobody covers is never glued on (it would vanish inside the neighbour's answer)."""
346
  segs = []
347
  ok = lambda x: x[1] is not None or bool(x[2].get("disagree")) # noqa: E731
348
  for p in parts:
@@ -353,13 +414,13 @@ class Combo:
353
  segs[-1] = (joined,) + self._one(joined, session_id)
354
  continue
355
  cur = (p,) + self._one(p, session_id)
356
- if segs and not ok(cur) and len(WORD.findall(p.lower())) <= 6:
357
  joined = segs[-1][0] + " " + p
358
  j = (joined,) + self._one(joined, session_id)
359
  if ok(j):
360
  segs[-1] = j
361
  continue
362
- if segs and not ok(segs[-1]) and len(WORD.findall(segs[-1][0].lower())) <= 6:
363
  joined = segs[-1][0] + " " + p
364
  j = (joined,) + self._one(joined, session_id)
365
  if ok(j):
 
238
  "please tell give name".split())
239
 
240
 
241
+ CONDITION_START = re.compile(
242
+ r"^(?:please\s+)?(?:use|using|assume|assuming|take|given|with|without|ignore|ignoring|neglect|neglecting)\b", re.I)
243
+ CONDITION_NEW_TASK = re.compile(
244
+ r"\b(?:calculate|compute|determine|find|tell|give|show|convert|list|summarize|compare|explain|"
245
+ r"evaluate|solve|plot|draw|write|read|search|estimate|measure|prove|derive|differentiate|integrate|"
246
+ r"derivatives?|integrals?|square root|report|what|who|whom|whose|which|where|when|why|how)\b", re.I)
247
+ DECLARED_CONSTANT = re.compile(
248
+ r"^(?:the\s+)?(?:load|gravity|density|specific heat(?: capacity)?|pressure|volume|mass|"
249
+ r"flow(?: rate)?|temperature|stiffness|efficiency)\s+(?:is|remains|stays)\s+"
250
+ r"(?:constant|steady|fixed)(?:\s+throughout)?\s*[.!]?$", re.I)
251
+ CONDITION_PARAMETER = re.compile(
252
+ r"\b(?:specific heat|heat capacity|gravity|density|efficiency|temperature|pressure|mass|volume|"
253
+ r"flow|stiffness|load|capacity|loss(?:es)?|friction|phase change|constant)\b|\b[a-z]\s*=", re.I)
254
+
255
+
256
+ def _condition_prefix(part: str) -> tuple[str, str]:
257
+ """Separate explicit declarative conditions from a later independent task.
258
+
259
+ Only condition-led sentences without an unquoted question or request verb
260
+ attach backwards. A new question stays separate even when its preceding
261
+ sentence starts with 'Use'. Quoted/code text is never a split boundary.
262
+ This is deliberately a small untrusted English recognizer, not a proof that
263
+ arbitrary prose expresses only conditions.
264
+ """
265
+ masked = CODE_SPAN.sub(lambda m: "\x00" * len(m.group(0)), part)
266
+ # A 'Given ...' premise before another question normally belongs to that
267
+ # new question; it is only a backwards continuation when no question follows.
268
+ if re.match(r"^(?:please\s+)?given\b", masked, re.I) and "?" in masked:
269
+ return "", part
270
+ ends = [m.end() for m in re.finditer(r"[.!]\s+", masked)] + [len(part)]
271
+ start, consumed = 0, 0
272
+ for end in ends:
273
+ sentence = masked[start:end].strip()
274
+ if not sentence:
275
+ start = end
276
+ continue
277
+ if not (CONDITION_START.match(sentence) or DECLARED_CONSTANT.fullmatch(sentence)) or "?" in sentence or CONDITION_NEW_TASK.search(sentence):
278
+ break
279
+ if re.match(r"^(?:please\s+)?(?:use|using|take)\b", sentence, re.I) and not CONDITION_PARAMETER.search(sentence):
280
+ break # "Take the square root ..." / "Use a calculator ..." is a task, not a model input.
281
+ consumed = end
282
+ start = end
283
+ return part[:consumed].strip(), part[consumed:].strip()
284
+
285
+
286
+ def _short_followup_fragment(text: str) -> bool:
287
+ """A small explicit elliptical vocabulary; brevity alone never implies dependence."""
288
+ return bool(re.fullmatch(
289
+ r"\s*(?:and\s+)?(?:how\s+(?:many|much|long|far)|what(?:\s+else)?|who|when|where|why|which(?:\s+one)?)\s*[?!.]*\s*",
290
+ text, re.I))
291
+
292
+
293
  def _split_parts(text: str) -> list[str]:
294
  """split a turn at its question marks; never inside `code` or quoted spans (SQL, JSON, strings).
295
  Semicolons never split: "10 of 40 deer were tagged; 50 had been tagged ..." is one question."""
 
303
  out.append(t[prev:c].strip())
304
  prev = c
305
  out.append(t[prev:].strip())
306
+ grouped = []
307
+ for part in out:
308
+ if not part:
309
+ continue
310
+ condition, remainder = _condition_prefix(part) if grouped else ("", part)
311
+ if condition:
312
+ grouped[-1] += " " + condition
313
+ if remainder:
314
+ grouped.append(remainder)
315
+ return grouped
316
 
317
 
318
  class Combo:
 
401
  return self._arbitrate(text, self._ask_all(text, session_id), session_id)
402
 
403
  def _segment(self, parts, session_id):
404
+ """An explicit elliptical fragment that no organ takes alone may rejoin its neighbour
405
+ ("... in 5-card poker? How many?"). Pronoun backreferences are handled separately.
406
+ A full question nobody covers is never glued on just because it is short."""
407
  segs = []
408
  ok = lambda x: x[1] is not None or bool(x[2].get("disagree")) # noqa: E731
409
  for p in parts:
 
414
  segs[-1] = (joined,) + self._one(joined, session_id)
415
  continue
416
  cur = (p,) + self._one(p, session_id)
417
+ if segs and not ok(cur) and _short_followup_fragment(p):
418
  joined = segs[-1][0] + " " + p
419
  j = (joined,) + self._one(joined, session_id)
420
  if ok(j):
421
  segs[-1] = j
422
  continue
423
+ if segs and not ok(segs[-1]) and _short_followup_fragment(segs[-1][0]):
424
  joined = segs[-1][0] + " " + p
425
  j = (joined,) + self._one(joined, session_id)
426
  if ok(j):
root/specialists/energy-v3/organ_energy_v1.py CHANGED
@@ -2,9 +2,11 @@
2
  """organ_energy_v1 - household energy arithmetic, every step shown: energy used by a device (W x h -> kWh), what it costs
3
  at a stated tariff, how long a battery runs a load (Wh, or V x Ah), a solar array's daily yield (kW x peak-sun-hours,
4
  x a stated derate, otherwise said to be before losses), a payback period, and the yearly saving from swapping one
5
- device for another. It never assumes a tariff, a price or sun-hours it was not given. Returns {kind, status, response, value}."""
 
6
  from __future__ import annotations
7
 
 
8
  import re
9
 
10
  N = r"(\d+(?:,\d{3})*(?:\.\d+)?)"
@@ -60,6 +62,150 @@ def money(x):
60
  return "$%.2f" % x if x < 1000 else "$%s" % format(round(x), ",")
61
 
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  class EnergyOrgan:
64
  def _say(self, kind, text, value=None, status="answered"):
65
  return {"kind": kind, "status": status, "response": text, "value": value}
@@ -70,6 +216,63 @@ class EnergyOrgan:
70
  def answer(self, text, session_id="default"):
71
  t = " ".join(str(text or "").split())
72
  low = t.lower()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  # payback
74
  if re.search(r"\bpayback|\bpay (?:for )?itself|\bbreak even\b|\bpay back\b", low):
75
  cost = re.search(r"\$\s*" + N + r"(?:\s*(k))?", low)
@@ -83,26 +286,17 @@ class EnergyOrgan:
83
  y = c / s
84
  return self._say("payback", "The payback period is %s years (%s ÷ %s saved per year), ignoring price changes and financing." % (g(y), money(c), money(s)), y)
85
  # battery runtime
86
- if re.search(r"\bbattery|\bpower bank\b|\bups\b", low) and re.search(r"\bhow long\b|\bruntime\b|\brun time\b|\blast\b", low):
87
- wh = re.search(N + r"\s*(kwh|wh)\b", low)
88
- ah = re.search(N + r"\s*ah\b", low)
89
- v = re.search(N + r"\s*v(?:olts?)?\b", low)
90
- load = re.search(r"\b(?:a|an|the)?\s*" + N + r"\s*(w|watts?)\b(?!\s*h\b)\s*(?:device|load|appliance|fridge|light|laptop|lamp|fan|tv|router|pump|heater)?", low)
91
- if wh:
92
- cap = num(wh.group(1)) * (1000 if wh.group(2) == "kwh" else 1)
93
- capnote = "%s Wh" % g(cap)
94
- elif ah and v:
95
- cap = num(ah.group(1)) * num(v.group(1))
96
- capnote = "%s V × %s Ah = %s Wh" % (g(num(v.group(1))), g(num(ah.group(1))), g(cap))
97
- else:
98
- return self._need("the battery's capacity (Wh, or volts and amp-hours)")
99
- if not load:
100
- return self._need("the load in watts")
101
- w = num(load.group(1))
102
- if w <= 0:
103
- return self._say("impossible", "A load must draw more than 0 W.", status="withheld")
104
- hrs = cap / w
105
- return self._say("runtime", "The runtime is about %s hours (%s ÷ %s W), before inverter and depth-of-discharge losses." % (g(hrs), capnote, g(w)), hrs)
106
  # solar daily yield
107
  if re.search(r"\bsolar\b|\bpv\b|\bpanels?\b", low) and re.search(r"\bsun ?hours?\b|\bpeak sun\b|\bsunlight hours?\b", low):
108
  k = re.search(N + r"\s*(kw|kilowatts?|w|watts?)\b(?!\s*h\b)", low)
 
2
  """organ_energy_v1 - household energy arithmetic, every step shown: energy used by a device (W x h -> kWh), what it costs
3
  at a stated tariff, how long a battery runs a load (Wh, or V x Ah), a solar array's daily yield (kW x peak-sun-hours,
4
  x a stated derate, otherwise said to be before losses), a payback period, and the yearly saving from swapping one
5
+ device for another. It never assumes a tariff, a price or sun-hours it was not given. Returns {kind, status, response, value}.
6
+ The English recognizer and Python calculations are untrusted; these are explicit constant-energy models, not proofs."""
7
  from __future__ import annotations
8
 
9
+ import math
10
  import re
11
 
12
  N = r"(\d+(?:,\d{3})*(?:\.\d+)?)"
 
62
  return "$%.2f" % x if x < 1000 else "$%s" % format(round(x), ",")
63
 
64
 
65
+ QN = r"(?<![\w.,])([+-]?(?:\d+(?:,\d{3})*(?:\.\d+)?|\.\d+))"
66
+ POWER = re.compile(QN + r"\s*(kw|kilowatts?|w|watts?)\b")
67
+ DEVICE = r"(?:load|device|appliance|fridge|light|laptop|lamp|fan|tv|router|pump|heater|bulb|motor)\b"
68
+
69
+
70
+ def check_energy_conditions(low):
71
+ for m in re.finditer(r"(?<![\w.,])[+-]?\d[\d,.]*", low):
72
+ token = m[0].rstrip(".")
73
+ if "," in token and not re.fullmatch(r"[+-]?\d{1,3}(?:,\d{3})+(?:\.\d+)?", token):
74
+ raise ValueError("numbers with unambiguous decimal points and three-digit comma groups")
75
+ if re.search(r"\bbatteries\b|\b(?:loads|devices|appliances|fridges|lights|laptops|lamps|fans|routers|pumps|heaters|bulbs|motors)\b", low):
76
+ raise ValueError("one battery source and one total consumed load; multiple sources or loads need an explicit combined model")
77
+
78
+
79
+ def one_value(values, description):
80
+ if not values:
81
+ raise ValueError(description)
82
+ if len(set(values)) != 1:
83
+ raise ValueError("one unambiguous " + description)
84
+ value = values[0]
85
+ if not math.isfinite(value) or value < 0:
86
+ raise ValueError("a finite nonnegative " + description)
87
+ return value
88
+
89
+
90
+ def capacity_wh(low):
91
+ check_energy_conditions(low)
92
+ values = [num(m[1]) * (1000 if m[2] == "kwh" else 1)
93
+ for m in re.finditer(QN + r"\s*(kwh|wh)\b", low)]
94
+ if values:
95
+ energy = one_value(values, "battery energy in Wh or kWh")
96
+ if re.search(QN + r"\s*ah\b", low):
97
+ volts = [num(m[1]) for m in re.finditer(QN + r"\s*v(?:olts?)?\b", low)]
98
+ amps = [num(m[1]) for m in re.finditer(QN + r"\s*ah\b", low)]
99
+ equivalent = one_value(volts, "battery voltage") * one_value(amps, "battery capacity in Ah")
100
+ if not math.isclose(energy, equivalent, rel_tol=1e-9, abs_tol=1e-9):
101
+ raise ValueError("consistent Wh and voltage/amp-hour capacity values")
102
+ return energy
103
+ volts = [num(m[1]) for m in re.finditer(QN + r"\s*v(?:olts?)?\b", low)]
104
+ amps = [num(m[1]) for m in re.finditer(QN + r"\s*ah\b", low)]
105
+ return one_value([one_value(volts, "battery voltage") * one_value(amps, "battery capacity in Ah")], "battery energy")
106
+
107
+
108
+ def load_and_rating(low):
109
+ """Untrusted grammar: inverter rating is a constraint, never consumed power."""
110
+ loads, ratings, unbound = [], [], []
111
+ for m in POWER.finditer(low):
112
+ value = num(m[1]) * (1000 if m[2].startswith("k") else 1)
113
+ before, after = low[max(0, m.start()-55):m.start()], low[m.end():m.end()+55]
114
+ is_rating = (re.match(r"\s*(?:rated\s+)?inverter\b", after)
115
+ or re.search(r"\binverter(?:\s+(?:rating|rated|is|of|at|for|has|a))*(?:\s*[:=])?\s*$", before)
116
+ or re.match(r"\s*(?:inverter\s+)?(?:rating|rated)\b", after))
117
+ is_load = (re.match(r"\s*(?:(?:ac|dc|constant)\s+)?" + DEVICE, after)
118
+ or re.search(r"\b(?:load|device|appliance)\s*(?:(?:draws|uses|is|of|at)\s*)?[:=]?\s*$", before))
119
+ if is_rating and not is_load:
120
+ ratings.append(value)
121
+ elif is_load and not is_rating:
122
+ loads.append(value)
123
+ else:
124
+ unbound.append(value)
125
+ if not loads and len(unbound) == 1 and not ratings:
126
+ loads, unbound = unbound, [] # the legacy single-load form
127
+ if unbound:
128
+ raise ValueError("the consumed load and inverter rating labelled separately")
129
+ if len(loads) != 1:
130
+ raise ValueError("one total consumed load in watts")
131
+ load = one_value(loads, "consumed load in watts")
132
+ rating = one_value(ratings, "inverter rating in watts") if ratings else None
133
+ if load <= 0:
134
+ raise ValueError("a consumed load greater than zero watts")
135
+ if rating is not None and load > rating:
136
+ raise ValueError("a load within the stated inverter rating")
137
+ return load, rating
138
+
139
+
140
+ def energy_factors(low, storage=False):
141
+ """Bind each stated fraction to its role, or reject an unbound condition.
142
+
143
+ These are simple constant-energy models, not battery chemistry or a proof.
144
+ Efficiency and loss are alternative descriptions of the same conversion;
145
+ depth of discharge is a separate capacity fraction.
146
+ """
147
+ check_energy_conditions(low)
148
+ if (not storage and re.search(r"\b(?:charging|storage)\s+efficiency\b", low)) or (storage and re.search(r"\binverter\s+efficiency\b", low)):
149
+ raise ValueError("an efficiency bound to the requested conversion (charging and load supply are different stages)")
150
+ groups = {
151
+ "efficiency": r"(?:(?:inverter|overall|conversion|charging|storage|battery)\s+)?efficiency",
152
+ "depth": r"(?:depth of discharge|usable (?:depth|capacity|fraction)|discharge depth)",
153
+ "loss": r"(?:(?:inverter|conversion|charging|storage|system)\s+)?loss(?:es)?",
154
+ }
155
+ if storage:
156
+ groups["stored"] = r"(?:stored fraction|storage fraction)"
157
+ found, spans = {}, []
158
+ numeric = r"([+-]?(?:\d+(?:\.\d+)?|\.\d+))\s*(%?)"
159
+ for role, label in groups.items():
160
+ patterns = [r"(?<![\w.])" + numeric + r"\s+" + label + r"\b",
161
+ r"\b" + label + r"\s*(?:(?:of|is|=|:)\s*)?" + numeric + r"(?![\w.])"]
162
+ if role == "efficiency":
163
+ patterns.append(r"(?<![\w.])" + numeric + r"\s+efficient(?:\s+inverter)?\b")
164
+ if role == "stored":
165
+ patterns.append(r"\b(?:stores|store|retains|retain)\s+" + numeric + r"\s+of\b")
166
+ for pattern in patterns:
167
+ for m in re.finditer(pattern, low):
168
+ f = float(m[1]) / (100 if m[2] else 1)
169
+ if not math.isfinite(f) or not 0 <= f <= 1:
170
+ raise ValueError("efficiency, loss and usable fractions between 0 and 1 (or 0% and 100%)")
171
+ found.setdefault(role, []).append(f)
172
+ spans.append(m.span())
173
+ # Every percentage and every adjustment term must be accounted for. Ignoring
174
+ # losses is an explicit ideal-model assumption, not an unknown loss factor.
175
+ masked = list(low)
176
+ for start, end in spans:
177
+ masked[start:end] = " " * (end-start)
178
+ rest = "".join(masked)
179
+ rest = re.sub(r"\b(?:ignoring|ignore|without|no|neglecting)\s+(?:(?:inverter|conversion)\s+)?loss(?:es)?\b", "", rest)
180
+ if re.search(r"%|\befficien\w*|\bloss(?:es)?\b|\bdepth of discharge\b|\busable (?:depth|capacity|fraction)\b|\b(?:idle|standby|variable|surge|peukert|temperature|degradation|derate|reserve)\b|\bperformance ratio\b|\b(?:half full|partly charged|partially charged|state of charge)\b", rest):
181
+ raise ValueError("each loss, efficiency, usable-capacity or variable-load condition explicitly bound to this model")
182
+ factors = {role: one_value(values, role + " fraction") for role, values in found.items()}
183
+ efficiency = factors.get("efficiency", 1)
184
+ if "loss" in factors:
185
+ retained = 1 - factors["loss"]
186
+ if "efficiency" in factors and not math.isclose(efficiency, retained, abs_tol=1e-12):
187
+ raise ValueError("consistent efficiency and loss fractions")
188
+ efficiency = retained
189
+ if "stored" in factors:
190
+ if "efficiency" in factors or "loss" in factors:
191
+ if not math.isclose(efficiency, factors["stored"], abs_tol=1e-12):
192
+ raise ValueError("one unambiguous stored fraction (not two unexplained conversion stages)")
193
+ efficiency = factors["stored"]
194
+ return efficiency, factors.get("depth", 1), bool(found)
195
+
196
+
197
+ def duration_hours(low):
198
+ durations = [num(m[1]) for m in re.finditer(QN + r"\s*(?:hours?|hrs?|h|minutes?)\b", low)]
199
+ if any(not math.isfinite(x) or x < 0 for x in durations):
200
+ raise ValueError("a finite nonnegative load duration")
201
+ if len(durations) > 1:
202
+ raise ValueError("one unambiguous load duration")
203
+ duration = hours(low)
204
+ if duration is not None and not math.isfinite(duration[0]):
205
+ raise ValueError("a finite load duration")
206
+ return duration
207
+
208
+
209
  class EnergyOrgan:
210
  def _say(self, kind, text, value=None, status="answered"):
211
  return {"kind": kind, "status": status, "response": text, "value": value}
 
216
  def answer(self, text, session_id="default"):
217
  t = " ".join(str(text or "").split())
218
  low = t.lower()
219
+ # Explicit continuations. The goal is read from the question, not from
220
+ # a noun in the initial conditions ("initially stores" is not a goal).
221
+ question = re.search(r"\b(?:what|how much)\b[^?]*\??$", low)
222
+ goal = question.group(0) if question else ""
223
+ remaining = re.search(r"\bremain\w*\b|\bleft\b", goal)
224
+ stored = re.search(r"\bstored\b|\bstore\b", goal)
225
+ if remaining and re.search(r"\bbatter(?:y|ies)\b", low):
226
+ try:
227
+ if not re.search(r"\binitial\w*\b|\bstarts? with\b|\bfully charged\b", low):
228
+ raise ValueError("the initially stored energy or an explicit fully charged battery")
229
+ initial = capacity_wh(low)
230
+ load, rating = load_and_rating(low)
231
+ efficiency, depth, adjusted = energy_factors(low)
232
+ duration = duration_hours(low)
233
+ if duration is None:
234
+ raise ValueError("the duration of the constant load")
235
+ if efficiency == 0 and duration[0] > 0:
236
+ raise ValueError("positive conversion efficiency to supply this load")
237
+ spent = load * duration[0] / efficiency if efficiency else 0
238
+ if spent > initial * depth + 1e-10:
239
+ raise ValueError("a duration within the battery's available energy at the stated load and factors")
240
+ base = initial * depth if re.search(r"\busable\b", goal) else initial
241
+ value = (base - spent) / 1000
242
+ return self._say("energy", "The %sremaining energy is %s kWh (%s Wh initially%s − %s Wh withdrawn; load %s W × %s, conversion efficiency %s)." % (
243
+ "usable " if base != initial else "", g(value), g(initial), " × %s usable fraction" % g(depth) if base != initial else "", g(spent), g(load), duration[1], g(efficiency)), value)
244
+ except ValueError as error:
245
+ return self._need(str(error))
246
+ if stored and re.search(r"\b(?:solar|panel|batter(?:y|ies)|charging)\b", low):
247
+ try:
248
+ efficiency, depth, adjusted = energy_factors(low, storage=True)
249
+ if depth != 1:
250
+ raise ValueError("charging/storage efficiency; discharge depth does not specify stored energy")
251
+ if not adjusted and not re.search(r"\b(?:ignoring|without|no|neglecting)\s+loss", low):
252
+ raise ValueError("the fraction of generated/input energy stored, or an explicit no-loss assumption")
253
+ powers = list(POWER.finditer(low))
254
+ duration = duration_hours(low)
255
+ sun = re.search(N + r"\s*(?:peak\s+)?sun[ -]?hours?\b", low)
256
+ if sun:
257
+ duration = (num(sun[1]), "%s peak sun hours" % sun[1])
258
+ if len(powers) == 1 and duration:
259
+ power = one_value([num(powers[0][1]) * (1000 if powers[0][2].startswith("k") else 1)], "input/generating power")
260
+ panel_count = re.search(QN + r"\s+panels?\b", low)
261
+ if panel_count:
262
+ if not re.search(r"\beach\b|\bper panel\b", low):
263
+ raise ValueError("whether panel power is per panel or for the whole array")
264
+ power *= one_value([num(panel_count[1])], "panel count")
265
+ generated = power * duration[0] / 1000
266
+ elif not powers:
267
+ generated = capacity_wh(low) / 1000
268
+ else:
269
+ raise ValueError("one input/generating power and duration, or one stated input energy")
270
+ if re.search(r"\binitial\w*|\bcapacity|\bfull\b", low):
271
+ raise ValueError("a charging plan with initial charge and capacity bounds; this calculation gives newly stored energy only")
272
+ value = one_value([generated * efficiency], "newly stored energy")
273
+ return self._say("energy", "The newly stored energy is %s kWh (%s kWh generated/input energy × %s storage fraction)." % (g(value), g(generated), g(efficiency)), value)
274
+ except ValueError as error:
275
+ return self._need(str(error))
276
  # payback
277
  if re.search(r"\bpayback|\bpay (?:for )?itself|\bbreak even\b|\bpay back\b", low):
278
  cost = re.search(r"\$\s*" + N + r"(?:\s*(k))?", low)
 
286
  y = c / s
287
  return self._say("payback", "The payback period is %s years (%s ÷ %s saved per year), ignoring price changes and financing." % (g(y), money(c), money(s)), y)
288
  # battery runtime
289
+ if re.search(r"\bbatter(?:y|ies)|\bpower bank\b|\bups\b", low) and re.search(r"\bhow long\b|\bruntime\b|\brun time\b|\blast\b", low):
290
+ try:
291
+ cap = capacity_wh(low)
292
+ w, rating = load_and_rating(low)
293
+ efficiency, depth, adjusted = energy_factors(low)
294
+ hrs = one_value([cap * efficiency * depth / w], "runtime")
295
+ note = "at the stated conversion and usable-capacity factors" if adjusted else "before inverter and depth-of-discharge losses"
296
+ return self._say("runtime", "The runtime is about %s hours (%s Wh × %s conversion efficiency × %s usable fraction ÷ %s W load), %s%s." % (
297
+ g(hrs), g(cap), g(efficiency), g(depth), g(w), note, "; load is within the %s W inverter rating" % g(rating) if rating is not None else ""), hrs)
298
+ except ValueError as error:
299
+ return self._need(str(error))
 
 
 
 
 
 
 
 
 
300
  # solar daily yield
301
  if re.search(r"\bsolar\b|\bpv\b|\bpanels?\b", low) and re.search(r"\bsun ?hours?\b|\bpeak sun\b|\bsunlight hours?\b", low):
302
  k = re.search(N + r"\s*(kw|kilowatts?|w|watts?)\b(?!\s*h\b)", low)
root/specialists/geometry-v4/manifest.json CHANGED
@@ -45,7 +45,7 @@
45
  "underspecified",
46
  "impossible"
47
  ],
48
- "context_regex": "\\b(?:circle|sphere|cylinder|cone|cube|rectangle|triangle|hypotenuse|polygon|pentagon|hexagon|heptagon|octagon|nonagon|decagon|quadrilateral)\\b"
49
  },
50
  "panel": {
51
  "path": "__ORACLE7_ROOT__/specialists/geometry-v4/geometry_panel_v3.json",
@@ -73,4 +73,4 @@
73
  },
74
  "notes": "v4 = v3 + imperative verbs (compute / calculate / determine / give / work out) read as 'find' for the hypotenuse. Judged under spec_gate_v2 on a FRESH panel (new seeds, new phrasings both splits including the class). Same author - disclosed. Earlier notes: v3 = v1 + narrowing (v2: word problems to the head; no false 'I need X' when numbers are present - the full-core sweep found v1 taking 17 core word problems incl. a confident wrong 10.95 for 5) + two reading classes v1 missed (sides stated with a verb, 'sides are a, b and c' - all 8 v1 held-out misses; a hypotenuse value stated after the verb). Judged under spec_gate_v2 on a FRESH panel v2 (new seeds, new phrasings both splits, new declines). Same author - disclosed. Deliberately does NOT read '10 feet across' as a diameter: those core textbook problems use pi = 3.14.",
75
  "supersedes": "geometry-v3 (ADMIT gate v2, 93/100; 7 misses = imperative 'Compute the hypotenuse')"
76
- }
 
45
  "underspecified",
46
  "impossible"
47
  ],
48
+ "context_regex": "\\b(?:circles?|circular|spheres?|cylinders?|cones?|cubes?|rectangles?|rectangular|squares?|triangles?|box|boxes|cuboids?|hypotenuse|polygons?|pentagons?|hexagons?|heptagons?|octagons?|nonagons?|decagons?|quadrilaterals?)\\b"
49
  },
50
  "panel": {
51
  "path": "__ORACLE7_ROOT__/specialists/geometry-v4/geometry_panel_v3.json",
 
73
  },
74
  "notes": "v4 = v3 + imperative verbs (compute / calculate / determine / give / work out) read as 'find' for the hypotenuse. Judged under spec_gate_v2 on a FRESH panel (new seeds, new phrasings both splits including the class). Same author - disclosed. Earlier notes: v3 = v1 + narrowing (v2: word problems to the head; no false 'I need X' when numbers are present - the full-core sweep found v1 taking 17 core word problems incl. a confident wrong 10.95 for 5) + two reading classes v1 missed (sides stated with a verb, 'sides are a, b and c' - all 8 v1 held-out misses; a hypotenuse value stated after the verb). Judged under spec_gate_v2 on a FRESH panel v2 (new seeds, new phrasings both splits, new declines). Same author - disclosed. Deliberately does NOT read '10 feet across' as a diameter: those core textbook problems use pi = 3.14.",
75
  "supersedes": "geometry-v3 (ADMIT gate v2, 93/100; 7 misses = imperative 'Compute the hypotenuse')"
76
+ }
root/specialists/geometry-v4/organ_geometry_v4.py CHANGED
@@ -1,16 +1,189 @@
1
  #!/usr/bin/env python3
2
  """organ_geometry_v4 - organ_geometry_v3 (admitted 2026-09-29, held-out 93/100) plus imperative verbs: all 7 v3 held-out
3
  misses were "Compute the hypotenuse of a right triangle whose legs are a and b." (v1's reader knew only find / what is /
4
- how long is). Compute / calculate / determine / give / work out now read as "find". Everything else v3."""
 
 
 
 
 
5
  from __future__ import annotations
6
 
7
  import re
 
8
 
9
  from organ_geometry_v3 import GeometryOrganV3
 
10
 
11
  IMPERATIVE = re.compile(r"\b(?:compute|calculate|determine|give|work out|get)\s+(?=the (?:length of the )?hypotenuse\b)", re.I)
12
 
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  class GeometryOrganV4(GeometryOrganV3):
15
  def answer(self, text, session_id="default"):
16
- return super().answer(IMPERATIVE.sub("find ", str(text or "")), session_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  #!/usr/bin/env python3
2
  """organ_geometry_v4 - organ_geometry_v3 (admitted 2026-09-29, held-out 93/100) plus imperative verbs: all 7 v3 held-out
3
  misses were "Compute the hypotenuse of a right triangle whose legs are a and b." (v1's reader knew only find / what is /
4
+ how long is). Compute / calculate / determine / give / work out now read as "find". Everything else v3.
5
+
6
+ This Python dimensional reader is untrusted; it grants no proof or admission authority.
7
+ Explicit same-unit dimensions retain their units, bound rectangles may normalize mixed
8
+ length units, and inherited formulas decline unsupported or inconsistent dimensions.
9
+ """
10
  from __future__ import annotations
11
 
12
  import re
13
+ import math
14
 
15
  from organ_geometry_v3 import GeometryOrganV3
16
+ from organ_geometry_v1 import g
17
 
18
  IMPERATIVE = re.compile(r"\b(?:compute|calculate|determine|give|work out|get)\s+(?=the (?:length of the )?hypotenuse\b)", re.I)
19
 
20
 
21
+ # This organ remains standalone: its dimensional reader must not depend on the
22
+ # head's import path or turn an unsupported binding into a legacy raw product.
23
+ _LENGTHS = {}
24
+ for _unit, _scale, _names in (
25
+ ("mm", .001, "mm millimeter millimeters"), ("cm", .01, "cm centimeter centimeters"),
26
+ ("m", 1., "m meter meters"), ("km", 1000., "km kilometer kilometers"),
27
+ ("in", .0254, "in inch inches"), ("ft", .3048, "ft foot feet"),
28
+ ("yd", .9144, "yd yard yards"), ("mi", 1609.344, "mi mile miles"),
29
+ ):
30
+ for _name in _names.split():
31
+ _LENGTHS[_name] = (_unit, _scale)
32
+ _NUMBER = r"(\d+(?:\.\d+)?)"
33
+ _FIELDS = re.compile(r"\b(length|width|radius|diameter|height|side|edge|base|leg|hypotenuse)\s*"
34
+ r"(?:(?:of|is|equals|exactly|measures|=|:)\s*)*" + _NUMBER, re.I)
35
+ _NUMBERS = re.compile(r"(?<![\w.+-])" + _NUMBER + r"(?!\d|\.\d)")
36
+ _POWERS = {"rect_area": 2, "rect_perimeter": 1, "circle_area": 2, "circumference": 1,
37
+ "sphere_volume": 3, "sphere_surface": 2, "cylinder_volume": 3, "cylinder_surface": 2,
38
+ "cone_volume": 3, "cube_volume": 3, "cube_surface": 2, "box_volume": 3,
39
+ "square_area": 2, "square_perimeter": 1, "tri_area": 2, "heron": 2,
40
+ "hypotenuse": 1, "leg": 1}
41
+
42
+
43
+ def _unit_at(tail, strict=False):
44
+ tail = tail.lstrip()
45
+ if re.match(r"in\s+(?:the|this|a)\s+(?:diagram|drawing|figure)\b", tail):
46
+ return None, True, None
47
+ word = re.match(r"([^\W\d_]+)", tail)
48
+ if not word:
49
+ return None, False, None
50
+ token = word.group(1)
51
+ if token in _LENGTHS:
52
+ rest = tail[word.end():].lstrip()
53
+ if re.match(r"(?:\^|\d|[²³]|/|per\b)", rest):
54
+ return None, False, "These dimensions require length units, not powered or compound units."
55
+ if token == "in" and rest and not re.match(r"[.,;:?!)]|(?:and|long|wide|tall|high)\b", rest):
56
+ return None, False, "I cannot tell whether 'in' denotes inches or a preposition here. Please spell out the length unit."
57
+ return _LENGTHS[token], False, None
58
+ if strict and token not in ("and", "or", "by", "x", "units", "unit", "long", "wide", "high", "tall"):
59
+ return None, False, "I cannot bind the stated length unit '%s'; please use an explicit supported length unit." % token
60
+ return None, False, None
61
+
62
+
63
+ def _dimensions(t):
64
+ fields, positions, units, diagram = {}, set(), [], False
65
+ for match in _FIELDS.finditer(t):
66
+ role, raw = match.group(1), match.group(2)
67
+ unit, drawn, error = _unit_at(t[match.end():], strict=True)
68
+ if error:
69
+ return None, None, False, error
70
+ record = (float(raw), unit)
71
+ if role in fields:
72
+ previous = fields[role]
73
+ if (previous[1] is None) != (unit is None) or previous[0] * (previous[1][1] if previous[1] else 1) != record[0] * (unit[1] if unit else 1):
74
+ return None, None, False, "The supplied %s values contradict each other; please supply one consistent measurement." % role
75
+ fields[role] = record
76
+ positions.add(match.start(2))
77
+ units.append(unit)
78
+ diagram = diagram or drawn
79
+ # A unit or diagram dimension not bound to an explicit role cannot be
80
+ # silently fed to the old first-number reader.
81
+ for number in _NUMBERS.finditer(t):
82
+ unit, drawn, error = _unit_at(t[number.end():])
83
+ if error:
84
+ return None, None, False, error
85
+ if (unit or drawn) and number.start(1) not in positions:
86
+ return None, None, False, "I cannot bind every supplied length to a geometry role; please name each dimension explicitly."
87
+ if (any(units) or diagram) and number.start(1) not in positions:
88
+ # Explicit apartment identifiers are not shape dimensions. Other
89
+ # unused numbers (including a second '5 by 6' tuple) remain ambiguous.
90
+ if not re.search(r"\bapartment\s*$", t[:number.start(1)]):
91
+ return None, None, False, "I cannot account for every number in this dimensional request; please clarify the extra dimensions or constraints."
92
+ return fields, units, diagram, None
93
+
94
+
95
+ def _request_issue(t):
96
+ asking = re.split(r"(?<=[.!?])\s+", t)[-1]
97
+ if len(set(re.findall(r"\b(?:area|perimeter|volume|circumference)\b", asking))) > 1:
98
+ return "I can calculate one geometry measure at a time; please ask for the measures separately."
99
+ if re.search(r"\b(?:rectangles|rooms|circles|spheres|cylinders|cones|cubes|boxes|triangles|squares|cuboids)\b", t) or re.search(r"\b(?:their|both)\b", asking):
100
+ return "I can calculate one shape at a time; this request refers to multiple shapes."
101
+ for requested in re.finditer(r"\b(?:in|into|to)\s+(?:(square|cubic|sq\.?|cu\.?)\s+|(units?\s+of)\s+)?([a-z]+)(?:\^?[23]|[²³])?\b", asking):
102
+ if requested.group(1) or requested.group(2) or requested.group(3) in _LENGTHS:
103
+ return "I cannot yet bind an explicitly requested output unit for this geometry calculation."
104
+ return None
105
+
106
+
107
  class GeometryOrganV4(GeometryOrganV3):
108
  def answer(self, text, session_id="default"):
109
+ t = IMPERATIVE.sub("find ", str(text or ""))
110
+ asking = re.split(r"(?<=[.!?])\s+", t.strip())[-1]
111
+ if (re.match(r"^(?:who|when|where|why|explain|describe|tell me)\b", asking, re.I)
112
+ or re.search(r"\b(?:colou?r|name|history|origin)\b", asking, re.I)):
113
+ return None
114
+ if not re.search(r"\b(?:area|perimeter|volume|circumference|hypotenuse|leg|angles?)\b", asking, re.I):
115
+ return None # shape mentions alone are not requests for a calculation
116
+ low = " ".join(t.lower().replace("−", "-").split())
117
+ issue = _request_issue(low)
118
+ if issue and re.search(r"\b(?:rectangles?|rectangular|circles?|spheres?|cylinders?|cones?|cubes?|squares?|triangles?|box|boxes|cuboids?)\b", low):
119
+ return self._say("underspecified", issue, status="withheld")
120
+ reply = super().answer(t, session_id)
121
+ if not reply or reply.get("status") != "answered" or reply.get("kind") not in _POWERS:
122
+ return reply
123
+ def refuse(message):
124
+ return self._say("underspecified", message, status="withheld")
125
+ if re.search(r"(?<!\w)-\d", low):
126
+ return refuse("A negative length is not a valid dimension for this geometry calculation.")
127
+ fields, units, diagram, issue = _dimensions(low)
128
+ if issue:
129
+ return refuse(issue)
130
+ physical = [u for u in units if u]
131
+ if physical and len(physical) != len(units):
132
+ return refuse("Some dimensions have length units and others do not; please specify the missing units.")
133
+ if "radius" in fields and "diameter" in fields:
134
+ r, ru = fields["radius"]; d, du = fields["diameter"]
135
+ if 2 * r * (ru[1] if ru else 1) != d * (du[1] if du else 1):
136
+ return refuse("The radius and diameter contradict each other: the diameter must be twice the radius.")
137
+ kind = reply["kind"]
138
+ if physical or diagram:
139
+ allowed_roles = {
140
+ "rect_area": {"length", "width"}, "rect_perimeter": {"length", "width"},
141
+ "circle_area": {"radius", "diameter"}, "circumference": {"radius", "diameter"},
142
+ "sphere_volume": {"radius", "diameter"}, "sphere_surface": {"radius", "diameter"},
143
+ "cylinder_volume": {"radius", "diameter", "height"}, "cylinder_surface": {"radius", "diameter", "height"},
144
+ "cone_volume": {"radius", "diameter", "height"},
145
+ "cube_volume": {"side", "edge"}, "cube_surface": {"side", "edge"},
146
+ "box_volume": {"length", "width", "height"},
147
+ "square_area": {"side"}, "square_perimeter": {"side"},
148
+ "tri_area": {"base", "height"}, "hypotenuse": {"leg"}, "leg": {"hypotenuse", "leg"},
149
+ }.get(kind, set())
150
+ if set(fields) - allowed_roles:
151
+ return refuse("This formula does not account for all supplied geometry roles; please clarify the extra dimensions or constraints.")
152
+ if kind.startswith("cube_") and "side" in fields and "edge" in fields:
153
+ side, su = fields["side"]; edge, eu = fields["edge"]
154
+ if side * (su[1] if su else 1) != edge * (eu[1] if eu else 1):
155
+ return refuse("The cube's side and edge values contradict each other; they describe the same length.")
156
+ scales = {u[1] for u in physical}
157
+ power = _POWERS[kind]
158
+ unit = physical[0][0] if physical else None
159
+ value = reply.get("value")
160
+ steps = None
161
+ if kind in ("rect_area", "rect_perimeter") and (physical or diagram):
162
+ if set(fields) != {"length", "width"}:
163
+ return refuse("I need exactly a bound length and width for this rectangle calculation.")
164
+ length, lu = fields["length"]; width, wu = fields["width"]
165
+ if len(scales) > 1:
166
+ length, width = length * lu[1], width * wu[1]
167
+ unit = "m"
168
+ value = length * width if power == 2 else 2 * (length + width)
169
+ steps = ("A = l × w = %g × %g" if power == 2 else "P = 2(l + w) = 2(%g + %g)") % (length, width)
170
+ elif len(scales) > 1:
171
+ return refuse("I cannot combine mixed length units for this geometry formula; please express its dimensions in one unit.")
172
+ # Unitless published formulas retain their original behavior. Explicit
173
+ # diagram dimensions remain useful but must not be presented as inches.
174
+ if not physical and not diagram:
175
+ return reply
176
+ if not math.isfinite(value):
177
+ return refuse("The geometry result is outside the finite numeric range I can return.")
178
+ rounding = re.search(r"round(?:ed)?\s+to\s+(?:the\s+nearest\s+)?(tenth|hundredth|thousandth|whole)\b", low)
179
+ if re.search(r"\bround(?:ed)?\b", low) and not rounding:
180
+ return refuse("I cannot bind that rounding request; please name the nearest tenth, hundredth, thousandth or whole unit.")
181
+ if rounding:
182
+ value = round(value, {"tenth": 1, "hundredth": 2, "thousandth": 3, "whole": 0}[rounding.group(1)])
183
+ label = (unit + ("²" if power == 2 else "³" if power == 3 else "")) if unit else {1: "units", 2: "square units", 3: "cubic units"}[power]
184
+ response = reply["response"]
185
+ if steps is not None:
186
+ response = "The %s is %s %s (%s)." % ("area" if power == 2 else "perimeter", g(value), label, steps)
187
+ else:
188
+ response = re.sub(r"\bis\s+-?\d+(?:\.\d+)?(?:e[+-]?\d+)?", "is %s %s" % (g(value), label), response, count=1)
189
+ return {**reply, "value": value, "unit": unit if power == 1 else ("%s^%d" % (unit, power) if unit else label), "response": response}
root/wander-train/math_organ_v1.py CHANGED
@@ -1,9 +1,11 @@
1
- """math_organ_v1 - exact mathematics as specified free moves, every result independently checked, the method cited.
 
 
2
 
3
  REQUEST directive (solve / simplify / factor / expand / evaluate / differentiate / integrate / limit / decompose)
4
  + the expression(s), read from the question or from its exercise set's instruction
5
  MOVES SymPy operations legal for that directive and that expression's type
6
- CHECK independent of the move that produced the answer:
7
  solve every solution substituted back satisfies every equation / inequality
8
  factor the product expands back to the input, and is a product
9
  simplify equivalent to the input at random points, and no more complex than the input
@@ -15,6 +17,7 @@ STORED FORM the textbook's own method for the directive (a howto or a worked exa
15
  Nothing is asserted unless the check passes; a failed check is an abstention with the reason."""
16
  from __future__ import annotations
17
 
 
18
  import random
19
  import re
20
  import sqlite3
@@ -189,6 +192,23 @@ def rand_pts(syms, k=5):
189
  return [{s: sp.Rational(rng.randint(2, 29), rng.randint(3, 11)) for s in syms} for _ in range(k)]
190
 
191
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  def equiv(a, b):
193
  try:
194
  d = sp.simplify(a - b)
@@ -196,8 +216,7 @@ def equiv(a, b):
196
  return True
197
  syms = sorted((a.free_symbols | b.free_symbols), key=str)
198
  for pt in rand_pts(syms):
199
- va, vb = complex(a.subs(pt).evalf()), complex(b.subs(pt).evalf())
200
- if abs(va - vb) > 1e-7 * max(1.0, abs(va)):
201
  return False
202
  return True
203
  except Exception: # noqa: BLE001
@@ -405,9 +424,9 @@ class MathOrgan:
405
  if e.free_symbols:
406
  return self._simplify(spans, q)
407
  v = sp.nsimplify(sp.simplify(e)) if not e.is_Float else e
408
- if abs(complex(v.evalf()) - complex(e.evalf())) > 1e-9 * max(1, abs(complex(e.evalf()))):
409
  return None
410
- return {"answer": sp.sstr(v), "value": v, "check": "recomputed numerically",
411
  "steps": ["read %s" % sp.sstr(e), "evaluate"]}
412
 
413
  # --- calculus -------------------------------------------------------------------------------------------------
 
1
+ """Untrusted math_organ_v1: SymPy operations with heuristic numerical checks.
2
+
3
+ These checks share the evaluator and are not independent proofs. The method is cited when available.
4
 
5
  REQUEST directive (solve / simplify / factor / expand / evaluate / differentiate / integrate / limit / decompose)
6
  + the expression(s), read from the question or from its exercise set's instruction
7
  MOVES SymPy operations legal for that directive and that expression's type
8
+ CHECK numerical or symbolic checks, not a theorem about the user question:
9
  solve every solution substituted back satisfies every equation / inequality
10
  factor the product expands back to the input, and is a product
11
  simplify equivalent to the input at random points, and no more complex than the input
 
17
  Nothing is asserted unless the check passes; a failed check is an abstention with the reason."""
18
  from __future__ import annotations
19
 
20
+ import math
21
  import random
22
  import re
23
  import sqlite3
 
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 finite_numeric_agreement(a, b, tolerance):
196
+ """Reject NaN/infinity before comparison; finite complex values are allowed.
197
+
198
+ Both evaluations use SymPy. Agreement is a heuristic check, not independent
199
+ verification, and values outside machine complex range are declined.
200
+ """
201
+ try:
202
+ va, vb = complex(a.evalf()), complex(b.evalf())
203
+ if not all(math.isfinite(x) for x in (va.real, va.imag, vb.real, vb.imag)):
204
+ return False
205
+ difference = abs(va - vb)
206
+ bound = tolerance * max(1.0, abs(va), abs(vb))
207
+ return math.isfinite(difference) and math.isfinite(bound) and difference <= bound
208
+ except (TypeError, ValueError, OverflowError):
209
+ return False
210
+
211
+
212
  def equiv(a, b):
213
  try:
214
  d = sp.simplify(a - b)
 
216
  return True
217
  syms = sorted((a.free_symbols | b.free_symbols), key=str)
218
  for pt in rand_pts(syms):
219
+ if not finite_numeric_agreement(a.subs(pt), b.subs(pt), 1e-7):
 
220
  return False
221
  return True
222
  except Exception: # noqa: BLE001
 
424
  if e.free_symbols:
425
  return self._simplify(spans, q)
426
  v = sp.nsimplify(sp.simplify(e)) if not e.is_Float else e
427
+ if not finite_numeric_agreement(v, e, 1e-9):
428
  return None
429
+ return {"answer": sp.sstr(v), "value": v, "check": "finite numerical comparison using SymPy",
430
  "steps": ["read %s" % sp.sstr(e), "evaluate"]}
431
 
432
  # --- calculus -------------------------------------------------------------------------------------------------
root/wander-train/math_organ_v2.py CHANGED
@@ -1,12 +1,13 @@
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
@@ -23,6 +24,7 @@ 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
 
@@ -199,8 +201,7 @@ def equiv(a, b):
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
@@ -412,9 +413,9 @@ class MathOrgan:
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 -------------------------------------------------------------------------------------------------
 
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
+ Untrusted SymPy operations with heuristic numerical checks; the method is cited when available.
5
+ These checks share the evaluator and are not independent proofs.
6
 
7
  REQUEST directive (solve / simplify / factor / expand / evaluate / differentiate / integrate / limit / decompose)
8
  + the expression(s), read from the question or from its exercise set's instruction
9
  MOVES SymPy operations legal for that directive and that expression's type
10
+ CHECK numerical or symbolic checks, not a theorem about the user question:
11
  solve every solution substituted back satisfies every equation / inequality
12
  factor the product expands back to the input, and is a product
13
  simplify equivalent to the input at random points, and no more complex than the input
 
24
  import sqlite3
25
 
26
  import sympy as sp
27
+ from math_organ_v1 import finite_numeric_agreement
28
  from sympy.parsing.sympy_parser import (convert_xor, implicit_multiplication_application, parse_expr,
29
  split_symbols_custom, standard_transformations)
30
 
 
201
  return True
202
  syms = sorted((a.free_symbols | b.free_symbols), key=str)
203
  for pt in rand_pts(syms):
204
+ if not finite_numeric_agreement(a.subs(pt), b.subs(pt), 1e-7):
 
205
  return False
206
  return True
207
  except Exception: # noqa: BLE001
 
413
  if e.free_symbols:
414
  return self._simplify(spans, q)
415
  v = sp.nsimplify(sp.simplify(e)) if not e.is_Float else e
416
+ if not finite_numeric_agreement(v, e, 1e-9):
417
  return None
418
+ return {"answer": sp.sstr(v), "value": v, "check": "finite numerical comparison using SymPy",
419
  "steps": ["read %s" % sp.sstr(e), "evaluate"]}
420
 
421
  # --- calculus -------------------------------------------------------------------------------------------------
root/wander-train/math_organ_v3.py CHANGED
@@ -3,12 +3,13 @@ variables (found by the proposer brief: "On what date in 1969 did ..." -> 1969*d
3
  math_organ_v2 - v1 (admitted in w480, now immutable) + two fixes found on the gate panel: "factorial" is not a
4
  request to factor, and a request for a PROGRAM is the code organ's, not a mathematical request.
5
 
6
- exact mathematics as specified free moves, every result independently checked, the method cited.
 
7
 
8
  REQUEST directive (solve / simplify / factor / expand / evaluate / differentiate / integrate / limit / decompose)
9
  + the expression(s), read from the question or from its exercise set's instruction
10
  MOVES SymPy operations legal for that directive and that expression's type
11
- CHECK independent of the move that produced the answer:
12
  solve every solution substituted back satisfies every equation / inequality
13
  factor the product expands back to the input, and is a product
14
  simplify equivalent to the input at random points, and no more complex than the input
@@ -25,6 +26,7 @@ import re
25
  import sqlite3
26
 
27
  import sympy as sp
 
28
  from sympy.parsing.sympy_parser import (convert_xor, implicit_multiplication_application, parse_expr,
29
  split_symbols_custom, standard_transformations)
30
 
@@ -207,8 +209,7 @@ def equiv(a, b):
207
  return True
208
  syms = sorted((a.free_symbols | b.free_symbols), key=str)
209
  for pt in rand_pts(syms):
210
- va, vb = complex(a.subs(pt).evalf()), complex(b.subs(pt).evalf())
211
- if abs(va - vb) > 1e-7 * max(1.0, abs(va)):
212
  return False
213
  return True
214
  except Exception: # noqa: BLE001
@@ -420,9 +421,9 @@ class MathOrgan:
420
  if e.free_symbols:
421
  return self._simplify(spans, q)
422
  v = sp.nsimplify(sp.simplify(e)) if not e.is_Float else e
423
- if abs(complex(v.evalf()) - complex(e.evalf())) > 1e-9 * max(1, abs(complex(e.evalf()))):
424
  return None
425
- return {"answer": sp.sstr(v), "value": v, "check": "recomputed numerically",
426
  "steps": ["read %s" % sp.sstr(e), "evaluate"]}
427
 
428
  # --- calculus -------------------------------------------------------------------------------------------------
 
3
  math_organ_v2 - v1 (admitted in w480, now immutable) + two fixes found on the gate panel: "factorial" is not a
4
  request to factor, and a request for a PROGRAM is the code organ's, not a mathematical request.
5
 
6
+ Untrusted SymPy operations with heuristic numerical checks; the method is cited when available.
7
+ These checks share the evaluator and are not independent proofs.
8
 
9
  REQUEST directive (solve / simplify / factor / expand / evaluate / differentiate / integrate / limit / decompose)
10
  + the expression(s), read from the question or from its exercise set's instruction
11
  MOVES SymPy operations legal for that directive and that expression's type
12
+ CHECK numerical or symbolic checks, not a theorem about the user question:
13
  solve every solution substituted back satisfies every equation / inequality
14
  factor the product expands back to the input, and is a product
15
  simplify equivalent to the input at random points, and no more complex than the input
 
26
  import sqlite3
27
 
28
  import sympy as sp
29
+ from math_organ_v1 import finite_numeric_agreement
30
  from sympy.parsing.sympy_parser import (convert_xor, implicit_multiplication_application, parse_expr,
31
  split_symbols_custom, standard_transformations)
32
 
 
209
  return True
210
  syms = sorted((a.free_symbols | b.free_symbols), key=str)
211
  for pt in rand_pts(syms):
212
+ if not finite_numeric_agreement(a.subs(pt), b.subs(pt), 1e-7):
 
213
  return False
214
  return True
215
  except Exception: # noqa: BLE001
 
421
  if e.free_symbols:
422
  return self._simplify(spans, q)
423
  v = sp.nsimplify(sp.simplify(e)) if not e.is_Float else e
424
+ if not finite_numeric_agreement(v, e, 1e-9):
425
  return None
426
+ return {"answer": sp.sstr(v), "value": v, "check": "finite numerical comparison using SymPy",
427
  "steps": ["read %s" % sp.sstr(e), "evaluate"]}
428
 
429
  # --- calculus -------------------------------------------------------------------------------------------------
root/wander-train/oracle_operator_specs_v1.py CHANGED
@@ -198,33 +198,63 @@ def _gcd_ref(a):
198
 
199
 
200
  # ── 3. base conversion ────────────────────────────────────────────────────────
201
- # fn is repeated divmod; ref is positional expansion of the digits it produced.
202
- _BASE_RX = re.compile(
203
- r'\b(?:convert|write|express)?\s*(-?\d+)\s*(?:from\s*)?(?:base\s*(\d+))?\s*'
204
- r'(?:in|to|into)\s*(?:base\s*)?(\d+|binary|hex|hexadecimal|octal|decimal)\b', re.I)
205
  _BASE_NAME = {'binary': 2, 'hex': 16, 'hexadecimal': 16, 'octal': 8, 'decimal': 10}
 
 
 
 
 
 
 
 
206
  _DIGITS = '0123456789abcdefghijklmnopqrstuvwxyz'
207
 
208
 
209
  def _base_parse(s):
210
- m = _BASE_RX.search(s)
211
  if not m:
212
- raise NotOurs('no number and target base here')
213
- n = int(m.group(1))
214
- frm = int(m.group(2)) if m.group(2) else 10
215
- tgt = m.group(3).lower()
216
- to = _BASE_NAME.get(tgt, None)
217
- if to is None:
218
- to = int(tgt)
219
- if not (2 <= to <= 36) or not (2 <= frm <= 36):
220
- raise OutOfFragment('I convert between base 2 and base 36 only')
221
- if n < 0:
 
 
 
 
 
 
 
222
  raise OutOfFragment('I convert non-negative whole numbers only')
223
- return {'n': n, 'frm': frm, 'to': to}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
 
225
 
226
  def _base_fn(a):
227
- n, steps, out = a['n'], [], ''
228
  if n == 0:
229
  return '0', ['0 in any base is 0']
230
  while n:
@@ -235,8 +265,13 @@ def _base_fn(a):
235
 
236
 
237
  def _base_ref(a):
238
- """Independent: build with a positional table, then check by expanding it."""
239
- n = a['n']
 
 
 
 
 
240
  if n == 0:
241
  return '0'
242
  digits, powers = [], []
@@ -418,8 +453,9 @@ SPECS = [
418
  'parse': _base_parse, 'fn': _base_fn, 'ref': _base_ref,
419
  'render': lambda a, v, s: '%d in base %d is %s.' % (a['n'], a['to'], v),
420
  'warrant': 'computation',
421
- 'independence': 'moderate: repeated division against positional expansion; '
422
- 'both walk the same digits, in opposite directions',
 
423
  'fragment': 'non-negative whole numbers, bases 2 to 36'},
424
 
425
  {'name': 'statistics', 'family': 'measurement',
 
198
 
199
 
200
  # ── 3. base conversion ────────────────────────────────────────────────────────
201
+ # The parser binds the original digits and source base. The function decodes
202
+ # with int(); the reference independently expands source digits positionally.
 
 
203
  _BASE_NAME = {'binary': 2, 'hex': 16, 'hexadecimal': 16, 'octal': 8, 'decimal': 10}
204
+ _BASE_ATOM = r'(?:base\s*\d+|binary|hexadecimal|hex|octal|decimal)'
205
+ _BASE_RX = re.compile(
206
+ r'^\s*(?:(?:convert|write|express|what\s+is)\s+)?'
207
+ r'(?:(?P<prefix>' + _BASE_ATOM + r')\s+)?'
208
+ r'(?P<digits>[+-]?[0-9a-z]+)'
209
+ r'(?:\s+(?:(?:from|in)\s+)?(?P<source>' + _BASE_ATOM + r'))?'
210
+ r'\s+(?:in|to|into|as)\s+(?P<target>' + _BASE_ATOM + r'|\d+)'
211
+ r'\s*[.?!]?\s*$', re.I | re.ASCII)
212
  _DIGITS = '0123456789abcdefghijklmnopqrstuvwxyz'
213
 
214
 
215
  def _base_parse(s):
216
+ m = _BASE_RX.fullmatch(s)
217
  if not m:
218
+ raise NotOurs('no complete integer base conversion here')
219
+
220
+ def base(token):
221
+ token = token.lower()
222
+ value = _BASE_NAME.get(token)
223
+ if value is None:
224
+ numeral = re.sub(r'^base\s*', '', token)
225
+ if len(numeral) > 3:
226
+ raise OutOfFragment('I convert between base 2 and base 36 only')
227
+ value = int(numeral)
228
+ if not 2 <= value <= 36:
229
+ raise OutOfFragment('I convert between base 2 and base 36 only')
230
+ return value
231
+
232
+ sources = [base(token) for token in (m['prefix'], m['source']) if token]
233
+ raw = m['digits'].lower()
234
+ if raw.startswith('-'):
235
  raise OutOfFragment('I convert non-negative whole numbers only')
236
+ raw = raw.lstrip('+')
237
+ # A conventional literal prefix is source evidence, not a digit to discard.
238
+ # Explicit bases above 16 may use x as a digit; only infer prefixes when no
239
+ # explicit source is supplied, or that source matches the prefix convention.
240
+ literal = {'0b': 2, '0o': 8, '0x': 16}.get(raw[:2])
241
+ if literal is not None and (not sources or all(b == literal for b in sources)):
242
+ sources.append(literal)
243
+ raw = raw[2:]
244
+ elif literal is not None and sources and sources[0] in (2, 8, 10, 16):
245
+ raise OutOfFragment('the literal prefix conflicts with the stated source base')
246
+ if len(set(sources)) > 1:
247
+ raise OutOfFragment('the stated source bases disagree')
248
+ frm = sources[0] if sources else 10
249
+ to = base(m['target'])
250
+ if not raw or len(raw) > 128 or any(_DIGITS.index(c) >= frm for c in raw):
251
+ raise OutOfFragment('I need 1 to 128 valid digits in the stated source base')
252
+ n = int(raw, frm)
253
+ return {'n': n, 'digits': raw, 'frm': frm, 'to': to}
254
 
255
 
256
  def _base_fn(a):
257
+ n, steps, out = int(a['digits'], a['frm']), [], ''
258
  if n == 0:
259
  return '0', ['0 in any base is 0']
260
  while n:
 
265
 
266
 
267
  def _base_ref(a):
268
+ """Expand original source digits, then encode using a positional table.
269
+
270
+ This does not share int() source decoding or repeated output division with
271
+ the primary function. Both still depend on the same untrusted parse.
272
+ """
273
+ n = sum(_DIGITS.index(digit) * a['frm'] ** power
274
+ for power, digit in enumerate(reversed(a['digits'])))
275
  if n == 0:
276
  return '0'
277
  digits, powers = [], []
 
453
  'parse': _base_parse, 'fn': _base_fn, 'ref': _base_ref,
454
  'render': lambda a, v, s: '%d in base %d is %s.' % (a['n'], a['to'], v),
455
  'warrant': 'computation',
456
+ 'independence': 'moderate: int source decoding and repeated output division '
457
+ 'against positional source expansion and output encoding; '
458
+ 'both still depend on the same untrusted parse',
459
  'fragment': 'non-negative whole numbers, bases 2 to 36'},
460
 
461
  {'name': 'statistics', 'family': 'measurement',
root/wander-train/oracle_release_runtime_ao.py CHANGED
@@ -22,6 +22,16 @@ import re
22
 
23
  from oracle_release_runtime_an import OracleReleaseRuntimeAN
24
 
 
 
 
 
 
 
 
 
 
 
25
  DEICTIC = re.compile(r"\b(it|there|that|this|its|they|them|those|these|the same)\b", re.I)
26
  ELLIPSIS = re.compile(r"^(?:and|what about|the same for)\s+(?P<t>.+?)\??$", re.I)
27
  BARE = re.compile(r"^(?:the\s+)?[a-z][a-z \-']{2,40}$", re.I)
@@ -41,6 +51,17 @@ PREDICATE_OF = {
41
  }
42
 
43
 
 
 
 
 
 
 
 
 
 
 
 
44
  class OracleReleaseRuntimeAO(OracleReleaseRuntimeAN):
45
  """Conversions are applied ONLY when the unchanged chain would not answer."""
46
 
@@ -103,10 +124,11 @@ class OracleReleaseRuntimeAO(OracleReleaseRuntimeAN):
103
  if not CONTINUATION.match(s):
104
  return f"Summarize {s.strip()}.", "ELLIPSIS_COMPLETION"
105
  topic = self._live_topic(session_id)
106
- if topic and DEICTIC.search(s):
 
107
  if low in PREDICATE_OF:
108
  return PREDICATE_OF[low].format(t=topic), "REFERENT_SUBSTITUTION"
109
- return DEICTIC.sub(topic, s, count=1), "REFERENT_SUBSTITUTION"
110
  return None, None
111
 
112
  def chat(self, text: str, session_id: str = "default") -> dict:
 
22
 
23
  from oracle_release_runtime_an import OracleReleaseRuntimeAN
24
 
25
+ # Untrusted grammar guard: in "does it take/cost ... to <action>", it
26
+ # may be a grammatical subject rather than an entity from the conversation.
27
+ # Decline substitution for that token; other references remain eligible. The
28
+ # match stays within a clause and accepts decimal amounts before the infinitive.
29
+ DUMMY_IT = re.compile(
30
+ r"\b(?:does|did|will|would|can|could|should|has|had)\s+(?P<it>it)\s+"
31
+ r"(?:take|taken|cost)\s+"
32
+ r"(?:(?:\d+(?:\.\d+)?\s+(?:milliseconds?|seconds?|minutes?|hours?|days?|weeks?|"
33
+ r"months?|years?|dollars?|euros?|pounds?)|[$€£]\d+(?:\.\d+)?)\s+)?"
34
+ r"to\s+[a-z]", re.I)
35
  DEICTIC = re.compile(r"\b(it|there|that|this|its|they|them|those|these|the same)\b", re.I)
36
  ELLIPSIS = re.compile(r"^(?:and|what about|the same for)\s+(?P<t>.+?)\??$", re.I)
37
  BARE = re.compile(r"^(?:the\s+)?[a-z][a-z \-']{2,40}$", re.I)
 
51
  }
52
 
53
 
54
+ def referential_deictic(text):
55
+ """First eligible reference, excluding only take/cost-infinitive dummy it.
56
+
57
+ This does not claim to parse all English expletives. Skipping an ambiguous
58
+ token leaves the original question available to the unchanged handler.
59
+ """
60
+ dummy_spans = {match.span("it") for match in DUMMY_IT.finditer(text)}
61
+ return next((match for match in DEICTIC.finditer(text)
62
+ if match.span() not in dummy_spans), None)
63
+
64
+
65
  class OracleReleaseRuntimeAO(OracleReleaseRuntimeAN):
66
  """Conversions are applied ONLY when the unchanged chain would not answer."""
67
 
 
124
  if not CONTINUATION.match(s):
125
  return f"Summarize {s.strip()}.", "ELLIPSIS_COMPLETION"
126
  topic = self._live_topic(session_id)
127
+ reference = referential_deictic(s)
128
+ if topic and reference:
129
  if low in PREDICATE_OF:
130
  return PREDICATE_OF[low].format(t=topic), "REFERENT_SUBSTITUTION"
131
+ return s[:reference.start()] + topic + s[reference.end():], "REFERENT_SUBSTITUTION"
132
  return None, None
133
 
134
  def chat(self, text: str, session_id: str = "default") -> dict:
root/wander-train/reach_router_v1.py CHANGED
@@ -17,6 +17,7 @@ identifiers, links and media are never given unless asked for; quantities only w
17
  quantities without their units). At most 5 stored values, listed; more is declined.
18
  Refused as in exact_router_v1: yes/no questions, a hidden hop, a quoted title that is not the span, a past-time qualifier on
19
  a relation that changes over time, blank nodes, an answer that restates the question.
 
20
  use: answer(question) -> {"answer", "values", "property", "direction", "subject_qid", "route", "pattern"} or None"""
21
  import json
22
  import os
@@ -138,7 +139,7 @@ def fragment(q, span, p):
138
  j += step
139
  if 0 <= j < len(toks):
140
  w = toks[j]
141
- if w not in L.STOP and w not in cues and not w.isdigit() and len(w) >= 3:
142
  return True
143
  return False
144
  return False
@@ -368,8 +369,9 @@ def answer(q, pinned=None):
368
  return None # a value with no English name is not shown
369
  shown = sorted(shown)
370
  aw = [w for x in shown for w in L.norm(x).split() if w not in L.STOP]
371
- if aw and all(w in set(L.norm(q).split()) for w in aw):
372
- return None # the answer restates the question
 
373
  text = shown[0] if len(shown) == 1 else ", ".join(shown[:-1]) + " and " + shown[-1]
374
  return {"answer": text, "values": shown, "raw": sorted(v, key=str), "property": p, "direction": d, "subject_qid": subj,
375
  "route": route, "pattern": pat}
 
17
  quantities without their units). At most 5 stored values, listed; more is declined.
18
  Refused as in exact_router_v1: yes/no questions, a hidden hop, a quoted title that is not the span, a past-time qualifier on
19
  a relation that changes over time, blank nodes, an answer that restates the question.
20
+ Untrusted routing policy: stored rows and type ancestry are evidence, not proof of source truth or completeness.
21
  use: answer(question) -> {"answer", "values", "property", "direction", "subject_qid", "route", "pattern"} or None"""
22
  import json
23
  import os
 
139
  j += step
140
  if 0 <= j < len(toks):
141
  w = toks[j]
142
+ if w not in L.STOP and w not in cues and not w.isdigit():
143
  return True
144
  return False
145
  return False
 
369
  return None # a value with no English name is not shown
370
  shown = sorted(shown)
371
  aw = [w for x in shown for w in L.norm(x).split() if w not in L.STOP]
372
+ if subj in v or (not all(isinstance(x, int) for x in v)
373
+ and aw and all(w in set(L.norm(q).split()) for w in aw)):
374
+ return None # entity identity, not overlapping labels, is restatement
375
  text = shown[0] if len(shown) == 1 else ", ".join(shown[:-1]) + " and " + shown[-1]
376
  return {"answer": text, "values": shown, "raw": sorted(v, key=str), "property": p, "direction": d, "subject_qid": subj,
377
  "route": route, "pattern": pat}
root/wander-train/reach_router_v2.py CHANGED
@@ -23,6 +23,7 @@ identifiers, links and media are never given unless asked for; quantities only w
23
  quantities without their units). At most 5 stored values, listed; more is declined.
24
  Refused as in exact_router_v1: yes/no questions, a hidden hop, a quoted title that is not the span, a past-time qualifier on
25
  a relation that changes over time, blank nodes, an answer that restates the question.
 
26
  use: answer(question) -> {"answer", "values", "property", "direction", "subject_qid", "route", "pattern"} or None"""
27
  import json
28
  import os
@@ -150,7 +151,7 @@ def fragment(q, span, p):
150
  j += step
151
  if 0 <= j < len(toks):
152
  w = toks[j]
153
- if w not in L.STOP and w not in cues and not w.isdigit() and len(w) >= 3:
154
  return True
155
  return False
156
  return False
@@ -409,8 +410,9 @@ def answer(q, pinned=None):
409
  return None # a value with no English name is not shown
410
  shown = sorted(shown)
411
  aw = [w for x in shown for w in L.norm(x).split() if w not in L.STOP]
412
- if aw and all(w in set(L.norm(q).split()) for w in aw):
413
- return None # the answer restates the question
 
414
  text = shown[0] if len(shown) == 1 else ", ".join(shown[:-1]) + " and " + shown[-1]
415
  return {"answer": text, "values": shown, "raw": sorted(v, key=str), "property": p, "direction": d, "subject_qid": subj,
416
  "route": route, "pattern": pat}
 
23
  quantities without their units). At most 5 stored values, listed; more is declined.
24
  Refused as in exact_router_v1: yes/no questions, a hidden hop, a quoted title that is not the span, a past-time qualifier on
25
  a relation that changes over time, blank nodes, an answer that restates the question.
26
+ Untrusted routing policy: stored rows and type ancestry are evidence, not proof of source truth or completeness.
27
  use: answer(question) -> {"answer", "values", "property", "direction", "subject_qid", "route", "pattern"} or None"""
28
  import json
29
  import os
 
151
  j += step
152
  if 0 <= j < len(toks):
153
  w = toks[j]
154
+ if w not in L.STOP and w not in cues and not w.isdigit():
155
  return True
156
  return False
157
  return False
 
410
  return None # a value with no English name is not shown
411
  shown = sorted(shown)
412
  aw = [w for x in shown for w in L.norm(x).split() if w not in L.STOP]
413
+ if subj in v or (not all(isinstance(x, int) for x in v)
414
+ and aw and all(w in set(L.norm(q).split()) for w in aw)):
415
+ return None # entity identity, not overlapping labels, is restatement
416
  text = shown[0] if len(shown) == 1 else ", ".join(shown[:-1]) + " and " + shown[-1]
417
  return {"answer": text, "values": shown, "raw": sorted(v, key=str), "property": p, "direction": d, "subject_qid": subj,
418
  "route": route, "pattern": pat}
root/wander-train/reach_router_v3.py CHANGED
@@ -27,6 +27,7 @@ identifiers, links and media are never given unless asked for; quantities only w
27
  quantities without their units). At most 5 stored values, listed; more is declined.
28
  Refused as in exact_router_v1: yes/no questions, a hidden hop, a quoted title that is not the span, a past-time qualifier on
29
  a relation that changes over time, blank nodes, an answer that restates the question.
 
30
  use: answer(question) -> {"answer", "values", "property", "direction", "subject_qid", "route", "pattern"} or None"""
31
  import json
32
  import os
@@ -144,9 +145,9 @@ def fits(q, p, d, vals):
144
  return True
145
 
146
 
147
- def fragment(q, span, p):
148
  """the span is part of a longer name: a word next to it (past name particles) is a content word nothing explains"""
149
- cues = _load()["cues"].get(p, set())
150
  toks, sp = L.norm(q).split(), L.norm(span).split()
151
  for i in range(len(toks) - len(sp) + 1):
152
  if toks[i:i + len(sp)] == sp:
@@ -155,7 +156,7 @@ def fragment(q, span, p):
155
  j += step
156
  if 0 <= j < len(toks):
157
  w = toks[j]
158
- if w not in L.STOP and w not in cues and not w.isdigit() and len(w) >= 3:
159
  return True
160
  return False
161
  return False
@@ -167,17 +168,17 @@ GENERIC = set(("name names called known located famous kind type sort also exact
167
  MAX_UNEXPLAINED = int(os.environ.get("REACH_MAX_UNEXPLAINED", "2")) # development: 2 stopped a Mintaka error, lost nothing
168
 
169
 
170
- def unexplained(q, span, p):
171
  """content words of the question that neither the subject, the relation's own words nor ordinary question words explain"""
172
- cues = _load()["cues"].get(p, set())
173
  sp = set(L.norm(span).split())
174
  return [w for w in L.norm(q).split() if w not in sp and w not in cues and w not in GENERIC and w not in L.STOP
175
  and not w.isdigit() and len(w) >= 3]
176
 
177
 
178
- def _agree(q, span, cands, p, d):
179
  """the readings of the name that hold the relation must agree; a one-word name answers through its top reading only"""
180
- if fragment(q, span, p) or len(unexplained(q, span, p)) > MAX_UNEXPLAINED:
181
  return None, None
182
  one = len(L.norm(span).split()) == 1
183
  use = cands[:1] if one else cands[:3]
@@ -310,6 +311,10 @@ def _agent_routes(q):
310
  return []
311
  al = _load()["alias"]
312
  v = m.group("v").lower()
 
 
 
 
313
  passive, agents = (IRREG[v][0], {IRREG[v][1]}) if v in IRREG else (v, _agent_nouns(v))
314
  props = set(al.get(passive + " by", set()))
315
  for a in agents:
@@ -317,6 +322,43 @@ def _agent_routes(q):
317
  return [(props, m.group("y"), "agent:" + v)] if props else []
318
 
319
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
  def _label_routes(q):
321
  """the question names the relation: (relation, subject text, the asked phrase)"""
322
  al = _load()["alias"]
@@ -408,14 +450,18 @@ def answer(q, pinned=None):
408
  sp = L.link(q, 3)[:3]
409
  if pinned:
410
  sp = [(pinned[0], [(pinned[1], 10 ** 6, label(pinned[1]))])] + [x for x in sp if L.norm(x[0]) != L.norm(pinned[0])]
 
 
411
  found = []
412
  for span, cands in sp: # PATTERN route: the first span whose reading holds the relation
413
  if XR.hop(q, span) or not XR.subject_ok(q, span):
414
  continue
415
  for (p, d), route, pat in _routes(q, span, cands):
 
 
416
  if p in XR.TIME_VARIANT and XR.PAST.search(q):
417
  continue
418
- v, subj = _agree(q, span, cands, p, d)
419
  if v is not None and fits(q, p, d, v):
420
  found.append((p, d, v, subj, route, pat))
421
  break
@@ -429,9 +475,11 @@ def answer(q, pinned=None):
429
  span, cands = ysp[0]
430
  hits = []
431
  for p in sorted(props):
 
 
432
  if p in XR.TIME_VARIANT and XR.PAST.search(q):
433
  continue
434
- v, subj = _agree(q, span, cands, p, "fwd")
435
  if v is not None and fits(q, p, "fwd", v):
436
  hits.append((p, "fwd", v, subj, "when" if asked.startswith("when:") else "count" if asked.startswith("count:") else "agent" if asked.startswith("agent:") else "label", asked))
437
  if len({h[2] for h in hits}) == 1:
@@ -445,8 +493,9 @@ def answer(q, pinned=None):
445
  return None # a value with no English name is not shown
446
  shown = sorted(shown)
447
  aw = [w for x in shown for w in L.norm(x).split() if w not in L.STOP]
448
- if aw and all(w in set(L.norm(q).split()) for w in aw):
449
- return None # the answer restates the question
 
450
  text = shown[0] if len(shown) == 1 else ", ".join(shown[:-1]) + " and " + shown[-1]
451
  return {"answer": text, "values": shown, "raw": sorted(v, key=str), "property": p, "direction": d, "subject_qid": subj,
452
  "route": route, "pattern": pat}
 
27
  quantities without their units). At most 5 stored values, listed; more is declined.
28
  Refused as in exact_router_v1: yes/no questions, a hidden hop, a quoted title that is not the span, a past-time qualifier on
29
  a relation that changes over time, blank nodes, an answer that restates the question.
30
+ Untrusted routing policy: stored rows and type ancestry are evidence, not proof of source truth or completeness.
31
  use: answer(question) -> {"answer", "values", "property", "direction", "subject_qid", "route", "pattern"} or None"""
32
  import json
33
  import os
 
145
  return True
146
 
147
 
148
+ def fragment(q, span, p, extra_cues=()):
149
  """the span is part of a longer name: a word next to it (past name particles) is a content word nothing explains"""
150
+ cues = _load()["cues"].get(p, set()) | set(extra_cues)
151
  toks, sp = L.norm(q).split(), L.norm(span).split()
152
  for i in range(len(toks) - len(sp) + 1):
153
  if toks[i:i + len(sp)] == sp:
 
156
  j += step
157
  if 0 <= j < len(toks):
158
  w = toks[j]
159
+ if w not in L.STOP and w not in cues and not w.isdigit():
160
  return True
161
  return False
162
  return False
 
168
  MAX_UNEXPLAINED = int(os.environ.get("REACH_MAX_UNEXPLAINED", "2")) # development: 2 stopped a Mintaka error, lost nothing
169
 
170
 
171
+ def unexplained(q, span, p, extra_cues=()):
172
  """content words of the question that neither the subject, the relation's own words nor ordinary question words explain"""
173
+ cues = _load()["cues"].get(p, set()) | set(extra_cues)
174
  sp = set(L.norm(span).split())
175
  return [w for w in L.norm(q).split() if w not in sp and w not in cues and w not in GENERIC and w not in L.STOP
176
  and not w.isdigit() and len(w) >= 3]
177
 
178
 
179
+ def _agree(q, span, cands, p, d, extra_cues=()):
180
  """the readings of the name that hold the relation must agree; a one-word name answers through its top reading only"""
181
+ if fragment(q, span, p, extra_cues) or len(unexplained(q, span, p, extra_cues)) > MAX_UNEXPLAINED:
182
  return None, None
183
  one = len(L.norm(span).split()) == 1
184
  use = cands[:1] if one else cands[:3]
 
311
  return []
312
  al = _load()["alias"]
313
  v = m.group("v").lower()
314
+ if v == "created":
315
+ # A developer can be a later maintainer, not the original creator.
316
+ # Designer is admitted only with programming-language type evidence below.
317
+ return [({170, 287}, m.group("y"), "agent:created")]
318
  passive, agents = (IRREG[v][0], {IRREG[v][1]}) if v in IRREG else (v, _agent_nouns(v))
319
  props = set(al.get(passive + " by", set()))
320
  for a in agents:
 
322
  return [(props, m.group("y"), "agent:" + v)] if props else []
323
 
324
 
325
+ def _programming_language(qid):
326
+ """Positive bounded P31/P279 evidence only; missing ancestry is not a negative fact."""
327
+ db = _load()["db"]
328
+ frontier, seen = {qid}, set()
329
+ for depth in range(5):
330
+ if 9143 in frontier and depth > 0:
331
+ return True
332
+ next_frontier = set()
333
+ for item in frontier - seen:
334
+ rows = db.execute("SELECT o FROM fact INDEXED BY fact_sp WHERE s=? AND p=? LIMIT 33",
335
+ (item, 31 if depth == 0 else 279)).fetchall()
336
+ if len(rows) > 32:
337
+ return False
338
+ next_frontier.update(o for (o,) in rows if o is not None)
339
+ seen.update(frontier)
340
+ if len(seen | next_frontier) > 64:
341
+ return False
342
+ frontier = next_frontier - seen
343
+ if not frontier:
344
+ return False
345
+ return False
346
+
347
+
348
+ def _creation_property_allowed(p, cands):
349
+ """Creator means P170; a language's original designer may also answer creation.
350
+
351
+ Every candidate with a designer row must have positive language ancestry.
352
+ Never select a language merely because another reading lacks that type.
353
+ """
354
+ if p == 170:
355
+ return True
356
+ if p != 287:
357
+ return False
358
+ holders = [qid for qid, _, _ in cands[:3] if has(qid, p)]
359
+ return bool(holders) and all(_programming_language(qid) for qid in holders)
360
+
361
+
362
  def _label_routes(q):
363
  """the question names the relation: (relation, subject text, the asked phrase)"""
364
  al = _load()["alias"]
 
450
  sp = L.link(q, 3)[:3]
451
  if pinned:
452
  sp = [(pinned[0], [(pinned[1], 10 ** 6, label(pinned[1]))])] + [x for x in sp if L.norm(x[0]) != L.norm(pinned[0])]
453
+ agent = AGENT.match(q)
454
+ asks_creation = bool(agent and agent.group("v").lower() == "created")
455
  found = []
456
  for span, cands in sp: # PATTERN route: the first span whose reading holds the relation
457
  if XR.hop(q, span) or not XR.subject_ok(q, span):
458
  continue
459
  for (p, d), route, pat in _routes(q, span, cands):
460
+ if asks_creation and (d != "fwd" or not _creation_property_allowed(p, cands)):
461
+ continue
462
  if p in XR.TIME_VARIANT and XR.PAST.search(q):
463
  continue
464
+ v, subj = _agree(q, span, cands, p, d, {"created"} if asks_creation and p == 287 else ())
465
  if v is not None and fits(q, p, d, v):
466
  found.append((p, d, v, subj, route, pat))
467
  break
 
475
  span, cands = ysp[0]
476
  hits = []
477
  for p in sorted(props):
478
+ if asks_creation and not _creation_property_allowed(p, cands):
479
+ continue
480
  if p in XR.TIME_VARIANT and XR.PAST.search(q):
481
  continue
482
+ v, subj = _agree(q, span, cands, p, "fwd", {"created"} if asks_creation and p == 287 else ())
483
  if v is not None and fits(q, p, "fwd", v):
484
  hits.append((p, "fwd", v, subj, "when" if asked.startswith("when:") else "count" if asked.startswith("count:") else "agent" if asked.startswith("agent:") else "label", asked))
485
  if len({h[2] for h in hits}) == 1:
 
493
  return None # a value with no English name is not shown
494
  shown = sorted(shown)
495
  aw = [w for x in shown for w in L.norm(x).split() if w not in L.STOP]
496
+ if subj in v or (not all(isinstance(x, int) for x in v)
497
+ and aw and all(w in set(L.norm(q).split()) for w in aw)):
498
+ return None # entity identity, not overlapping labels, is restatement
499
  text = shown[0] if len(shown) == 1 else ", ".join(shown[:-1]) + " and " + shown[-1]
500
  return {"answer": text, "values": shown, "raw": sorted(v, key=str), "property": p, "direction": d, "subject_qid": subj,
501
  "route": route, "pattern": pat}
root/wander-train/relation_spec_v1.py CHANGED
@@ -11,6 +11,7 @@ A question is linked ONCE per turn however many relation specialists look at it
11
  import collections
12
  import functools
13
  import json
 
14
 
15
  import exact_router_v1 as X
16
 
@@ -21,6 +22,11 @@ _anc, _cls, _props = {}, {}, {}
21
 
22
 
23
  def prop_label(p):
 
 
 
 
 
24
  if not _PL:
25
  try:
26
  with open(PROP_LABELS, encoding="utf-8") as f:
@@ -128,8 +134,64 @@ def kind_conflict(q, did):
128
  return bool(named) and did in KIND_NOUNS and did not in named
129
 
130
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  def answer(q, did, patterns):
132
  """patterns: {"fwd|key|word": [p, ...], "inv|key|word": [p, ...]} -> {answer, property, subject_qid, direction, pattern} or None"""
 
 
 
 
 
133
  a = analyse(str(q))
134
  if a is None or kind_conflict(str(q), did):
135
  return None
 
11
  import collections
12
  import functools
13
  import json
14
+ import re
15
 
16
  import exact_router_v1 as X
17
 
 
22
 
23
 
24
  def prop_label(p):
25
+ # The shipped label dump includes aliases ("wives", "written by", "wrote")
26
+ # for these properties. Those aliases can misstate the role in a receipt.
27
+ canonical = {26: "spouse", 86: "composer", 800: "notable work"}
28
+ if p in canonical:
29
+ return canonical[p]
30
  if not _PL:
31
  try:
32
  with open(PROP_LABELS, encoding="utf-8") as f:
 
134
  return bool(named) and did in KIND_NOUNS and did not in named
135
 
136
 
137
+ _COMPOSER_REQUEST = re.compile(
138
+ r"^(?:who composed(?: the (?:music|score|soundtrack) (?:for|of|to))?"
139
+ r"|who (?:is|was) the composer of"
140
+ r"|who wrote the (?:music|score|soundtrack) (?:for|of|to)"
141
+ r"|which composer wrote)\s+(?P<title>.+?)$", re.I)
142
+
143
+
144
+ def _composer_request(q):
145
+ """Untrusted, whole-question recognizer: musical composition means P86.
146
+
147
+ A mined correlation with P800 (notable work) cannot establish the role.
148
+ Keep the entire title; do not silently fall back to a linked title fragment.
149
+ """
150
+ text = " ".join(str(q).split()).rstrip("?.! ")
151
+ text = re.sub(r"^(?:please tell me|(?:can|could) you (?:please )?tell me)\s+", "", text, flags=re.I)
152
+ match = _COMPOSER_REQUEST.fullmatch(text)
153
+ if not match:
154
+ return None
155
+ title = match.group("title").strip()
156
+ title = re.sub(r"^the (?:film|movie)\s+", "", title, flags=re.I)
157
+ dated = re.fullmatch(r"(.+?)\s*\((\d{4})\)", title)
158
+ year = dated.group(2) if dated else None
159
+ title = (dated.group(1) if dated else title).strip(" \"'“”‘’")
160
+ return title, year
161
+
162
+
163
+ def _composer_answer(q, did, request):
164
+ title, year = request
165
+ # Fetch one extra candidate so a capped name lookup is not mistaken for full
166
+ # coverage. Domain and optional publication year must select agreeing rows.
167
+ candidates = list(X.L.candidates(X.norm(title), 65)) if title else []
168
+ if not candidates and title.lower().startswith("the "):
169
+ candidates = list(X.L.candidates(X.norm(title[4:]), 65))
170
+ if not candidates or len(candidates) > 64:
171
+ return None
172
+ mine = [c[0] for c in candidates if in_domain(c[0], did)]
173
+ if year:
174
+ mine = [qid for qid in mine if any(
175
+ re.fullmatch(r"\+?" + year + r"(?:-\d{2}-\d{2}(?:T[^\s]+)?)?", str(value))
176
+ for value in values(qid, 577))]
177
+ sets = [values(qid, 86) for qid in mine]
178
+ if not mine or any(not found for found in sets) or len(set(sets)) != 1 or len(sets[0]) != 1:
179
+ return None
180
+ val = next(iter(sets[0]))
181
+ if _restates(q, val):
182
+ return None
183
+ return {"answer": X._show(val, q), "property": 86, "subject_qid": mine[0],
184
+ "direction": "fwd", "pattern": "explicit|composer|P86",
185
+ "linked_qid": mine[0]}
186
+
187
+
188
  def answer(q, did, patterns):
189
  """patterns: {"fwd|key|word": [p, ...], "inv|key|word": [p, ...]} -> {answer, property, subject_qid, direction, pattern} or None"""
190
+ request = _composer_request(q)
191
+ if request is not None:
192
+ if kind_conflict(str(q), did):
193
+ return None
194
+ return _composer_answer(q, did, request)
195
  a = analyse(str(q))
196
  if a is None or kind_conflict(str(q), did):
197
  return None
root/wander-train/word_problems_v1.py CHANGED
@@ -1,5 +1,7 @@
1
  """word_problems_v1 - prose to equations for the recurring shapes of textbook word problems.
2
 
 
 
3
  Each SCHEMA is a stored form: trigger words, the TYPED quantities it needs (money, percent, time, length ...), and a
4
  formula. A schema fires only when every quantity it needs is found with the right type AND the question asks for the
5
  kind of quantity the formula produces (the CHECK: units must compose - money x rate x years -> money). Quantities are
@@ -178,38 +180,158 @@ def s_convert(t, q, qs):
178
  tab[src[0][2]], tab[to], to)]}
179
 
180
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  def s_geometry(t, q, qs):
182
- L = [x for x in qs if x[0] == "length"]
 
183
  tl = t.lower()
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
  def near(word):
186
- m = re.search(r"%s[^.]{0,40}?%s\s*([a-z]+)" % (word, NUM), tl)
187
- return num(m.group(1)) if m else None
188
  if "rectangle" in tl or "rectangular" in tl:
189
  ln, wd = near("length"), near("width")
190
  if ln is not None and wd is not None:
191
  if re.search(r"\barea\b", q):
192
- return {"value": ln * wd, "unit": "sq", "steps": ["A = L x W = %g x %g" % (ln, wd)]}
193
  if re.search(r"perimeter", q):
194
- return {"value": 2 * (ln + wd), "unit": "", "steps": ["P = 2(L + W) = 2(%g + %g)" % (ln, wd)]}
195
  if "circle" in tl or "circular" in tl:
196
  r, d = near("radius"), near("diameter")
197
  r = r if r is not None else (d / 2 if d is not None else None)
198
  if r is not None:
199
  pi = 3.14 if "3.14" in tl else math.pi
200
  if re.search(r"\barea\b", q):
201
- return {"value": pi * r * r, "unit": "sq", "steps": ["A = pi r^2, r = %g" % r]}
202
  if re.search(r"circumference", q):
203
- return {"value": 2 * pi * r, "unit": "", "steps": ["C = 2 pi r, r = %g" % r]}
204
  if re.search(r"\bvolume\b", q):
205
  r, h = near("radius"), near("height")
206
  pi = 3.14 if "3.14" in tl else math.pi
207
  if "cone" in tl and r is not None and h is not None:
208
- return {"value": pi * r * r * h / 3, "unit": "cu", "steps": ["V = (1/3) pi r^2 h, r = %g, h = %g" % (r, h)]}
209
  if "cylinder" in tl and r is not None and h is not None:
210
- return {"value": pi * r * r * h, "unit": "cu", "steps": ["V = pi r^2 h, r = %g, h = %g" % (r, h)]}
211
  if "sphere" in tl and r is not None:
212
- return {"value": 4 / 3 * pi * r ** 3, "unit": "cu", "steps": ["V = (4/3) pi r^3, r = %g" % r]}
213
  return None
214
 
215
 
 
1
  """word_problems_v1 - prose to equations for the recurring shapes of textbook word problems.
2
 
3
+ This Python recognizer is untrusted; its checks are not proof or admission authority.
4
+
5
  Each SCHEMA is a stored form: trigger words, the TYPED quantities it needs (money, percent, time, length ...), and a
6
  formula. A schema fires only when every quantity it needs is found with the right type AND the question asks for the
7
  kind of quantity the formula produces (the CHECK: units must compose - money x rate x years -> money). Quantities are
 
180
  tab[src[0][2]], tab[to], to)]}
181
 
182
 
183
+ def _geometry_dimensions(t, qs):
184
+ """Untrusted complete binding for explicit geometry roles; no proof authority.
185
+
186
+ Return (bindings, conflict). Unknown syntax/units return (None, None).
187
+ Prefix roles and postfix long/wide/tall share coverage and uniqueness checks.
188
+ """
189
+ tl = t.lower().replace("−", "-")
190
+ lengths = [x for x in qs if x[0] == "length"]
191
+ mentions = {}
192
+ for mention in re.finditer(r"\b(length|width|radius|diameter|height)\b", tl):
193
+ match = re.match(r"\s*(?:(?:is|of|exactly|equals|measures|=|:)\s*)*"
194
+ + NUM + r"\s*([a-z]+)\b", tl[mention.end():])
195
+ if not match:
196
+ return None, None
197
+ # "in" is also a preposition. Accept the inch abbreviation only at
198
+ # a measurement boundary; "length 3 in the diagram" supplies no unit.
199
+ tail = tl[mention.end() + match.end():].lstrip()
200
+ if match.group(2) == "in" and tail and not re.match(r"[.,;:?!)]|(?:and|long|wide|tall|high)\b", tail):
201
+ return None, None
202
+ mentions[mention.end() + match.start(1)] = (mention.group(1), match.group(1), match.group(2))
203
+ for match in re.finditer(r"(?<![\w.+-])" + NUM + r"\s*([a-z]+)\s+(long|wide|tall|high)\b", tl):
204
+ role = {"long": "length", "wide": "width", "tall": "height", "high": "height"}[match.group(3)]
205
+ binding = (role, match.group(1), match.group(2))
206
+ if match.start(1) in mentions and mentions[match.start(1)] != binding:
207
+ return None, None
208
+ mentions[match.start(1)] = binding
209
+ # Every typed length must be bound, including repeats. Apartment IDs are
210
+ # unitless, so they do not masquerade as dimensions or block a valid room.
211
+ if len(mentions) != len(lengths):
212
+ return None, None
213
+ bindings, unit_scales, records = {}, set(), {}
214
+ conflict = None
215
+ for _, (role, raw, unit_word) in sorted(mentions.items()):
216
+ unit = unit_of(unit_word)
217
+ if unit is None or unit[0] != "length":
218
+ return None, None
219
+ value, scale = num(raw), LENGTH[unit[1]]
220
+ current = {"value": value, "unit": unit_word}
221
+ if role in bindings and bindings[role] != (value, scale):
222
+ if bindings[role][0] * bindings[role][1] != value * scale:
223
+ conflict = conflict or {"role": role, "values": [records[role], current]}
224
+ bindings[role] = (value, scale)
225
+ records[role] = current
226
+ unit_scales.add(scale)
227
+ if conflict:
228
+ return None, conflict
229
+ # Conversion between mixed units needs an output-unit contract first.
230
+ if len(unit_scales) > 1:
231
+ return None, None
232
+ if "radius" in bindings and "diameter" in bindings:
233
+ if bindings["radius"][0] * 2 != bindings["diameter"][0]:
234
+ return None, {"role": "radius/diameter", "values": [records["radius"], records["diameter"]]}
235
+ return bindings, None
236
+
237
+
238
+ def _geometry_request_issue(t, q):
239
+ """Bound the subject, requested measure and output unit before calculating."""
240
+ shapes = re.findall(r"\b(?:rectangles?|rectangular|circles?|circular|cones?|cylinders?|spheres?)\b", t, re.I)
241
+ if not shapes:
242
+ return None
243
+ measures = set(re.findall(r"\b(?:area|perimeter|circumference|volume)\b", q))
244
+ if not measures:
245
+ return None
246
+ if len(measures) > 1:
247
+ return "I can calculate one geometry measure per question. Please ask for each requested measure separately."
248
+ # These are subject multiplicity markers, not arbitrary numbers: an
249
+ # apartment identifier is not a count of rectangles or a dimension.
250
+ plural = re.search(r"\b(?:rectangles|circles|cones|cylinders|spheres|rooms|boxes)\b", t, re.I)
251
+ counted = re.search(r"\b(?:" + "|".join(w for w, v in WORDNUM.items() if v != 1)
252
+ + r"|[2-9]\d*|1\d+)\s+(?:(?:identical|rectangular|circular)\s+)*"
253
+ r"(?:room|box|rectangle|circle|cone|cylinder|sphere)\b", t, re.I)
254
+ if len(shapes) > 1 or plural or counted or re.search(r"\b(?:their|both)\b", q):
255
+ return "I can calculate one shape at a time; this question refers to multiple shapes."
256
+ for requested in re.finditer(r"\b(?:in|into|to)\s+(?:(square|cubic|sq\.?|cu\.?)\s+|(units?\s+of)\s+)?"
257
+ r"([a-z]+)(?:\^?[23]|[²³])?\b", q):
258
+ unit = unit_of(requested.group(3))
259
+ if requested.group(1) or requested.group(2) or (unit and unit[0] == "length"):
260
+ return "I cannot yet bind the requested output unit for this geometry question."
261
+ return None
262
+
263
+
264
+ def explain_refusal(text):
265
+ """Explain a bounded geometry conflict, without certifying an English parse."""
266
+ t = " ".join(str(text).split())
267
+ q = question_part(t)
268
+ issue = _geometry_request_issue(t, q)
269
+ if issue:
270
+ return {"reason": "unsupported_geometry_request", "schema": "geometry", "conflict": None, "response": issue}
271
+ # Do not conflate dimensions belonging to two explicitly named shapes.
272
+ shapes = re.findall(r"\b(?:rectangle|rectangular|circle|circular|cone|cylinder|sphere)\b", t, re.I)
273
+ if len(shapes) != 1 or MULTISTEP.search(t) or not re.search(r"\b(?:area|perimeter|circumference|volume)\b", q):
274
+ return None
275
+ _, conflict = _geometry_dimensions(t, quantities(t))
276
+ if not conflict:
277
+ return None
278
+ values = conflict["values"]
279
+ if conflict["role"] == "radius/diameter":
280
+ message = ("The supplied radius (%g %s) and diameter (%g %s) disagree: "
281
+ "the diameter must be twice the radius. Which measurement should I use?" %
282
+ (values[0]["value"], values[0]["unit"], values[1]["value"], values[1]["unit"]))
283
+ else:
284
+ message = ("The supplied %s values disagree (%g %s and %g %s). Which %s should I use?" %
285
+ (conflict["role"], values[0]["value"], values[0]["unit"],
286
+ values[1]["value"], values[1]["unit"], conflict["role"]))
287
+ return {"reason": "conflicting_dimension", "schema": "geometry", "conflict": conflict, "response": message}
288
+
289
+
290
  def s_geometry(t, q, qs):
291
+ if _geometry_request_issue(t, q):
292
+ return None
293
  tl = t.lower()
294
+ bindings, conflict = _geometry_dimensions(t, qs)
295
+ if bindings is None:
296
+ return None
297
+
298
+ # Every binding has the same checked length scale. Keep that unit and
299
+ # raise its dimension with the formula; never relabel raw cm as meters.
300
+ units_by_scale = {LENGTH[u]: u for u in ("mm", "cm", "m", "km", "in", "ft", "yd", "mi")}
301
+ base_unit = units_by_scale.get(next(iter(bindings.values()))[1]) if bindings else None
302
+ if base_unit is None:
303
+ return None
304
+
305
+ def result(value, power, steps):
306
+ return {"value": value, "unit": base_unit if power == 1 else "%s^%d" % (base_unit, power), "steps": steps}
307
 
308
  def near(word):
309
+ return bindings[word][0] if word in bindings else None
 
310
  if "rectangle" in tl or "rectangular" in tl:
311
  ln, wd = near("length"), near("width")
312
  if ln is not None and wd is not None:
313
  if re.search(r"\barea\b", q):
314
+ return result(ln * wd, 2, ["A = L x W = %g x %g" % (ln, wd)])
315
  if re.search(r"perimeter", q):
316
+ return result(2 * (ln + wd), 1, ["P = 2(L + W) = 2(%g + %g)" % (ln, wd)])
317
  if "circle" in tl or "circular" in tl:
318
  r, d = near("radius"), near("diameter")
319
  r = r if r is not None else (d / 2 if d is not None else None)
320
  if r is not None:
321
  pi = 3.14 if "3.14" in tl else math.pi
322
  if re.search(r"\barea\b", q):
323
+ return result(pi * r * r, 2, ["A = pi r^2, r = %g" % r])
324
  if re.search(r"circumference", q):
325
+ return result(2 * pi * r, 1, ["C = 2 pi r, r = %g" % r])
326
  if re.search(r"\bvolume\b", q):
327
  r, h = near("radius"), near("height")
328
  pi = 3.14 if "3.14" in tl else math.pi
329
  if "cone" in tl and r is not None and h is not None:
330
+ return result(pi * r * r * h / 3, 3, ["V = (1/3) pi r^2 h, r = %g, h = %g" % (r, h)])
331
  if "cylinder" in tl and r is not None and h is not None:
332
+ return result(pi * r * r * h, 3, ["V = pi r^2 h, r = %g, h = %g" % (r, h)])
333
  if "sphere" in tl and r is not None:
334
+ return result(4 / 3 * pi * r ** 3, 3, ["V = (4/3) pi r^3, r = %g" % r])
335
  return None
336
 
337
 
tests/conversation_grammar/test_m53_dummy_it.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Untrusted handler tests: real AO conversion and AN recording, controlled AM replies.
2
+
3
+ No full Engine or source stores are loaded. These tests establish conversion
4
+ behavior, not whether the full engine can compute the requested duration/cost.
5
+ """
6
+ from copy import deepcopy
7
+ import importlib.util
8
+ from pathlib import Path
9
+ import sys
10
+ import types
11
+ import unittest
12
+ from unittest.mock import patch
13
+
14
+ ROOT = Path(__file__).resolve().parents[2]
15
+ WANDER = ROOT / 'root/wander-train'
16
+ MIRROR = ROOT / 'root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_ao.py'
17
+
18
+
19
+ def load(name, path):
20
+ spec = importlib.util.spec_from_file_location(name, path)
21
+ module = importlib.util.module_from_spec(spec)
22
+ sys.modules[name] = module
23
+ spec.loader.exec_module(module)
24
+ return module
25
+
26
+
27
+ def answer(body):
28
+ return {'status': 'answered', 'path': 'verified:test', 'response': body}
29
+
30
+
31
+ def withheld():
32
+ return {'status': 'withheld', 'path': 'missing:test', 'response': 'No answer for that question.'}
33
+
34
+
35
+ class Plain:
36
+ def __init__(self):
37
+ self.dialogue = {}
38
+ self.calls = []
39
+ self.routes = {}
40
+
41
+ def chat(self, text, session_id='default'):
42
+ self.calls.append((text, session_id))
43
+ result = deepcopy(self.routes.get(text, withheld()))
44
+ self.dialogue.setdefault(session_id, {})['last_result'] = deepcopy(result)
45
+ return result
46
+
47
+
48
+ class DummyItTests(unittest.TestCase):
49
+ def setUp(self):
50
+ self.modules = patch.dict(sys.modules, {'oracle_release_runtime_am':
51
+ types.SimpleNamespace(OracleReleaseRuntimeAM=Plain)})
52
+ self.modules.start()
53
+ load('oracle_release_runtime_an', WANDER / 'oracle_release_runtime_an.py')
54
+ self.ao = load('oracle_release_runtime_ao', WANDER / 'oracle_release_runtime_ao.py')
55
+ self.rt = self.ao.OracleReleaseRuntimeAO()
56
+ self.rt.dialogue['s'] = {'last_topic': 'that times 2'}
57
+ self.rt.routes['Summarize that times 2.'] = answer('A summary unrelated to transfer duration.')
58
+
59
+ def tearDown(self):
60
+ self.modules.stop()
61
+
62
+ def test_recorded_transfer_failure_does_not_become_old_topic_summary(self):
63
+ question = 'How long does it take to transfer 1GB at100Mbps?'
64
+ reply = self.rt.chat(question, 's')
65
+ self.assertEqual(reply, withheld())
66
+ self.assertEqual(self.rt.calls, [(question, 's'), (question, 's')])
67
+ self.assertEqual(len(self.rt.path('s')), 1)
68
+ self.assertEqual(self.rt.path('s')[0]['in_surface'], question)
69
+ self.assertNotIn('conversion', self.rt.path('s')[0])
70
+
71
+ def test_duration_and_cost_grammar_generalizes_beyond_transfer_wording(self):
72
+ questions = ['How long will it take to boil 2 litres of water?',
73
+ 'How much does it cost to run a 2 kW heater for 3 hours?',
74
+ 'What would it take to build a bridge?',
75
+ 'Does it take 1.5 hours to charge a battery?',
76
+ 'Would it cost $2.50 to run the heater?',
77
+ 'How much time has it taken to compile the program?',
78
+ 'HOW LONG COULD IT TAKE TO WALK 10 KM?']
79
+ for question in questions:
80
+ with self.subTest(question=question):
81
+ self.assertEqual(self.rt._convert(question, 's'), (None, None))
82
+
83
+ def test_real_referential_it_and_other_pronouns_still_resolve(self):
84
+ self.rt.dialogue['s']['last_topic'] = 'Moon'
85
+ for question, expected in [('Who discovered it?', 'Who discovered Moon?'),
86
+ ('What is it made of?', 'What is Moon made of?'),
87
+ ('What lives there?', 'What lives in Moon?'),
88
+ ('How long does it take?', 'How long does Moon take?'),
89
+ ('Does it take photos?', 'Does Moon take photos?'),
90
+ ('Does it take photos to upload?', 'Does Moon take photos to upload?'),
91
+ ('Does it take me to London?', 'Does Moon take me to London?')]:
92
+ with self.subTest(question=question):
93
+ self.assertEqual(self.rt._convert(question, 's'), (expected, 'REFERENT_SUBSTITUTION'))
94
+
95
+ def test_dummy_it_does_not_hide_a_later_real_reference(self):
96
+ self.rt.dialogue['s']['last_topic'] = 'the battery'
97
+ question = 'How long does it take to charge that?'
98
+ rewritten = 'How long does it take to charge the battery?'
99
+ self.assertEqual(self.rt._convert(question, 's'), (rewritten, 'REFERENT_SUBSTITUTION'))
100
+ self.rt.routes[rewritten] = answer('Two hours. [source:test]')
101
+ reply = self.rt.chat(question, 's')
102
+ self.assertEqual(reply['response'], 'Two hours. [source:test]')
103
+ self.assertEqual(reply['conversion']['to'], rewritten)
104
+ self.assertEqual(len(self.rt.path('s')), 1)
105
+
106
+ def test_known_answer_and_conversation_moves_are_preserved(self):
107
+ question = 'How long does it take to transfer 1 GB at 100 Mbps?'
108
+ self.rt.routes[question] = answer('80 seconds. [source:test]')
109
+ self.assertEqual(self.rt.chat(question, 's'), self.rt.routes[question])
110
+ self.assertEqual(len(self.rt.path('s')), 1)
111
+ self.assertEqual(self.rt._convert('I meant the Pacific Ocean', 's'),
112
+ ('Summarize the Pacific Ocean.', 'REPAIR_ACCEPT'))
113
+ self.assertEqual(self.rt._convert('What about Venus?', 's'),
114
+ ('Summarize Venus.', 'ELLIPSIS_COMPLETION'))
115
+ self.assertEqual(self.rt._convert('Go on', 's'), (None, None))
116
+
117
+ def test_absent_topic_and_other_session_never_borrow_referents(self):
118
+ self.assertEqual(self.rt._convert('Who discovered it?', 'other'), (None, None))
119
+ self.assertEqual(self.rt._convert('How long does it take to charge that?', 'other'), (None, None))
120
+ self.assertEqual(self.rt.dialogue['s']['last_topic'], 'that times 2')
121
+
122
+ def test_source_mirror_matches(self):
123
+ self.assertEqual((WANDER / 'oracle_release_runtime_ao.py').read_bytes(), MIRROR.read_bytes())
124
+
125
+ if __name__ == '__main__':
126
+ unittest.main()
tests/conversation_grammar_r468/test_door_dummy_it.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Untrusted tests of real r468 and its real obligation compiler, without an Engine.
2
+
3
+ The controlled parent supplies explicit replies. No real transfer-duration
4
+ answer or privacy-guard implementation is claimed by these component tests.
5
+ """
6
+ import ast
7
+ from copy import deepcopy
8
+ import importlib.util
9
+ from pathlib import Path
10
+ import sys
11
+ import types
12
+ import unittest
13
+ from unittest.mock import patch
14
+
15
+ ROOT = Path(__file__).resolve().parents[2]
16
+ WANDER = ROOT / 'root/wander-train'
17
+ ADMITTED = ROOT / 'root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted'
18
+ QUESTION = 'How long does it take to transfer 1GB at100Mbps?'
19
+ GENERIC = {'status':'clarification', 'path':'act:clarify', 'response':"I didn't fully parse that, so I won't guess.",
20
+ 'provenance':{'kind':'dialogue_state'}}
21
+ PRIVATE = {'status':'withheld', 'path':'precision_guard:G2_private_state',
22
+ 'response':'That depends on something only you know; my records cannot tell me.',
23
+ 'provenance':{'kind':'precision_guard','rule':'G2_private_state'}}
24
+
25
+
26
+ def load(name, path):
27
+ spec = importlib.util.spec_from_file_location(name, path)
28
+ module = importlib.util.module_from_spec(spec)
29
+ sys.modules[name] = module
30
+ spec.loader.exec_module(module)
31
+ return module
32
+
33
+
34
+ class Parent:
35
+ def __init__(self):
36
+ self.routes = {}
37
+ self.calls = []
38
+
39
+ def chat(self, text, session_id='default'):
40
+ self.calls.append((text, session_id))
41
+ return deepcopy(self.routes.get(text, GENERIC))
42
+
43
+
44
+ class DoorGrammarTests(unittest.TestCase):
45
+ def setUp(self):
46
+ self.modules = patch.dict(sys.modules, {'oracle_release_runtime_w313':
47
+ types.SimpleNamespace(OracleReleaseRuntimeW313=Parent)})
48
+ self.modules.start()
49
+ load('oracle_program_ir_v1', WANDER / 'oracle_program_ir_v1.py')
50
+ self.module = load('oracle_release_runtime_w468', ADMITTED / 'oracle_release_runtime_w468.py')
51
+ self.rt = self.module.OracleReleaseRuntimeW468()
52
+
53
+ def tearDown(self):
54
+ self.modules.stop()
55
+
56
+ def seed_arithmetic(self):
57
+ for question, result in [('What is 6 multiplied by 7?', '42'), ('that times 2', '84')]:
58
+ self.rt.routes[question] = {'status':'answered','path':'compute:verified','response':result}
59
+ self.rt.chat(question, 'arithmetic')
60
+ self.assertEqual(self.rt._r468_thread('arithmetic'), ['multiplied', 'times'])
61
+ self.rt.calls.clear()
62
+
63
+ def test_actual_arithmetic_thread_does_not_ask_dummy_it_to_choose_old_words(self):
64
+ self.seed_arithmetic()
65
+ actual = self.rt.chat(QUESTION, 'arithmetic')
66
+ self.assertEqual(actual, GENERIC)
67
+ self.assertEqual(self.rt.calls, [(QUESTION, 'arithmetic')])
68
+ self.assertNotIn('r468_door', actual['provenance'])
69
+
70
+ def test_same_exclusion_applies_to_binding_and_clarification(self):
71
+ self.seed_arithmetic()
72
+ questions = [QUESTION, 'How much does it cost to run a heater?',
73
+ 'What would it take to build a bridge?',
74
+ 'Does it take 1.5 hours to charge a battery?',
75
+ 'Would it cost $2.50 to run the heater?',
76
+ 'How much time has it taken to compile the program?']
77
+ for question in questions:
78
+ with self.subTest(question=question):
79
+ self.assertIsNone(self.rt._r468_bind(question, 'times'))
80
+ self.assertIsNone(self.rt._r468_door(question, 'arithmetic', deepcopy(GENERIC)))
81
+
82
+ def test_later_genuine_reference_is_bound_or_asked_about_instead(self):
83
+ self.rt._r468_thread('s').append('battery')
84
+ question = 'How long does it take to charge that?'
85
+ self.assertEqual(self.rt._r468_bind(question, 'battery'),
86
+ 'How long does it take to charge battery?')
87
+ actual = self.rt.chat(question, 's')
88
+ self.assertEqual(actual['provenance']['r468_door']['phrase'], 'that')
89
+ self.assertEqual(actual['provenance']['r468_door']['offered'], ['battery'])
90
+ self.assertEqual(self.rt._r468_bind('How long does it take to cool its contents?', 'battery'),
91
+ "How long does it take to cool battery's contents?")
92
+
93
+ def test_real_reference_binds_and_preserves_evidence_from_same_session(self):
94
+ self.rt._r468_thread('s').append('Eiffel Tower')
95
+ source = {'status':'answered', 'path':'verified:place', 'response':'It is in Paris, France. [source:place]',
96
+ 'references':['source:place']}
97
+ self.rt.routes['Where is Eiffel Tower?'] = source
98
+ actual = self.rt.chat('Where is it?', 's')
99
+ self.assertEqual(actual['provenance']['r468_door']['act'], 'bound')
100
+ self.assertEqual(actual['references'], ['source:place'])
101
+ self.assertEqual(self.rt.calls, [('Where is it?', 's'), ('Where is Eiffel Tower?', 's::r468')])
102
+
103
+ def test_direct_object_it_and_non_dummy_possessives_are_unchanged(self):
104
+ for question, expected in [('Does it take me to London?', 'Does train take me to London?'),
105
+ ('Does it take photos to upload?', 'Does train take photos to upload?'),
106
+ ('How long does it take?', 'How long does train take?'),
107
+ ('What is its speed?', "What is train's speed?")]:
108
+ with self.subTest(question=question):
109
+ self.assertEqual(self.rt._r468_bind(question, 'train'), expected)
110
+
111
+ def test_private_refusal_is_never_rebound_or_replaced(self):
112
+ self.seed_arithmetic()
113
+ for question in ['What is my password?',
114
+ 'How long does it take to recover my password?',
115
+ 'How long does it take to read that in another conversation?']:
116
+ with self.subTest(question=question):
117
+ self.rt.routes[question] = deepcopy(PRIVATE)
118
+ self.rt.calls.clear()
119
+ self.assertEqual(self.rt.chat(question, 'arithmetic'), PRIVATE)
120
+ self.assertEqual(self.rt.calls, [(question, 'arithmetic')])
121
+
122
+ def test_empty_session_does_not_offer_other_sessions_subjects(self):
123
+ self.seed_arithmetic()
124
+ actual = self.rt.chat('Where is it?', 'empty')
125
+ self.assertEqual(actual['provenance']['r468_door']['offered'], [])
126
+ self.assertNotIn('times', actual['response'])
127
+ self.assertNotIn('multiplied', actual['response'])
128
+ self.assertEqual(self.rt._r468_thread('arithmetic'), ['multiplied', 'times'])
129
+
130
+ def test_answered_parent_still_passes_through(self):
131
+ self.rt.routes[QUESTION] = {'status':'answered','path':'specialist:transfer','response':'80 seconds.'}
132
+ self.assertEqual(self.rt.chat(QUESTION, 's'), self.rt.routes[QUESTION])
133
+ self.assertEqual(self.rt.calls, [(QUESTION, 's')])
134
+
135
+ def test_dummy_it_grammar_matches_existing_ao_without_importing_its_chain(self):
136
+ tree = ast.parse((WANDER / 'oracle_release_runtime_ao.py').read_text())
137
+ node = next(n for n in tree.body if isinstance(n, ast.Assign) and
138
+ any(isinstance(t, ast.Name) and t.id == 'DUMMY_IT' for t in n.targets))
139
+ self.assertEqual(self.module.DUMMY_IT.pattern, ast.literal_eval(node.value.args[0]))
140
+
141
+ if __name__ == '__main__':
142
+ unittest.main()
tests/conversation_guard/test_last_question.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """G2 regression using real dialogue production and guard methods, without a head."""
2
+ import ast
3
+ from copy import deepcopy
4
+ import importlib.util
5
+ from pathlib import Path
6
+ import sys
7
+ from types import ModuleType
8
+ import unittest
9
+ from unittest.mock import patch
10
+
11
+ ROOT = Path(__file__).resolve().parents[2]
12
+ ADMITTED = ROOT / "root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted"
13
+
14
+
15
+ def load(name, path):
16
+ spec = importlib.util.spec_from_file_location(name,path)
17
+ module = importlib.util.module_from_spec(spec)
18
+ spec.loader.exec_module(module)
19
+ return module
20
+
21
+
22
+ DIALOGUE = load("actual_dialogue",ROOT / "root/wander-train/organ_dialogue_v1.py")
23
+ # Only the actual envelope builder is needed from w480; its module imports the
24
+ # head and unrelated math/relation stores. Compile this unchanged method alone.
25
+ source = ast.parse((ADMITTED / "oracle_release_runtime_w480.py").read_text())
26
+ cls = next(node for node in source.body if isinstance(node,ast.ClassDef))
27
+ method = next(node for node in cls.body if isinstance(node,ast.FunctionDef) and node.name == "_c480_dialogue_reply")
28
+ namespace = {}
29
+ exec(compile(ast.Module(body=[method],type_ignores=[]),"actual_w480_reply", "exec"),namespace)
30
+ reply_builder = namespace["_c480_dialogue_reply"]
31
+
32
+
33
+ class Parent:
34
+ def _c480_dialogue(self):
35
+ if not hasattr(self,"_c480_d"):
36
+ self._c480_d = DIALOGUE.DialogueOrgan()
37
+ return self._c480_d
38
+
39
+ def chat(self, text, session_id="default"):
40
+ dialogue = self._c480_dialogue()
41
+ if getattr(self,"forced_reply",None) is not None:
42
+ reply = deepcopy(self.forced_reply)
43
+ else:
44
+ answer = dialogue.answer(text,session_id)
45
+ reply = reply_builder(self,{"status":"clarification","path":"act:clarify"},answer)
46
+ dialogue.log(session_id,text,reply)
47
+ return reply
48
+
49
+
50
+ parent_module = ModuleType("oracle_release_runtime_w533")
51
+ parent_module.OracleReleaseRuntimeW533 = Parent
52
+ with patch.dict(sys.modules,{"oracle_release_runtime_w533":parent_module,
53
+ "chat_context_v2":None,"entity_linker_v2":None,"relation_spec_v1":None}):
54
+ GUARD = load("actual_w534",ADMITTED / "oracle_release_runtime_w534.py")
55
+
56
+
57
+ QUESTION = "What is 6 multiplied by 7?"
58
+ ANSWER = "In steps: 6 x 7 = 42. The answer is 42."
59
+
60
+
61
+ class ConversationGuard(unittest.TestCase):
62
+ def setUp(self):
63
+ self.runtime = GUARD.OracleReleaseRuntimeW534()
64
+ self.dialogue = self.runtime._c480_dialogue()
65
+ self.dialogue.log("a",QUESTION,dict(status="answered",path="compute:verified",response=ANSWER))
66
+
67
+ def history_reply(self, session="a", question="What did I just ask?"):
68
+ return reply_builder(self.runtime,{"status":"clarification","path":"act:clarify"},
69
+ self.dialogue.answer(question,session))
70
+
71
+ def test_recorded_real_repro_passes_with_exact_turn_evidence(self):
72
+ expected = self.history_reply()
73
+ self.assertTrue(GUARD.private_state("What did I just ask?"))
74
+ self.assertEqual(GUARD.final_guards("What did I just ask?",expected)["status"],"withheld")
75
+ actual = self.runtime.chat("What did I just ask?","a")
76
+ self.assertEqual(actual,expected)
77
+ self.assertEqual(actual["response"],f'You asked: "{QUESTION}" [session:turn 1]. I answered: "{ANSWER}".')
78
+ self.assertEqual(actual["provenance"]["turns"],[1])
79
+ self.assertEqual(len(self.dialogue.logs["a"]),2) # guard read added no turn
80
+
81
+ def test_empty_session_does_not_recall_another_session(self):
82
+ actual = self.runtime.chat("What did I just ask?","empty")
83
+ self.assertEqual(actual["status"],"answered")
84
+ self.assertIn("first thing",actual["response"])
85
+ self.assertEqual(actual["references"],[])
86
+ self.assertNotIn(QUESTION,actual["response"])
87
+ self.assertEqual(len(self.dialogue.logs["empty"]),1)
88
+ self.assertEqual(len(self.dialogue.logs["a"]),1)
89
+
90
+ def test_repeat_recall_and_supported_variants_do_not_recall_themselves(self):
91
+ for question in ("What did I just ask?","What was my previous question?","What did I last say?"):
92
+ actual = self.runtime.chat(question,"a")
93
+ self.assertEqual(actual["status"],"answered")
94
+ self.assertIn(QUESTION,actual["response"])
95
+ self.assertEqual(actual["provenance"]["turns"],[1])
96
+ self.assertEqual(len(self.dialogue.logs["a"]),4)
97
+
98
+ def test_withheld_turn_is_recalled_as_a_request_not_as_a_true_answer(self):
99
+ self.dialogue.log("a","What is my bank balance?",dict(status="withheld",path="precision_guard:G2_private_state",
100
+ response="I do not know your balance."))
101
+ actual = self.runtime.chat("What did I just ask?","a")
102
+ self.assertEqual(actual["status"],"answered")
103
+ self.assertIn('You asked: "What is my bank balance?"',actual["response"])
104
+ self.assertIn("I did not answer it",actual["response"])
105
+ self.assertEqual(actual["provenance"]["turns"],[2])
106
+
107
+ def test_private_and_compound_questions_are_still_withheld(self):
108
+ for question in ("What is my bank account balance?","What is my password?",
109
+ "What did I dream about last night?","What number am I thinking of?",
110
+ "What did I just ask? And what is my PIN?",
111
+ "What did I just ask and what is my password?",
112
+ "What did I just ask in another conversation?"):
113
+ with self.subTest(question=question):
114
+ self.runtime.forced_reply = self.history_reply()
115
+ actual = self.runtime.chat(question,"a")
116
+ self.assertEqual(actual["status"],"withheld")
117
+ self.assertIn("G2_private_state",actual["path"])
118
+
119
+ def test_route_or_turn_labels_do_not_license_fabricated_content(self):
120
+ for field, altered in (("response","Your password is swordfish."),
121
+ ("references",[]),
122
+ ("provenance",dict(kind="organ_dialogue",move="discourse:asked_last",turns=[99]))):
123
+ with self.subTest(field=field):
124
+ self.runtime.forced_reply = self.history_reply()
125
+ self.runtime.forced_reply[field] = altered
126
+ self.assertEqual(self.runtime.chat("What did I just ask?","a")["status"],"withheld")
127
+
128
+ def test_stale_reply_and_other_session_reply_are_withheld(self):
129
+ stale = self.history_reply()
130
+ self.dialogue.log("a","What is 9 multiplied by 9?",dict(status="answered",path="compute:verified",response="81."))
131
+ self.runtime.forced_reply = stale
132
+ self.assertEqual(self.runtime.chat("What did I just ask?","a")["status"],"withheld")
133
+ self.runtime.forced_reply = self.history_reply("a")
134
+ self.assertEqual(self.runtime.chat("What did I just ask?","b")["status"],"withheld")
135
+
136
+ def test_missing_history_handler_fails_closed(self):
137
+ self.runtime.forced_reply = self.history_reply()
138
+ self.runtime._c480_dialogue = lambda: None
139
+ # Invoke the real guard with a controlled parent that does not need its log.
140
+ with patch.object(Parent,"chat",return_value=self.runtime.forced_reply):
141
+ self.assertEqual(self.runtime.chat("What did I just ask?","a")["status"],"withheld")
142
+
143
+ def test_other_guards_and_quantity_computations_remain_active(self):
144
+ reply = dict(status="answered",path="specialist_call>x",response="The speed is 3 m/s.")
145
+ self.assertIn("G1_sign_kept",GUARD.final_guards("Compute speed for -3 m/s",reply)["path"])
146
+ self.assertIs(GUARD.final_guards("My car does 30 mpg; how far for 2 gallons?",reply),reply)
147
+ with patch.object(GUARD,"premise_years",return_value=("Painting",1990,[1889])):
148
+ self.assertIn("G3_date_premise",GUARD.final_guards("Who painted Painting in 1990?",reply)["path"])
149
+
150
+
151
+ if __name__ == "__main__":
152
+ unittest.main()
tests/usage/README.md ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Focused usage regressions
2
+
3
+ These tests exercise the changed runtime functions and actual specialist/family
4
+ consumers with controlled expensive parents. They do not start the full Engine,
5
+ download stores, rerun release admission or make theorem claims. Source defaults
6
+ to this checkout; use the upstream Python requirements for the SymPy tests.
7
+
8
+ From the repository root, run these selected directories individually:
9
+
10
+ ```sh
11
+ for group in computing conversation composition evidence documents engineering research reliability reliability_route; do
12
+ python -B -m unittest discover -s tests/usage/$group -v || exit 1
13
+ done
14
+ for group in conversation_guard conversation_grammar conversation_grammar_r468; do
15
+ python -B -m unittest discover -s tests/$group -v || exit 1
16
+ done
17
+ for version in 2 3; do
18
+ MATH_ORGAN_SOURCE="$PWD/root/wander-train/math_organ_v$version.py" \
19
+ python -B -m unittest discover -s tests/usage/evidence -v || exit 1
20
+ done
21
+ ```
22
+
23
+ The fixtures contain bounded synthetic questions, their independent expectations,
24
+ and selected public-fact reply shapes. Tests of the local appliance HTTP adapter,
25
+ Mini authority, workflow tools and data-transfer extension are not part of this
26
+ source contribution. No historical verdict or release hash is rewritten.
tests/usage/composition/test_energy_repair.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Independent requested-quantity checks for the actual v3 organ chain."""
2
+ import importlib
3
+ import json
4
+ import math
5
+ import os
6
+ from pathlib import Path
7
+ import sys
8
+ import unittest
9
+
10
+ ROOT = Path(__file__).resolve().parents[3]
11
+ DIRECTORY = Path(os.environ.get('ENERGY_ORGAN_DIRECTORY', ROOT / 'root/specialists/energy-v3'))
12
+ sys.path.insert(0, str(DIRECTORY))
13
+ from organ_energy_v3 import EnergyOrganV3
14
+
15
+
16
+ class EnergyRepairTests(unittest.TestCase):
17
+ def setUp(self):
18
+ self.organ = EnergyOrganV3()
19
+
20
+ def value(self, q, expected, kind=None):
21
+ reply = self.organ.answer(q)
22
+ self.assertIsNotNone(reply, q)
23
+ self.assertEqual(reply['status'], 'answered', reply)
24
+ self.assertAlmostEqual(reply['value'], expected, msg=reply)
25
+ self.assertTrue(math.isfinite(reply['value']))
26
+ if kind: self.assertEqual(reply['kind'],kind)
27
+ return reply
28
+
29
+ def refusal(self, q):
30
+ reply = self.organ.answer(q)
31
+ self.assertTrue(reply is None or reply['status'] != 'answered', reply)
32
+ return reply
33
+
34
+ def test_original_six_energy_failures(self):
35
+ results = {'solar_useful':1.6, 'net_energy':3.5, 'battery_roles':7.2,
36
+ 'battery_plain':7.2, 'battery_rating_first':7.2, 'battery_invalid':None}
37
+ for file in ['composition/cases.json','engineering/cases.json','engineering/followup_cases.json']:
38
+ for c in json.loads((ROOT/'tests/usage/fixtures'/file).read_text()):
39
+ if c['id'] in results:
40
+ with self.subTest(case=c['id']):
41
+ value=results[c['id']]
42
+ if value is None: self.refusal(c['text'])
43
+ else: self.value(c['text'],value)
44
+
45
+ def test_runtime_role_permutations_and_kw_units(self):
46
+ for detail in ['with a 500 W rated inverter run a 100 W load',
47
+ 'run a 100 W load through a 500 W rated inverter',
48
+ 'with an inverter rated at 0.5 kW run a 0.1 kW load']:
49
+ self.value('How long can a 1000 Wh battery '+detail+' with 90% inverter efficiency and 80% depth of discharge?',7.2,'runtime')
50
+ self.value('How long can a 24 V 50 Ah battery run a 200 W load with efficiency 0.9 and usable fraction 0.8?',4.32)
51
+
52
+ def test_zero_and_full_factors(self):
53
+ for eff,depth,value in [(0,80,0),(90,0,0),(100,100,10)]:
54
+ self.value(f'How long can a 1000 Wh battery run a 100 W load with {eff}% efficiency and {depth}% depth of discharge?',value)
55
+
56
+ def test_bad_factor_ranges_and_unbound_conditions(self):
57
+ for adjustment in ['120% efficiency','-10% efficiency','110% depth of discharge',
58
+ '90% charging efficiency','90% motor efficiency','20% reserve',
59
+ 'inverter efficiency unspecified','temperature derate 0.8',
60
+ 'half full','a variable load','0.8 state of charge']:
61
+ with self.subTest(adjustment=adjustment):
62
+ self.refusal('How long can a 1000 Wh battery run a 100 W load with '+adjustment+'?')
63
+
64
+ def test_loss_is_not_multiplied_twice(self):
65
+ self.value('How long can a 1000 Wh battery run a 100 W load with 10% inverter losses and 80% depth of discharge?',7.2)
66
+ self.value('How long can a 1000 Wh battery run a 100 W load with 90% efficiency and 10% losses?',9)
67
+ self.refusal('How long can a 1000 Wh battery run a 100 W load with 90% efficiency and 20% losses?')
68
+ self.refusal('How long can a 1000 Wh battery run a 100 W load with 90% efficiency and 80% efficiency?')
69
+
70
+ def test_power_constraints_and_ambiguity(self):
71
+ for q in ['How long can a 1000 Wh battery run a 600 W load through a 500 W inverter?',
72
+ 'How long can a 1000 Wh battery run a 0 W load?',
73
+ 'How long can a 1000 Wh battery run a -100 W load?',
74
+ 'How long can a 1000 Wh battery run a 100 W fan and a 200 W heater?',
75
+ 'How long can a 1000 Wh battery with a 500 W inverter run?',
76
+ 'How long can a 1000 Wh battery run a 100 W load with a 5 W standby draw?']:
77
+ self.refusal(q)
78
+
79
+ def test_conflicting_capacity_and_negative_values(self):
80
+ self.refusal('How long can a 1000 Wh battery with 12 V and 10 Ah run a 100 W load?')
81
+ self.refusal('How long can a -1000 Wh battery run a 100 W load?')
82
+ self.value('How long can a 120 Wh battery with 12 V and 10 Ah run a 100 W load?',1.2)
83
+
84
+ def test_remaining_uses_battery_side_withdrawal(self):
85
+ r=self.value('A battery initially stores 5 kWh. A 500 W load runs for 3 hours with 90% inverter efficiency. How much energy remains in the battery?',5-1.5/0.9)
86
+ self.assertIn('remaining energy',r['response'])
87
+ r=self.value('A battery initially stores 5 kWh. A 500 W load runs for 5 hours with 80% depth of discharge. How much usable energy remains?',1.5)
88
+ self.assertIn('usable remaining',r['response'])
89
+
90
+ def test_remaining_requires_initial_and_feasible_duration(self):
91
+ for q in ['A 5 kWh battery supplies a 500 W load for 3 hours. How much energy remains?',
92
+ 'A battery initially stores 1 kWh. A 500 W load runs for 3 hours. How much energy remains?',
93
+ 'A battery initially stores 1 kWh. A 500 W load runs for -1 hours. How much energy remains?',
94
+ 'A battery initially stores 1 kWh. A 500 W load runs for 1 hours with 0% efficiency. How much energy remains?',
95
+ 'A battery initially stores 1 kWh. A 500 W load runs for 1 hours and then 2 hours. How much energy remains?']:
96
+ self.refusal(q)
97
+
98
+ def test_storage_from_power_or_energy(self):
99
+ self.value('A 400 W solar panel produces power for 5 hours with 80% charging efficiency. How much energy is stored?',1.6)
100
+ self.value('A solar panel produces 2 kWh. A battery stores 80% of this energy. What is the stored energy?',1.6)
101
+ self.value('A 400 W solar panel gets 5 peak sun hours. A battery stores 80% of this energy. What is the stored energy?',1.6)
102
+ self.value('3 panels each produce 400 W for 5 hours. A battery stores 80% of this energy. What is the stored energy?',4.8)
103
+ self.value('A 400 W solar panel produces power for 5 hours. Ignoring losses, how much energy is stored?',2)
104
+
105
+ def test_storage_unbound_and_conflicting_conditions(self):
106
+ for q in ['A 400 W solar panel runs for 5 hours. What is the stored energy?',
107
+ 'A 400 W solar panel runs for 5 hours. A battery stores 120% of this energy. What is the stored energy?',
108
+ 'A 400 W solar panel runs for 5 hours. A battery stores 80% of this energy with 50% depth of discharge. What is the stored energy?',
109
+ '3 panels produce 400 W for 5 hours. A battery stores 80% of this energy. What is the stored energy?',
110
+ 'A 400 W solar panel runs for -5 hours. A battery stores 80% of this energy. What is the stored energy?',
111
+ 'A 400 W solar panel runs for 5 hours. A battery stores 80% of this energy at 90% storage efficiency. What is the stored energy?']:
112
+ self.refusal(q)
113
+
114
+ def test_changed_numbers_check_arithmetic_independently(self):
115
+ # Independent arithmetic, outside the shipped examples/templates.
116
+ for cap,load,eff,depth in [(1630,270,86,70),(300,50,75,40),(4200,350,95,90)]:
117
+ self.value(f'How long can a {cap} Wh battery run a {load} W load with {eff}% inverter efficiency and {depth}% depth of discharge?', cap*eff*depth/(10000*load))
118
+
119
+ def test_malformed_groups_and_multiple_sources_do_not_salvage_a_number(self):
120
+ for q in ['A battery initially stores 1,2 kWh. A 100 W load runs for 1 hour. How much energy remains?',
121
+ 'How long can a 1,2 kWh battery run a 100 W load?',
122
+ 'How long can a 1000 Wh battery run a 1,00 W load?',
123
+ 'How long can a 1000 Wh battery run a 100 W load with 90,5% efficiency?',
124
+ 'How long can two 1000 Wh batteries run a 100 W load?',
125
+ 'How long can 2 batteries each 1000 Wh run a 100 W load?',
126
+ 'How long can a 1000 Wh battery run two 100 W fans?']:
127
+ self.refusal(q)
128
+ self.value('How long can a 1,000 Wh battery run a 100 W load?',10)
129
+
130
+
131
+ if __name__ == '__main__': unittest.main()
tests/usage/computing/test_base_conversion.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Narrow regression checks; no engine startup or specialist panel claim."""
2
+ import importlib.util
3
+ import os
4
+ from pathlib import Path
5
+ import unittest
6
+
7
+ ROOT = Path(__file__).resolve().parents[3]
8
+ MODULE = Path(os.environ.get('ORACLE_OPERATOR_SPECS', ROOT / 'root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_operator_specs_v1.py'))
9
+
10
+
11
+ def load(name, path):
12
+ spec = importlib.util.spec_from_file_location(name, path)
13
+ module = importlib.util.module_from_spec(spec)
14
+ spec.loader.exec_module(module)
15
+ return module
16
+
17
+
18
+ op = load('computing_test_operator', MODULE)
19
+
20
+
21
+ class BaseConversionRepair(unittest.TestCase):
22
+ def check(self, prompt, expected, value, source):
23
+ args = op._base_parse(prompt)
24
+ self.assertEqual((args['n'], args['frm']), (value, source))
25
+ actual, _ = op._base_fn(args)
26
+ self.assertEqual(actual, expected)
27
+ self.assertEqual(op._base_ref(args), expected)
28
+
29
+ def test_real_wrong_answer_and_explicit_source(self):
30
+ self.check('Convert binary 11111111 to decimal.', '255', 255, 2)
31
+ self.check('Convert 11111111 from base 2 to base 10.', '255', 255, 2)
32
+ self.check('Convert 17 from base 8 to base 10.', '15', 15, 8)
33
+ self.check('Convert 0010 from base 2 to base 10.', '2', 2, 2)
34
+
35
+ def test_hexadecimal_words_and_prefix(self):
36
+ for text in ('Convert hex ff to binary.', 'Convert ff from hexadecimal to binary.',
37
+ 'What is 0xff in binary?', 'Convert +0xff to binary.'):
38
+ self.check(text, '11111111', 255, 16)
39
+ self.check('Convert 0b1010 to decimal.', '10', 10, 2)
40
+ self.check('Convert 0o17 to decimal.', '15', 15, 8)
41
+ self.check('Convert 1010 in binary to decimal.', '10', 10, 2)
42
+
43
+ def test_decimal_control_and_zero(self):
44
+ self.check('Convert 255 to hexadecimal.', 'ff', 255, 10)
45
+ self.check('Write 0 in binary.', '0', 0, 10)
46
+ self.check('Express +15 as hex.', 'f', 15, 10)
47
+ self.check('Convert decimal 255 to hex.', 'ff', 255, 10)
48
+
49
+ def test_declines_invalid_digits_signs_and_conflicting_bases(self):
50
+ for text in ('Convert 102 from base 2 to base 10.', 'Convert binary 102 to decimal.',
51
+ 'Convert -10 to binary.', 'Convert -0xff to binary.',
52
+ 'Convert 0x to decimal.', 'Convert 19 from base 8 to decimal.',
53
+ 'Convert 10 from base 1 to decimal.', 'Convert 10 to base 37.',
54
+ 'Convert binary 11 from base 8 to decimal.',
55
+ 'Convert decimal 0xff to binary.'):
56
+ with self.subTest(text=text), self.assertRaises(op.OutOfFragment):
57
+ op._base_parse(text)
58
+
59
+ def test_never_takes_a_suffix_of_malformed_or_compound_input(self):
60
+ for text in ('Convert - 10 to binary.', 'Convert 12.5 to binary.',
61
+ 'Convert 10 to binary and 20 to hexadecimal.',
62
+ 'Convert 1011/2 to decimal.', 'Convert 0b102 to decimal.',
63
+ 'Convert --10 to binary.', 'Convert ++10 to binary.'):
64
+ with self.subTest(text=text), self.assertRaises(op.OutOfFragment):
65
+ op._base_parse(text)
66
+
67
+ def test_every_source_target_base_with_independent_integer_values(self):
68
+ # 35*35*5 known source values; output decoding is stdlib int rather
69
+ # than either target-encoding implementation under test.
70
+ for source in range(2, 37):
71
+ for target in range(2, 37):
72
+ for digits in ('0', '1', '10', '11', '101'):
73
+ args = op._base_parse(f'Convert {digits} from base {source} to base {target}.')
74
+ expected = {'0': 0, '1': 1, '10': source,
75
+ '11': source+1, '101': source*source+1}[digits]
76
+ output, _ = op._base_fn(args)
77
+ self.assertEqual(int(output, target), expected)
78
+ self.assertEqual(op._base_ref(args), output)
79
+
80
+ def test_arithmetic_does_not_reuse_a_corrupted_decimal_binding(self):
81
+ args = op._base_parse('Convert binary 11111111 to decimal.')
82
+ args['n'] = 11111111
83
+ self.assertEqual(op._base_fn(args)[0], '255')
84
+ self.assertEqual(op._base_ref(args), '255')
85
+
86
+
87
+ if __name__ == '__main__':
88
+ unittest.main()
tests/usage/conversation/birth-replies.json ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "text": "When was Albert Einstein born?",
4
+ "reply": {
5
+ "status": "answered",
6
+ "path": "family_precedence>specialist_call>specialist:rel-engineers>engineers",
7
+ "response": "14 March 1879. From the Wikidata relation store: Albert Einstein (Q937) - year of birth (P569) - 14 March 1879. [rel-engineers]",
8
+ "provenance": {
9
+ "kind": "family_precedence",
10
+ "replaced_route": "capability:when_did",
11
+ "replaced_response": "[ev:direct] According to Albert Einstein, Albert Einstein (14 March 1879 – 18 April 1955) was a German-born Jewish scientist. [simplewiki_a9a88ffc711143a397966482] Albert Einstein also records: Einstein was born in Ulm, Württemberg, Germany, on 14 March 1879. [simplewiki_e131399c5b98026c642abcac]",
12
+ "specialist": "rel-engineers",
13
+ "chat_resolved": null
14
+ },
15
+ "latency_ms": 0.2,
16
+ "zero_model_gate": {
17
+ "lm_calls": 0,
18
+ "transformer_calls": 0
19
+ },
20
+ "evidential": {
21
+ "input_marked": null,
22
+ "answer_licensed": "direct"
23
+ },
24
+ "continuation": {
25
+ "doors": [
26
+ "germany",
27
+ "scientist"
28
+ ],
29
+ "offer": "from here, germany or scientist stand open"
30
+ },
31
+ "references": []
32
+ },
33
+ "receipt": "var/usage-20261026/evidence/run-1/results.jsonl"
34
+ },
35
+ {
36
+ "text": "When was John Williams born?",
37
+ "reply": {
38
+ "status": "answered",
39
+ "path": "family_precedence>specialist_call>specialist:rel-composers>composers",
40
+ "response": "8 February 1932. From the Wikidata relation store: John Williams (Q131285) - year of birth (P569) - 8 February 1932. [rel-composers]",
41
+ "provenance": {
42
+ "kind": "family_precedence",
43
+ "replaced_route": "capability:when_did",
44
+ "replaced_response": "[ev:direct] According to John Williams, John Towner Williams (born February 8, 1932) is an American composer and conductor. [simplewiki_8cb1cf64a055db807a2b8983] John Williams also records: John Towner Williams was born in Queens, New York City, New York on February 8, 1932. [simplewiki_26349c5d4659",
45
+ "specialist": "rel-composers",
46
+ "chat_resolved": null
47
+ },
48
+ "latency_ms": 0.1,
49
+ "zero_model_gate": {
50
+ "lm_calls": 0,
51
+ "transformer_calls": 0
52
+ },
53
+ "evidential": {
54
+ "input_marked": null,
55
+ "answer_licensed": "direct"
56
+ },
57
+ "continuation": {
58
+ "doors": [
59
+ "city",
60
+ "composer",
61
+ "conductor"
62
+ ],
63
+ "offer": "from here, city or composer stand open"
64
+ },
65
+ "references": []
66
+ },
67
+ "receipt": "var/usage-20261026/research/run-1/results.jsonl"
68
+ }
69
+ ]
tests/usage/conversation/test_state.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Scoped legacy-state regressions; no engine, corpus writes, or proof claims."""
2
+ import importlib.util
3
+ import json
4
+ from pathlib import Path
5
+ import sys
6
+ import types
7
+ import unittest
8
+ from unittest.mock import patch
9
+
10
+ ROOT = Path(__file__).resolve().parents[3]
11
+ spec = importlib.util.spec_from_file_location('conversation_reconciliation', ROOT/'root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/runtime_conversation_state.py')
12
+ helper = importlib.util.module_from_spec(spec)
13
+ spec.loader.exec_module(helper)
14
+
15
+
16
+ class ConversationTests(unittest.TestCase):
17
+ def setUp(self):
18
+ self.modules = patch.dict(sys.modules, {'relation_spec_v1': types.SimpleNamespace()})
19
+ self.modules.start()
20
+ path = ROOT/'root/wander-train/chat_context_v2.py'
21
+ spec = importlib.util.spec_from_file_location('chat_context_v2', path)
22
+ self.cc = importlib.util.module_from_spec(spec)
23
+ spec.loader.exec_module(self.cc)
24
+ sys.modules['chat_context_v2'] = self.cc
25
+ self.cc.recognised = lambda q: q.startswith(('Who', 'When', 'What'))
26
+ self.cc._entity_of_question = lambda q: None
27
+ self.cc._entity_of_reply = lambda reply, prefer_person=False: (1, 'Jane Austen', True) if reply.startswith('Jane Austen') else None
28
+ self.rt = types.SimpleNamespace(_last_value={}, _v4_disc={})
29
+
30
+ def tearDown(self):
31
+ self.modules.stop()
32
+
33
+ def turn(self, text, reply, sid='s', supported=True, mutate=None, runtime=None):
34
+ rt = runtime or self.rt
35
+ frame = helper.begin(rt, text, sid)
36
+ if mutate:
37
+ mutate(rt, self.cc.state(sid))
38
+ helper.finish(rt, text, reply, frame, supported)
39
+ helper.release(frame)
40
+ return rt._conversation_contexts[sid]
41
+
42
+ def test_final_author_supports_pronoun(self):
43
+ self.turn('Who wrote Emma?', {'status':'answered','path':'organ:relation>form:author','response':'Jane Austen.'})
44
+ frame = helper.begin(self.rt, 'When was she born?', 's')
45
+ self.assertEqual(self.cc._person(self.cc.state('s')), 'Jane Austen')
46
+ helper.release(frame)
47
+ self.assertNotIn('s', self.cc._STATES)
48
+
49
+ def test_unknown_new_topic_discards_previous_person_and_number(self):
50
+ self.turn('Who wrote Emma?', {'status':'answered','path':'specialist_call','response':'Jane Austen.'})
51
+ self.rt._last_value['s'] = 12
52
+ self.rt._v4_disc['s'] = {'entity':'Jane Austen'}
53
+ state = self.turn('New topic: who wrote Zeralune?', {'status':'withheld','path':'factoid:withheld'}, supported=False)
54
+ self.assertIsNone(self.cc._person(state))
55
+ self.assertNotIn('s', self.rt._last_value)
56
+ self.assertNotIn('s', self.rt._v4_disc)
57
+
58
+ def test_final_scalar_replaces_stale_number(self):
59
+ self.rt._last_value['s'] = 12
60
+ r={'status':'answered','path':'organ:math>evaluate','response':'[ev:derived] 126. In steps: read 126; then evaluate.',
61
+ 'provenance':{'kind':'organ_math','directive':'evaluate'}}
62
+ state=self.turn('Correction: calculate 21 * 6 instead.',r)
63
+ self.assertEqual(self.rt._last_value['s'],126)
64
+ self.assertEqual(state.result,(126,''))
65
+
66
+ def test_quantity_keeps_unit_without_becoming_dimensionless_scalar(self):
67
+ r={'status':'answered','path':'specialist_call','response':'The answer is 300 kWh.'}
68
+ state=self.turn('What is the energy for 10 hours?',r)
69
+ self.assertEqual(state.result,(300,'kwh'))
70
+ self.assertNotIn('s', self.rt._last_value)
71
+
72
+ def test_acknowledgement_preserves_context(self):
73
+ self.turn('Who wrote Emma?', {'status':'answered','path':'specialist_call','response':'Jane Austen.'})
74
+ state=self.turn('Thanks', {'status':'answered','path':'act:acknowledge','response':'You are welcome.'})
75
+ self.assertEqual(self.cc._person(state),'Jane Austen')
76
+
77
+ def test_clarification_does_not_learn_speculative_person(self):
78
+ def speculate(rt,state): state.ans=(2,'Wrong Person',True)
79
+ state=self.turn('Who wrote unknown?',{'status':'clarification','path':'act:clarify'},supported=False,mutate=speculate)
80
+ self.assertIsNone(state.ans)
81
+
82
+ def test_same_session_id_in_two_instances_is_isolated(self):
83
+ self.turn('Who wrote Emma?', {'status':'answered','path':'specialist_call','response':'Jane Austen.'})
84
+ other=types.SimpleNamespace(_last_value={},_v4_disc={})
85
+ outer=helper.begin(self.rt,'When was she born?','s')
86
+ nested=helper.begin(other,'When was she born?','s')
87
+ self.assertIsNone(self.cc._person(self.cc.state('s')))
88
+ helper.release(nested)
89
+ self.assertEqual(self.cc._person(self.cc.state('s')),'Jane Austen')
90
+ helper.release(outer)
91
+
92
+ def test_different_sessions_and_nested_turn_restore_ambient_context(self):
93
+ self.turn('Who wrote Emma?', {'status':'answered','path':'specialist_call','response':'Jane Austen.'},sid='a')
94
+ a=helper.begin(self.rt,'When was she born?','a')
95
+ b=helper.begin(self.rt,'When was she born?','b')
96
+ self.assertIsNone(self.cc._person(self.cc.state('b')))
97
+ helper.release(b)
98
+ self.assertEqual(self.cc._person(self.cc.state('a')),'Jane Austen')
99
+ helper.release(a)
100
+ self.assertEqual(self.cc._STATES,{})
101
+
102
+ def test_real_direct_birth_replies_with_null_chat_resolution(self):
103
+ rows = json.loads((Path(__file__).with_name('birth-replies.json')).read_text())
104
+ for row in rows:
105
+ with self.subTest(question=row['text']):
106
+ self.assertIsNone(row['reply']['provenance']['chat_resolved'])
107
+ state = self.turn(row['text'], row['reply'])
108
+ self.assertEqual(state.q, row['text'])
109
+ self.assertNotIn('s', self.cc._STATES)
110
+
111
+ def test_optional_rewrite_metadata_is_typed_and_nonempty(self):
112
+ for prov in (None, [], 'none', {'chat_resolved': None}, {'chat_resolved': []},
113
+ {'chat_resolved': {'read_as': None}}, {'chat_resolved': {'read_as': ''}}):
114
+ with self.subTest(provenance=prov):
115
+ state = self.turn('When was Jane Austen born?',
116
+ {'status':'answered','path':'specialist_call','response':'16 December 1775.', 'provenance':prov})
117
+ self.assertEqual(state.q, 'When was Jane Austen born?')
118
+
119
+ def test_not_a_scalar_route_and_nonfinite_or_unit_answer_never_become_number(self):
120
+ for path,body in [('pretend-compute:verified','The answer is 7.'),('organ:math>evaluate','[ev:derived] 1e999. In steps:'),('organ:math>evaluate','[ev:derived] 3 kg. In steps:')]:
121
+ r={'status':'answered','path':path,'response':body,'provenance':{'kind':'organ_math','directive':'evaluate'}}
122
+ self.rt._last_value['s']=12
123
+ self.turn('Calculate 3',r)
124
+ self.assertNotIn('s',self.rt._last_value)
125
+
126
+ if __name__=='__main__': unittest.main()
tests/usage/documents/test_conflict_repair.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Untrusted isolated-method regression, not a full-engine or panel test.
2
+
3
+ Load actual passage helpers using AST, without importing the heavyweight engine.
4
+ WordNet aliases are disabled: these regressions all use literal matching terms.
5
+ Reads the current checkout; no source mutation.
6
+ """
7
+ import ast
8
+ from pathlib import Path
9
+ import re
10
+ import time
11
+ import types
12
+ import unittest
13
+
14
+ ROOT = Path(__file__).resolve().parents[3]
15
+ REL = Path('root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted')
16
+ CANDIDATE = ROOT
17
+
18
+
19
+ def definitions(source, names, namespace):
20
+ nodes = []
21
+ for node in ast.parse(source).body:
22
+ if isinstance(node, (ast.FunctionDef, ast.ClassDef)) and node.name in names:
23
+ nodes.append(node)
24
+ elif isinstance(node, ast.Assign) and any(isinstance(t, ast.Name) and t.id in names for t in node.targets):
25
+ nodes.append(node)
26
+ exec(compile(ast.Module(nodes, type_ignores=[]), '<isolated-source>', 'exec'), namespace)
27
+
28
+
29
+ def reader(source):
30
+ ns420 = {'re': re}
31
+ definitions((CANDIDATE / REL / 'oracle_release_runtime_w420.py').read_text(), {'IRREGULAR', '_stem', '_toks'}, ns420)
32
+ ns434 = {'re': re, '_w420': types.SimpleNamespace(_toks=ns420['_toks'])}
33
+ names = {'_stem', '_in_order', 'NEG', 'NEG_TOK', 'MODAL', 'PRESENT_Q', 'PAST', 'PRESENT', 'CMP_Q', 'IDENTITY', 'OB_Q', 'OB_S', 'TEMP_Q', 'TEMP_S', 'SUP_Q', 'HEDGE_S', 'USE_ORDER', 'USE_TEMPORAL', 'USE_SUPERL'}
34
+ text434 = (CANDIDATE / REL / 'oracle_release_runtime_w434.py').read_text()
35
+ definitions(text434, names, ns434)
36
+ cls434 = next(n for n in ast.parse(text434).body if isinstance(n, ast.ClassDef))
37
+ method = next(n for n in cls434.body if isinstance(n, ast.FunctionDef) and n.name == '_r434_verdict')
38
+ exec(compile(ast.Module([method], type_ignores=[]), '<actual-verdict>', 'exec'), ns434)
39
+ class Base:
40
+ _r434_verdict = ns434['_r434_verdict']
41
+ def _r434_receipt(self, out, res, text, reason):
42
+ return {'status': 'withheld', 'response': text, 'reason': reason}
43
+ ns = {'re': re, 'time': time, '_Base': Base}
44
+ definitions(source, {'PASSAGE', 'YESNO_HEAD', 'STOP', 'REQUIRED', '_toks', '_stem', '_sentences', '_qterms', 'OracleReleaseRuntimeW442'}, ns)
45
+ instance = ns['OracleReleaseRuntimeW442']()
46
+ instance._r436_aliases = lambda term: {}
47
+ return instance
48
+
49
+
50
+ class ConflictTests(unittest.TestCase):
51
+ @classmethod
52
+ def setUpClass(cls):
53
+ cls.after = reader((CANDIDATE / REL / 'oracle_release_runtime_w442.py').read_text())
54
+
55
+ def ask(self, engine, text):
56
+ return engine._r434_passage('Passage: '+text+' Question: Is the Lumen Archive open on Sunday?', {}, time.time())
57
+
58
+ def test_reproduces_and_repairs_both_orders(self):
59
+ yes = 'Version one says the Lumen Archive is open on Sunday.'
60
+ no = 'Version two says the Lumen Archive is not open on Sunday.'
61
+ for text in (yes+' '+no, no+' '+yes):
62
+ result = self.ask(self.after, text)
63
+ self.assertEqual(result['status'], 'withheld')
64
+ self.assertEqual(result['reason'], 'conflicting_covering_sentences')
65
+ self.assertEqual({r['text'] for r in result['references']}, {yes, no})
66
+
67
+ def test_preserves_nonconflicting_answers(self):
68
+ for text, expected in (
69
+ ('The Lumen Archive is open on Sunday.', 'Yes.'),
70
+ ('The Lumen Archive is not open on Sunday.', 'No.'),
71
+ ('The Lumen Archive is open on Sunday. The Cedar Archive is not open on Sunday.', 'Yes.'),
72
+ ('The Lumen Archive is open on Sunday. The Lumen Archive is open on Sunday.', 'Yes.'),
73
+ ):
74
+ self.assertTrue(self.ask(self.after, text)['response'].startswith(expected))
75
+
76
+ def test_preserves_missing_and_modal_refusals(self):
77
+ for text in ('The Lumen Archive is in Alderport.', 'The Lumen Archive will open on Sunday.'):
78
+ self.assertEqual(self.ask(self.after, text)['status'], 'withheld')
79
+
80
+
81
+ if __name__ == '__main__':
82
+ unittest.main()
tests/usage/engineering/test_condition_attachment.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Narrow source regression: trailing physical assumptions remain with their question."""
2
+ import importlib.util
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ import unittest
7
+
8
+ ROOT = Path(__file__).resolve().parents[3]
9
+ SLIM = Path(os.environ.get("ENGINEERING_SLIM_ROOT", ROOT))
10
+ SOURCE = SLIM / "root/specialists/_gate/combo_runtime_v1.py"
11
+
12
+
13
+ def load_source():
14
+ spec = importlib.util.spec_from_file_location("engineering_condition_combo", SOURCE)
15
+ module = importlib.util.module_from_spec(spec)
16
+ spec.loader.exec_module(module)
17
+ return module
18
+
19
+
20
+ @unittest.skipUnless(SOURCE.is_file(), "requires private Slim source (set ENGINEERING_SLIM_ROOT)")
21
+ class ConditionAttachmentTests(unittest.TestCase):
22
+ @classmethod
23
+ def setUpClass(cls):
24
+ cls.combo = load_source()
25
+ cases = json.loads((ROOT / "tests/usage/fixtures/engineering/cases.json").read_text())
26
+ cls.heat = next(case["text"] for case in cases if case["id"] == "heat_endpoints")
27
+
28
+ def test_exact_observed_heat_failure_through_real_thermo(self):
29
+ directory = SLIM / "root/specialists/thermo"
30
+ combo = self.combo.Combo([str(directory / "manifest.json")], [str(directory)])
31
+ reply = combo.reply(self.heat)
32
+ self.assertEqual(reply["status"], "answered")
33
+ self.assertEqual(reply["path"], "specialist:thermo>sensible_heat")
34
+ self.assertIn("5.016e+05 J", reply["response"])
35
+ self.assertIn("4180", reply["response"])
36
+ self.assertNotIn("4186", reply["response"])
37
+ self.assertNotIn("I need", reply["response"])
38
+
39
+ def test_real_combo_keeps_independent_second_task_visible(self):
40
+ directory = SLIM / "root/specialists/thermo"
41
+ combo = self.combo.Combo([str(directory / "manifest.json")], [str(directory)])
42
+ reply = combo.reply(self.heat + " What is 2+3?")
43
+ parts = reply["provenance"]["parts"]
44
+ self.assertEqual(len(parts), 2)
45
+ self.assertIn("4180", parts[0]["response"])
46
+ self.assertEqual(parts[1]["part"], "What is 2+3?")
47
+ self.assertIn("What is 2+3?", reply["response"])
48
+
49
+ def test_real_combo_retains_elliptical_followup(self):
50
+ directory = SLIM / "root/specialists/thermo"
51
+ combo = self.combo.Combo([str(directory / "manifest.json")], [str(directory)])
52
+ reply = combo.reply(self.heat + " How much?")
53
+ self.assertEqual(reply["path"], "specialist:thermo>sensible_heat")
54
+ self.assertIn("5.016e+05 J", reply["response"])
55
+
56
+ def test_full_short_question_before_thermo_not_swallowed(self):
57
+ directory = SLIM / "root/specialists/thermo"
58
+ combo = self.combo.Combo([str(directory / "manifest.json")], [str(directory)])
59
+ reply = combo.reply("Who wrote Hamlet? " + self.heat)
60
+ parts = reply["provenance"]["parts"]
61
+ self.assertEqual(len(parts), 2)
62
+ self.assertEqual(parts[0]["part"], "Who wrote Hamlet?")
63
+ self.assertIn("5.016e+05 J", parts[1]["response"])
64
+
65
+ def test_constant_load_request_through_real_energy(self):
66
+ cases = json.loads((ROOT / "tests/usage/fixtures/engineering/cases.json").read_text())
67
+ question = next(case["text"] for case in cases if case["id"] == "battery_roles")
68
+ directory = SLIM / "root/specialists/energy-v3"
69
+ combo = self.combo.Combo([str(directory / "manifest.json")], [str(directory)])
70
+ reply = combo.reply(question)
71
+ self.assertEqual(reply["path"], "specialist:energy-v3>runtime")
72
+ self.assertIn("7.2 hours", reply["response"])
73
+ self.assertIn("100 W load", reply["response"])
74
+ self.assertIn("500 W inverter rating", reply["response"])
75
+
76
+ def test_named_constant_conditions_attach(self):
77
+ for condition in ("The load is constant.", "The specific heat remains fixed.",
78
+ "Flow rate stays steady throughout.", "The gravity is constant."):
79
+ with self.subTest(condition=condition):
80
+ q = "How long can the battery run? " + condition
81
+ self.assertEqual(self.combo._split_parts(q), [q])
82
+
83
+ def test_declarative_condition_does_not_absorb_second_task(self):
84
+ first = "How long can the battery run? The load is constant."
85
+ second = "What is 2+3?"
86
+ self.assertEqual(self.combo._split_parts(first + " " + second), [first, second])
87
+ for other in ("The load is constant; calculate its current.", "The load is a tree.",
88
+ "The capital is Constant.", "The temperature is rising.",
89
+ "The load is constant and what is its current?"):
90
+ with self.subTest(other=other):
91
+ self.assertEqual(self.combo._split_parts("How long? " + other), ["How long?", other])
92
+
93
+ def test_condition_forms_attach(self):
94
+ for condition in ("Use c=4180.", "Using c=4180.", "Assume no losses.",
95
+ "Assuming no losses.", "Take gravity as 9.81.", "Given c=4180.",
96
+ "With no phase change.", "Without friction.", "Ignore heat loss.",
97
+ "Neglect pipe friction.", "Please use c=4180."):
98
+ q = "How much heat? " + condition
99
+ with self.subTest(condition=condition):
100
+ self.assertEqual(self.combo._split_parts(q), [q])
101
+
102
+ def test_multiple_conditions_and_second_question_preserved(self):
103
+ first = "How much heat? Use c=4180. Ignore heat loss."
104
+ second = "What is 2 + 3?"
105
+ self.assertEqual(self.combo._split_parts(first + " " + second), [first, second])
106
+
107
+ def test_second_question_with_its_own_premise_not_absorbed(self):
108
+ first = "What is 2 + 3?"
109
+ second = "Given 2 kg of water. How much heat raises it by 10 K?"
110
+ self.assertEqual(self.combo._split_parts(first + " " + second), [first, second])
111
+
112
+ def test_new_task_disguised_as_condition_not_absorbed(self):
113
+ for second in ("Use a calculator to compute 2+3.", "Use c=4180 and convert 2 m to cm.",
114
+ "Assume no losses and find the motor power.", "Use c=4180; what is its unit?",
115
+ "Take the square root of 9.", "Take a derivative of sin(x).",
116
+ "Assume x=2 and evaluate x+1.", "Use a dictionary."):
117
+ with self.subTest(second=second):
118
+ self.assertEqual(self.combo._split_parts("How much heat? " + second), ["How much heat?", second])
119
+
120
+ def test_plain_independent_questions_still_split(self):
121
+ self.assertEqual(self.combo._split_parts("What is 2+3? What is 4+5?"), ["What is 2+3?", "What is 4+5?"])
122
+
123
+ def test_quoted_and_code_questions_not_boundaries(self):
124
+ for quote in ('"why? use c=1."', "`why? use c=1.`", "'why? use c=1.'"):
125
+ first = "What does " + quote + " mean?"
126
+ self.assertEqual(self.combo._split_parts(first + " What is 2+3?"), [first, "What is 2+3?"])
127
+
128
+ def test_quoted_question_inside_condition_retained(self):
129
+ q = 'How much heat? Use c=4180 with label "why? answer." for the sample.'
130
+ self.assertEqual(self.combo._split_parts(q), [q])
131
+
132
+ def test_decimal_and_semicolon_conditions_retained(self):
133
+ q = "How much heat? Use specific heat 4.18 kJ/(kg K); ignore losses."
134
+ self.assertEqual(self.combo._split_parts(q), [q])
135
+
136
+ def test_unrelated_declarative_tail_stays_visible(self):
137
+ self.assertEqual(self.combo._split_parts("How much heat? Copper has atomic number 29."),
138
+ ["How much heat?", "Copper has atomic number 29."])
139
+
140
+
141
+ if __name__ == "__main__":
142
+ unittest.main()
tests/usage/evidence/test_math_finite.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Focused source repair regression; no corpus, native build or specialist panel.
2
+
3
+ Set MATH_ORGAN_SOURCE to test a provisioned copy. The default is this checkout. This file does not mutate or import the full engine.
4
+ """
5
+ import importlib.util
6
+ import os
7
+ import sys
8
+ from pathlib import Path
9
+ import unittest
10
+ from unittest.mock import patch
11
+
12
+ ROOT = Path(__file__).resolve().parents[3]
13
+ SOURCE = Path(os.environ.get('MATH_ORGAN_SOURCE', ROOT / 'root/wander-train/math_organ_v1.py'))
14
+
15
+
16
+ @unittest.skipUnless(SOURCE.is_file(), 'optional appliance source not provisioned')
17
+ class FiniteMathTests(unittest.TestCase):
18
+ @classmethod
19
+ def setUpClass(cls):
20
+ helper_spec = importlib.util.spec_from_file_location('finite_math_helper', SOURCE.parent / 'math_organ_v1.py')
21
+ helper = importlib.util.module_from_spec(helper_spec)
22
+ helper_spec.loader.exec_module(helper)
23
+ spec = importlib.util.spec_from_file_location('finite_math_organ', SOURCE)
24
+ cls.module = importlib.util.module_from_spec(spec)
25
+ # v2/v3 share the helper from their own provisioned source directory.
26
+ with patch.dict(sys.modules, {'math_organ_v1': helper}):
27
+ spec.loader.exec_module(cls.module)
28
+ # No corpus opening: exercise the answer pipeline with method lookup absent.
29
+ cls.organ = cls.module.MathOrgan.__new__(cls.module.MathOrgan)
30
+ cls.organ.db = None
31
+ cls.organ.why = ''
32
+
33
+ def test_nonfinite_direct_evaluation_declined(self):
34
+ for expression in ('1/0', '0/0', 'oo', '-oo', 'oo-oo', '0*oo', 'log(0)'):
35
+ with self.subTest(expression=expression):
36
+ self.assertIsNone(self.organ._evaluate([expression], 'Evaluate ' + expression))
37
+
38
+ def test_real_question_route_declines_undefined(self):
39
+ for question in ('What is 1 / 0?', 'What is 0 / 0?', 'Evaluate log(0).'):
40
+ with self.subTest(question=question):
41
+ self.assertIsNone(self.organ.answer(question))
42
+
43
+ def test_finite_real_and_complex_results_preserved(self):
44
+ for expression, expected in (('17*23', '391'), ('1/3', '1/3'), ('sqrt(-1)', 'I'),
45
+ ('(1+I)*(1-I)', '2'), ('sqrt(2)', 'sqrt(2)')):
46
+ with self.subTest(expression=expression):
47
+ result = self.organ._evaluate([expression], 'Evaluate ' + expression)
48
+ self.assertIsNotNone(result)
49
+ self.assertEqual(result['answer'], expected)
50
+ self.assertEqual(result['check'], 'finite numerical comparison using SymPy')
51
+
52
+ def test_equiv_does_not_accept_nan_or_infinity(self):
53
+ sp = self.module.sp
54
+ for a, b in ((sp.nan, sp.nan), (sp.zoo, sp.zoo), (sp.oo, sp.oo), (sp.oo, 1)):
55
+ with self.subTest(a=a, b=b):
56
+ self.assertFalse(self.module.equiv(sp.sympify(a), sp.sympify(b)))
57
+
58
+ def test_numerical_comparison_checks_all_complex_components(self):
59
+ sp = self.module.sp
60
+ compare = self.module.finite_numeric_agreement
61
+ self.assertTrue(compare(sp.I, sp.I, 1e-9))
62
+ self.assertFalse(compare(sp.I, 2*sp.I, 1e-9))
63
+ self.assertFalse(compare(sp.Float('nan'), sp.Integer(1), 1e-9))
64
+ self.assertFalse(compare(sp.Integer(10)**1000, sp.Integer(10)**1000, 1e-9))
65
+
66
+ def test_ordinary_question_is_still_answered(self):
67
+ self.assertEqual(self.organ.answer('What is 17 * 23?')['answer'], '391')
68
+
69
+
70
+ if __name__ == '__main__':
71
+ unittest.main()
tests/usage/fixtures/composition/cases.json ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": "journey",
4
+ "conversation_id": "composition_journey",
5
+ "text": "A car travels 150 km at a constant speed of 60 km/h. Parking then takes 15 minutes. What is the total elapsed time in hours?"
6
+ },
7
+ {
8
+ "id": "energy_cost",
9
+ "conversation_id": "composition_energy_cost",
10
+ "text": "A 1500 W heater runs for 2 hours. Electricity costs 0.20 dollars per kWh. What is the electricity cost in dollars?"
11
+ },
12
+ {
13
+ "id": "material_cost",
14
+ "conversation_id": "composition_material_cost",
15
+ "text": "An aluminium block has volume 0.002 m^3 and density 2700 kg/m^3. Aluminium costs 4 dollars per kg. What is the material cost in dollars?"
16
+ },
17
+ {
18
+ "id": "trip_fuel",
19
+ "conversation_id": "composition_trip_fuel",
20
+ "text": "A car uses 6 litres of fuel per 100 km and travels 250 km. Fuel costs 1.60 dollars per litre. What is the total fuel cost in dollars?"
21
+ },
22
+ {
23
+ "id": "resistor_energy",
24
+ "conversation_id": "composition_resistor_energy",
25
+ "text": "A 10 ohm resistor has 5 A of current for 60 seconds. What is the energy dissipated in joules?"
26
+ },
27
+ {
28
+ "id": "solar_useful",
29
+ "conversation_id": "composition_solar_useful",
30
+ "text": "A solar panel produces 400 W for 5 hours. A battery stores 80% of this energy. What is the stored energy in kWh?"
31
+ },
32
+ {
33
+ "id": "tank_fill",
34
+ "conversation_id": "composition_tank_fill",
35
+ "text": "A rectangular tank is 2 m long, 1 m wide and 0.5 m deep. A pump supplies 20 litres per minute. How many minutes does it take to fill the empty tank?"
36
+ },
37
+ {
38
+ "id": "work_price",
39
+ "conversation_id": "composition_work_price",
40
+ "text": "Three workers each work 8 hours at 20 dollars per hour. Materials cost 100 dollars. What is the total project cost in dollars?"
41
+ },
42
+ {
43
+ "id": "date_relation",
44
+ "conversation_id": "composition_date_relation",
45
+ "text": "Who wrote Leaves of Grass, and how many years after the author was born was the first edition published?"
46
+ },
47
+ {
48
+ "id": "orbital_ratio",
49
+ "conversation_id": "composition_orbital_ratio",
50
+ "text": "Earth takes 365.25 days to orbit the Sun and Mars takes 687 days. How many Earth years are 3 Mars years?"
51
+ },
52
+ {
53
+ "id": "net_energy",
54
+ "conversation_id": "composition_net_energy",
55
+ "text": "A battery initially stores 5 kWh. A 500 W appliance runs for 3 hours. Ignoring losses, how much energy remains in the battery in kWh?"
56
+ },
57
+ {
58
+ "id": "ambiguous_efficiency",
59
+ "conversation_id": "composition_ambiguous_efficiency",
60
+ "text": "A heater uses 2 kWh of electricity. How much useful heat does it produce?"
61
+ }
62
+ ]
tests/usage/fixtures/engineering/cases.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {"id":"lift_ideal","conversation_id":"lift","text":"How much energy is needed to lift a 25 kg crate vertically by 4 m? Use g = 9.81 m/s^2 and ignore losses."},
3
+ {"id":"lift_efficiency","conversation_id":"lift","text":"For that same lift, how much electrical energy is needed if the motor efficiency is 80%?"},
4
+ {"id":"pump_power","conversation_id":"pump","text":"A pump raises water at 0.002 m^3/s through a vertical height of 10 m. Use water density 1000 kg/m^3, g = 9.81 m/s^2, and 70% overall efficiency. What electrical input power is needed? Ignore pipe friction."},
5
+ {"id":"spring_work","conversation_id":"spring-work","text":"A linear spring has stiffness 200 N/m. Its extension changes from 0.1 m to 0.3 m. How much additional elastic energy is stored?"},
6
+ {"id":"spring_force","conversation_id":"spring-force","text":"A spring has unstretched length 0.4 m, final length 0.5 m and stiffness 200 N/m. What is its force magnitude?"},
7
+ {"id":"spring_simple","conversation_id":"spring-simple","text":"What is the elastic energy stored in a spring with stiffness 200 N/m and extension 0.3 m?"},
8
+ {"id":"heat_endpoints","conversation_id":"heat-endpoints","text":"How much heat is needed to warm 2 kg of water from 20 C to 80 C? Use constant specific heat 4180 J/(kg K), with no phase change or heat loss."},
9
+ {"id":"heat_interval","conversation_id":"heat-interval","text":"How much heat is needed to raise the temperature of 2 kg of water by 60 K? Use specific heat 4180 J/(kg K)."},
10
+ {"id":"battery_roles","conversation_id":"battery-roles","text":"How long can a 1000 Wh battery run a 100 W load through a 500 W rated inverter with 90% inverter efficiency and 80% depth of discharge? The load is constant."},
11
+ {"id":"battery_plain","conversation_id":"battery-plain","text":"How long can a 1000 Wh battery run a 100 W load with 90% inverter efficiency and 80% depth of discharge?"},
12
+ {"id":"battery_invalid","conversation_id":"battery-invalid","text":"How long can a 1000 Wh battery run a 100 W load with 120% inverter efficiency and 80% depth of discharge?"},
13
+ {"id":"conversion_flow","conversation_id":"conversion-flow","text":"Convert 30 litres per minute to cubic metres per second."},
14
+ {"id":"temperature_interval","conversation_id":"temperature-interval","text":"Convert a temperature increase of 18 F to a temperature increase in C."},
15
+ {"id":"dimension_mismatch","conversation_id":"dimension-mismatch","text":"Convert 2 metres to kilograms."}
16
+ ]
tests/usage/fixtures/engineering/followup_cases.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": "interval_helper_v2",
4
+ "conversation_id": "interval_helper_v2",
5
+ "text": "What is 18 * (5 / 9)?"
6
+ },
7
+ {
8
+ "id": "battery_rating_first",
9
+ "conversation_id": "battery_rating_first",
10
+ "text": "How long can a 1000 Wh battery with a 500 W rated inverter run a 100 W load with 90% inverter efficiency and 80% depth of discharge?"
11
+ },
12
+ {
13
+ "id": "spring_energy_rephrase",
14
+ "conversation_id": "spring_energy_rephrase",
15
+ "text": "How much energy is stored in a spring with stiffness 200 N/m and extension 0.3 m?"
16
+ },
17
+ {
18
+ "id": "heat_one_sentence",
19
+ "conversation_id": "heat_one_sentence",
20
+ "text": "How much heat is needed to warm 2 kg of water from 20 C to 80 C using specific heat 4180 J/(kg K), with no phase change or heat loss?"
21
+ }
22
+ ]
tests/usage/fixtures/reliability/cases.json ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": "percent_base",
4
+ "conversation_id": "rel_percent_base",
5
+ "text": "What is 20% of 80?"
6
+ },
7
+ {
8
+ "id": "percent_paraphrase",
9
+ "conversation_id": "rel_percent_paraphrase",
10
+ "text": "Calculate twenty percent of eighty."
11
+ },
12
+ {
13
+ "id": "percent_changed",
14
+ "conversation_id": "rel_percent_changed",
15
+ "text": "What is 25% of 80?"
16
+ },
17
+ {
18
+ "id": "area_base",
19
+ "conversation_id": "rel_area_base",
20
+ "text": "A rectangular room is 3 meters long and 4 meters wide. What is its area?"
21
+ },
22
+ {
23
+ "id": "area_paraphrase",
24
+ "conversation_id": "rel_area_paraphrase",
25
+ "text": "What is the area of a rectangle with width 4 meters and length 3 meters?"
26
+ },
27
+ {
28
+ "id": "area_incidental",
29
+ "conversation_id": "rel_area_incidental",
30
+ "text": "Apartment 12 has a rectangular room 3 meters long and 4 meters wide. What is the room area?"
31
+ },
32
+ {
33
+ "id": "perimeter_requested",
34
+ "conversation_id": "rel_perimeter_requested",
35
+ "text": "A rectangular room is 3 meters long and 4 meters wide. What is its perimeter?"
36
+ },
37
+ {
38
+ "id": "temperature_negative",
39
+ "conversation_id": "rel_temperature_negative",
40
+ "text": "Convert -5 degrees Celsius to Fahrenheit."
41
+ },
42
+ {
43
+ "id": "temperature_paraphrase",
44
+ "conversation_id": "rel_temperature_paraphrase",
45
+ "text": "What is five degrees below zero Celsius in Fahrenheit?"
46
+ },
47
+ {
48
+ "id": "zero_denominator",
49
+ "conversation_id": "rel_zero_denominator",
50
+ "text": "What is 10 divided by 0?"
51
+ },
52
+ {
53
+ "id": "zero_numerator",
54
+ "conversation_id": "rel_zero_numerator",
55
+ "text": "What is 0 divided by 10?"
56
+ },
57
+ {
58
+ "id": "speed_zero_time",
59
+ "conversation_id": "rel_speed_zero_time",
60
+ "text": "A car travels 150 kilometers in 0 hours. What is its average speed?"
61
+ },
62
+ {
63
+ "id": "speed_base",
64
+ "conversation_id": "rel_speed_base",
65
+ "text": "A car travels 150 kilometers in 3 hours. What is its average speed?"
66
+ },
67
+ {
68
+ "id": "speed_contradiction",
69
+ "conversation_id": "rel_speed_contradiction",
70
+ "text": "The same trip took exactly 3 hours and exactly 4 hours. The car traveled 150 kilometers. What was its average speed?"
71
+ },
72
+ {
73
+ "id": "area_contradiction",
74
+ "conversation_id": "rel_area_contradiction",
75
+ "text": "A rectangular room has length exactly 3 meters and length exactly 5 meters, and width 4 meters. What is its area?"
76
+ }
77
+ ]
tests/usage/fixtures/reliability/preserved_answers.json ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "scope": "Saved successful parent replies from U1 run-2; test fixtures, not fresh Engine output.",
3
+ "source": "var/usage-20261026/reliability/run-2/results.jsonl",
4
+ "cases": [
5
+ {
6
+ "text": "What is 20% of 80?",
7
+ "reply": {
8
+ "status": "answered",
9
+ "path": "organ:math>word:percent_of",
10
+ "response": "[ev:derived] 16. Read as percent of: 20% of 80. Checked: every quantity the formula needs was found with its unit, and the question asks for what the formula gives.",
11
+ "provenance": {
12
+ "kind": "organ_word_problem",
13
+ "schema": "percent_of",
14
+ "value": 16.0,
15
+ "parent_path": "arb:definitional->document_runtime|substitution_disclosed|math:out_of_fragment|vessel:receipt",
16
+ "parent_status": "withheld"
17
+ },
18
+ "latency_ms": 9.0,
19
+ "zero_model_gate": {
20
+ "lm_calls": 0,
21
+ "transformer_calls": 0
22
+ },
23
+ "arbitration": {
24
+ "winner": "definitional",
25
+ "runner_up": "act_continue",
26
+ "margin": 0.799,
27
+ "floor": false,
28
+ "regex_family": "open_topic",
29
+ "agrees": false,
30
+ "mode": "authoritative",
31
+ "arbiter": "v3",
32
+ "canonical_surface": "What is a 80?",
33
+ "capture_point": "capability"
34
+ },
35
+ "evidential": {
36
+ "input_marked": null,
37
+ "answer_licensed": "unknown"
38
+ },
39
+ "continuation": {
40
+ "doors": [
41
+ "cardinal",
42
+ "number",
43
+ "product"
44
+ ],
45
+ "offer": "from here, cardinal or number stand open"
46
+ }
47
+ }
48
+ },
49
+ {
50
+ "text": "Calculate twenty percent of eighty.",
51
+ "reply": {
52
+ "status": "answered",
53
+ "path": "compute:verified",
54
+ "response": "In steps: 20 % of 80 = 16. The answer is 16.",
55
+ "provenance": {
56
+ "kind": "computation",
57
+ "verified": true,
58
+ "steps": [
59
+ "20 % of 80 = 16"
60
+ ],
61
+ "carried_prior": false,
62
+ "evaluator": "w27_closed_grammar"
63
+ },
64
+ "latency_ms": 1
65
+ }
66
+ },
67
+ {
68
+ "text": "What is 25% of 80?",
69
+ "reply": {
70
+ "status": "answered",
71
+ "path": "organ:math>word:percent_of",
72
+ "response": "[ev:derived] 20. Read as percent of: 25% of 80. Checked: every quantity the formula needs was found with its unit, and the question asks for what the formula gives.",
73
+ "provenance": {
74
+ "kind": "organ_word_problem",
75
+ "schema": "percent_of",
76
+ "value": 20.0,
77
+ "parent_path": "arb:definitional->document_runtime|substitution_disclosed|math:out_of_fragment|vessel:receipt",
78
+ "parent_status": "withheld"
79
+ },
80
+ "latency_ms": 7.7,
81
+ "zero_model_gate": {
82
+ "lm_calls": 0,
83
+ "transformer_calls": 0
84
+ },
85
+ "arbitration": {
86
+ "winner": "definitional",
87
+ "runner_up": "timeline",
88
+ "margin": 0.688,
89
+ "floor": false,
90
+ "regex_family": "open_topic",
91
+ "agrees": false,
92
+ "mode": "authoritative",
93
+ "arbiter": "v3",
94
+ "canonical_surface": "What is a 80?",
95
+ "capture_point": "capability"
96
+ },
97
+ "evidential": {
98
+ "input_marked": null,
99
+ "answer_licensed": "unknown"
100
+ },
101
+ "continuation": {
102
+ "doors": [
103
+ "cardinal",
104
+ "number",
105
+ "product"
106
+ ],
107
+ "offer": "from here, cardinal or number stand open"
108
+ }
109
+ }
110
+ },
111
+ {
112
+ "text": "What is the area of a rectangle with width 4 meters and length 3 meters?",
113
+ "reply": {
114
+ "status": "answered",
115
+ "path": "organ:math>word:geometry",
116
+ "response": "[ev:derived] 12 square units. Read as geometry: A = L x W = 3 x 4. Checked: every quantity the formula needs was found with its unit, and the question asks for what the formula gives.",
117
+ "provenance": {
118
+ "kind": "organ_word_problem",
119
+ "schema": "geometry",
120
+ "value": 12.0,
121
+ "parent_path": "corpus_query:property_of|vessel:receipt",
122
+ "parent_status": "withheld"
123
+ },
124
+ "latency_ms": 2.0,
125
+ "zero_model_gate": {
126
+ "lm_calls": 0,
127
+ "transformer_calls": 0
128
+ },
129
+ "arbitration": {
130
+ "winner": "definitional",
131
+ "runner_up": "multi_step",
132
+ "margin": 0.722,
133
+ "floor": false,
134
+ "regex_family": null,
135
+ "agrees": null,
136
+ "mode": "shadow",
137
+ "arbiter": "v3"
138
+ },
139
+ "evidential": {
140
+ "input_marked": null,
141
+ "answer_licensed": "unknown"
142
+ }
143
+ }
144
+ },
145
+ {
146
+ "text": "Convert -5 degrees Celsius to Fahrenheit.",
147
+ "reply": {
148
+ "status": "answered",
149
+ "path": "specialist_call>specialist:units-v3>temperature",
150
+ "response": "-5 \u00b0C is 23 \u00b0F. [units-v3]",
151
+ "provenance": {
152
+ "kind": "core_called_specialist",
153
+ "core_status": "clarification",
154
+ "core_path": "act:clarify|vessel:receipt",
155
+ "specialist": "units-v3",
156
+ "parts": null,
157
+ "trace": {
158
+ "rule": "only"
159
+ }
160
+ },
161
+ "arbitration": {
162
+ "winner": "clarification",
163
+ "runner_up": "summarize",
164
+ "margin": 0.022,
165
+ "floor": true,
166
+ "regex_family": "open_topic",
167
+ "agrees": false,
168
+ "mode": "shadow",
169
+ "arbiter": "v3"
170
+ },
171
+ "latency_ms": 6.6,
172
+ "zero_model_gate": {
173
+ "lm_calls": 0,
174
+ "transformer_calls": 0
175
+ },
176
+ "evidential": {
177
+ "input_marked": null,
178
+ "answer_licensed": "unknown"
179
+ },
180
+ "references": []
181
+ }
182
+ },
183
+ {
184
+ "text": "What is 0 divided by 10?",
185
+ "reply": {
186
+ "status": "answered",
187
+ "path": "compute:verified",
188
+ "response": "In steps: 0 / 10 = 0. The answer is 0.",
189
+ "provenance": {
190
+ "kind": "computation",
191
+ "verified": true,
192
+ "steps": [
193
+ "0 / 10 = 0"
194
+ ],
195
+ "carried_prior": false,
196
+ "evaluator": "w27_closed_grammar"
197
+ },
198
+ "latency_ms": 1
199
+ }
200
+ }
201
+ ]
202
+ }
tests/usage/reliability/test_geometry_consumer.py ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Untrusted focused harness: actual word module/w488 consumer, substituted parent.
2
+
3
+ The w480 timed-call helper and answered predicate are AST-selected unchanged.
4
+ No full Engine, stores, model or admission gate is launched.
5
+ """
6
+ import ast
7
+ import importlib.util
8
+ import json
9
+ import os
10
+ from pathlib import Path
11
+ import sys
12
+ import threading
13
+ import types
14
+ import unittest
15
+ from unittest.mock import patch
16
+
17
+ ROOT = Path(__file__).resolve().parents[3]
18
+ SOURCE = Path(os.environ.get('RELIABILITY_SOURCE_ROOT', ROOT))
19
+ ADMITTED = SOURCE / 'root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted'
20
+
21
+
22
+ def load(path, name):
23
+ spec = importlib.util.spec_from_file_location(name, path)
24
+ module = importlib.util.module_from_spec(spec)
25
+ spec.loader.exec_module(module)
26
+ return module
27
+
28
+
29
+ def consumer(word_path=None, fold_path=None):
30
+ wp = load(word_path or SOURCE / 'root/wander-train/word_problems_v1.py', 'reliability_word_subject')
31
+ tree = ast.parse((ADMITTED / 'oracle_release_runtime_w480.py').read_text())
32
+ selected = [n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name == '_run']
33
+ parent_class = next(n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == 'OracleReleaseRuntimeW480')
34
+ selected += [n for n in parent_class.body if isinstance(n, ast.FunctionDef) and n.name == '_c480_answered']
35
+ ns = {'threading': threading, 'BUDGET_S': 4.0}
36
+ exec(compile(ast.Module(body=selected, type_ignores=[]), '<actual-w480-helpers>', 'exec'), ns)
37
+
38
+ class Parent:
39
+ _c480_answered = ns['_c480_answered']
40
+
41
+ def _c480_dispatch(self, text, session_id, res):
42
+ return res
43
+
44
+ base = types.ModuleType('oracle_release_runtime_w486')
45
+ base.OracleReleaseRuntimeW486 = Parent
46
+ timed = types.ModuleType('oracle_release_runtime_w480')
47
+ timed._run = ns['_run']
48
+ with patch.dict(sys.modules, {'word_problems_v1': wp, 'oracle_release_runtime_w486': base,
49
+ 'oracle_release_runtime_w480': timed}):
50
+ fold = load(fold_path or ADMITTED / 'oracle_release_runtime_w488.py', 'reliability_fold_subject')
51
+ return wp, fold.OracleReleaseRuntimeW488()
52
+
53
+
54
+ class GeometryConsumerTests(unittest.TestCase):
55
+ @classmethod
56
+ def setUpClass(cls):
57
+ cls.wp, cls.runtime = consumer()
58
+ cls.original = json.loads((ROOT / 'tests/usage/fixtures/reliability/cases.json').read_text())
59
+ cls.cases = {c['id']: c['text'] for c in cls.original}
60
+
61
+ def dispatch(self, text, parent=None):
62
+ if parent is None:
63
+ parent = {'status': 'clarification', 'path': 'act:clarify_specific',
64
+ 'response': 'What does "its" refer to?', 'latency_ms': 1}
65
+ return self.runtime._c480_dispatch(text, 'isolated', parent)
66
+
67
+ def test_original_room_words_and_requested_quantity(self):
68
+ # Independent equations: area = 3 * 4; perimeter = 2 * (3 + 4).
69
+ for key, value in [('area_base', 12), ('area_incidental', 12), ('area_paraphrase', 12), ('perimeter_requested', 14)]:
70
+ with self.subTest(key=key):
71
+ result = self.dispatch(self.cases[key])
72
+ self.assertEqual(result['status'], 'answered')
73
+ self.assertEqual(result['provenance']['value'], value)
74
+
75
+ def test_changed_dimension_changes_area(self):
76
+ self.assertEqual(self.dispatch('A rectangular room is 5 meters long and 4 meters wide. What is its area?')['provenance']['value'], 20)
77
+
78
+ def test_real_conflict_has_useful_explanation(self):
79
+ result = self.dispatch(self.cases['area_contradiction'])
80
+ self.assertEqual(result['status'], 'clarification')
81
+ self.assertEqual(result['path'], 'organ:math>word:conflicting_dimension')
82
+ self.assertEqual(result['response'], 'The supplied length values disagree (3 meters and 5 meters). Which length should I use?')
83
+ self.assertEqual(result['provenance']['parent_path'], 'act:clarify_specific')
84
+ self.assertNotIn('value', result['provenance'])
85
+
86
+ def test_postfix_conflict_also_explained(self):
87
+ result = self.dispatch('A rectangular room is 3 meters long and 5 meters long and 4 meters wide. What is its area?')
88
+ self.assertEqual(result['provenance']['reason'], 'conflicting_dimension')
89
+
90
+ def test_equal_prefix_postfix_mentions_count_once(self):
91
+ for text in ['A rectangle has length 3 meters long and width 4 meters wide. What is its area?',
92
+ 'A rectangle is 3 meters long and 3 meters long and 4 meters wide. What is its area?']:
93
+ self.assertEqual(self.dispatch(text)['provenance']['value'], 12)
94
+
95
+ def test_malformed_negative_or_unbound_dimensions_decline(self):
96
+ for text in ['A rectangle is -3 meters long and 4 meters wide. What is its area?',
97
+ 'A rectangle is −3 meters long and 4 meters wide. What is its area?',
98
+ 'A rectangle is 3 bananas long and 4 meters wide. What is its area?',
99
+ 'A rectangle is 3 meters long and 4 meters wide and another side 5 meters. What is its area?',
100
+ 'A rectangle is 3 meters long and 4 centimeters wide. What is its area?']:
101
+ with self.subTest(text=text):
102
+ result = self.dispatch(text)
103
+ self.assertEqual(result['path'], 'act:clarify_specific')
104
+ self.assertIsNone(self.wp.explain_refusal(text))
105
+
106
+ def test_equivalent_unit_duplicates_not_called_contradictory(self):
107
+ text = 'A rectangle has length 3 meters and length 300 centimeters and width 4 meters. What is its area?'
108
+ self.assertIsNone(self.wp.explain_refusal(text))
109
+ self.assertEqual(self.dispatch(text)['status'], 'clarification')
110
+
111
+ def test_different_shapes_not_conflated(self):
112
+ text = 'A rectangle has length 3 meters and width 4 meters. Another rectangle has length 5 meters and width 4 meters. What is the area?'
113
+ self.assertEqual(self.wp.explain_refusal(text)['reason'], 'unsupported_geometry_request')
114
+
115
+ def test_conflict_malformed_remainder_not_claimed_complete(self):
116
+ text = 'A rectangle has length 3 meters and length 5 meters and width 4 bananas. What is its area?'
117
+ self.assertIsNone(self.wp.explain_refusal(text))
118
+
119
+ def test_radius_diameter_diagnosis(self):
120
+ result = self.dispatch('A circle has radius 3 meters and diameter 8 meters. What is its area?')
121
+ self.assertEqual(result['status'], 'clarification')
122
+ self.assertIn('diameter must be twice the radius', result['response'])
123
+
124
+ def test_parent_answers_remain_unchanged(self):
125
+ # Replay retained successful outputs as parent responses; these are not
126
+ # fresh Engine output and the harness does not claim to recompute them.
127
+ answered = json.loads((ROOT / 'tests/usage/fixtures/reliability/preserved_answers.json').read_text())['cases']
128
+ self.assertEqual(len(answered), 6)
129
+ for row in answered:
130
+ reply = row['reply']
131
+ self.assertIs(self.dispatch(row['text'], reply), reply)
132
+
133
+ def test_multiple_rooms_not_silently_one_room(self):
134
+ # Two disjoint 3*4 rooms have total area 2*12=24; a scalar 12 is false.
135
+ for text in ['Two rectangular rooms are each 3 meters long and 4 meters wide. What is their total area?',
136
+ '2 rooms are rectangular and each is 3 meters long and 4 meters wide. What is their total area?']:
137
+ result = self.dispatch(text)
138
+ self.assertEqual(result['status'], 'clarification')
139
+ self.assertIn('multiple shapes', result['response'])
140
+ self.assertNotIn('value', result['provenance'])
141
+
142
+ def test_multiple_requested_measures_not_partial_answer(self):
143
+ # Required pair is area12, perimeter14, not an unqualified scalar12.
144
+ text = 'A rectangle is 3 meters long and 4 meters wide. What are its area and perimeter?'
145
+ result = self.dispatch(text)
146
+ self.assertEqual(result['status'], 'clarification')
147
+ self.assertIn('each requested measure', result['response'])
148
+
149
+ def test_requested_area_unit_not_ignored(self):
150
+ # 12m^2 = 12*(100cm)^2 =120000cm^2; bare12 does not answer this.
151
+ text = 'A rectangle is 3 meters long and 4 meters wide. What is its area in square centimeters?'
152
+ result = self.dispatch(text)
153
+ self.assertEqual(result['status'], 'clarification')
154
+ self.assertIn('requested output unit', result['response'])
155
+
156
+ def test_apartment_identifier_is_not_shape_count(self):
157
+ text = 'Apartment 16 has a rectangular room 3 meters long and 4 meters wide. What is the room area?'
158
+ self.assertEqual(self.dispatch(text)['provenance']['value'], 12)
159
+
160
+ def test_rounding_and_location_are_not_output_units(self):
161
+ for question in ['What is its area rounded to the nearest tenth?', 'What is its area in this diagram?']:
162
+ text = 'A rectangle is 3 meters long and 4 meters wide. ' + question
163
+ result = self.dispatch(text)
164
+ self.assertEqual(result['status'], 'answered')
165
+ self.assertEqual(result['provenance']['value'], 12)
166
+ rounded = self.dispatch('A rectangle is 3.12 meters long and 4 meters wide. What is its area rounded to the nearest tenth?')
167
+ self.assertEqual(rounded['provenance']['value'], 12.5)
168
+
169
+ def test_explicit_unknown_units_still_decline(self):
170
+ for suffix in ['in square glorks', 'in units of glorks', 'in cm2']:
171
+ result = self.dispatch('A rectangle is 3 meters long and 4 meters wide. What is its area ' + suffix + '?')
172
+ self.assertEqual(result['path'], 'organ:math>word:unsupported_geometry_request')
173
+ self.assertIn('requested output unit', result['response'])
174
+
175
+ def test_non_measure_shape_questions_not_intercepted(self):
176
+ for text in ['Two rectangles are blue. What color are they?',
177
+ 'A rectangle is 3 meters long and 4 meters wide. What is its name?']:
178
+ parent = {'status': 'withheld', 'path': 'other:reader', 'response': 'Unanswered by parent.'}
179
+ self.assertIs(self.dispatch(text, parent), parent)
180
+
181
+ def test_non_geometry_refusals_preserved(self):
182
+ for text in ['What does its name mean?', 'A car travels 150 kilometers in 0 hours. What is its average speed?',
183
+ 'What is five degrees below zero Celsius in Fahrenheit?']:
184
+ original = {'status': 'withheld', 'path': 'existing:guard', 'response': 'No verified answer.'}
185
+ self.assertIs(self.dispatch(text, original), original)
186
+
187
+
188
+ if __name__ == '__main__':
189
+ unittest.main()
tests/usage/reliability/test_geometry_units.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Independent geometry-unit checks through the actual untrusted w488 consumer.
2
+
3
+ Uses the existing focused parent substitute; no full Engine or panel is run.
4
+ """
5
+ import math
6
+ import unittest
7
+
8
+ from test_geometry_consumer import consumer
9
+
10
+
11
+ class GeometryUnitTests(unittest.TestCase):
12
+ @classmethod
13
+ def setUpClass(cls):
14
+ cls.wp, cls.runtime = consumer()
15
+
16
+ def dispatch(self, text):
17
+ return self.runtime._c480_dispatch(text, 'units',
18
+ {'status': 'clarification', 'path': 'controlled:parent', 'response': 'Parent clarification.'})
19
+
20
+ def check(self, text, value, unit):
21
+ module = self.wp.answer(text)
22
+ self.assertIsNotNone(module)
23
+ self.assertAlmostEqual(module['value'], value, places=10)
24
+ self.assertEqual(module['unit'], unit)
25
+ reply = self.dispatch(text)
26
+ self.assertEqual(reply['status'], 'answered')
27
+ self.assertAlmostEqual(reply['provenance']['value'], value, places=10)
28
+ self.assertEqual(reply['provenance']['unit'], unit)
29
+ first_sentence = reply['response'].split('. Read as ', 1)[0]
30
+ self.assertTrue(first_sentence.endswith(' ' + unit.replace('^2', '²').replace('^3', '³')), first_sentence)
31
+ self.assertNotIn('square units', reply['response'])
32
+ self.assertNotIn('cubic units', reply['response'])
33
+ return reply
34
+
35
+ def test_original_room_area_and_perimeter(self):
36
+ # L=3m,W=4m: A=12m²; P=14m. These are the actual U4 question strings.
37
+ self.check('A rectangular room is 3 meters long and 4 meters wide. What is its area?', 12, 'm^2')
38
+ self.check('A rectangular room is 3 meters long and 4 meters wide. What is its perimeter?', 14, 'm')
39
+
40
+ def test_paraphrase_identifiers_and_location(self):
41
+ for text in [
42
+ 'What is the area of a rectangle with width 4 meters and length 3 meters?',
43
+ 'Apartment 12 has a rectangular room 3 meters long and 4 meters wide. What is the room area?',
44
+ 'Apartment 16 has a rectangular room 3 meters long and 4 meters wide. What is the room area?',
45
+ 'A rectangle is 3 meters long and 4 meters wide. What is its area in this diagram?']:
46
+ with self.subTest(text=text):
47
+ self.check(text, 12, 'm^2')
48
+
49
+ def test_supported_same_unit_scales_not_relabelled(self):
50
+ for spelling, canonical in [('millimeters', 'mm'), ('centimeters', 'cm'), ('meters', 'm'),
51
+ ('kilometers', 'km'), ('inches', 'in'), ('feet', 'ft'),
52
+ ('yards', 'yd'), ('miles', 'mi')]:
53
+ with self.subTest(unit=spelling):
54
+ self.check(f'A rectangle has length 3 {spelling} and width 4 {spelling}. What is its area?', 12, canonical+'^2')
55
+ self.check(f'A rectangle has length 3 {spelling} and width 4 {spelling}. What is its perimeter?', 14, canonical)
56
+
57
+ def test_equal_scale_aliases(self):
58
+ self.check('A rectangle has length 3 feet and width 4 ft. What is its area?', 12, 'ft^2')
59
+ self.check('A rectangle has length 3 m and width 4 meters. What is its perimeter?', 14, 'm')
60
+
61
+ def test_in_is_inches_only_in_bound_dimension_position(self):
62
+ self.check('A rectangle has length 3 in and width 4 inches. What is its area?', 12, 'in^2')
63
+ # A preposition without a measurement unit must not acquire inches.
64
+ text = 'A rectangle has length 3 in the diagram and width 4 in the diagram. What is its area?'
65
+ self.assertIsNone(self.wp.answer(text))
66
+
67
+ def test_area_rounding_retains_squared_unit(self):
68
+ self.check('A rectangle is 3.12 meters long and 4 meters wide. What is its area rounded to the nearest tenth?', 12.5, 'm^2')
69
+
70
+ def test_circle_radius_diameter_and_circumference(self):
71
+ self.check('A circle has radius 3 centimeters. What is its area?', 9*math.pi, 'cm^2')
72
+ self.check('A circle has diameter 6 centimeters. What is its area?', 9*math.pi, 'cm^2')
73
+ self.check('A circle has radius 3 centimeters and diameter 6 cm. What is its circumference?', 6*math.pi, 'cm')
74
+
75
+ def test_circumference_rounding_retains_linear_unit(self):
76
+ self.check('A circle has radius 1 meters. What is its circumference rounded to the nearest tenth?', 6.3, 'm')
77
+
78
+ def test_supported_volumes_retain_cubed_unit(self):
79
+ # Vcone=πr²h/3; Vcylinder=πr²h; Vsphere=4πr³/3.
80
+ self.check('A cone has radius 2 centimeters and height 3 centimeters. What is its volume?', 4*math.pi, 'cm^3')
81
+ self.check('A cylinder has radius 2 inches and height 3 in. What is its volume?', 12*math.pi, 'in^3')
82
+ self.check('A sphere has radius 2 feet. What is its volume?', 32*math.pi/3, 'ft^3')
83
+
84
+ def test_volume_rounding_retains_cubed_unit(self):
85
+ self.check('A cylinder has radius 1.2 centimeters and height 3.4 centimeters. What is its volume rounded to the nearest tenth?', 15.4, 'cm^3')
86
+
87
+ def test_mixed_units_and_malformed_units_not_silently_relabelled(self):
88
+ for text in [
89
+ 'A rectangle has length 3 meters and width 4 centimeters. What is its area?',
90
+ 'A cylinder has radius 2 inches and height 3 feet. What is its volume?',
91
+ 'A rectangle has length 3 minutes and width 4 meters. What is its area?',
92
+ 'A rectangle has length 3 smoots and width 4 smoots. What is its area?']:
93
+ with self.subTest(text=text):
94
+ self.assertIsNone(self.wp.answer(text))
95
+ self.assertNotEqual(self.dispatch(text)['status'], 'answered')
96
+
97
+ def test_conflict_and_scope_stay_bounded(self):
98
+ for text in [
99
+ 'A circle has radius 3 meters and diameter 8 meters. What is its area?',
100
+ 'A rectangle has length 3 meters and length 5 meters and width 4 meters. What is its area?',
101
+ 'Two rectangular rooms are each 3 meters long and 4 meters wide. What is their total area?',
102
+ 'A rectangle is 3 meters long and 4 meters wide. What are its area and perimeter?',
103
+ 'A rectangle is 3 meters long and 4 meters wide. What is its area in square centimeters?']:
104
+ with self.subTest(text=text):
105
+ self.assertEqual(self.dispatch(text)['status'], 'clarification')
106
+ self.assertIsNone(self.wp.answer(text))
107
+
108
+ def test_zero_area_keeps_unit(self):
109
+ self.check('A rectangle has length 0 centimeters and width 4 centimeters. What is its area?', 0, 'cm^2')
110
+
111
+
112
+ if __name__ == '__main__':
113
+ unittest.main()
tests/usage/reliability_route/test_geometry_route.py ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Actual geometry-v4 -> Family -> w504 checks with only the expensive parent substituted.
2
+
3
+ Untrusted test harness; no full Engine, native build or admission authority.
4
+ """
5
+ import importlib.util
6
+ import json
7
+ import math
8
+ import os
9
+ from pathlib import Path
10
+ import sys
11
+ import types
12
+ import unittest
13
+ from unittest.mock import patch
14
+
15
+ ROOT = Path(__file__).resolve().parents[3]
16
+ SOURCE = Path(os.environ.get('RELIABILITY_SOURCE_ROOT', ROOT))
17
+ SPECIALIST = SOURCE / 'root/specialists/geometry-v4'
18
+ ADMITTED = SOURCE / 'root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted'
19
+
20
+
21
+ def load(path, name):
22
+ spec = importlib.util.spec_from_file_location(name, path)
23
+ module = importlib.util.module_from_spec(spec)
24
+ spec.loader.exec_module(module)
25
+ return module
26
+
27
+
28
+ def actual_consumer():
29
+ combo = load(SOURCE / 'root/specialists/_gate/combo_runtime_v1.py', 'geometry_route_combo')
30
+ family = combo.Family([str(SPECIALIST / 'manifest.json')], [str(SPECIALIST)])
31
+ class Parent:
32
+ def chat(self, text, session_id='default'):
33
+ return self.parent_reply
34
+ parent_module = types.ModuleType('oracle_release_runtime_w366')
35
+ parent_module.OracleReleaseRuntimeW366 = Parent
36
+ with patch.dict(sys.modules, {'oracle_release_runtime_w366': parent_module}):
37
+ fold = load(ADMITTED / 'oracle_release_runtime_w504.py', 'geometry_route_fold')
38
+ fold._FAM['f'] = family
39
+ runtime = fold.OracleReleaseRuntimeW504()
40
+ runtime.parent_reply = {'status': 'clarification', 'path': 'act:clarify_specific', 'response': 'Unresolved parent reading.'}
41
+ return family, runtime
42
+
43
+
44
+ class GeometryRouteTests(unittest.TestCase):
45
+ @classmethod
46
+ def setUpClass(cls):
47
+ cls.family, cls.runtime = actual_consumer()
48
+
49
+ def answer(self, text):
50
+ self.runtime.parent_reply = {'status': 'clarification', 'path': 'act:clarify_specific', 'response': 'Unresolved parent reading.'}
51
+ return self.runtime.chat(text)
52
+
53
+ def test_actual_recorded_mixed_units(self):
54
+ reply = self.answer('A rectangle has length 3 meters and width 4 centimeters. What is its area?')
55
+ self.assertEqual(reply['status'], 'answered')
56
+ self.assertEqual(reply['path'], 'specialist_call>specialist:geometry-v4>rect_area')
57
+ # 3m * .04m = .12m², not raw 3*4=12.
58
+ self.assertIn('area is 0.12 m²', reply['response'])
59
+
60
+ def test_actual_recorded_diagram_units(self):
61
+ reply = self.answer('A rectangle has length 3 in the diagram and width 4 in the diagram. What is its area?')
62
+ self.assertEqual(reply['status'], 'answered')
63
+ self.assertIn('area is 12 square units', reply['response'])
64
+ self.assertNotIn('in²', reply['response'])
65
+
66
+ def test_mixed_rectangle_perimeter(self):
67
+ # 2*(3+.04)m=6.08m.
68
+ self.assertIn('perimeter is 6.08 m', self.answer('A rectangle has length 3 meters and width 4 centimeters. What is its perimeter?')['response'])
69
+
70
+ def test_same_units_and_aliases(self):
71
+ for length, width, unit in [('centimeters', 'cm', 'cm²'), ('feet', 'ft', 'ft²'), ('meters', 'm', 'm²')]:
72
+ result = self.answer(f'A rectangle has length 3 {length} and width 4 {width}. What is its area?')
73
+ self.assertEqual(result['status'], 'answered')
74
+ self.assertIn('area is 12 '+unit, result['response'])
75
+
76
+ def test_explicit_inches_do_not_become_diagram_units(self):
77
+ self.assertIn('area is 12 in²', self.answer('A rectangle has length 3 in and width 4 inches. What is its area?')['response'])
78
+
79
+ def test_diagram_and_physical_dimensions_not_combined(self):
80
+ result = self.answer('A rectangle has length 3 meters and width 4 in the diagram. What is its area?')
81
+ self.assertEqual(result['status'], 'withheld')
82
+ self.assertIn('missing units', result['response'])
83
+
84
+ def test_unknown_and_ambiguous_units(self):
85
+ for text in ['A rectangle has length 3 smoots and width 4 meters. What is its area?',
86
+ 'A circle has radius 3 bananas. What is its area?',
87
+ 'A rectangle has length 3 in another place and width 4 inches. What is its area?']:
88
+ result = self.answer(text)
89
+ self.assertEqual(result['status'], 'withheld')
90
+ self.assertTrue(result['path'].endswith('geometry-v4>underspecified'))
91
+
92
+ def test_missing_unit_must_not_inherit_silently(self):
93
+ self.assertEqual(self.answer('A cylinder has radius 3 meters and height 4. What is its volume?')['status'], 'withheld')
94
+
95
+ def test_mixed_units_do_not_escape_through_parent_formulas(self):
96
+ for text in ['A cylinder has radius 3 meters and height 4 centimeters. What is its volume?',
97
+ 'A cone has radius 3 meters and height 4 centimeters. What is its volume?',
98
+ 'A triangle has base 3 meters and height 4 centimeters. What is its area?',
99
+ 'A box has length 3 meters and width 4 centimeters and height 5 meters. What is its volume?']:
100
+ with self.subTest(text=text):
101
+ result = self.answer(text)
102
+ self.assertEqual(result['status'], 'withheld')
103
+ self.assertIn('mixed length units', result['response'])
104
+
105
+ def test_inconsistent_radius_diameter_and_duplicates(self):
106
+ for text in ['A circle has radius 3 meters and diameter 8 centimeters. What is its area?',
107
+ 'A rectangle has length 3 meters and length 5 meters and width 4 meters. What is its area?',
108
+ 'A square has side 3 meters and side 5 meters. What is its area?']:
109
+ result = self.answer(text)
110
+ self.assertEqual(result['status'], 'withheld')
111
+ self.assertIn('contradict', result['response'])
112
+
113
+ def test_equal_dimension_repeats_and_aliases(self):
114
+ self.assertIn('area is 12 m²', self.answer('A rectangle has length 3 meters and length 300 centimeters and width 4 meters. What is its area?')['response'])
115
+
116
+ def test_other_same_unit_formulas_remain_useful(self):
117
+ for text, fragment in [
118
+ ('A cylinder has radius 2 centimeters and height 3 cm. What is its volume?', '37.6991 cm³'),
119
+ ('A circle has radius 3 centimeters and diameter 6 cm. What is its area?', '28.2743 cm²'),
120
+ ('A cube has side 2 feet. What is its volume?', '8 ft³')]:
121
+ reply = self.answer(text)
122
+ self.assertEqual(reply['status'], 'answered')
123
+ self.assertIn(fragment, reply['response'])
124
+
125
+ def test_powered_or_compound_units_not_read_as_lengths(self):
126
+ for unit in ['m^2', 'm2', 'm²', 'meters per second', 'm/s', 'µm']:
127
+ result = self.answer(f'A triangle has base 3 {unit} and height 4 meters. What is its area?')
128
+ self.assertEqual(result['status'], 'withheld', (unit, result))
129
+
130
+ def test_cube_side_and_edge_consistency(self):
131
+ self.assertEqual(self.answer('A cube has side 2 meters and edge 3 meters. What is its volume?')['status'], 'withheld')
132
+ self.assertIn('8 m³', self.answer('A cube has side 2 meters and edge 2 meters. What is its volume?')['response'])
133
+
134
+ def test_requests_respect_scope(self):
135
+ for text in ['Two rectangles have length 3 meters and width 4 meters. What is their total area?',
136
+ 'A rectangle has length 3 meters and width 4 meters. What are its area and perimeter?',
137
+ 'A rectangle has length 3 meters and width 4 meters. What is its area in square centimeters?']:
138
+ self.assertEqual(self.answer(text)['status'], 'withheld')
139
+
140
+ def test_rounding_and_location(self):
141
+ self.assertIn('area is 12.5 m²', self.answer('A rectangle has length 3.12 meters and width 4 meters. What is its area rounded to the nearest tenth?')['response'])
142
+ self.assertIn('area is 12 m²', self.answer('A rectangle has length 3 meters and width 4 meters. What is its area in this diagram?')['response'])
143
+
144
+ def test_parent_success_and_deliberate_refusal_preserved(self):
145
+ for original in [{'status': 'answered', 'path': 'existing:answer', 'response': 'Existing useful answer.'},
146
+ {'status': 'withheld', 'path': 'guard:out_of_fragment', 'response': 'Deliberate refusal.'}]:
147
+ self.runtime.parent_reply = original
148
+ self.assertIs(self.runtime.chat('A rectangle has length 3 meters and width 4 centimeters. What is its area?'), original)
149
+
150
+ def test_shape_mentions_without_measure_request_stay_with_parent(self):
151
+ for text in ['A rectangle has length 3 meters and width 4 centimeters. Who invented rectangles?',
152
+ 'A rectangle has area 12 and length 3 meters and width 4 meters. What color is it?',
153
+ 'Two squares are drawn in blue. Who drew them?']:
154
+ self.runtime.parent_reply = {'status': 'clarification', 'path': 'act:clarify_specific', 'response': 'Original non-measure response.'}
155
+ self.assertIs(self.runtime.chat(text), self.runtime.parent_reply)
156
+
157
+ def test_non_calculation_question_with_measure_word_stays_with_parent(self):
158
+ for text in ['Who discovered the area formula of a circle?',
159
+ 'A square has side 3 meters. What is the history of its area formula?',
160
+ 'A rectangle has length 3 meters and width 4 meters. What is the name of its area formula?']:
161
+ self.runtime.parent_reply = {'status':'clarification','path':'act:clarify_specific','response':'Existing non-calculation answer.'}
162
+ self.assertIs(self.runtime.chat(text), self.runtime.parent_reply)
163
+
164
+ def test_extra_numeric_tuple_is_not_ignored(self):
165
+ for text in ['A rectangle has length 3 meters and width 4 meters, also 5 by 6. What is its area?',
166
+ 'A rectangle has length 3 meters and width 4 meters, also 5 by 6. What is its perimeter?']:
167
+ reply = self.answer(text)
168
+ self.assertEqual(reply['status'], 'withheld')
169
+ self.assertIn('every number', reply['response'])
170
+
171
+ def test_unused_physical_roles_are_not_ignored(self):
172
+ for text in ['A sphere has radius 2 centimeters and height 3 centimeters. What is its volume?',
173
+ 'A circle has radius 3 meters and height 4 meters. What is its area?',
174
+ 'A rectangle has length 3 meters and width 4 meters and radius 2 meters. What is its area?']:
175
+ reply = self.answer(text)
176
+ self.assertEqual(reply['status'], 'withheld')
177
+ self.assertIn('all supplied geometry roles', reply['response'])
178
+
179
+ def test_apartment_identifier_is_not_a_length(self):
180
+ self.assertIn('area is 0.12 m²', self.answer('Apartment 16 contains a rectangle with length 3 meters and width 4 centimeters. What is its area?')['response'])
181
+
182
+ def test_unrelated_question_does_not_cost_parent(self):
183
+ self.runtime.parent_reply = {'status':'clarification','path':'act:clarify_specific','response':'Original unrelated answer.'}
184
+ self.assertIs(self.runtime.chat('What music do you enjoy?'), self.runtime.parent_reply)
185
+
186
+
187
+ if __name__ == '__main__':
188
+ unittest.main()
tests/usage/research/test_relation_semantics.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Regression tests for the granted Oracle relation_spec_v1 source repair.
2
+
3
+ Set RESEARCH_RELATION_SOURCE to test the same module in another package. The
4
+ default is the root-approved build source, never the immutable passing candidate.
5
+ """
6
+ import importlib.util
7
+ import os
8
+ import re
9
+ import sys
10
+ import types
11
+ import unittest
12
+ from pathlib import Path
13
+ from unittest.mock import patch
14
+
15
+ ROOT = Path(__file__).resolve().parents[3]
16
+ SOURCE = Path(os.environ.get("RESEARCH_RELATION_SOURCE", ROOT / "root/wander-train/relation_spec_v1.py"))
17
+
18
+
19
+ @unittest.skipUnless(SOURCE.exists(), "Set RESEARCH_RELATION_SOURCE to the patched relation_spec_v1.py")
20
+ class RelationSemanticsTests(unittest.TestCase):
21
+ def setUp(self):
22
+ self.names = {"star wars": [(462, 184, 0), (17738, 96, 1)]}
23
+ self.props = {(17738, 86): frozenset({"John Williams"}),
24
+ (17738, 577): frozenset({"1977-05-25T00:00:00Z"})}
25
+ linker = types.SimpleNamespace(candidates=lambda name, limit: self.names.get(name, [])[:limit], STOP=set())
26
+ x = types.SimpleNamespace(L=linker, norm=lambda s: re.sub(r"[^a-z0-9]+", " ", s.lower()).strip(),
27
+ _show=lambda value, q: value, TIME_VARIANT=set())
28
+ spec = importlib.util.spec_from_file_location("test_relation_spec", SOURCE)
29
+ self.module = importlib.util.module_from_spec(spec)
30
+ with patch.dict(sys.modules, {"exact_router_v1": x}):
31
+ spec.loader.exec_module(self.module)
32
+ self.module.in_domain = lambda qid, domain: (qid == 17738 and domain == "films") or (qid == 2536187 and domain == "singers")
33
+ self.module.values = lambda qid, prop: self.props.get((qid, prop), frozenset())
34
+ self.module.analyse = lambda q: {"cands": [17738], "feats": {("where", "born")}}
35
+ self.patterns = {"inv|who|composed": [800], "fwd|where|born": [19]}
36
+
37
+ def test_observed_failure_now_reads_composer_of_dated_film(self):
38
+ answer = self.module.answer("Who composed the music for Star Wars (1977)?", "films", self.patterns)
39
+ self.assertEqual((answer["answer"], answer["property"], answer["subject_qid"]), ("John Williams", 86, 17738))
40
+
41
+ def test_singer_notable_work_never_satisfies_composer(self):
42
+ self.assertIsNone(self.module.answer("Who composed the music for Star Wars (1977)?", "singers", self.patterns))
43
+
44
+ def test_supported_composer_phrasings(self):
45
+ for question in ["Who composed Star Wars?", "Who wrote the music for Star Wars?",
46
+ "Who is the composer of Star Wars?", "Which composer wrote Star Wars?",
47
+ "Could you please tell me who composed the music for the film Star Wars (1977)?"]:
48
+ with self.subTest(question=question):
49
+ self.assertEqual(self.module.answer(question, "films", self.patterns)["property"], 86)
50
+
51
+ def test_missing_composer_does_not_fall_back_to_notable_work(self):
52
+ self.props.pop((17738, 86))
53
+ self.assertIsNone(self.module.answer("Who composed Star Wars?", "films", self.patterns))
54
+
55
+ def test_unmatched_year_is_not_ignored(self):
56
+ self.assertIsNone(self.module.answer("Who composed Star Wars (1777)?", "films", self.patterns))
57
+
58
+ def test_extra_condition_is_not_ignored(self):
59
+ self.assertIsNone(self.module.answer("Who composed Star Wars and directed Jaws?", "films", self.patterns))
60
+
61
+ def test_capped_candidates_do_not_count_as_complete(self):
62
+ self.names["star wars"] = [(17738, 96, 1)] * 65
63
+ self.assertIsNone(self.module.answer("Who composed Star Wars?", "films", self.patterns))
64
+
65
+ def test_disagreeing_composers_are_not_selected_by_popularity(self):
66
+ self.names["star wars"].append((999, 1, 1))
67
+ self.module.in_domain = lambda qid, domain: qid in {17738, 999}
68
+ self.props[(999, 86)] = frozenset({"Someone else"})
69
+ self.assertIsNone(self.module.answer("Who composed Star Wars?", "films", self.patterns))
70
+
71
+ def test_other_relations_remain_available(self):
72
+ self.props[(17738, 19)] = frozenset({"Test City"})
73
+ answer = self.module.answer("Where was Test Person born?", "films", self.patterns)
74
+ self.assertEqual((answer["answer"], answer["property"]), ("Test City", 19))
75
+
76
+ def test_receipt_labels_preserve_property_meaning(self):
77
+ self.assertEqual(self.module.prop_label(26), "spouse")
78
+ self.assertEqual(self.module.prop_label(86), "composer")
79
+ self.assertEqual(self.module.prop_label(800), "notable work")
80
+
81
+
82
+ if __name__ == "__main__":
83
+ unittest.main()