CAEL-0.4.0 / CORE_0_4.patch
vynly's picture
Release CAEL 0.4.0 core automata and official CellARC reproduction
ad91e1f verified
Raw History Blame Contribute Delete
41.6 kB
--- a/cael_unified/__init__.py
+++ b/cael_unified/__init__.py
@@ -1,5 +1,5 @@
-"""CAEL 0.3.9.1: non-neural conversation runtime with the unchanged 0.3.7 resource bundle."""
-__version__ = '0.3.9.1'
+"""CAEL 0.4.0 core-engine candidate; not a published or deployed release."""
+__version__ = '0.4.0'
def __getattr__(name):
--- a/cael_unified/_bounded_python_help.py
+++ b/cael_unified/_bounded_python_help.py
@@ -48,5 +48,9 @@
verify.sandbox_function = denied
apply = verify.apply
- verify.apply = lambda text, answer: apply(text, answer, record=False)
+ def unrecorded_apply(text, answer, record=False):
+ # Preserve the checker's call contract for orchestrators that explicitly
+ # request no recording. Keep the bounded policy's no-record behaviour.
+ return apply(text, answer, record=False)
+ verify.apply = unrecorded_apply
--- a/cael_unified/assistant.py
+++ b/cael_unified/assistant.py
@@ -181,6 +181,15 @@
# spelling repair, fact route or web lookup can read "CAEL" as someone else (self_identity.py, 0.3.8 fix).
from . import self_identity
answer = self_identity.reply(messages) if mode in ('auto', 'chat') else None
+ if (answer is None and mode in ('auto', 'chat') and isinstance(messages, list)
+ and messages and len(messages)<=MAX_MESSAGES
+ and all(isinstance(m,dict) and m.get('role') in ('user','assistant','system')
+ and isinstance(m.get('content'),str) for m in messages)
+ and sum(len(m['content']) for m in messages)<=MAX_CHARACTERS
+ and messages[-1].get('role') == 'user' and type(n) is int and 1<=n<=8
+ and (seed is None or type(seed) is int and 0<=seed<2**31)):
+ from .local_automata import reply as automaton_reply
+ answer = automaton_reply(messages[-1].get('content'))
# A complete, single-request symbolic document owns its literal
# symbols before dialogue acts, spelling and memory heuristics.
if (answer is None and mode in ('auto','chat') and isinstance(messages,list)
--- a/cael_unified/bundle.py
+++ b/cael_unified/bundle.py
@@ -8,8 +8,8 @@
import zipfile
ROOT = Path(__file__).resolve().parents[1]
-VERSION = '0.3.9.1'
-# 0.3.9.1 adds native interpretation; the public resource bundle remains byte-identical 0.3.7.
+VERSION = '0.4.0'
+# Core algorithms evolve in source; inherited public assets remain immutable.
BUNDLE_VERSION = '0.3.7'
SUPPORTED = {'0.2.1', '0.2.2', '0.2.3', '0.2.4', '0.2.5', '0.2.6', '0.2.7', '0.2.8', '0.2.9', '0.3', '0.3.1', '0.3.2', '0.3.3', '0.3.4', '0.3.5', '0.3.6', '0.3.7', '0.3.8'}
DEFAULT_BUNDLE = ROOT / 'models' / f'cael-{BUNDLE_VERSION}.cael'
--- a/cael_unified/checked_tasks.py
+++ b/cael_unified/checked_tasks.py
@@ -172,7 +172,7 @@
m=re.fullmatch(r'(?is)([^:.\n]{1,140}):\s*([^:.\n]+)\.\s*(.+)',text)
if not m or not re.match(r'(?i)^(?:items|objects|participants|on |in |at |there (?:are|stand) )',m[1]):return None
names=[clean(p.strip()) for p in re.split(r',\s*(?:and\s+)?|\s+and\s+',m[2])]
- if not 2<=len(names)<=8 or len(set(names))!=len(names):raise Unclear('Use two to eight distinct explicitly listed objects.')
+ if not 2<=len(names)<=12 or len(set(names))!=len(names):raise Unclear('Use two to twelve distinct explicitly listed objects.')
declared=re.search(r'(?i)\b('+ '|'.join(NUMBERS)+r'|\d+)\b',m[1])
if declared and _number(declared[1])!=len(names):raise Unclear('The stated object count and complete item list disagree.')
parts=[p.strip() for p in re.split(r'[.!?]\s*|\n+',m[3]) if p.strip()]
--- a/cael_unified/constraint_automaton.py
+++ b/cael_unified/constraint_automaton.py
@@ -0,0 +1,76 @@
+"""Finite-domain constraint propagation with exhaustive bounded witnesses.
+
+Only explicit trusted predicates are called. No generated code, neural model,
+training answers or benchmark-dependent rule selection.
+"""
+from itertools import product
+
+
+class SearchLimit(ValueError):
+ pass
+
+
+def enumerate_orders(names, constraints, holds, max_nodes=250000, max_checks=3000000,
+ max_solutions=50000):
+ names = tuple(names)
+ if not 2 <= len(names) <= 12 or len(set(names)) != len(names):
+ raise ValueError('Use 2 to 12 distinct items')
+ scopes = [tuple(dict.fromkeys(c[1:2] + (() if c[0].removeprefix('not_') == 'at' else c[2:])))
+ for c in constraints]
+ if any(any(n not in names for n in scope) for scope in scopes):
+ raise ValueError('A constraint references an unlisted item')
+ stats = dict(nodes=0, predicate_checks=0, propagations=0, complete=False)
+ solutions = []
+
+ def propagate(domains):
+ while True:
+ before = tuple(tuple(sorted(domains[n])) for n in names)
+ singles = [next(iter(v)) for v in domains.values() if len(v) == 1]
+ if len(singles) != len(set(singles)):
+ return False
+ used = set(singles)
+ for name, values in domains.items():
+ if len(values) > 1: values.difference_update(used)
+ if not values: return False
+ # Hall's union necessary condition, alongside singleton all-different.
+ if len(set().union(*domains.values())) < len(names): return False
+ for constraint, scope in zip(constraints, scopes):
+ supported = {name: set() for name in scope}
+ for values in product(*(sorted(domains[n]) for n in scope)):
+ stats['predicate_checks'] += 1
+ if stats['predicate_checks'] > max_checks:
+ raise SearchLimit('Constraint propagation work budget reached')
+ if len(values) != len(set(values)): continue
+ if holds(constraint, dict(zip(scope, values))):
+ for name, value in zip(scope, values): supported[name].add(value)
+ for name in scope:
+ domains[name].intersection_update(supported[name])
+ if not domains[name]: return False
+ stats['propagations'] += 1
+ if before == tuple(tuple(sorted(domains[n])) for n in names): return True
+
+ def visit(domains):
+ stats['nodes'] += 1
+ if stats['nodes'] > max_nodes: raise SearchLimit('Ordering search node budget reached')
+ if not propagate(domains): return
+ pending = [n for n in names if len(domains[n]) > 1]
+ if not pending:
+ positions = {n: next(iter(domains[n])) for n in names}
+ order = tuple(sorted(names, key=positions.get))
+ solutions.append(order)
+ if len(solutions) > max_solutions:
+ raise SearchLimit('Too many valid orders for complete enumeration')
+ return
+ name = min(pending, key=lambda n: (len(domains[n]), names.index(n)))
+ for value in sorted(domains[name]):
+ child = {n: set(v) for n, v in domains.items()}
+ child[name] = {value}
+ visit(child)
+
+ visit({name: set(range(len(names))) for name in names})
+ # Preserve the old public ordering of witnesses, independent of MRV order.
+ rank = {name: i for i, name in enumerate(names)}
+ solutions.sort(key=lambda row: tuple(rank[n] for n in row))
+ stats['complete'] = True
+ stats['solutions'] = len(solutions)
+ return tuple(solutions), stats
--- a/cael_unified/count_language.py
+++ b/cael_unified/count_language.py
@@ -66,9 +66,12 @@
def neighbours(self,context,limit=128):
np=self.np
- if context in self.cache:
- self.cache.move_to_end(context)
- return self.cache[context]
+ if type(limit) is not int or not 1<=limit<=2048:
+ raise ValueError('Neighbour limit must be an integer from 1 to 2048')
+ cache_key=(context,limit)
+ if cache_key in self.cache:
+ self.cache.move_to_end(cache_key)
+ return self.cache[cache_key]
query=self.query_features(context)
# Both stored/query rows are L2 normalized; this is sparse cosine recall.
from sklearn.preprocessing import normalize
@@ -76,13 +79,17 @@
hits=(query@self.contexts.T).tocsr()
if hits.nnz:
take=min(limit,hits.nnz)
- selection=np.argpartition(hits.data,-take)[-take:]
+ threshold=np.partition(hits.data,-take)[-take]
+ greater=np.flatnonzero(hits.data>threshold)
+ equal=np.flatnonzero(hits.data==threshold)
+ equal=equal[np.argsort(hits.indices[equal],kind='stable')]
+ selection=np.concatenate((greater,equal[:take-len(greater)]))
order=np.lexsort((hits.indices[selection],-hits.data[selection]))
selection=selection[order]
result=(hits.indices[selection],hits.data[selection])
else:
result=(np.array([],dtype=np.int64),np.array([],dtype=np.float64))
- self.cache[context]=result
+ self.cache[cache_key]=result
if len(self.cache)>32:
self.cache.popitem(last=False)
return result
@@ -90,7 +97,7 @@
def conditional(self,context,settings=None):
np=self.np
settings=settings or self.settings
- indices,similarities=self.neighbours(context)
+ indices,similarities=self.neighbours(context,limit=max(128,settings['neighbours']))
count=min(len(indices),settings['neighbours'])
if count:
weights=similarities[:count].astype(np.float64)**settings['recall_power']
--- a/cael_unified/event_memory.py
+++ b/cael_unified/event_memory.py
@@ -43,14 +43,18 @@
def key(entity,attribute):return clean(entity).casefold()+'\x1f'+clean(attribute).casefold()
def resolve(state,cell,seen=None):
- seen=set() if seen is None else seen
- if cell in seen or len(seen)>128:raise ValueError('Circular or too-deep reference')
- seen.add(cell)
- value=state.get(cell,UNKNOWN)
- if isinstance(value,dict):
- if 'ref' in value:return resolve(state,value['ref'],seen)
- raise ValueError('Unknown or conflicting value')
- return value
+ # Iterative traversal supports the stated cell budget, instead of an
+ # unrelated 128-link or Python recursion ceiling. References remain lazy:
+ # changing a source affects live links but never earlier copied values.
+ seen=set() if seen is None else set(seen)
+ while True:
+ if cell in seen:raise ValueError('Circular live dependency')
+ if len(seen)>=MAX_CELLS:raise ValueError('Reference traversal exceeds the state-cell budget')
+ seen.add(cell)
+ value=state.get(cell,UNKNOWN)
+ if not isinstance(value,dict):return value
+ if 'ref' not in value:raise ValueError('Unknown or conflicting value')
+ cell=value['ref']
def transition(state,slots,operation):
"""The candidate algebra used by both the trainer and runtime.
@@ -241,8 +245,8 @@
boundary=versions[0][0] if past=='initial' else versions[-1][0]-1
state={}
for k,history in self.history.items():
- prior=[v for i,v in history if i<=boundary]
- if prior:state[k]=prior[-1]
+ offset=bisect_right(history,boundary,key=lambda entry:entry[0])
+ if offset:state[k]=history[offset-1][1]
return resolve(state,cell)
def reply(messages):
--- a/cael_unified/interpretation_relations.py
+++ b/cael_unified/interpretation_relations.py
@@ -109,6 +109,16 @@
if not any(re.search(r'(?i)\b(?:before|after|ahead of|behind|precedes|follows)\b',p) for p in parts[:-1]):return None
boundary(parts[-1])
if query[0]=='unsupported':raise TaskError('The complete ordering question contains an unsupported qualifier.')
+ # Richer explicit constraints belong to the same finite-domain engine used
+ # by ordinary reasoning. Do not reject them in the precedence-only reader.
+ if re.match(r'(?i)^(?:items|people|objects|participants|runners|books|cars|order)\s*:', parts[0]) and any(
+ re.search(r'(?i)\b(?:immediately|directly|adjacent|next to|between|not|first|last|second|third|fourth|fifth)\b', p)
+ for p in parts[1:-1]):
+ from .reasoning_order import ordering
+ try: result = ordering('. '.join(parts[:-1]), parts[-1])
+ except ValueError as exc: raise TaskError(str(exc)) from exc
+ return Result(result['answer'], 'ordering', {'coverage': spans(text, parts),
+ 'engine': 'finite-domain-constraint-automaton', 'proof': result})
names=[];edges=[]
for raw in parts[:-1]:
p=raw.strip()
@@ -178,6 +188,10 @@
words=set(re.findall(r'[a-z]+',p.casefold()))
if words and words<=header_words and words&{'log','events','entries','chronological','updates'}:continue
boundary(p)
+ m=re.fullmatch(rf'(?i)({ATOM}?) is either in (?:the )?({ATOM}?) or (?:the )?({ATOM})',p)
+ if m:events.append(('uncertain',symbol(m[1]),symbol(m[2]),symbol(m[3])));continue
+ m=re.fullmatch(rf'(?i)({ATOM}?) is (?:not|no longer) in (?:the )?({ATOM})',p)
+ if m:events.append(('exclude',symbol(m[1]),symbol(m[2])));continue
m=re.fullmatch(rf'(?i)({ATOM}?) (?:went|moved|relocated|walked|travelled|traveled|journeyed|returned)(?: back)? (?:to|into) (?:the )?({ATOM})',p)
if m:events.append(('move',symbol(m[1]),symbol(m[2])));continue
m=re.fullmatch(rf'(?i)({ATOM}?) (?:entered|visited) (?:the )?({ATOM})',p)
@@ -227,13 +241,17 @@
if cell and cell.locations and cell.locations!={name(value)}:raise TaskError('A departure location conflicts with the preceding event state.')
event_text=f'{name(actor)} went to {name(extra[0])}'
elif op=='move':event_text=f'{name(actor)} went to {name(value)}'
+ elif op=='uncertain':event_text=f'{name(actor)} is either in {name(value)} or {name(extra[0])}'
+ elif op=='exclude':event_text=f'{name(actor)} is not in {name(value)}'
elif op=='take':event_text=f'{name(actor)} got {name(value)}'
elif op=='drop':event_text=f'{name(actor)} dropped {name(value)}'
else:
old=world.cells.get(name(value))
if old and old.owner not in (None,name(actor)):raise TaskError('The giver is not the established holder of the object.')
event_text=f'{name(actor)} gave {name(value)} to {name(extra[0])}'
- world.apply(world.event(event_text),index);canonical.append(event_text+'.')
+ try: world.apply(world.event(event_text),index)
+ except ValueError as exc: raise TaskError(str(exc)) from exc
+ canonical.append(event_text+'.')
target_value=query[1]
if target_value in ('he','she','they','it'):
referents=known_objects if query[0]=='owner' else known_actors
--- a/cael_unified/local_automata.py
+++ b/cael_unified/local_automata.py
@@ -0,0 +1,181 @@
+"""Example-induced finite local transition models, without neural inference.
+
+The inductive bias is explicit: stationary 1-D discrete updates, a bounded
+neighbourhood, exact lookup consistency and aggregate/symmetry hypotheses.
+Example-pair cross-validation ranks hypotheses. Query answers never enter fit.
+Unseen neighbourhoods use labelled empirical guesses, never claimed proofs.
+"""
+from collections import Counter, defaultdict
+from dataclasses import dataclass
+import json
+import math
+
+MAX_RADIUS = 3
+MAX_EXAMPLES = 32
+MAX_WIDTH = 512
+MAX_SYMBOL = 31
+
+
+class OutsideDomain(ValueError):
+ pass
+
+
+def row(value):
+ if type(value) is not list or not 1 <= len(value) <= MAX_WIDTH:
+ raise OutsideDomain('Expected a nonempty discrete row of at most 512 cells')
+ if any(type(v) is not int or not 0 <= v <= MAX_SYMBOL for v in value):
+ raise OutsideDomain('Cells must be integer symbols from 0 through 31')
+ return tuple(value)
+
+
+def neighbourhood(source, position, radius, boundary):
+ if boundary == 'wrap':
+ return tuple(source[(position+d) % len(source)] for d in range(-radius, radius+1))
+ if boundary == 'zero':
+ return tuple(source[position+d] if 0 <= position+d < len(source) else 0
+ for d in range(-radius, radius+1))
+ raise OutsideDomain('Boundary must be wrap or zero')
+
+
+def signature(window, radius, kind):
+ if kind == 'left': return (window[0],)
+ if kind == 'right': return (window[-1],)
+ if kind == 'ordered': return window
+ if kind == 'multiset': return tuple(sorted(window))
+ if kind == 'sum': return (sum(window),)
+ outside = window[:radius] + window[radius+1:]
+ if kind == 'center_sum': return window[radius], sum(outside)
+ if kind == 'center_multiset': return window[radius], tuple(sorted(outside))
+ if kind == 'outer_sum': return (sum(outside),)
+ raise OutsideDomain('Unknown feature family')
+
+
+@dataclass
+class Hypothesis:
+ radius: int
+ kind: str
+ table: dict
+ counts: dict
+ validation_lnp: float
+ validation_seen: int
+ validation_cells: int
+ cost: float
+
+
+def fit_map(pairs, radius, kind, boundary):
+ table = {}; counts = Counter()
+ for source, target in pairs:
+ for position, value in enumerate(target):
+ key = signature(neighbourhood(source, position, radius, boundary), radius, kind)
+ if key in table and table[key] != value: return None
+ table[key] = value; counts[key] += 1
+ return table, counts
+
+
+def learn_transition(examples, query, boundary='wrap'):
+ """Predict a new row from demonstration pairs, retaining coverage/ambiguity.
+
+ All fitting is ephemeral and episode-local. This function accepts only
+ demonstrations and query, never a rule-table/family/label metadata object.
+ """
+ if type(examples) is not list or not 2 <= len(examples) <= MAX_EXAMPLES:
+ raise OutsideDomain('Provide 2 to 32 independent example pairs')
+ pairs = []
+ for example in examples:
+ if type(example) is not dict or set(example) != {'input', 'output'}:
+ raise OutsideDomain('Each example needs only input and output')
+ source, target = row(example['input']), row(example['output'])
+ if len(source) != len(target): raise OutsideDomain('Input/output widths must match')
+ pairs.append((source, target))
+ query = row(query)
+ if sum(len(x) for x, _ in pairs) > 16384:
+ raise OutsideDomain('Demonstration cell budget reached')
+ if boundary not in ('wrap', 'zero'): raise OutsideDomain('Unknown boundary')
+ # Contradictory full-row evidence is invalid even if a larger neighbourhood
+ # could hide a collision. Retain ambiguity instead of fitting the last row.
+ seen_rows = {}
+ for source, target in pairs:
+ if source in seen_rows and seen_rows[source] != target:
+ raise OutsideDomain('The demonstrations give conflicting outputs for the same input')
+ seen_rows[source] = target
+ symbols = max([*query, *(v for x, y in pairs for v in (*x, *y))]) + 1
+ symbols = max(2, symbols)
+ global_counts = Counter(v for _, target in pairs for v in target)
+ fallback = max(range(symbols), key=lambda v: (global_counts[v], -v))
+ models = []
+ families = [(0, 'ordered')]
+ families += [(r, kind) for r in range(1, MAX_RADIUS+1)
+ for kind in ('left', 'right', 'sum', 'outer_sum', 'center_sum', 'multiset', 'center_multiset', 'ordered')]
+ for radius, kind in families:
+ fitted = fit_map(pairs, radius, kind, boundary)
+ if fitted is None: continue
+ table, counts = fitted
+ lnp = 0.0; covered = 0; total = 0
+ # Hold out entire pairs, not individual neighbouring cells, to avoid
+ # scoring an example against its own transitions.
+ for excluded, (source, target) in enumerate(pairs):
+ rest = pairs[:excluded] + pairs[excluded+1:]
+ partial, witnesses = fit_map(rest, radius, kind, boundary)
+ prior = Counter(v for _, output in rest for v in output)
+ prior_total = sum(prior.values())
+ for position, value in enumerate(target):
+ key = signature(neighbourhood(source, position, radius, boundary), radius, kind)
+ if key in partial:
+ covered += 1
+ probability = (witnesses[key] + 0.5) / (witnesses[key] + 0.5*symbols)
+ else:
+ probability = (prior[value]+0.5) / (prior_total+0.5*symbols)
+ lnp += math.log(probability); total += 1
+ description = (2*radius+1 if kind == 'ordered' else radius+2 if 'multiset' in kind else 2)
+ cost = math.log2(2+len(table)) + 0.25*description
+ models.append(Hypothesis(radius, kind, table, counts, lnp, covered, total, cost))
+ models.sort(key=lambda m: (-(m.validation_lnp-0.10*m.cost), m.cost, m.radius, m.kind))
+ # No silent switch to an external model or query-label-derived rule. If
+ # local stationarity cannot fit, emit an explicit empirical-only result.
+ if not models:
+ return dict(prediction=[fallback]*len(query), supported=[False]*len(query),
+ alternatives=[list(range(symbols)) for _ in query],
+ status='empirical-only', fitted_hypotheses=0, selected=None,
+ coverage=0.0, neural=False, persistent_learning=False)
+ selected = models[0]
+ prediction = []; supported = []; alternatives = []
+ for position in range(len(query)):
+ key = signature(neighbourhood(query, position, selected.radius, boundary), selected.radius, selected.kind)
+ known = key in selected.table
+ # Back off through independently consistent hypotheses when the chosen
+ # compressed representation lacks a query signature.
+ choices = []
+ for model in models:
+ signature_key = signature(neighbourhood(query, position, model.radius, boundary), model.radius, model.kind)
+ if signature_key in model.table: choices.append(model.table[signature_key])
+ value = selected.table[key] if known else choices[0] if choices else fallback
+ prediction.append(value); supported.append(known)
+ alternatives.append(sorted(set(choices)) if choices else list(range(symbols)))
+ return dict(prediction=prediction, supported=supported, alternatives=alternatives,
+ status='inferred' if all(supported) else 'partially-supported',
+ coverage=sum(supported)/len(query), fitted_hypotheses=len(models),
+ selected=dict(radius=selected.radius, features=selected.kind,
+ observed_signatures=len(selected.table),
+ held_pair_coverage=selected.validation_seen/max(1, selected.validation_cells),
+ held_pair_log_likelihood=selected.validation_lnp),
+ disagreeing_cells=sum(len(v)>1 for v in alternatives),
+ neural=False, persistent_learning=False,
+ guarantee='Prediction under an inferred finite local hypothesis, not a uniquely identified general rule')
+
+
+def reply(text):
+ prefix = 'Infer discrete transition:'
+ if not isinstance(text, str) or not text.lstrip().startswith(prefix): return None
+ payload = text.lstrip()[len(prefix):].strip()
+ try:
+ if len(payload) > 200000: raise OutsideDomain('Structured task length limit')
+ task = json.loads(payload)
+ if type(task) is not dict or not {'examples', 'query'} <= set(task) or set(task)-{'examples','query','boundary'}:
+ raise OutsideDomain('Use examples, query, and optional boundary only')
+ result = learn_transition(task['examples'], task['query'], task.get('boundary', 'wrap'))
+ except (ValueError, TypeError, RecursionError) as exc:
+ return dict(route='chat/automaton-clarify', response='Please clarify the transition task: '+str(exc),
+ sources=[], automaton={'status':'invalid-or-unsupported', 'neural':False})
+ return dict(route='chat/automaton-induction', response=json.dumps(result['prediction']), sources=[],
+ automaton=result, details='Example-induced discrete local transitions. '+result.get('guarantee',
+ 'Only an empirical output-frequency guess; no supported local rule was found.'))
--- a/cael_unified/reasoning_logic.py
+++ b/cael_unified/reasoning_logic.py
@@ -1,5 +1,6 @@
"""Bounded Horn-rule graph automaton with explicit proof support and open-world answers."""
from dataclasses import dataclass
+from collections import defaultdict, deque
import re
@@ -89,33 +90,45 @@
def close(self, context):
facts, rules = self.parse(context)
entities = sorted({fact[0] for fact, _ in facts})
- if len(entities)*len(rules)>4096:
+ if len(entities)*len(rules)>32768:
raise Unparsed('Too many grounded rule cells')
- active = {fact for fact, _ in facts}
- proof = {fact: {index} for fact, index in facts}
- depths = {fact: 0 for fact, _ in facts}
+ active, proof, depths = set(), {}, {}
+ for fact, index in facts:
+ if fact not in active:
+ active.add(fact); proof[fact] = {index}; depths[fact] = 0
grounded = [(tuple((entity, p, neg) for p, neg in body), tuple((entity, p, neg) for p, neg in head), index)
for entity in entities for body, head, index in rules]
- # Bipartite graph CA: rule cells read only connected premise fact cells;
- # conclusion fact cells read only connected rule cells. No neural weights.
+ # Sparse graph CA: changes wake only connected rule cells. Proof
+ # improvements propagate too; truth and evidence share the fixed point.
+ readers = defaultdict(list)
+ for rule_id, (body, head, index) in enumerate(grounded):
+ for atom in set(body): readers[atom].append(rule_id)
+ agenda = deque(sorted(active)); queued = set(active)
table = self.model.get('and_table', {})
- for tick in range(24):
- next_active = set(active)
- next_proof, next_depth = dict(proof), dict(depths)
- for body, head, index in grounded:
+ work = 0
+ def rank(depth, support):
+ return depth, len(support), tuple(sorted(support))
+ while agenda:
+ changed = agenda.popleft(); queued.remove(changed)
+ for rule_id in readers[changed]:
+ work += 1
+ if work > 250000:
+ raise Unparsed('The sparse graph CA exceeded its rule-transition budget')
+ body, head, index = grounded[rule_id]
mask = sum((1 << i) for i, atom in enumerate(body) if atom in active)
if not table.get(f'{len(body)}:{mask}', 0):
continue
support = {index}.union(*(proof[atom] for atom in body))
depth = 1 + max(depths[atom] for atom in body)
for atom in head:
- next_active.add(atom)
- if atom not in next_depth or depth < next_depth[atom]:
- next_proof[atom], next_depth[atom] = support, depth
- if next_active == active:
- return active, proof, depths
- active, proof, depths = next_active, next_proof, next_depth
- raise Unparsed('The proof exceeded 24 rule-application rounds')
+ if atom not in active or rank(depth, support) < rank(depths[atom], proof[atom]):
+ active.add(atom); proof[atom], depths[atom] = support, depth
+ if atom not in queued:
+ agenda.append(atom); queued.add(atom)
+ self.last_stats = dict(engine='sparse-horn-graph-ca', grounded_rules=len(grounded),
+ rule_transitions=work, active_fact_cells=len(active),
+ maximum_proof_depth=max(depths.values(), default=0))
+ return active, proof, depths
def query(self, context, question):
active, proofs, depths = self.close(context)
--- a/cael_unified/reasoning_order.py
+++ b/cael_unified/reasoning_order.py
@@ -1,14 +1,13 @@
"""Bounded integer constraints over one explicit, tie-free ordering.
-Checks all permutations, distinguishes possibility from necessity, and requires
+Propagates finite domains, distinguishes possibility from necessity, and requires
complete surface clauses. No learned weights, network calls or benchmark data.
"""
from functools import lru_cache
-from itertools import permutations
import re
from .reasoning_logic import Unparsed, sentences
-ORDINAL = dict(zip('first second third fourth fifth sixth seventh eighth'.split(), range(8)))
+ORDINAL = dict(zip('first second third fourth fifth sixth seventh eighth ninth tenth eleventh twelfth'.split(), range(12)))
LIST = r'(?:items|people|objects|participants|runners|books|cars|order)\s*:'
@@ -92,16 +91,15 @@
if not parts or ':' not in parts[0]:raise Unparsed('An ordering puzzle needs an explicit item list')
items = re.sub(r'\s+and\s+', ',', parts[0].split(':',1)[1],flags=re.I)
names = tuple(clean(item) for item in items.split(',') if clean(item))
- if len(set(names)) != len(names) or not 2 <= len(names) <= 8:raise Unparsed('Use 2 to 8 distinct items')
+ if len(set(names)) != len(names) or not 2 <= len(names) <= 12:raise Unparsed('Use 2 to 12 distinct items')
if any(not re.fullmatch(r'[\w]+(?:[ -][\w]+)*',name) for name in names):raise Unparsed('Unsupported item name')
constraints = tuple(condition(part,names) for part in parts[1:])
if len(dimensions(' '.join(parts[1:]),names))>1:
raise Unparsed('Use one ordering dimension; age, price and spatial orders are separate')
- solutions = []
- for order in permutations(names):
- positions = {name:i for i,name in enumerate(order)}
- if all(holds(c,positions) for c in constraints):solutions.append(order)
- return names,tuple(solutions)
+ from .constraint_automaton import enumerate_orders, SearchLimit
+ try: solutions, stats = enumerate_orders(names, constraints, holds)
+ except (ValueError, SearchLimit) as exc: raise Unparsed(str(exc)) from exc
+ return names, solutions
def proposition(question, names):
--- a/cael_unified/reasoning_world.py
+++ b/cael_unified/reasoning_world.py
@@ -1,5 +1,6 @@
"""Local packet automaton for bounded chronological fact/ownership scenarios."""
from dataclasses import dataclass, field
+from collections import defaultdict
import re
from .reasoning_logic import Unparsed, sentences
@@ -20,6 +21,8 @@
self.model = model
self.cells = {}
self.gifts = []
+ self.carried = defaultdict(set)
+ self.transition_visits = 0
def event(self, statement):
statement = statement.lower().strip(' .')
@@ -52,6 +55,19 @@
def apply(self, event, index):
op, actor, value, *extra = event
+ if op not in ('move', 'uncertain', 'exclude', 'take', 'drop', 'give'):
+ raise Unparsed('Unknown world transition')
+ # Validate the complete transaction before mutating any cell.
+ old_actor = self.cells.get(actor)
+ old_object = self.cells.get(value) if op in ('take', 'drop', 'give') else None
+ if old_object and old_object.owner not in (None, actor):
+ raise Unparsed('The acting person is not the established holder of this object')
+ if op == 'give' and (len(extra) != 1 or extra[0] == actor):
+ raise Unparsed('A transfer requires one distinct recipient')
+ if op in ('take', 'give') and old_actor and old_actor.locations:
+ other = old_object if op == 'take' else self.cells.get(extra[0])
+ if other and other.locations and old_actor.locations.isdisjoint(other.locations):
+ raise Unparsed('The interaction conflicts with the stated locations')
if actor not in self.cells:
self.cells[actor] = Cell(actor, 'actor')
if op in ('take', 'drop','give') and value not in self.cells:
@@ -61,40 +77,36 @@
self.cells[extra[0]]=Cell(extra[0],'actor')
self.gifts.append((actor,value,extra[0],index))
actor_cell = self.cells[actor]
- packet = dict(op=op, actor=actor, value=value, extra=extra, support={index} | actor_cell.support,
- locations=set(actor_cell.locations))
- rows = list(self.cells.values())
- carriers = [None] * len(rows)
- carriers[0] = packet
- # A packet moves one neighbouring cell per synchronous tick. Each cell
- # reads only its own state and its local packet, as in the story CA.
- for _ in range(len(rows)):
- following = [None] * len(rows)
- for position, cell in enumerate(rows):
- message = carriers[position]
- if message is None:
- continue
- if op in ('move', 'uncertain', 'exclude') and (cell.name == actor or cell.owner == actor):
- if op == 'move':
- self.locate(cell, {value}, {index} if cell.name == actor else cell.support | {index})
- elif op == 'uncertain':
- self.locate(cell, {value, extra[0]}, {index})
+ # Ownership edges define the local neighbourhood. No empty-slot
+ # N-by-N packet scan: only actor/owned-object cells receive the event.
+ if op in ('move', 'uncertain', 'exclude'):
+ for name in [actor, *sorted(self.carried[actor])]:
+ cell = self.cells[name]; self.transition_visits += 1
+ if op == 'move':
+ self.locate(cell, {value}, {index} if name == actor else cell.support | {index})
+ elif op == 'uncertain':
+ self.locate(cell, {value, extra[0]}, {index} if name == actor else cell.support | {index})
+ else:
+ remaining = cell.locations - {value}; support = cell.support | {index}
+ if remaining:
+ self.locate(cell, remaining, support)
else:
- cell.locations = set()
- cell.excluded = {value}
- cell.support = {index}
- elif op in ('take', 'drop','give') and cell.name == value:
- if op == 'drop' and cell.owner not in (None, actor):
- raise Unparsed('An actor cannot drop an object held by someone else')
- cell.owner = extra[0] if op=='give' else actor if op == 'take' else None
- self.locate(cell, message['locations'], message['support'])
- if position + 1 < len(rows):
- following[position + 1] = message
- carriers = following
+ cell.locations = set(); cell.support = support
+ cell.excluded.add(value)
+ else:
+ cell = self.cells[value]; self.transition_visits += 1
+ if cell.owner is not None: self.carried[cell.owner].discard(value)
+ cell.owner = extra[0] if op == 'give' else actor if op == 'take' else None
+ if cell.owner is not None: self.carried[cell.owner].add(value)
+ support = {index} | actor_cell.support
+ if op in ('drop', 'give'): support |= cell.support
+ self.locate(cell, set(actor_cell.locations), support)
def query(self, context, question):
self.cells = {}
self.gifts = []
+ self.carried = defaultdict(set)
+ self.transition_visits = 0
for index, statement in enumerate(sentences(context), 1):
self.apply(self.event(statement), index)
question = question.lower().strip(' ?.!.')
--- a/cael_unified/self_knowledge.json
+++ b/cael_unified/self_knowledge.json
@@ -1,45 +1,26 @@
{
- "version": "0.3.9.1",
+ "version": "0.4.0",
"changes": [
- "Native readers now interpret complete supported arithmetic, rule, ordering, location, string and table tasks before unrelated chat fallbacks.",
- "Supported interpretations compile to checked symbolic operations using the existing non-neural engines; weights and the resource bundle are unchanged.",
- "Unsupported wording or constraints can still lead to clarification or abstention rather than a guessed answer.",
- "Installed version, change-list and capability questions use local product metadata before public web lookup."
+ "This is an isolated unpublished 0.4.0 core-engine candidate, not the current Vynly worker.",
+ "Indexed rule-graph propagation, sparse validated world transitions and finite-domain ordering replace earlier core algorithms.",
+ "A new non-neural local transition learner infers bounded discrete updates from explicit examples.",
+ "Live references are traversed iteratively; retrieval honours requested neighbour counts and deterministic ties."
],
"capabilities": [
- "Solve supported standalone arithmetic, rule chains, precedence, location updates, string operations and table reductions.",
- "Keep supported context and explicit preferences from the messages in this chat.",
- "Calculate exact arithmetic and recheck supported calculations.",
- "Write and check bounded Python examples; unsupported code needs clarification.",
- "Read public sources and show links when a supported answer can be extracted.",
- "Explain Vynly features and help write image prompts."
+ "Execute supported exact arithmetic, rule, state, ordering and bounded Python tasks.",
+ "Infer finite local discrete transitions from explicit input/output examples, reporting empirical guesses and ambiguity.",
+ "Retain supported context from the supplied conversation while isolating independent requests."
],
"limitations": [
- "Finite readers miss unsupported wording. In the fresh 900-question study, 123 questions received no scored answer.",
- "I still miss some phrasings, option switches, dictionary follow-ups and requests for richer examples.",
- "My Python interpreter and language readers cover finite patterns, not arbitrary programs or English.",
- "A retrieved source is evidence, not proof that an answer is true.",
- "Supported forgetting is not general transcript erasure."
+ "This candidate is not published, deployed or independently evaluated.",
+ "Finite readers still miss wording and complete context; no general chatbot or Qwen superiority is established.",
+ "An inferred local transition is a hypothesis, not proof of a unique general rule.",
+ "Inherited weights, protected solvers, image CA kernels and the 0.3.7 public resource bundle are unchanged."
],
- "evidence": "On one fresh 900-question synthetic study, the evaluated native candidate scored 777/900 versus 414/900 for the unchanged 0.3.9 baseline. The release preserves those task modules and adds version metadata. Known task families, maintainer/AI-assisted authoring and fixed neural letter scoring limit the result. This does not establish general chat competence or a Qwen win; Qwen was not part of this study. The evaluated count fallback was inactive; a fresh installation activates it, so that primary score is configuration-specific.",
- "sources": [
- {
- "title": "CAEL 0.3.9.1 release notes",
- "url": "https://github.com/Vovala14/CAEL/releases/tag/v0.3.9.1"
- },
- {
- "title": "CAEL 0.3.9.1 model card",
- "url": "https://huggingface.co/vynly/CAEL-0.3.9.1"
- },
- {
- "title": "Fresh interpretation study and complete evidence",
- "url": "https://huggingface.co/datasets/vynly/nnai-bench/tree/main/fresh-transfer/2026-11-07-native"
- }
+ "evidence": "Authored core development and regressions are separate from external benchmark results. This source snapshot claims no CellARC score or broad 0.4 win. See local CORE_0_4.md for scope.",
+ "sources": [],
+ "local_document_provenance": [
+ "CORE_0_4.md"
],
- "local_document_provenance": [
- "README.md",
- "RELEASE_0_3_9_1.md",
- "benchmarks/native_interpretation_0391/REPORT.md"
- ],
- "scope": "Curated installed product facts; never user messages or retrieved instructions."
+ "scope": "Curated isolated-candidate product facts, not user or benchmark answer data."
}