tensorcode 0.1.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tensorcode/__init__.py +84 -0
- tensorcode/actions.py +137 -0
- tensorcode/answer_type.py +222 -0
- tensorcode/awareness.py +344 -0
- tensorcode/backends/__init__.py +0 -0
- tensorcode/backends/builtin.py +167 -0
- tensorcode/backends/hf_local.py +89 -0
- tensorcode/backends/linear.py +133 -0
- tensorcode/backends/neural.py +361 -0
- tensorcode/causal.py +262 -0
- tensorcode/change.py +566 -0
- tensorcode/chunking.py +195 -0
- tensorcode/cognition.py +311 -0
- tensorcode/context.py +97 -0
- tensorcode/control.py +291 -0
- tensorcode/cues.py +192 -0
- tensorcode/expectation.py +270 -0
- tensorcode/frames.py +232 -0
- tensorcode/language/__init__.py +36 -0
- tensorcode/language/chart.py +558 -0
- tensorcode/language/discourse.py +132 -0
- tensorcode/language/domains/__init__.py +0 -0
- tensorcode/language/domains/desktop.py +552 -0
- tensorcode/language/english.py +459 -0
- tensorcode/language/features.py +112 -0
- tensorcode/language/generate.py +574 -0
- tensorcode/language/grammar.py +893 -0
- tensorcode/language/semantics.py +349 -0
- tensorcode/learning/__init__.py +30 -0
- tensorcode/learning/certificate.py +148 -0
- tensorcode/learning/induce.py +304 -0
- tensorcode/learning/library.py +217 -0
- tensorcode/learning/literals.py +126 -0
- tensorcode/learning/verify.py +253 -0
- tensorcode/memory.py +303 -0
- tensorcode/metacognition.py +351 -0
- tensorcode/ops.py +207 -0
- tensorcode/outcomes.py +99 -0
- tensorcode/permanence.py +376 -0
- tensorcode/priming.py +191 -0
- tensorcode/py.typed +0 -0
- tensorcode/quantity.py +311 -0
- tensorcode/records.py +728 -0
- tensorcode/relation.py +771 -0
- tensorcode/runtime.py +471 -0
- tensorcode/semantics_bridge.py +308 -0
- tensorcode/social.py +380 -0
- tensorcode/temporal.py +189 -0
- tensorcode/wants.py +185 -0
- tensorcode-0.1.0a1.dist-info/METADATA +196 -0
- tensorcode-0.1.0a1.dist-info/RECORD +53 -0
- tensorcode-0.1.0a1.dist-info/WHEEL +4 -0
- tensorcode-0.1.0a1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""What an utterance means, and when that meaning may become a claim.
|
|
2
|
+
|
|
3
|
+
Two value types carry meaning: an :class:`Entity` (what a referring expression
|
|
4
|
+
picks out) and a :class:`Frame` (a predication over entities and other frames).
|
|
5
|
+
Frames carry the features that decide whether the sentence asserts anything at
|
|
6
|
+
all: mood, polarity, modality, tense.
|
|
7
|
+
|
|
8
|
+
The conversion to ``tensorcode`` claims is deliberately conservative, because the
|
|
9
|
+
cheap version of it is how a reader ends up believing the opposite of what it
|
|
10
|
+
read:
|
|
11
|
+
|
|
12
|
+
* an imperative or a question asserts nothing, and converts to ``Unknown``;
|
|
13
|
+
* a negated, modal or non-past-or-present frame is **reified** — the event gets
|
|
14
|
+
a ``Ref`` and the polarity/modality become claims about it — rather than
|
|
15
|
+
flattened into a triple that would state the bare proposition;
|
|
16
|
+
* reported speech becomes claims in a scope of their own, sourced to the
|
|
17
|
+
speaker, so "Anem said the field failed" never enters the shared world as
|
|
18
|
+
"the field failed";
|
|
19
|
+
* an unresolved reference converts to ``Unknown``, never to a guess.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import hashlib
|
|
25
|
+
import json
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from functools import cached_property
|
|
28
|
+
from datetime import datetime, timezone
|
|
29
|
+
from typing import Any, Callable, Iterable, Mapping
|
|
30
|
+
|
|
31
|
+
from ..outcomes import Score, Unknown
|
|
32
|
+
from ..records import Claim, Evidence, Ref
|
|
33
|
+
|
|
34
|
+
MOODS = ("declarative", "interrogative", "imperative")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _key_of(value: Any) -> Any:
|
|
38
|
+
"""Content identity for a semantic value, without building any strings."""
|
|
39
|
+
key = getattr(value, "key", None)
|
|
40
|
+
if key is not None and isinstance(value, (Entity, Frame)):
|
|
41
|
+
return key
|
|
42
|
+
if isinstance(value, tuple):
|
|
43
|
+
return tuple(_key_of(v) for v in value)
|
|
44
|
+
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
45
|
+
return value
|
|
46
|
+
if isinstance(value, Ref):
|
|
47
|
+
return value.id
|
|
48
|
+
if isinstance(value, (Request, Question)):
|
|
49
|
+
return value.key
|
|
50
|
+
return str(value)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class Entity:
|
|
55
|
+
"""What a referring expression picks out.
|
|
56
|
+
|
|
57
|
+
``kind`` is how it was named, not what it is: ``name``, ``pronoun``,
|
|
58
|
+
``path``, ``literal``, ``number``, ``quantified`` or ``skolem`` (introduced
|
|
59
|
+
by the predication itself and given a minted identity).
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
kind: str
|
|
63
|
+
text: str
|
|
64
|
+
features: Mapping[str, Any] = field(default_factory=dict)
|
|
65
|
+
ref: Ref | None = None # set once discourse resolution has settled it
|
|
66
|
+
candidates: tuple["Entity", ...] = () # kept when two antecedents are equally good
|
|
67
|
+
|
|
68
|
+
@cached_property
|
|
69
|
+
def key(self) -> tuple:
|
|
70
|
+
"""A cheap, stable content identity, computed once.
|
|
71
|
+
|
|
72
|
+
The chart dedupes distinct meanings per span, which needs content identity for
|
|
73
|
+
thousands of values per parse. ``repr`` was doing that job and cost 520k calls
|
|
74
|
+
on one benchmark run; this is the same distinction without building strings.
|
|
75
|
+
"""
|
|
76
|
+
feats = tuple(sorted((k, _key_of(v)) for k, v in self.features.items()))
|
|
77
|
+
return ("e", self.kind, self.text, feats, self.ref.id if self.ref else None,
|
|
78
|
+
tuple(c.key for c in self.candidates))
|
|
79
|
+
|
|
80
|
+
def __hash__(self) -> int:
|
|
81
|
+
return hash(self.key)
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def resolved(self) -> bool:
|
|
85
|
+
return not self.candidates and (self.kind != "pronoun" or self.ref is not None)
|
|
86
|
+
|
|
87
|
+
def with_ref(self, ref: Ref) -> "Entity":
|
|
88
|
+
return Entity(self.kind, self.text, self.features, ref, ())
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class Frame:
|
|
93
|
+
"""A predication: ``predicate`` over ``roles``, qualified by ``features``."""
|
|
94
|
+
|
|
95
|
+
predicate: str
|
|
96
|
+
roles: Mapping[str, Any] = field(default_factory=dict)
|
|
97
|
+
features: Mapping[str, Any] = field(default_factory=dict)
|
|
98
|
+
|
|
99
|
+
@cached_property
|
|
100
|
+
def key(self) -> tuple:
|
|
101
|
+
"""A cheap, stable content identity, computed once (see :meth:`Entity.key`)."""
|
|
102
|
+
roles = tuple(sorted((k, _key_of(v)) for k, v in self.roles.items()))
|
|
103
|
+
feats = tuple(sorted((k, _key_of(v)) for k, v in self.features.items()))
|
|
104
|
+
return ("f", self.predicate, roles, feats)
|
|
105
|
+
|
|
106
|
+
def __hash__(self) -> int:
|
|
107
|
+
return hash(self.key)
|
|
108
|
+
|
|
109
|
+
def role(self, name: str, default: Any = None) -> Any:
|
|
110
|
+
return self.roles.get(name, default)
|
|
111
|
+
|
|
112
|
+
def feature(self, name: str, default: Any = None) -> Any:
|
|
113
|
+
return self.features.get(name, default)
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def mood(self) -> str:
|
|
117
|
+
return self.features.get("mood", "declarative")
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def negated(self) -> bool:
|
|
121
|
+
return self.features.get("polarity") == "negative"
|
|
122
|
+
|
|
123
|
+
def added(self, **features: Any) -> "Frame":
|
|
124
|
+
return Frame(self.predicate, self.roles, {**self.features, **features})
|
|
125
|
+
|
|
126
|
+
def filled(self, **roles: Any) -> "Frame":
|
|
127
|
+
return Frame(self.predicate, {**self.roles, **roles}, self.features)
|
|
128
|
+
|
|
129
|
+
def walk(self) -> Iterable["Frame"]:
|
|
130
|
+
yield self
|
|
131
|
+
for value in self.roles.values():
|
|
132
|
+
if isinstance(value, Frame):
|
|
133
|
+
yield from value.walk()
|
|
134
|
+
elif isinstance(value, tuple):
|
|
135
|
+
for item in value:
|
|
136
|
+
if isinstance(item, Frame):
|
|
137
|
+
yield from item.walk()
|
|
138
|
+
|
|
139
|
+
def entities(self) -> Iterable[Entity]:
|
|
140
|
+
"""Every entity in the frame, including the ones nested inside another's features.
|
|
141
|
+
|
|
142
|
+
PP attachment decides *where* a modifier lands, not whether it was said: "make
|
|
143
|
+
a folder in it" may hang "it" on the verb or inside the noun. A caller asking
|
|
144
|
+
what this frame refers to wants the same answer either way, so the walk
|
|
145
|
+
descends into features too.
|
|
146
|
+
"""
|
|
147
|
+
for frame in self.walk():
|
|
148
|
+
for value in frame.roles.values():
|
|
149
|
+
yield from _entities_in(value)
|
|
150
|
+
|
|
151
|
+
def describe(self) -> str:
|
|
152
|
+
parts = [f"{k}={_short(v)}" for k, v in sorted(self.roles.items())]
|
|
153
|
+
flags = [f"{k}={v}" for k, v in sorted(self.features.items()) if k != "mood"]
|
|
154
|
+
head = f"{self.predicate}({', '.join(parts)})"
|
|
155
|
+
return head + (f" [{', '.join(flags)}]" if flags else "")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _short(value: Any) -> str:
|
|
159
|
+
if isinstance(value, Entity):
|
|
160
|
+
return value.ref.id if value.ref else f"{value.kind}:{value.text}"
|
|
161
|
+
if isinstance(value, Frame):
|
|
162
|
+
return "{" + value.describe() + "}"
|
|
163
|
+
if isinstance(value, tuple):
|
|
164
|
+
return "[" + ", ".join(_short(v) for v in value) + "]"
|
|
165
|
+
return repr(value)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _entities_in(value: Any, depth: int = 0) -> Iterable[Entity]:
|
|
169
|
+
if depth > 8: # a guard, not a limit: nothing this deep is a reference anyone means
|
|
170
|
+
return
|
|
171
|
+
if isinstance(value, Entity):
|
|
172
|
+
yield value
|
|
173
|
+
for inner in value.features.values():
|
|
174
|
+
yield from _entities_in(inner, depth + 1)
|
|
175
|
+
elif isinstance(value, Frame):
|
|
176
|
+
yield from value.entities()
|
|
177
|
+
elif isinstance(value, tuple):
|
|
178
|
+
for item in value:
|
|
179
|
+
yield from _entities_in(item, depth + 1)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# --------------------------------------------------------------------- claims
|
|
183
|
+
|
|
184
|
+
ASSERTIVE_FEATURES = {"mood", "tense", "person", "number"}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _mint(frame: Frame, prefix: str = "event") -> Ref:
|
|
188
|
+
"""Content identity for a reified event, so the same reading is the same event."""
|
|
189
|
+
payload = json.dumps(_canonical(frame), sort_keys=True, separators=(",", ":"))
|
|
190
|
+
return Ref(f"{prefix}:{hashlib.sha256(payload.encode()).hexdigest()[:12]}")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _canonical(value: Any) -> Any:
|
|
194
|
+
if isinstance(value, Frame):
|
|
195
|
+
return {"p": value.predicate, "r": {k: _canonical(v) for k, v in sorted(value.roles.items())},
|
|
196
|
+
"f": {k: _canonical(v) for k, v in sorted(value.features.items()) if k != "mood"}}
|
|
197
|
+
if isinstance(value, Entity):
|
|
198
|
+
return value.ref.id if value.ref else f"{value.kind}:{value.text}"
|
|
199
|
+
if isinstance(value, tuple):
|
|
200
|
+
return [_canonical(v) for v in value]
|
|
201
|
+
return value
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def default_ref(entity: Entity) -> Ref | Any:
|
|
205
|
+
"""Namespace an entity by how it was named; callers usually override this."""
|
|
206
|
+
if entity.ref is not None:
|
|
207
|
+
return entity.ref
|
|
208
|
+
if entity.kind in ("number", "literal"):
|
|
209
|
+
return entity.text
|
|
210
|
+
prefix = {"name": "entity", "path": "path", "skolem": "thing", "quantified": "set"}.get(entity.kind, "entity")
|
|
211
|
+
return Ref(f"{prefix}:{entity.text}")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def to_claims(
|
|
215
|
+
frame: Frame,
|
|
216
|
+
*,
|
|
217
|
+
source: Ref,
|
|
218
|
+
observed_at: datetime | None = None,
|
|
219
|
+
method: str = "grammar",
|
|
220
|
+
confidence: Score | None = None,
|
|
221
|
+
resolve: Callable[[Entity], Any] = default_ref,
|
|
222
|
+
subject_roles: tuple[str, ...] = ("subject", "agent", "theme"),
|
|
223
|
+
object_roles: tuple[str, ...] = ("object", "patient", "value", "complement"),
|
|
224
|
+
scope: Ref | None = None,
|
|
225
|
+
) -> list[tuple[Claim, Evidence]] | Unknown:
|
|
226
|
+
"""Claims for one frame, with the evidence that records where they came from.
|
|
227
|
+
|
|
228
|
+
Returns ``Unknown`` when the frame asserts nothing (a request or a question)
|
|
229
|
+
or when a reference did not resolve — never a guess.
|
|
230
|
+
"""
|
|
231
|
+
at = observed_at or datetime.now(timezone.utc)
|
|
232
|
+
if frame.mood != "declarative":
|
|
233
|
+
return Unknown("not_an_assertion", f"{frame.mood} frames carry no claims")
|
|
234
|
+
for entity in frame.entities():
|
|
235
|
+
if not entity.resolved:
|
|
236
|
+
detail = ", ".join(c.text for c in entity.candidates) or entity.text
|
|
237
|
+
return Unknown("unresolved_reference", f"“{entity.text}” could be {detail}")
|
|
238
|
+
out: list[tuple[Claim, Evidence]] = []
|
|
239
|
+
_emit(frame, out, source=source, at=at, method=method, confidence=confidence, resolve=resolve,
|
|
240
|
+
subject_roles=subject_roles, object_roles=object_roles, scope=scope)
|
|
241
|
+
return out
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _emit(frame: Frame, out: list, *, source: Ref, at: datetime, method: str, confidence: Score | None,
|
|
245
|
+
resolve: Callable[[Entity], Any], subject_roles, object_roles, scope: Ref | None) -> Any:
|
|
246
|
+
evidence = Evidence(source=source, observed_at=at, method=method, confidence=confidence)
|
|
247
|
+
|
|
248
|
+
def say(claim: Claim) -> None:
|
|
249
|
+
out.append((claim, evidence))
|
|
250
|
+
|
|
251
|
+
# reported speech: the content becomes its own scope, sourced to the speaker
|
|
252
|
+
content = frame.role("content")
|
|
253
|
+
if isinstance(content, Frame) and frame.predicate in ("say", "ask", "think", "believe", "promise", "claim"):
|
|
254
|
+
speaker = frame.role("speaker") or frame.role("subject")
|
|
255
|
+
speaker_ref = resolve(speaker) if isinstance(speaker, Entity) else speaker
|
|
256
|
+
said = _mint(frame, "utterance")
|
|
257
|
+
inner_scope = Ref(f"scope:{said.id.split(':', 1)[1]}")
|
|
258
|
+
say(Claim(_as_ref(speaker_ref), frame.predicate, said, scope=scope))
|
|
259
|
+
say(Claim(said, "in_scope", inner_scope, scope=scope))
|
|
260
|
+
inner_source = _as_ref(speaker_ref) if isinstance(speaker_ref, Ref) else source
|
|
261
|
+
nested = _emit(content, out, source=inner_source, at=at, method=f"{method}:reported", confidence=confidence,
|
|
262
|
+
resolve=resolve, subject_roles=subject_roles, object_roles=object_roles, scope=inner_scope)
|
|
263
|
+
say(Claim(said, "content", nested, scope=scope))
|
|
264
|
+
return said
|
|
265
|
+
|
|
266
|
+
simple = (not frame.negated
|
|
267
|
+
and not frame.features.get("modality")
|
|
268
|
+
and not frame.features.get("degree")
|
|
269
|
+
and all(k in ASSERTIVE_FEATURES for k in frame.features))
|
|
270
|
+
subject = next((frame.role(r) for r in subject_roles if frame.role(r) is not None), None)
|
|
271
|
+
obj = next((frame.role(r) for r in object_roles if frame.role(r) is not None), None)
|
|
272
|
+
others = {k: v for k, v in frame.roles.items() if k not in subject_roles + object_roles}
|
|
273
|
+
|
|
274
|
+
if simple and subject is not None and obj is not None and not others:
|
|
275
|
+
subject_ref = _as_ref(resolve(subject) if isinstance(subject, Entity) else subject)
|
|
276
|
+
value = _value(obj, out, source=source, at=at, method=method, confidence=confidence, resolve=resolve,
|
|
277
|
+
subject_roles=subject_roles, object_roles=object_roles, scope=scope)
|
|
278
|
+
say(Claim(subject_ref, frame.predicate, value, scope=scope))
|
|
279
|
+
return subject_ref
|
|
280
|
+
|
|
281
|
+
# anything qualified (negated, modal, comparative, extra roles) is reified:
|
|
282
|
+
# the event gets an identity and its qualifications are claims about it
|
|
283
|
+
event = _mint(frame)
|
|
284
|
+
say(Claim(event, "is_a", frame.predicate, scope=scope))
|
|
285
|
+
for role, value in sorted(frame.roles.items()):
|
|
286
|
+
filled = _value(value, out, source=source, at=at, method=method, confidence=confidence, resolve=resolve,
|
|
287
|
+
subject_roles=subject_roles, object_roles=object_roles, scope=scope)
|
|
288
|
+
say(Claim(event, role, filled, scope=scope))
|
|
289
|
+
for key, value in sorted(frame.features.items()):
|
|
290
|
+
if key != "mood":
|
|
291
|
+
say(Claim(event, key, value, scope=scope))
|
|
292
|
+
return event
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _value(value: Any, out: list, **kw: Any) -> Any:
|
|
296
|
+
if isinstance(value, Frame):
|
|
297
|
+
return _emit(value, out, **kw)
|
|
298
|
+
if isinstance(value, Entity):
|
|
299
|
+
resolved = kw["resolve"](value)
|
|
300
|
+
return resolved
|
|
301
|
+
if isinstance(value, tuple):
|
|
302
|
+
return tuple(_value(v, out, **kw) for v in value)
|
|
303
|
+
return value
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _as_ref(value: Any) -> Ref:
|
|
307
|
+
return value if isinstance(value, Ref) else Ref(f"entity:{value}")
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# ------------------------------------------------------------------- requests
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
@dataclass(frozen=True)
|
|
314
|
+
class Request:
|
|
315
|
+
"""An imperative reading: what the speaker wants done."""
|
|
316
|
+
|
|
317
|
+
frame: Frame
|
|
318
|
+
|
|
319
|
+
@cached_property
|
|
320
|
+
def key(self) -> tuple:
|
|
321
|
+
return ("r", self.frame.key)
|
|
322
|
+
|
|
323
|
+
def __hash__(self) -> int:
|
|
324
|
+
return hash(self.key)
|
|
325
|
+
|
|
326
|
+
@property
|
|
327
|
+
def act(self) -> str:
|
|
328
|
+
return self.frame.predicate
|
|
329
|
+
|
|
330
|
+
def describe(self) -> str:
|
|
331
|
+
return self.frame.describe()
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@dataclass(frozen=True)
|
|
335
|
+
class Question:
|
|
336
|
+
"""An interrogative reading: which role is being asked about."""
|
|
337
|
+
|
|
338
|
+
frame: Frame
|
|
339
|
+
asked: str # the role the wh-word or polarity question targets ("polarity" for yes/no)
|
|
340
|
+
|
|
341
|
+
@cached_property
|
|
342
|
+
def key(self) -> tuple:
|
|
343
|
+
return ("q", self.asked, self.frame.key)
|
|
344
|
+
|
|
345
|
+
def __hash__(self) -> int:
|
|
346
|
+
return hash(self.key)
|
|
347
|
+
|
|
348
|
+
def describe(self) -> str:
|
|
349
|
+
return f"?{self.asked} in {self.frame.describe()}"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Inducing readable artifacts from traces, and the controls that decide adoption.
|
|
2
|
+
|
|
3
|
+
from tensorcode.learning import candidate_literals, decision_list, verify_decision_list, Library
|
|
4
|
+
|
|
5
|
+
literals = candidate_literals(train) # the space, read off the data
|
|
6
|
+
rules = decision_list(train, literals) # MDL-stopped, printable
|
|
7
|
+
check = verify_decision_list(rules, train=train, held_out=test, literals=literals)
|
|
8
|
+
if check.adopted: # four controls, not one fit
|
|
9
|
+
Library(root).publish("router", rules, provenance={...}, fixture=test[:5])
|
|
10
|
+
|
|
11
|
+
The controls (held-out, same-size random, wrong-question, consistent renaming) and
|
|
12
|
+
the "a fit is not a finding" discipline come from the user's own repos; see
|
|
13
|
+
``docs/revival/09-language-and-induction.md``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .certificate import MISSING, ReadSet, Reader, certified, revalidate, value_digest
|
|
17
|
+
from .induce import (
|
|
18
|
+
DecisionList, Precondition, Rule, RoleType, decision_list, effects, preconditions, role_type, shape,
|
|
19
|
+
)
|
|
20
|
+
from .library import Entry, FixtureMismatch, Library, LibraryError, MissingArtifact, digest_of, from_json, to_json
|
|
21
|
+
from .literals import Case, Literal, candidate_literals, facts_of, rename_case, rename_facts, rename_map
|
|
22
|
+
from .verify import Concept, ConceptCheck, Verification, check_concept, propose_concepts, verify_decision_list
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"MISSING", "ReadSet", "Reader", "certified", "revalidate", "value_digest",
|
|
26
|
+
"Case", "Concept", "ConceptCheck", "DecisionList", "Entry", "FixtureMismatch", "Library", "LibraryError",
|
|
27
|
+
"Literal", "MissingArtifact", "Precondition", "RoleType", "Rule", "Verification", "candidate_literals",
|
|
28
|
+
"check_concept", "decision_list", "digest_of", "effects", "facts_of", "from_json", "preconditions",
|
|
29
|
+
"propose_concepts", "rename_case", "rename_facts", "rename_map", "role_type", "shape", "to_json", "verify_decision_list",
|
|
30
|
+
]
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""What an answer rested on, so it can be re-checked later without recomputing it.
|
|
2
|
+
|
|
3
|
+
A computation declares its dependencies by *reading* them, so recording the reads
|
|
4
|
+
recovers the dependency set with no annotation. Revalidating is then one digest
|
|
5
|
+
comparison per key read, rather than running the computation again.
|
|
6
|
+
|
|
7
|
+
Two details carry the value, and both come from ``symbolic-ai-models``'s
|
|
8
|
+
``symbolic_ai_core/runtime/certificate.py``:
|
|
9
|
+
|
|
10
|
+
**Misses are reads.** A key that was absent is recorded with the digest
|
|
11
|
+
``MISSING``. "This folder contains nothing" is load-bearing, and a certificate
|
|
12
|
+
that records only hits silently fails to notice an *addition* — which in that
|
|
13
|
+
repo's one real corpus delta was 411,122 of 744,136 changes.
|
|
14
|
+
|
|
15
|
+
**Digests, not values.** Revalidation compares one hash per key, so a key whose
|
|
16
|
+
value is a large collection costs one comparison instead of touching the whole
|
|
17
|
+
thing.
|
|
18
|
+
|
|
19
|
+
And the honest caveat that module's docstring exists to make: a certificate is
|
|
20
|
+
worth ``(cost of recomputing) / (cost of revalidating) × (fraction still valid)``.
|
|
21
|
+
For an answer that is itself one pass over its read set — a count, a max — those
|
|
22
|
+
two costs are the same and the certificate buys **nothing**. It pays where the
|
|
23
|
+
answer was expensive relative to what it depended on: a model call, a fixpoint, a
|
|
24
|
+
long agent trace.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import hashlib
|
|
30
|
+
import json
|
|
31
|
+
import time
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
from typing import Any, Callable, Iterable, Mapping, Sequence
|
|
34
|
+
|
|
35
|
+
from ..outcomes import Verdict
|
|
36
|
+
|
|
37
|
+
MISSING = "MISSING"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def value_digest(value: Any) -> str:
|
|
41
|
+
"""A short, order-independent digest of a read value."""
|
|
42
|
+
if value is None:
|
|
43
|
+
return MISSING
|
|
44
|
+
try:
|
|
45
|
+
payload = json.dumps(_canonical(value), sort_keys=True, separators=(",", ":"))
|
|
46
|
+
except TypeError:
|
|
47
|
+
payload = repr(value)
|
|
48
|
+
return hashlib.sha256(payload.encode()).hexdigest()[:12]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _canonical(value: Any) -> Any:
|
|
52
|
+
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
53
|
+
return value
|
|
54
|
+
if isinstance(value, Mapping):
|
|
55
|
+
return {"$map": sorted(([str(k), _canonical(v)] for k, v in value.items()), key=repr)}
|
|
56
|
+
if isinstance(value, (set, frozenset)):
|
|
57
|
+
return {"$set": sorted((_canonical(v) for v in value), key=repr)}
|
|
58
|
+
if isinstance(value, (list, tuple)):
|
|
59
|
+
return [_canonical(v) for v in value]
|
|
60
|
+
return repr(value)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class ReadSet:
|
|
65
|
+
"""The keys an answer consulted, with a digest each. Absent keys are included."""
|
|
66
|
+
|
|
67
|
+
reads: tuple[tuple[str, str], ...] = ()
|
|
68
|
+
at: float = field(default_factory=time.time)
|
|
69
|
+
note: str = ""
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def keys(self) -> tuple[str, ...]:
|
|
73
|
+
return tuple(k for k, _ in self.reads)
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def misses(self) -> tuple[str, ...]:
|
|
77
|
+
return tuple(k for k, d in self.reads if d == MISSING)
|
|
78
|
+
|
|
79
|
+
def digest(self) -> str:
|
|
80
|
+
return value_digest(sorted(self.reads))
|
|
81
|
+
|
|
82
|
+
def to_dict(self) -> dict[str, Any]:
|
|
83
|
+
return {"reads": [list(r) for r in self.reads], "at": self.at, "note": self.note}
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def from_dict(cls, data: Mapping[str, Any]) -> "ReadSet":
|
|
87
|
+
return cls(tuple((k, d) for k, d in (tuple(r) for r in data.get("reads", ()))),
|
|
88
|
+
data.get("at", 0.0), data.get("note", ""))
|
|
89
|
+
|
|
90
|
+
def __repr__(self) -> str:
|
|
91
|
+
return f"ReadSet({len(self.reads)} reads, {len(self.misses)} absent)"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class Reader:
|
|
95
|
+
"""A fact source that records what was consulted, including what was not there.
|
|
96
|
+
|
|
97
|
+
reader = Reader(facts)
|
|
98
|
+
rules.predict(reader) # or any code that calls .get()/.has()
|
|
99
|
+
certificate = reader.readset()
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(self, facts: Mapping[str, Any] | frozenset, *, note: str = "") -> None:
|
|
103
|
+
self.facts: Mapping[str, Any] = dict(facts) if isinstance(facts, frozenset) else facts
|
|
104
|
+
self.note = note
|
|
105
|
+
self._reads: dict[str, str] = {}
|
|
106
|
+
|
|
107
|
+
# the mapping surface a Literal or a rule uses
|
|
108
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
109
|
+
present = key in self.facts
|
|
110
|
+
value = self.facts[key] if present else default
|
|
111
|
+
self._reads.setdefault(key, value_digest(self.facts[key]) if present else MISSING)
|
|
112
|
+
return value
|
|
113
|
+
|
|
114
|
+
def __contains__(self, key: str) -> bool:
|
|
115
|
+
self._reads.setdefault(key, value_digest(self.facts[key]) if key in self.facts else MISSING)
|
|
116
|
+
return key in self.facts
|
|
117
|
+
|
|
118
|
+
def __getitem__(self, key: str) -> Any:
|
|
119
|
+
return self.get(key)
|
|
120
|
+
|
|
121
|
+
def items(self) -> Iterable[tuple[str, Any]]:
|
|
122
|
+
"""A full scan is a read of every key, and is recorded as such."""
|
|
123
|
+
for key, value in self.facts.items():
|
|
124
|
+
self._reads.setdefault(key, value_digest(value))
|
|
125
|
+
return self.facts.items()
|
|
126
|
+
|
|
127
|
+
def readset(self) -> ReadSet:
|
|
128
|
+
return ReadSet(tuple(sorted(self._reads.items())), note=self.note)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def revalidate(certificate: ReadSet, facts: Mapping[str, Any] | frozenset) -> Verdict:
|
|
132
|
+
"""Does the answer still hold? One digest comparison per key that was read."""
|
|
133
|
+
table: Mapping[str, Any] = dict(facts) if isinstance(facts, frozenset) else facts
|
|
134
|
+
changed: list[str] = []
|
|
135
|
+
for key, digest in certificate.reads:
|
|
136
|
+
now = value_digest(table[key]) if key in table else MISSING
|
|
137
|
+
if now != digest:
|
|
138
|
+
changed.append(f"{key}: {digest} -> {now}")
|
|
139
|
+
if changed:
|
|
140
|
+
return Verdict("fails", tuple(changed), tuple(certificate.keys))
|
|
141
|
+
return Verdict("holds", (f"{len(certificate.reads)} reads unchanged",), tuple(certificate.keys))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def certified(answer_fn: Callable[[Reader], Any], facts: Mapping[str, Any] | frozenset,
|
|
145
|
+
*, note: str = "") -> tuple[Any, ReadSet]:
|
|
146
|
+
"""Run something over the facts and return its answer with its certificate."""
|
|
147
|
+
reader = Reader(facts, note=note)
|
|
148
|
+
return answer_fn(reader), reader.readset()
|