The falsifiable experiment's measurements #2 (ablation) and #3 (diversity). Builds the competent, code-disjoint SYMBOLIC reader (the control arm AND the C3 capability path) and the ablation instrument that runs both readers through the real verify_tier2_agreement gate. VERDICT: C3 — the field is decoration on this domain (a sanctioned, honest negative): - field_wrong_commits = [] (wrong=0 holds; the per-step drift guard refuses bad ints) - field_caught_symbolic_errors = [] (the field caught ZERO symbolic errors) - per-class diversity = 0 everywhere (both readers agree and are both correct) - the only admitted-set change is the field LOSING coverage at the precision ceiling. Insight: on forward-substitutable relations, geometric translation IS arithmetic addition, so there is no metric over-determination for the field to exploit — field and symbol are common-mode (Knight-Leveson), not a genuine second derivation. This is the deductive finding's twin: logic was combinatorial (field can't earn it), additive is arithmetically trivial (field adds nothing). The field needs metric-nontrivial AND arithmetically-hard structure to earn a reasoning role — dedicated research, not near-term. Field-as-reasoner is NOT earned; no field vote enters any serving path; the field stays a servant. Capability path = symbolic (C3), not shipped here. - generate/relational_symbolic_reader.py: competent independent reader (pure int). - evals/relational_metric/ablation.py: the reusable decoration instrument. - docs/analysis/field-wedge-ablation-result-2026-06-04.md: the recorded verdict. All prior artifacts STAY (field reader = real wrong=0 read demo + 3rd panel domain). Green: full wedge suite 104; 53 architectural invariants.
127 lines
4.8 KiB
Python
127 lines
4.8 KiB
Python
"""The SYMBOLIC reader for forward-substitutable quantitative relations.
|
|
|
|
The second, code-disjoint reading of the same micro-domain as
|
|
``generate.relational_field_reader`` — but it resolves by plain integer arithmetic
|
|
over a parsed schema, importing **no** ``algebra`` / ``field`` (proven import-disjoint
|
|
from the field reader by INV-27). It is:
|
|
|
|
- the control arm of the ablation experiment (does the geometric FIELD reader add any
|
|
signal over a competent symbolic reader, or is it decoration?), and
|
|
- the C3 capability path if the field does not earn its role (a second independent
|
|
reading whose agreement with another reader is the wrong=0 gate).
|
|
|
|
It is a *competent* reader, not a strawman: it reads the same grammar and applies the
|
|
same fences (multiplicative/ratio, forward-substitutability, negatives), and it
|
|
detects over-determination (a quantity stated two ways that conflict) and refuses —
|
|
so the ablation is a fair test. It has NO float/precision limit (pure int), so unlike
|
|
the field it commits arbitrarily large quantities.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
|
|
# Stable identity for the Tier-2 gate (registered in INV-27).
|
|
READER_LINEAGE: str = "symbolic.relational_schema"
|
|
|
|
_FENCED_CUE = re.compile(
|
|
r"\b(times|twice|double|triple|thrice|half|quarter|ratio|per|each)\b"
|
|
)
|
|
|
|
# Independent regex set (own spelling/order) — same grammar, different implementation.
|
|
_REL_MORE = re.compile(r"(\w+) (?:has|have) (\d+) more \w+ than (\w+)")
|
|
_REL_FEWER = re.compile(r"(\w+) (?:has|have) (\d+) (?:fewer|less) \w+ than (\w+)")
|
|
_REL_FACT = re.compile(r"(\w+) (?:has|have) (\d+) (\w+)")
|
|
_Q_SINGLE = re.compile(r"how many (\w+) does (\w+) have")
|
|
_Q_SUM = re.compile(r"how many (\w+) do (\w+) and (\w+)(?: and (\w+))? have")
|
|
|
|
|
|
class SymbolicReaderError(ValueError):
|
|
"""Out-of-grammar case → typed refusal."""
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SymbolicReading:
|
|
refused: bool
|
|
answer: int | None = None
|
|
answer_unit: str | None = None
|
|
refusal_reason: str | None = None
|
|
reader_lineage: str = READER_LINEAGE
|
|
|
|
|
|
def _refuse(reason: str) -> SymbolicReading:
|
|
return SymbolicReading(refused=True, refusal_reason=reason)
|
|
|
|
|
|
def read_relational(text: str) -> SymbolicReading:
|
|
if not isinstance(text, str) or not text.strip():
|
|
return _refuse("empty_input")
|
|
if _FENCED_CUE.search(text.lower()):
|
|
return _refuse("fenced_multiplicative")
|
|
try:
|
|
return _read(text.lower())
|
|
except SymbolicReaderError as exc:
|
|
return _refuse(str(exc))
|
|
|
|
|
|
def _set(values: dict[str, int], entity: str, value: int) -> None:
|
|
"""Assign, detecting over-determination (a conflicting re-statement)."""
|
|
if entity in values and values[entity] != value:
|
|
raise SymbolicReaderError("over_determined_conflict")
|
|
values[entity] = value
|
|
|
|
|
|
def _read(text: str) -> SymbolicReading:
|
|
parts = [s.strip() for s in re.split(r"[.?!]", text) if s.strip()]
|
|
|
|
unit: str | None = None
|
|
q_single: str | None = None
|
|
q_sum: list[str] | None = None
|
|
for s in parts:
|
|
if "how many" not in s:
|
|
continue
|
|
if (m := _Q_SUM.search(s)) is not None:
|
|
unit = m.group(1)
|
|
q_sum = [g for g in m.groups()[1:] if g]
|
|
elif (m := _Q_SINGLE.search(s)) is not None:
|
|
unit, q_single = m.group(1), m.group(2)
|
|
else:
|
|
raise SymbolicReaderError("unparsed_query")
|
|
break
|
|
if q_single is None and q_sum is None:
|
|
raise SymbolicReaderError("no_query")
|
|
|
|
values: dict[str, int] = {}
|
|
for s in parts:
|
|
if "how many" in s:
|
|
continue
|
|
if (m := _REL_MORE.search(s)) is not None:
|
|
ent, n, ref = m.group(1), int(m.group(2)), m.group(3)
|
|
if ref not in values:
|
|
raise SymbolicReaderError("non_forward_substitutable")
|
|
_set(values, ent, values[ref] + n)
|
|
elif (m := _REL_FEWER.search(s)) is not None:
|
|
ent, n, ref = m.group(1), int(m.group(2)), m.group(3)
|
|
if ref not in values:
|
|
raise SymbolicReaderError("non_forward_substitutable")
|
|
_set(values, ent, values[ref] - n)
|
|
elif (m := _REL_FACT.search(s)) is not None:
|
|
ent, n = m.group(1), int(m.group(2))
|
|
_set(values, ent, n)
|
|
else:
|
|
raise SymbolicReaderError("unparsed_sentence")
|
|
|
|
if q_single is not None:
|
|
if q_single not in values:
|
|
raise SymbolicReaderError("query_entity_unresolved")
|
|
answer = values[q_single]
|
|
else:
|
|
assert q_sum is not None
|
|
if any(p not in values for p in q_sum):
|
|
raise SymbolicReaderError("query_entity_unresolved")
|
|
answer = sum(values[p] for p in q_sum)
|
|
|
|
if answer < 0:
|
|
raise SymbolicReaderError("negative_quantity")
|
|
return SymbolicReading(refused=False, answer=answer, answer_unit=unit)
|