File size: 41,582 Bytes
ad91e1f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 | --- 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."
}
|