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.
Files changed (53) hide show
  1. tensorcode/__init__.py +84 -0
  2. tensorcode/actions.py +137 -0
  3. tensorcode/answer_type.py +222 -0
  4. tensorcode/awareness.py +344 -0
  5. tensorcode/backends/__init__.py +0 -0
  6. tensorcode/backends/builtin.py +167 -0
  7. tensorcode/backends/hf_local.py +89 -0
  8. tensorcode/backends/linear.py +133 -0
  9. tensorcode/backends/neural.py +361 -0
  10. tensorcode/causal.py +262 -0
  11. tensorcode/change.py +566 -0
  12. tensorcode/chunking.py +195 -0
  13. tensorcode/cognition.py +311 -0
  14. tensorcode/context.py +97 -0
  15. tensorcode/control.py +291 -0
  16. tensorcode/cues.py +192 -0
  17. tensorcode/expectation.py +270 -0
  18. tensorcode/frames.py +232 -0
  19. tensorcode/language/__init__.py +36 -0
  20. tensorcode/language/chart.py +558 -0
  21. tensorcode/language/discourse.py +132 -0
  22. tensorcode/language/domains/__init__.py +0 -0
  23. tensorcode/language/domains/desktop.py +552 -0
  24. tensorcode/language/english.py +459 -0
  25. tensorcode/language/features.py +112 -0
  26. tensorcode/language/generate.py +574 -0
  27. tensorcode/language/grammar.py +893 -0
  28. tensorcode/language/semantics.py +349 -0
  29. tensorcode/learning/__init__.py +30 -0
  30. tensorcode/learning/certificate.py +148 -0
  31. tensorcode/learning/induce.py +304 -0
  32. tensorcode/learning/library.py +217 -0
  33. tensorcode/learning/literals.py +126 -0
  34. tensorcode/learning/verify.py +253 -0
  35. tensorcode/memory.py +303 -0
  36. tensorcode/metacognition.py +351 -0
  37. tensorcode/ops.py +207 -0
  38. tensorcode/outcomes.py +99 -0
  39. tensorcode/permanence.py +376 -0
  40. tensorcode/priming.py +191 -0
  41. tensorcode/py.typed +0 -0
  42. tensorcode/quantity.py +311 -0
  43. tensorcode/records.py +728 -0
  44. tensorcode/relation.py +771 -0
  45. tensorcode/runtime.py +471 -0
  46. tensorcode/semantics_bridge.py +308 -0
  47. tensorcode/social.py +380 -0
  48. tensorcode/temporal.py +189 -0
  49. tensorcode/wants.py +185 -0
  50. tensorcode-0.1.0a1.dist-info/METADATA +196 -0
  51. tensorcode-0.1.0a1.dist-info/RECORD +53 -0
  52. tensorcode-0.1.0a1.dist-info/WHEEL +4 -0
  53. 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()