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,304 @@
1
+ """Inducing readable artifacts: decision lists, preconditions, and role types.
2
+
3
+ Three inducers, each with the discipline its source repo learned the hard way:
4
+
5
+ * :func:`decision_list` — greedy separate-and-conquer with an **MDL stop**, so a
6
+ rule has to save more bits than it costs to state. (The algorithm is
7
+ ``symbolic-ai-models``'s ``models/ruleinduce_001/dlist.py``, re-expressed over
8
+ claims instead of that repo's graph facts.)
9
+ * :func:`preconditions` — the most-specific cover of the states in which an action
10
+ fired, with optional **interventional pruning**: keep a condition only if
11
+ removing it changes what the world does. (``synthEX``'s
12
+ ``perception/induce_rules.py`` reports precision 0.44 → 0.74 from exactly this.)
13
+ * :func:`role_type` — the least general type covering every observed filler, with a
14
+ **productivity gate**: a shape generalises only when many distinct fillers share
15
+ it and almost all of them are of that kind, otherwise the forms are memorised.
16
+ (``symbolic-ai-models``'s ``reader/learned.py`` ``Lexicon.induce``.)
17
+
18
+ Nothing here adopts what it induces. Adoption is :mod:`tensorcode.learning.verify`,
19
+ which is where the controls live.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import math
25
+ from collections import Counter, defaultdict
26
+ from dataclasses import dataclass, field
27
+ from typing import Any, Callable, Hashable, Iterable, Mapping, Sequence
28
+
29
+ from ..outcomes import Score, Unknown
30
+ from .literals import Case, Literal
31
+
32
+ # ------------------------------------------------------------- decision lists
33
+
34
+
35
+ @dataclass
36
+ class Rule:
37
+ conditions: tuple[Literal, ...]
38
+ label: Any
39
+ support: int = 0
40
+ correct: int = 0
41
+
42
+ def matches(self, facts: Any) -> bool:
43
+ return all(c.holds(facts) for c in self.conditions)
44
+
45
+ @property
46
+ def confidence(self) -> float:
47
+ return self.correct / max(1, self.support)
48
+
49
+ def cost(self) -> int:
50
+ return 2 + sum(c.cost() for c in self.conditions)
51
+
52
+ def __repr__(self) -> str:
53
+ body = " ∧ ".join(repr(c) for c in self.conditions) or "true"
54
+ return f"IF {body} THEN {self.label!r} [{self.correct}/{self.support}]"
55
+
56
+
57
+ @dataclass
58
+ class DecisionList:
59
+ """An ordered list of rules you can read, argue with, and price."""
60
+
61
+ rules: list[Rule] = field(default_factory=list)
62
+ default: Any = None
63
+ considered: int = 0
64
+
65
+ def predict(self, facts: Any) -> Any:
66
+ for rule in self.rules:
67
+ if rule.matches(facts):
68
+ return rule.label
69
+ return self.default
70
+
71
+ def explain(self, facts: Any) -> tuple[Any, Rule | None]:
72
+ for rule in self.rules:
73
+ if rule.matches(facts):
74
+ return rule.label, rule
75
+ return self.default, None
76
+
77
+ def decide(self, facts: Any) -> tuple[Any, "Rule | None", Any]:
78
+ """Predict, and return the certificate of what the decision actually read.
79
+
80
+ The read set includes predicates that were *absent*, so an answer is
81
+ invalidated by a fact appearing as well as by one changing.
82
+ """
83
+ from .certificate import Reader
84
+
85
+ reader = Reader(dict(facts) if isinstance(facts, frozenset) else facts, note="decision_list")
86
+ label, rule = self.explain(reader)
87
+ return label, rule, reader.readset()
88
+
89
+ def score(self, facts: Any) -> Score:
90
+ _, rule = self.explain(facts)
91
+ return Score(rule.confidence if rule else 0.0, "uncalibrated")
92
+
93
+ def cost(self) -> int:
94
+ return sum(r.cost() for r in self.rules) + 2
95
+
96
+ def accuracy(self, cases: Sequence[Case]) -> float:
97
+ if not cases:
98
+ return 0.0
99
+ return sum(self.predict(facts) == label for facts, label in cases) / len(cases)
100
+
101
+ def __repr__(self) -> str:
102
+ return "\n".join([*(repr(r) for r in self.rules), f"ELSE {self.default!r}"])
103
+
104
+
105
+ def _entropy(counts: Counter) -> float:
106
+ total = sum(counts.values())
107
+ if not total:
108
+ return 0.0
109
+ return -sum((c / total) * math.log2(c / total) for c in counts.values() if c)
110
+
111
+
112
+ def decision_list(cases: Sequence[Case], literals: Sequence[Literal], *, max_conditions: int = 3,
113
+ min_support: int = 2, min_confidence: float = 0.6, max_rules: int = 40,
114
+ beam: int = 4, mdl: bool = True) -> DecisionList:
115
+ """Induce an ordered rule list. Discrete throughout: no gradients, five knobs."""
116
+ remaining = list(cases)
117
+ out = DecisionList(considered=len(literals))
118
+ labels = Counter(label for _, label in cases)
119
+ out.default = labels.most_common(1)[0][0] if labels else None
120
+
121
+ while remaining and len(out.rules) < max_rules:
122
+ rule = _grow(remaining, literals, max_conditions=max_conditions, min_support=min_support, beam=beam)
123
+ if rule is None or rule.confidence < min_confidence:
124
+ break
125
+ if mdl:
126
+ covered = [(f, y) for f, y in remaining if rule.matches(f)]
127
+ before = _entropy(Counter(y for _, y in remaining)) * len(covered)
128
+ after = _entropy(Counter(y for _, y in covered)) * len(covered)
129
+ if before - after < rule.cost(): # the rule must pay for its own statement
130
+ break
131
+ out.rules.append(rule)
132
+ remaining = [(f, y) for f, y in remaining if not rule.matches(f)]
133
+ if remaining:
134
+ out.default = Counter(y for _, y in remaining).most_common(1)[0][0]
135
+ return out
136
+
137
+
138
+ def _grow(cases: Sequence[Case], literals: Sequence[Literal], *, max_conditions: int, min_support: int,
139
+ beam: int) -> Rule | None:
140
+ """Beam search over conjunctions, scored by information gain weighted by purity."""
141
+ hits = {literal: [literal.holds(facts) for facts, _ in cases] for literal in literals}
142
+ labels = [label for _, label in cases]
143
+ base = _entropy(Counter(labels))
144
+ n = len(cases)
145
+
146
+ def score(mask: Sequence[bool]) -> tuple[float, Counter, int]:
147
+ picked = [i for i, m in enumerate(mask) if m]
148
+ if len(picked) < min_support:
149
+ return -1e9, Counter(), 0
150
+ counts = Counter(labels[i] for i in picked)
151
+ return (base - _entropy(counts)) * (len(picked) / n), counts, len(picked)
152
+
153
+ beams: list[tuple[float, tuple[Literal, ...], list[bool]]] = [(0.0, (), [True] * n)]
154
+ best: tuple[float, tuple[Literal, ...], Counter, int] | None = None
155
+ for _ in range(max_conditions):
156
+ nxt: list[tuple[float, tuple[Literal, ...], list[bool]]] = []
157
+ for _, conditions, mask in beams:
158
+ for literal in literals:
159
+ if literal in conditions:
160
+ continue
161
+ merged = [a and b for a, b in zip(mask, hits[literal])]
162
+ gain, counts, support = score(merged)
163
+ if gain <= 0:
164
+ continue
165
+ adjusted = gain * (max(counts.values()) / max(1, support))
166
+ nxt.append((adjusted, conditions + (literal,), merged))
167
+ if best is None or adjusted > best[0]:
168
+ best = (adjusted, conditions + (literal,), counts, support)
169
+ if not nxt:
170
+ break
171
+ nxt.sort(key=lambda row: (-row[0], tuple(repr(c) for c in row[1])))
172
+ beams = nxt[:beam]
173
+ if best is None:
174
+ return None
175
+ _, conditions, counts, support = best
176
+ label, correct = counts.most_common(1)[0]
177
+ return Rule(conditions, label, support, correct)
178
+
179
+
180
+ # ---------------------------------------------------------------- preconditions
181
+
182
+
183
+ @dataclass(frozen=True)
184
+ class Precondition:
185
+ """What has to hold for an action to fire, and how it was established."""
186
+
187
+ action: str
188
+ conditions: tuple[Literal, ...]
189
+ support: int
190
+ method: str = "most-specific-cover"
191
+
192
+ def holds(self, facts: Any) -> bool:
193
+ return all(c.holds(facts) for c in self.conditions)
194
+
195
+ def __repr__(self) -> str:
196
+ body = " ∧ ".join(repr(c) for c in self.conditions) or "true"
197
+ return f"{self.action} needs {body} [{self.support} firings, {self.method}]"
198
+
199
+
200
+ def preconditions(action: str, fired: Sequence[frozenset], *,
201
+ intervene: Callable[[str, frozenset], bool] | None = None,
202
+ did_not_fire: Sequence[frozenset] = ()) -> Precondition:
203
+ """Conditions true in **every** state where the action fired.
204
+
205
+ The passive cover over-specialises: anything the world happened to keep constant
206
+ becomes a condition. Two optional correctives, in the order they should be tried:
207
+
208
+ * ``did_not_fire`` states drop conditions that also held when nothing happened;
209
+ * ``intervene(action, facts)`` is asked whether the action still fires with a
210
+ condition removed, which is the only way to tell a cause from a coincidence.
211
+ """
212
+ if not fired:
213
+ return Precondition(action, (), 0, "no firings")
214
+ common = set(fired[0])
215
+ for state in fired[1:]:
216
+ common &= set(state)
217
+ conditions = [Literal(p, v) for p, v in sorted(common, key=repr)]
218
+
219
+ if did_not_fire:
220
+ conditions = [c for c in conditions if not all(c.holds(state) for state in did_not_fire)]
221
+ method = "most-specific-cover" + ("+negatives" if did_not_fire else "")
222
+
223
+ if intervene is not None:
224
+ kept: list[Literal] = []
225
+ for condition in conditions:
226
+ weakened = [c for c in conditions if c is not condition]
227
+ probe = frozenset((c.predicate, c.value) for c in weakened)
228
+ if not intervene(action, probe): # removing it stopped the action: it matters
229
+ kept.append(condition)
230
+ conditions, method = kept, method + "+intervention"
231
+ return Precondition(action, tuple(conditions), len(fired), method)
232
+
233
+
234
+ def effects(before: Sequence[frozenset], after: Sequence[frozenset]) -> tuple[Literal, ...]:
235
+ """What every firing added: the union of new facts, kept only where it is unanimous."""
236
+ added: list[set] = [set(b) ^ (set(a) & set(b)) for a, b in zip(after, before)]
237
+ gained = [set(a) - set(b) for a, b in zip(after, before)]
238
+ if not gained:
239
+ return ()
240
+ common = set(gained[0])
241
+ for facts in gained[1:]:
242
+ common &= facts
243
+ _ = added
244
+ return tuple(Literal(p, v) for p, v in sorted(common, key=repr))
245
+
246
+
247
+ # ------------------------------------------------------------------ role types
248
+
249
+
250
+ @dataclass(frozen=True)
251
+ class RoleType:
252
+ """What fills a role: a generalising shape, or a memorised set of forms."""
253
+
254
+ role: str
255
+ concept: str | None
256
+ shapes: frozenset[str] = frozenset()
257
+ forms: frozenset[str] = frozenset()
258
+ productive: bool = False
259
+ support: int = 0
260
+
261
+ def admits(self, value: Any) -> bool:
262
+ text = value if isinstance(value, str) else str(value)
263
+ return text in self.forms or (self.productive and shape(text) in self.shapes)
264
+
265
+ def __repr__(self) -> str:
266
+ how = f"shape {sorted(self.shapes)}" if self.productive else f"{len(self.forms)} memorised forms"
267
+ return f"{self.role}: {self.concept or 'unknown'} ({how}, {self.support} observations)"
268
+
269
+
270
+ def shape(text: str) -> str:
271
+ """A token's shape: letters, digits and punctuation classes, run-length collapsed."""
272
+ out = []
273
+ for ch in text:
274
+ kind = "X" if ch.isupper() else "x" if ch.islower() else "9" if ch.isdigit() else ch
275
+ if not out or out[-1] != kind:
276
+ out.append(kind)
277
+ return "".join(out)
278
+
279
+
280
+ def role_type(role: str, fillers: Sequence[Any], *, subsumes: Callable[[Any], str] | None = None,
281
+ min_forms: int = 5, purity: float = 0.9, others: Sequence[Any] = ()) -> RoleType:
282
+ """The least general type covering every filler, generalised only if productive.
283
+
284
+ ``subsumes`` maps a filler to its concept; the induced concept is the most
285
+ specific one covering all fillers. A *shape* is trusted only when it covers
286
+ ``min_forms`` distinct fillers and almost nothing else in ``others``, so
287
+ ``region_0`` generalises to ``region_84`` while ``group`` must be memorised.
288
+ """
289
+ texts = [f if isinstance(f, str) else str(f) for f in fillers]
290
+ concepts = {subsumes(f) for f in fillers} if subsumes else set()
291
+ concept = concepts.pop() if len(concepts) == 1 else None
292
+ by_shape: dict[str, set[str]] = defaultdict(set)
293
+ for text in texts:
294
+ by_shape[shape(text)].add(text)
295
+ foreign: dict[str, set[str]] = defaultdict(set)
296
+ for other in others:
297
+ text = other if isinstance(other, str) else str(other)
298
+ foreign[shape(text)].add(text)
299
+ productive = {
300
+ s for s, forms in by_shape.items()
301
+ if len(forms) >= min_forms and len(forms) >= purity * (len(forms) + len(foreign.get(s, ())))
302
+ }
303
+ memorised = {t for t in texts if shape(t) not in productive}
304
+ return RoleType(role, concept, frozenset(productive), frozenset(memorised), bool(productive), len(texts))
@@ -0,0 +1,217 @@
1
+ """A persistent, versioned library of learned artifacts.
2
+
3
+ Learning that does not outlive the process is not learning, so artifacts go to a
4
+ directory of plain JSON: a manifest, one content-addressed file per artifact, and
5
+ one recorded fixture per artifact. The design is
6
+ ``typed-crystallization-networks``'s ``tcn/library.py``, kept because its rules are
7
+ the ones that stop a library quietly rotting:
8
+
9
+ * **content addressing** — two names that induce the same artifact share one file,
10
+ so a definition is stored and charged once;
11
+ * **versions** — publishing under an existing name allocates the next version and
12
+ marks every artifact that depends on the superseded digest ``stale``;
13
+ * **fixtures** — an artifact records cases it got right, and loading replays them.
14
+ A disagreement raises rather than loading;
15
+ * **provenance** — what it was induced from, by what method, when, and under which
16
+ verification. Nothing enters without it.
17
+
18
+ There is deliberately no "load anyway" policy.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import hashlib
24
+ import json
25
+ import time
26
+ from dataclasses import dataclass, field
27
+ from pathlib import Path
28
+ from typing import Any, Callable, Iterable, Mapping, Sequence
29
+
30
+ from .induce import DecisionList, Rule
31
+ from .literals import Literal
32
+
33
+
34
+ class LibraryError(Exception):
35
+ pass
36
+
37
+
38
+ class FixtureMismatch(LibraryError):
39
+ """A stored artifact no longer reproduces its recorded cases."""
40
+
41
+
42
+ class MissingArtifact(LibraryError):
43
+ pass
44
+
45
+
46
+ def digest_of(payload: Any) -> str:
47
+ return hashlib.sha256(json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()).hexdigest()[:16]
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class Entry:
52
+ name: str
53
+ version: int
54
+ digest: str
55
+ kind: str
56
+ provenance: Mapping[str, Any]
57
+ depends_on: tuple[str, ...] = ()
58
+ stale: bool = False
59
+ at: float = field(default_factory=time.time)
60
+
61
+ @property
62
+ def reference(self) -> str:
63
+ return f"{self.name}@{self.version}"
64
+
65
+ def to_dict(self) -> dict[str, Any]:
66
+ return {"name": self.name, "version": self.version, "digest": self.digest, "kind": self.kind,
67
+ "provenance": dict(self.provenance), "depends_on": list(self.depends_on), "stale": self.stale,
68
+ "at": self.at}
69
+
70
+ @classmethod
71
+ def from_dict(cls, data: Mapping[str, Any]) -> "Entry":
72
+ return cls(data["name"], data["version"], data["digest"], data["kind"], data.get("provenance", {}),
73
+ tuple(data.get("depends_on", ())), bool(data.get("stale")), data.get("at", 0.0))
74
+
75
+
76
+ # --------------------------------------------------------------- serialisation
77
+
78
+
79
+ def _literal_to_json(literal: Literal) -> dict[str, Any]:
80
+ return {"predicate": literal.predicate, "value": literal.value, "negated": literal.negated, "kind": literal.kind}
81
+
82
+
83
+ def _literal_from_json(data: Mapping[str, Any]) -> Literal:
84
+ return Literal(data["predicate"], data.get("value"), bool(data.get("negated")), data.get("kind", "equals"))
85
+
86
+
87
+ def to_json(artifact: Any) -> dict[str, Any]:
88
+ """Artifacts are data: a reader needs this module's *format*, not its code."""
89
+ if isinstance(artifact, DecisionList):
90
+ return {"kind": "decision_list", "default": artifact.default, "considered": artifact.considered,
91
+ "rules": [{"conditions": [_literal_to_json(c) for c in r.conditions], "label": r.label,
92
+ "support": r.support, "correct": r.correct} for r in artifact.rules]}
93
+ raise LibraryError(f"cannot store {type(artifact).__name__}")
94
+
95
+
96
+ def from_json(data: Mapping[str, Any]) -> Any:
97
+ if data.get("kind") == "decision_list":
98
+ rules = [Rule(tuple(_literal_from_json(c) for c in r["conditions"]), r["label"], r.get("support", 0), r.get("correct", 0))
99
+ for r in data.get("rules", ())]
100
+ return DecisionList(rules, data.get("default"), data.get("considered", 0))
101
+ raise LibraryError(f"unknown stored kind {data.get('kind')!r}")
102
+
103
+
104
+ # -------------------------------------------------------------------- library
105
+
106
+
107
+ class Library:
108
+ """A directory of learned artifacts, versioned and replayable."""
109
+
110
+ def __init__(self, root: str | Path) -> None:
111
+ self.root = Path(root)
112
+ self.root.mkdir(parents=True, exist_ok=True)
113
+ (self.root / "artifacts").mkdir(exist_ok=True)
114
+ (self.root / "fixtures").mkdir(exist_ok=True)
115
+ self.manifest_path = self.root / "manifest.json"
116
+ self.entries: list[Entry] = []
117
+ if self.manifest_path.exists():
118
+ self.entries = [Entry.from_dict(row) for row in json.loads(self.manifest_path.read_text())]
119
+
120
+ # -- reading
121
+ def names(self) -> list[str]:
122
+ return sorted({e.name for e in self.entries})
123
+
124
+ def versions(self, name: str) -> list[Entry]:
125
+ return [e for e in self.entries if e.name == name]
126
+
127
+ def head(self, name: str) -> Entry:
128
+ found = self.versions(name)
129
+ if not found:
130
+ raise MissingArtifact(name)
131
+ return found[-1]
132
+
133
+ def entry(self, reference: str) -> Entry:
134
+ if "@" not in reference:
135
+ return self.head(reference)
136
+ name, _, version = reference.partition("@")
137
+ for candidate in self.versions(name):
138
+ if candidate.version == int(version):
139
+ return candidate
140
+ raise MissingArtifact(reference)
141
+
142
+ def dependents(self, digest: str) -> list[Entry]:
143
+ return [e for e in self.entries if digest in e.depends_on]
144
+
145
+ # -- writing
146
+ def publish(self, name: str, artifact: Any, *, provenance: Mapping[str, Any],
147
+ fixture: Sequence[tuple[Any, Any]] = (), depends_on: Sequence[str] = ()) -> Entry:
148
+ """Store an artifact as the next version of ``name``, with a replayable fixture."""
149
+ payload = to_json(artifact)
150
+ digest = digest_of(payload)
151
+ (self.root / "artifacts" / f"{digest}.json").write_text(json.dumps(payload, indent=1, default=str))
152
+ (self.root / "fixtures" / f"{digest}.json").write_text(
153
+ json.dumps([{"facts": sorted(((p, v) for p, v in facts), key=repr), "expect": expect}
154
+ for facts, expect in fixture], indent=1, default=str))
155
+ superseded = self.versions(name)
156
+ version = superseded[-1].version + 1 if superseded else 1
157
+ entry = Entry(name, version, digest, payload["kind"], dict(provenance), tuple(depends_on))
158
+ # relearning marks every dependent stale: it must be revalidated, not trusted
159
+ if superseded:
160
+ old = superseded[-1].digest
161
+ self.entries = [
162
+ Entry(e.name, e.version, e.digest, e.kind, e.provenance, e.depends_on, True, e.at)
163
+ if old in e.depends_on else e
164
+ for e in self.entries
165
+ ]
166
+ self.entries.append(entry)
167
+ self._save()
168
+ return entry
169
+
170
+ def load(self, reference: str, *, replay: bool = True) -> Any:
171
+ """Load an artifact, replaying its fixture first unless told not to."""
172
+ entry = self.entry(reference)
173
+ path = self.root / "artifacts" / f"{entry.digest}.json"
174
+ if not path.exists():
175
+ raise MissingArtifact(f"{reference}: {entry.digest} is not in the library")
176
+ artifact = from_json(json.loads(path.read_text()))
177
+ if replay:
178
+ self._replay(entry, artifact)
179
+ return artifact
180
+
181
+ def revalidate(self, reference: str) -> Entry:
182
+ """Clear ``stale`` only if the recorded fixture still reproduces exactly."""
183
+ entry = self.entry(reference)
184
+ artifact = self.load(reference, replay=True)
185
+ _ = artifact
186
+ self.entries = [
187
+ Entry(e.name, e.version, e.digest, e.kind, e.provenance, e.depends_on, False, e.at)
188
+ if (e.name, e.version) == (entry.name, entry.version) else e
189
+ for e in self.entries
190
+ ]
191
+ self._save()
192
+ return self.entry(reference)
193
+
194
+ def verify(self) -> list[str]:
195
+ """Every artifact still present and still reproducing its fixture; problems listed."""
196
+ problems: list[str] = []
197
+ for entry in self.entries:
198
+ try:
199
+ self.load(entry.reference, replay=True)
200
+ except LibraryError as exc:
201
+ problems.append(f"{entry.reference}: {exc}")
202
+ return problems
203
+
204
+ def _replay(self, entry: Entry, artifact: Any) -> None:
205
+ path = self.root / "fixtures" / f"{entry.digest}.json"
206
+ if not path.exists():
207
+ return
208
+ for case in json.loads(path.read_text()):
209
+ facts = frozenset((p, v) for p, v in (tuple(pair) for pair in case["facts"]))
210
+ got = artifact.predict(facts)
211
+ if got != case["expect"]:
212
+ raise FixtureMismatch(f"{entry.reference}: fixture expected {case['expect']!r}, got {got!r}")
213
+
214
+ def _save(self) -> None:
215
+ tmp = self.manifest_path.with_suffix(".tmp")
216
+ tmp.write_text(json.dumps([e.to_dict() for e in self.entries], indent=1, default=str))
217
+ tmp.replace(self.manifest_path)
@@ -0,0 +1,126 @@
1
+ """The hypothesis language: conditions over claims, generated from what was observed.
2
+
3
+ Induction needs a space of candidate conditions, and the honest way to get one is
4
+ to read it off the data rather than to write it down: every literal below is
5
+ generated from claims that actually occurred. Nothing here names a domain.
6
+
7
+ A literal costs bits (:meth:`Literal.cost`), which is what lets an induced rule
8
+ be charged for its own description length instead of growing until it fits.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from collections import Counter, defaultdict
14
+ from dataclasses import dataclass
15
+ from typing import Any, Callable, Iterable, Mapping, Sequence
16
+
17
+ from ..records import Claim, Ref, Store
18
+
19
+ #: One case for induction: the claims that held, and the outcome to predict.
20
+ Case = tuple[frozenset[tuple[str, Any]], Any]
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class Literal:
25
+ """A readable condition over one case's facts."""
26
+
27
+ predicate: str
28
+ value: Any = None
29
+ negated: bool = False
30
+ kind: str = "equals" # "equals" | "present" | "at_least"
31
+
32
+ def holds(self, facts: Mapping[str, Any] | frozenset) -> bool:
33
+ table = dict(facts) if isinstance(facts, frozenset) else facts
34
+ got = table.get(self.predicate, _MISSING)
35
+ if self.kind == "present":
36
+ out = got is not _MISSING
37
+ elif self.kind == "at_least":
38
+ out = got is not _MISSING and isinstance(got, (int, float)) and got >= self.value
39
+ else:
40
+ out = got == self.value
41
+ return not out if self.negated else out
42
+
43
+ def cost(self) -> int:
44
+ """Bits-ish: a name, a test, and a value."""
45
+ return 2 + (0 if self.value is None else 1) + (1 if self.negated else 0)
46
+
47
+ def __repr__(self) -> str:
48
+ body = {"present": f"has({self.predicate})",
49
+ "at_least": f"{self.predicate}>={self.value}",
50
+ "equals": f"{self.predicate}={self.value!r}"}[self.kind]
51
+ return f"¬{body}" if self.negated else body
52
+
53
+
54
+ class _Missing:
55
+ def __repr__(self) -> str:
56
+ return "∅"
57
+
58
+
59
+ _MISSING = _Missing()
60
+
61
+
62
+ def facts_of(store: Store, subject: Ref, *, predicates: Sequence[str] | None = None) -> frozenset[tuple[str, Any]]:
63
+ """One subject's live claims as a flat fact table (missing predicates stay missing)."""
64
+ out = {}
65
+ for record in store.claims(subject):
66
+ if predicates is None or record.claim.predicate in predicates:
67
+ out[record.claim.predicate] = record.claim.object
68
+ return frozenset(out.items())
69
+
70
+
71
+ def candidate_literals(cases: Sequence[Case], *, min_count: int = 2, max_values: int = 12) -> list[Literal]:
72
+ """Every condition worth trying, from the values that actually occur.
73
+
74
+ A predicate seen with many distinct values contributes a presence test rather
75
+ than one literal per value, which keeps the space from exploding on identifiers.
76
+ """
77
+ values: dict[str, Counter] = defaultdict(Counter)
78
+ for facts, _ in cases:
79
+ for predicate, value in facts:
80
+ try:
81
+ values[predicate][value] += 1
82
+ except TypeError: # unhashable objects are still worth a presence test
83
+ values[predicate]["<unhashable>"] += 1
84
+ out: list[Literal] = []
85
+ for predicate, counts in sorted(values.items()):
86
+ out.append(Literal(predicate, kind="present"))
87
+ out.append(Literal(predicate, kind="present", negated=True))
88
+ if len(counts) > max_values:
89
+ continue
90
+ for value, count in sorted(counts.items(), key=lambda kv: (-kv[1], repr(kv[0]))):
91
+ if count < min_count or value == "<unhashable>":
92
+ continue
93
+ out.append(Literal(predicate, value))
94
+ out.append(Literal(predicate, value, negated=True))
95
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
96
+ out.append(Literal(predicate, value, kind="at_least"))
97
+ return out
98
+
99
+
100
+ def rename_case(case: Case, mapping: Mapping[Any, Any]) -> Case:
101
+ """A case with its symbols consistently renamed — the input to the rename control."""
102
+ facts, label = case
103
+ renamed = frozenset((p, mapping.get(v, v)) for p, v in facts)
104
+ return renamed, mapping.get(label, label)
105
+
106
+
107
+ def rename_facts(facts: frozenset, mapping: Mapping[Any, Any]) -> frozenset:
108
+ """Rename the symbols *inside* a case, leaving its label alone.
109
+
110
+ The renaming control asks whether an artifact's output is unchanged when the
111
+ vocabulary changes. Renaming the labels too would make every artifact fail it,
112
+ which measures nothing.
113
+ """
114
+ return frozenset((p, mapping.get(v, v)) for p, v in facts)
115
+
116
+
117
+ def rename_map(cases: Sequence[Case], *, prefix: str = "sym", labels: bool = False) -> dict[Any, Any]:
118
+ """A consistent, order-independent renaming of the symbols in the cases."""
119
+ seen: list[Any] = []
120
+ for facts, label in cases:
121
+ for _, value in facts:
122
+ if isinstance(value, str) and value not in seen:
123
+ seen.append(value)
124
+ if labels and isinstance(label, str) and label not in seen:
125
+ seen.append(label)
126
+ return {value: f"{prefix}{i}" for i, value in enumerate(sorted(seen))}