quantum-cortex 1.0.0__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.
- cortex_c2/__init__.py +161 -0
- cortex_c2/consolidation.py +146 -0
- cortex_c2/router_ablation.py +254 -0
- cortex_c2/router_v2.py +109 -0
- cortex_c2b/__init__.py +724 -0
- cortex_c2b/companion_demo.py +203 -0
- cortex_c2b/crypto.py +102 -0
- cortex_c2b/external_arm.py +591 -0
- cortex_c2b/hm_lm.py +983 -0
- cortex_c2b/hm_protocol.py +308 -0
- cortex_c2b/lifecycle.py +454 -0
- cortex_c2b/lm_bridge.py +338 -0
- cortex_c2b/organ_use.py +254 -0
- cortex_c2b/read_path.py +218 -0
- cortex_c2b/reconstruction_probe.py +471 -0
- cortex_c2b/write_path.py +176 -0
- cortex_data/__init__.py +439 -0
- cortex_data/relay.py +174 -0
- cortex_eval/__init__.py +20 -0
- cortex_eval/ablation_mqar.py +187 -0
- cortex_eval/confirmation_run.py +133 -0
- cortex_eval/domain_map.py +127 -0
- cortex_eval/external_baseline.py +148 -0
- cortex_eval/ingest_confirmation.py +194 -0
- cortex_eval/mqar.py +122 -0
- cortex_eval/multiseed.py +148 -0
- cortex_eval/recurrent_base.py +197 -0
- cortex_eval/recurrent_ladder.py +159 -0
- cortex_eval/resumable_ladder.py +231 -0
- cortex_eval/resumable_rb.py +207 -0
- cortex_eval/run_mqar.py +127 -0
- cortex_eval/serial_position.py +116 -0
- quantum_cortex-1.0.0.dist-info/METADATA +209 -0
- quantum_cortex-1.0.0.dist-info/RECORD +41 -0
- quantum_cortex-1.0.0.dist-info/WHEEL +5 -0
- quantum_cortex-1.0.0.dist-info/licenses/AUTHORS.md +30 -0
- quantum_cortex-1.0.0.dist-info/licenses/LICENSE +73 -0
- quantum_cortex-1.0.0.dist-info/licenses/LICENSE-DOCS.md +34 -0
- quantum_cortex-1.0.0.dist-info/licenses/NOTICE +50 -0
- quantum_cortex-1.0.0.dist-info/top_level.txt +5 -0
- train.py +885 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
"""cortex_c2b.reconstruction_probe -- the reconstruction probe on the organ's layer of
|
|
2
|
+
numbers (ADR-007, amendment of 2026-09-30, declared before any measurement; whitepaper
|
|
3
|
+
section 3.5, the four words, and section 5d, door 7).
|
|
4
|
+
|
|
5
|
+
The door asks whether a level of the organ earns the word *reconstructible at measured
|
|
6
|
+
defect*: derive what is hidden from what is kept plus the declared constants, compare with
|
|
7
|
+
what the log recorded, and call the gap the defect, under a threshold declared before any
|
|
8
|
+
number. On the text the organ has two words by construction, backup and declared loss;
|
|
9
|
+
this probe measures its layer of numbers, in three levels and a reading:
|
|
10
|
+
|
|
11
|
+
level 1 the counts per state along the consolidation index: from the visible counts
|
|
12
|
+
(live, consolidated, the summaries alive) and the constants (K, the number of
|
|
13
|
+
entries) derive the hidden pair (demoted, evicted); exact at every phase, or a
|
|
14
|
+
construction fault (a check, not a claim);
|
|
15
|
+
level 2 the salience line: from an entry's final salience, the decay constant and the
|
|
16
|
+
number of decays it received (a count of applied phases after its
|
|
17
|
+
consolidation, read from the log), derive its salience at consolidation and
|
|
18
|
+
compare with the value the log recorded; threshold, a relative defect of 1e-3
|
|
19
|
+
(the rounding to six decimals per phase is the known residual);
|
|
20
|
+
level 3 the persistent associative memory: reconstruct every replayed cue from the
|
|
21
|
+
memory alone and measure the cosine distance to the cue the log holds;
|
|
22
|
+
threshold, the lifecycle's own replay bound, 0.05, at the end of the run;
|
|
23
|
+
level 3b the reading declared with it: the share of evicted entries whose cue the
|
|
24
|
+
memory still reconstructs under the bound (an eviction never withdraws a
|
|
25
|
+
pattern; the code says so and the probe reports what that means);
|
|
26
|
+
level 4 the content: no released payload has a file, no evicted entry reads
|
|
27
|
+
(declared loss, a check).
|
|
28
|
+
|
|
29
|
+
The run: a sealed journal of the record is COPIED to a temporary directory and never
|
|
30
|
+
modified in place; the lifecycle plants its sentinels from the frozen generator at a seed
|
|
31
|
+
that must not collide with the journal's entities (the abort of row 40), then runs the
|
|
32
|
+
declared number of phases on the logical clock with no reads between them; then a NEW
|
|
33
|
+
PROCESS reopens the copy from the disk alone, rebuilds the memory from the consolidation
|
|
34
|
+
order (invariant 9) and recomputes every level: the two must agree number for number.
|
|
35
|
+
Nothing here is a claim. The first number comes from the first file.
|
|
36
|
+
"""
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import argparse
|
|
40
|
+
import hashlib
|
|
41
|
+
import json
|
|
42
|
+
import os
|
|
43
|
+
import shutil
|
|
44
|
+
import subprocess
|
|
45
|
+
import sys
|
|
46
|
+
import tempfile
|
|
47
|
+
from datetime import datetime, timezone
|
|
48
|
+
from pathlib import Path
|
|
49
|
+
|
|
50
|
+
import numpy as np
|
|
51
|
+
|
|
52
|
+
from cortex_c2b import (Journal, POLICY_STOP, MODE_DURABLE, STATE_LIVE, STATE_CONSOLIDATED, STATE_DEMOTED,
|
|
53
|
+
STATE_EVICTED, SCHEMA_SUMMARY, lifecycle_declaration)
|
|
54
|
+
from cortex_c2b.crypto import open_line
|
|
55
|
+
from cortex_c2b.hm_protocol import generate_facts, value_of_statement
|
|
56
|
+
from cortex_c2b.lifecycle import LifecycleConfig, LifecycleScheduler
|
|
57
|
+
from cortex_c2b.read_path import JournalPath
|
|
58
|
+
from cortex_c2b.write_path import WritePath
|
|
59
|
+
|
|
60
|
+
PHASES = 40 # declared: forty phases on the logical clock, no reads between them
|
|
61
|
+
SENTINEL_SEED = 2 # declared: the frozen generator's seed for the sentinels (not 0, 10 000 or 100 000)
|
|
62
|
+
INDEX_SEED = 0 # the cue index's declared seed, as session B of the record opened it
|
|
63
|
+
NOW_PLANT = None # the sentinels are planted at the journal's last write time plus one (200.0 for the record's journals)
|
|
64
|
+
THRESH_SALIENCE_REL = 1e-3 # level 2, declared
|
|
65
|
+
THRESH_MEMORY = None # level 3: the lifecycle's own replay bound (cfg.replay_error_max), never another
|
|
66
|
+
RESERVED_SEEDS = (0, 10_000, 100_000)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------- #
|
|
70
|
+
# The journal: copy, hash, meta #
|
|
71
|
+
# ---------------------------------------------------------------------------- #
|
|
72
|
+
def journal_file(path: str | Path) -> Path:
|
|
73
|
+
p = Path(path)
|
|
74
|
+
return p / "journal.jsonl" if p.is_dir() else p
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def sha256_file(path: Path) -> str:
|
|
78
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def copy_journal(src: Path, dst_dir: Path) -> Path:
|
|
82
|
+
"""The journal log and its payload directory, copied; the original is never touched."""
|
|
83
|
+
dst_dir.mkdir(parents=True, exist_ok=True)
|
|
84
|
+
dst = dst_dir / src.name
|
|
85
|
+
shutil.copyfile(src, dst)
|
|
86
|
+
pay = src.with_name(src.stem + ".payloads")
|
|
87
|
+
if pay.is_dir():
|
|
88
|
+
shutil.copytree(pay, dst.with_name(dst.stem + ".payloads"))
|
|
89
|
+
return dst
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def separation_of(journal: Journal) -> dict:
|
|
93
|
+
"""The write path's separation (dg, seed) the journal was written with: a journal
|
|
94
|
+
opened under another separation is refused by the write path, so the probe reads it."""
|
|
95
|
+
if not journal.ca3_order:
|
|
96
|
+
return {"dg": True, "seed": 0}
|
|
97
|
+
meta = journal.ca3_order[0][1]
|
|
98
|
+
return {"dg": bool(meta.get("dg", True)), "seed": int(meta.get("seed", 0))}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def entities_of(journal: Journal) -> set[str]:
|
|
102
|
+
"""The entities of the journal's planted statements, read by the organ's own parser."""
|
|
103
|
+
ents = set()
|
|
104
|
+
for e in journal._entries.values():
|
|
105
|
+
if e.schema_id == SCHEMA_SUMMARY:
|
|
106
|
+
continue
|
|
107
|
+
try:
|
|
108
|
+
text = journal.payloads.get(e.pointer).decode("utf-8", errors="replace")
|
|
109
|
+
except (KeyError, ValueError):
|
|
110
|
+
continue
|
|
111
|
+
v = value_of_statement(text)
|
|
112
|
+
if v:
|
|
113
|
+
ents.add(v[1])
|
|
114
|
+
return ents
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _open(copy: Path, key: bytes) -> tuple[Journal, WritePath, JournalPath]:
|
|
118
|
+
journal = Journal(copy, key=key, policy=POLICY_STOP)
|
|
119
|
+
sep = separation_of(journal)
|
|
120
|
+
wp = WritePath(journal, use_dg=sep["dg"], seed=sep["seed"])
|
|
121
|
+
jp = JournalPath(journal, seed=INDEX_SEED)
|
|
122
|
+
return journal, wp, jp
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------------------------------- #
|
|
126
|
+
# The log, read as events (the same reader as the journal's replay) #
|
|
127
|
+
# ---------------------------------------------------------------------------- #
|
|
128
|
+
def iter_events(path: Path, key: bytes | None):
|
|
129
|
+
for raw in path.read_text(encoding="utf-8").splitlines():
|
|
130
|
+
if raw.strip():
|
|
131
|
+
yield json.loads(open_line(key, raw))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ---------------------------------------------------------------------------- #
|
|
135
|
+
# The levels #
|
|
136
|
+
# ---------------------------------------------------------------------------- #
|
|
137
|
+
def level_1_counts(path: Path, key: bytes | None, k: int) -> dict:
|
|
138
|
+
"""At every phase event of the log: from (live, consolidated, summaries alive, entries)
|
|
139
|
+
and K, derive (demoted, evicted) and compare with the state the events built."""
|
|
140
|
+
state: dict[str, str] = {}
|
|
141
|
+
sources: dict[str, list[str]] = {}
|
|
142
|
+
phases, worst = [], 0
|
|
143
|
+
for ev in iter_events(path, key):
|
|
144
|
+
kind = ev["ev"]
|
|
145
|
+
if kind == "write":
|
|
146
|
+
state[ev["entry"]["entry_id"]] = STATE_LIVE
|
|
147
|
+
elif kind == "transition":
|
|
148
|
+
state[ev["entry_id"]] = ev["to"]
|
|
149
|
+
elif kind == "demote":
|
|
150
|
+
sources[ev["summary"]] = list(ev["sources"])
|
|
151
|
+
elif kind == "phase":
|
|
152
|
+
counts = {s: 0 for s in (STATE_LIVE, STATE_CONSOLIDATED, STATE_DEMOTED, STATE_EVICTED)}
|
|
153
|
+
for s in state.values():
|
|
154
|
+
counts[s] += 1
|
|
155
|
+
n = len(state)
|
|
156
|
+
alive = sum(1 for sid in sources if state.get(sid) != STATE_EVICTED)
|
|
157
|
+
derived_demoted = k * alive
|
|
158
|
+
derived_evicted = n - counts[STATE_LIVE] - counts[STATE_CONSOLIDATED] - derived_demoted
|
|
159
|
+
dd, de = derived_demoted - counts[STATE_DEMOTED], derived_evicted - counts[STATE_EVICTED]
|
|
160
|
+
worst = max(worst, abs(dd), abs(de))
|
|
161
|
+
phases.append({"phase": ev.get("phase"), "refused": bool(ev.get("refused")), "entries": n,
|
|
162
|
+
"live": counts[STATE_LIVE], "consolidated": counts[STATE_CONSOLIDATED],
|
|
163
|
+
"summaries_alive": alive, "demoted": counts[STATE_DEMOTED],
|
|
164
|
+
"evicted": counts[STATE_EVICTED], "derived_demoted": derived_demoted,
|
|
165
|
+
"derived_evicted": derived_evicted, "defect_demoted": dd, "defect_evicted": de})
|
|
166
|
+
return {"word_by_construction": "theorem (the closure of 3.5, rule 2)", "phases_checked": len(phases),
|
|
167
|
+
"exact_at_every_phase": worst == 0, "worst_defect": worst, "per_phase": phases}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def level_2_salience_line(path: Path, key: bytes | None, journal: Journal, decay: float, thresh: float) -> dict:
|
|
171
|
+
"""For every entry ever consolidated (kept, demoted, or since evicted; an evicted entry's
|
|
172
|
+
line ends at its eviction, where the journal freezes its salience): its salience at consolidation
|
|
173
|
+
as the log recorded it, the number of applied decays it received since (the decay
|
|
174
|
+
events that touched it: its state in the event's states and not contracted, as the
|
|
175
|
+
journal applies them), and the derivation from its final salience alone:
|
|
176
|
+
s_c_derived = s_final / decay ** n."""
|
|
177
|
+
sal: dict[str, float] = {}
|
|
178
|
+
state: dict[str, str] = {}
|
|
179
|
+
contracted: set[str] = set()
|
|
180
|
+
at_cons: dict[str, float] = {}
|
|
181
|
+
decays_after: dict[str, int] = {}
|
|
182
|
+
for ev in iter_events(path, key):
|
|
183
|
+
kind = ev["ev"]
|
|
184
|
+
if kind == "write":
|
|
185
|
+
d = ev["entry"]; sal[d["entry_id"]] = float(d["salience"]); state[d["entry_id"]] = STATE_LIVE
|
|
186
|
+
elif kind == "salience":
|
|
187
|
+
sal[ev["entry_id"]] = float(ev["to"])
|
|
188
|
+
if ev.get("why") == "contract":
|
|
189
|
+
contracted.add(ev["entry_id"])
|
|
190
|
+
elif kind == "transition":
|
|
191
|
+
state[ev["entry_id"]] = ev["to"]
|
|
192
|
+
if ev["to"] == STATE_CONSOLIDATED:
|
|
193
|
+
at_cons[ev["entry_id"]] = sal[ev["entry_id"]]
|
|
194
|
+
decays_after[ev["entry_id"]] = 0
|
|
195
|
+
elif kind == "decay":
|
|
196
|
+
states = tuple(ev["states"]); f = float(ev["factor"])
|
|
197
|
+
for eid, st in state.items():
|
|
198
|
+
if st in states and eid not in contracted:
|
|
199
|
+
sal[eid] = round(sal[eid] * f, 6)
|
|
200
|
+
if eid in decays_after:
|
|
201
|
+
decays_after[eid] += 1
|
|
202
|
+
rows, worst, total = [], 0.0, 0.0
|
|
203
|
+
for eid, e in journal._entries.items():
|
|
204
|
+
if eid not in at_cons: # every entry ever consolidated: kept, demoted, or since
|
|
205
|
+
continue # evicted (its line ends at its eviction, its salience frozen there)
|
|
206
|
+
n = decays_after[eid]
|
|
207
|
+
s_final = float(e.salience)
|
|
208
|
+
s_c = at_cons[eid]
|
|
209
|
+
derived = s_final / (decay ** n) if n else s_final
|
|
210
|
+
rel = abs(derived - s_c) / s_c if s_c > 0 else (0.0 if derived == s_c else float("inf"))
|
|
211
|
+
worst = max(worst, rel); total += rel
|
|
212
|
+
rows.append({"entry_id": eid, "state": e.state, "contracted": eid in contracted, "decays": n,
|
|
213
|
+
"salience_final": s_final, "salience_at_consolidation": s_c,
|
|
214
|
+
"derived": derived, "relative_defect": rel})
|
|
215
|
+
n_rows = len(rows)
|
|
216
|
+
return {"threshold_relative": thresh, "entries": n_rows, "kept": sum(1 for r in rows if r["state"] != STATE_EVICTED),
|
|
217
|
+
"evicted": sum(1 for r in rows if r["state"] == STATE_EVICTED), "contracted": sum(1 for r in rows if r["contracted"]),
|
|
218
|
+
"under_threshold": sum(1 for r in rows if r["relative_defect"] <= thresh),
|
|
219
|
+
"all_under_threshold": n_rows > 0 and all(r["relative_defect"] <= thresh for r in rows),
|
|
220
|
+
"max_relative_defect": worst, "mean_relative_defect": (total / n_rows) if n_rows else None,
|
|
221
|
+
"anchor": "without the log's count of decays, one number stays free: the origin (3.5, rule 1)",
|
|
222
|
+
"rows": rows}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def level_3_memory(journal: Journal, memory, bound: float, replay_errors: list[float] | None) -> dict:
|
|
226
|
+
"""Every replayed cue (the consolidation order), reconstructed from the memory alone."""
|
|
227
|
+
rows, by_state = [], {STATE_CONSOLIDATED: [], STATE_DEMOTED: [], STATE_EVICTED: []}
|
|
228
|
+
for eid in journal.consolidated_order:
|
|
229
|
+
e = journal._entries[eid]
|
|
230
|
+
cue = np.asarray(e.cue, dtype=np.float32)
|
|
231
|
+
q = cue / (np.linalg.norm(cue) + 1e-8)
|
|
232
|
+
r = memory.reconstruct(q)
|
|
233
|
+
d = float(np.clip(1.0 - float(r @ q / (np.linalg.norm(r) + 1e-8)), 0.0, 1.0)) if r is not None else 1.0
|
|
234
|
+
rows.append({"entry_id": eid, "state": e.state, "defect": d, "under_bound": d <= bound})
|
|
235
|
+
by_state.setdefault(e.state, []).append(d)
|
|
236
|
+
kept = [r for r in rows if r["state"] in (STATE_CONSOLIDATED, STATE_DEMOTED)]
|
|
237
|
+
evicted = [r for r in rows if r["state"] == STATE_EVICTED]
|
|
238
|
+
dist = [r["defect"] for r in rows]
|
|
239
|
+
return {"bound": bound, "patterns": len(memory), "replayed": len(rows),
|
|
240
|
+
"kept": {"entries": len(kept), "under_bound": sum(r["under_bound"] for r in kept),
|
|
241
|
+
"all_under_bound": len(kept) > 0 and all(r["under_bound"] for r in kept),
|
|
242
|
+
"max_defect": max((r["defect"] for r in kept), default=None),
|
|
243
|
+
"mean_defect": (sum(r["defect"] for r in kept) / len(kept)) if kept else None},
|
|
244
|
+
"evicted_address_reading": {"entries": len(evicted),
|
|
245
|
+
"still_reconstructed_under_bound": sum(r["under_bound"] for r in evicted),
|
|
246
|
+
"share": (sum(r["under_bound"] for r in evicted) / len(evicted)) if evicted else None},
|
|
247
|
+
"distribution": {"min": min(dist, default=None), "median": float(np.median(dist)) if dist else None,
|
|
248
|
+
"p95": float(np.percentile(dist, 95)) if dist else None, "max": max(dist, default=None)},
|
|
249
|
+
"positive_control": ({"replays": len(replay_errors), "max_error_at_replay": max(replay_errors, default=None),
|
|
250
|
+
"all_under_bound_at_replay": all(x <= bound for x in replay_errors)}
|
|
251
|
+
if replay_errors is not None else "not observable in this process"),
|
|
252
|
+
"rows": rows}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def level_4_content(journal: Journal, jp: JournalPath) -> dict:
|
|
256
|
+
"""Declared loss, checked: an evicted entry's payload has no file when its pointer is
|
|
257
|
+
unreferenced, and an evicted entry never comes back from a read."""
|
|
258
|
+
payload_dir = journal.payloads._dir
|
|
259
|
+
evicted = [e for e in journal._entries.values() if e.state == STATE_EVICTED]
|
|
260
|
+
files_present, reads_returning = 0, 0
|
|
261
|
+
for e in evicted:
|
|
262
|
+
referenced = e.pointer in journal.payloads
|
|
263
|
+
if not referenced and payload_dir is not None and (payload_dir / e.pointer).exists():
|
|
264
|
+
files_present += 1
|
|
265
|
+
cue = np.asarray(e.cue, dtype=np.float32)
|
|
266
|
+
if any(h[0].entry_id == e.entry_id for h in jp.retrieve(cue, k=4)):
|
|
267
|
+
reads_returning += 1
|
|
268
|
+
return {"word_by_construction": "declared loss (3.5, rule 5; theorem L2)", "evicted": len(evicted),
|
|
269
|
+
"released_payload_files_present": files_present, "evicted_entries_returned_by_a_read": reads_returning,
|
|
270
|
+
"holds": files_present == 0 and reads_returning == 0}
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def levels(copy: Path, key: bytes, journal: Journal, jp: JournalPath, memory, cfg: LifecycleConfig,
|
|
274
|
+
replay_errors: list[float] | None) -> dict:
|
|
275
|
+
return {"level_1_counts": level_1_counts(copy, key, cfg.demote_k),
|
|
276
|
+
"level_2_salience_line": level_2_salience_line(copy, key, journal, cfg.decay, THRESH_SALIENCE_REL),
|
|
277
|
+
"level_3_memory": level_3_memory(journal, memory, cfg.replay_error_max, replay_errors),
|
|
278
|
+
"level_4_content": level_4_content(journal, jp),
|
|
279
|
+
"snapshot": journal.snapshot()}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _compare(a: dict, b: dict, path: str = "") -> list[str]:
|
|
283
|
+
"""Number for number: the two processes must agree on every scalar of the levels."""
|
|
284
|
+
diffs = []
|
|
285
|
+
if isinstance(a, dict) and isinstance(b, dict):
|
|
286
|
+
for k in sorted(set(a) | set(b)):
|
|
287
|
+
if k in ("positive_control", "rows", "per_phase"):
|
|
288
|
+
continue
|
|
289
|
+
diffs += _compare(a.get(k), b.get(k), f"{path}/{k}")
|
|
290
|
+
elif isinstance(a, list) and isinstance(b, list):
|
|
291
|
+
if len(a) != len(b):
|
|
292
|
+
diffs.append(f"{path}: length {len(a)} != {len(b)}")
|
|
293
|
+
else:
|
|
294
|
+
for i, (x, y) in enumerate(zip(a, b)):
|
|
295
|
+
diffs += _compare(x, y, f"{path}[{i}]")
|
|
296
|
+
elif isinstance(a, float) or isinstance(b, float):
|
|
297
|
+
if not (a == b or (isinstance(a, float) and isinstance(b, float) and a != a and b != b)):
|
|
298
|
+
diffs.append(f"{path}: {a!r} != {b!r}")
|
|
299
|
+
elif a != b:
|
|
300
|
+
diffs.append(f"{path}: {a!r} != {b!r}")
|
|
301
|
+
return diffs
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# ---------------------------------------------------------------------------- #
|
|
305
|
+
# Session A: copy, plant the sentinels, run the phases, measure #
|
|
306
|
+
# ---------------------------------------------------------------------------- #
|
|
307
|
+
def session_a(src: Path, copy_dir: Path, key: bytes, phases: int = PHASES, sentinel_seed: int = SENTINEL_SEED,
|
|
308
|
+
cfg: LifecycleConfig | None = None) -> tuple[dict, Path]:
|
|
309
|
+
if int(sentinel_seed) in RESERVED_SEEDS:
|
|
310
|
+
raise SystemExit("reconstruction probe: the sentinel seed must differ from the protocol's seeds")
|
|
311
|
+
cfg = cfg or LifecycleConfig(sentinel_seed=int(sentinel_seed))
|
|
312
|
+
if cfg.sentinel_seed != int(sentinel_seed):
|
|
313
|
+
raise SystemExit("reconstruction probe: the configuration's sentinel seed must be the declared one")
|
|
314
|
+
src = journal_file(src)
|
|
315
|
+
src_hash = sha256_file(src)
|
|
316
|
+
copy = copy_journal(src, copy_dir)
|
|
317
|
+
journal, wp, jp = _open(copy, key)
|
|
318
|
+
n_before = len(journal)
|
|
319
|
+
if journal.phase_count:
|
|
320
|
+
raise SystemExit(f"reconstruction probe: the journal already holds {journal.phase_count} phases; the probe runs on a journal without lifecycle history")
|
|
321
|
+
# the collision abort of row 40: no sentinel entity may be one of the journal's
|
|
322
|
+
sentinels, _ = generate_facts(cfg.sentinel_facts, seed=cfg.sentinel_seed)
|
|
323
|
+
negctrl, _ = generate_facts(cfg.sentinel_negctrl, seed=cfg.sentinel_seed + 10_000)
|
|
324
|
+
domain = entities_of(journal)
|
|
325
|
+
collisions = sorted({f.entity for f in sentinels + negctrl} & domain)
|
|
326
|
+
if collisions:
|
|
327
|
+
raise SystemExit(f"reconstruction probe: sentinel entities collide with the journal's: {collisions}")
|
|
328
|
+
replay_errors: list[float] = []
|
|
329
|
+
now_plant = float(max((e.t_written for e in journal._entries.values()), default=-1.0) + 1.0)
|
|
330
|
+
sched = LifecycleScheduler(journal, wp, jp, cfg=cfg, now=now_plant) # plants the sentinels (an event each)
|
|
331
|
+
original_replay = sched.memory.replay
|
|
332
|
+
|
|
333
|
+
def recorded_replay(cue):
|
|
334
|
+
err = original_replay(cue)
|
|
335
|
+
replay_errors.append(float(err))
|
|
336
|
+
return err
|
|
337
|
+
sched.memory.replay = recorded_replay
|
|
338
|
+
records = []
|
|
339
|
+
for i in range(int(phases)):
|
|
340
|
+
rec = sched.phase(now_plant + 1.0 + i)
|
|
341
|
+
records.append({"phase": rec["phase"], "now": rec["now"], "pressure": rec["pressure"],
|
|
342
|
+
"floor_eff": rec["floor_eff"], "batch_eff": rec["batch_eff"], "planned": rec["planned"],
|
|
343
|
+
"sentinel_recall": rec["sentinel_recall"], "sentinel_negctrl": rec["sentinel_negctrl"],
|
|
344
|
+
"refused": rec["refused"], "reason": rec.get("reason"), "applied": rec.get("applied"),
|
|
345
|
+
"live": rec.get("live"), "consolidated": rec.get("consolidated"),
|
|
346
|
+
"demoted": rec.get("demoted"), "evicted": rec.get("evicted"),
|
|
347
|
+
"summaries": rec.get("summaries"), "memory_patterns": rec.get("memory_patterns")})
|
|
348
|
+
# the replays that failed were withdrawn: they are in replay_errors above the bound, by construction
|
|
349
|
+
withdrawn = sum(1 for x in replay_errors if x > cfg.replay_error_max)
|
|
350
|
+
out = {"probe": "reconstruction on the organ's layer of numbers (ADR-007 amendment 2026-09-30; whitepaper 3.5, 5d door 7)",
|
|
351
|
+
"timestamp_utc": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
352
|
+
"journal": {"source": str(src), "sha256": src_hash, "entries_before": n_before,
|
|
353
|
+
"separation": separation_of(journal), "sealed": True, "copied_to_temp": True},
|
|
354
|
+
"config": {"lifecycle": cfg.__dict__, "config_hash": cfg.config_hash(), "phases": int(phases),
|
|
355
|
+
"sentinel_seed": int(sentinel_seed), "index_seed": INDEX_SEED, "now_plant": now_plant,
|
|
356
|
+
"reads_between_phases": 0},
|
|
357
|
+
"thresholds": {"level_1": "exact", "level_2_relative": THRESH_SALIENCE_REL,
|
|
358
|
+
"level_3_bound": cfg.replay_error_max},
|
|
359
|
+
"sentinels": {"planted": len(sched.sentinels), "negctrl": len(sched.negctrl),
|
|
360
|
+
"collision_check": "passed (no sentinel entity among the journal's)"},
|
|
361
|
+
"phases": records,
|
|
362
|
+
"phases_refused": sum(1 for r in records if r["refused"]),
|
|
363
|
+
"replays": {"count": len(replay_errors), "withdrawn": withdrawn},
|
|
364
|
+
"storage": lifecycle_declaration(journal)["storage"],
|
|
365
|
+
"process_a": levels(copy, key, journal, jp, sched.memory, cfg, replay_errors)}
|
|
366
|
+
return out, copy
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# ---------------------------------------------------------------------------- #
|
|
370
|
+
# Session B: a new process, the disk alone #
|
|
371
|
+
# ---------------------------------------------------------------------------- #
|
|
372
|
+
def session_b(copy: Path, key: bytes, sentinel_seed: int = SENTINEL_SEED, cfg: LifecycleConfig | None = None) -> dict:
|
|
373
|
+
cfg = cfg or LifecycleConfig(sentinel_seed=int(sentinel_seed))
|
|
374
|
+
journal, wp, jp = _open(journal_file(copy), key)
|
|
375
|
+
sched = LifecycleScheduler(journal, wp, jp, cfg=cfg) # rebuilds the memory from the consolidation order
|
|
376
|
+
return {"process": "new (nothing of session A but the disk)", "phases_found": journal.phase_count,
|
|
377
|
+
"sentinels_found": len(sched.sentinels), "durable": journal.mode == MODE_DURABLE,
|
|
378
|
+
**levels(journal_file(copy), key, journal, jp, sched.memory, cfg, None)}
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def run(src: Path, key: bytes, phases: int = PHASES, sentinel_seed: int = SENTINEL_SEED, keep_copy: Path | None = None) -> dict:
|
|
382
|
+
tmp = Path(tempfile.mkdtemp(prefix="reconstruction-probe-")) if keep_copy is None else keep_copy
|
|
383
|
+
try:
|
|
384
|
+
out, copy = session_a(src, tmp, key, phases=phases, sentinel_seed=sentinel_seed)
|
|
385
|
+
env = dict(os.environ, QUANTUM_CORTEX_JOURNAL_KEY=key.hex())
|
|
386
|
+
proc = subprocess.run([sys.executable, "-m", "cortex_c2b.reconstruction_probe", "--session-b", str(copy),
|
|
387
|
+
"--sentinel-seed", str(sentinel_seed)],
|
|
388
|
+
capture_output=True, text=True, env=env, cwd=str(Path(__file__).resolve().parents[1]))
|
|
389
|
+
if proc.returncode != 0:
|
|
390
|
+
raise RuntimeError(f"session B failed:\n{proc.stderr[-3000:]}")
|
|
391
|
+
b = json.loads(proc.stdout.strip().splitlines()[-1])
|
|
392
|
+
diffs = _compare({k: v for k, v in out["process_a"].items()}, {k: b[k] for k in out["process_a"] if k in b})
|
|
393
|
+
out["process_b"] = b
|
|
394
|
+
out["agreement"] = {"number_for_number": not diffs, "differences": diffs[:50]}
|
|
395
|
+
a = out["process_a"]
|
|
396
|
+
out["readings"] = {
|
|
397
|
+
"level_1_exact": a["level_1_counts"]["exact_at_every_phase"],
|
|
398
|
+
"level_2_under_threshold": a["level_2_salience_line"]["all_under_threshold"],
|
|
399
|
+
"level_3_kept_under_bound": a["level_3_memory"]["kept"]["all_under_bound"],
|
|
400
|
+
"level_3b_evicted_address_share": a["level_3_memory"]["evicted_address_reading"]["share"],
|
|
401
|
+
"level_4_holds": a["level_4_content"]["holds"],
|
|
402
|
+
"two_processes_agree": not diffs,
|
|
403
|
+
}
|
|
404
|
+
out["verdict"] = _verdict(out["readings"])
|
|
405
|
+
return out
|
|
406
|
+
finally:
|
|
407
|
+
if keep_copy is None:
|
|
408
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _verdict(r: dict) -> str:
|
|
412
|
+
if not r["level_1_exact"] or not r["two_processes_agree"] or not r["level_4_holds"]:
|
|
413
|
+
return "CONSTRUCTION FAULT -- no row (reading d)"
|
|
414
|
+
words = ["level 1: theorem by construction", "level 4: declared loss by construction"]
|
|
415
|
+
words.append("level 2: reconstructible at measured defect" if r["level_2_under_threshold"] else "level 2: residual above the bound, to be attributed (reading c)")
|
|
416
|
+
words.append("level 3: reconstructible at measured defect" if r["level_3_kept_under_bound"] else "level 3: backup, the memory's capacity is the measured limit (reading b)")
|
|
417
|
+
share = r["level_3b_evicted_address_share"]
|
|
418
|
+
words.append("level 3b: " + ("no evicted entry" if share is None else
|
|
419
|
+
("the address of an evicted episode survives its eviction in the memory" if share > 0 else "the declared loss covers the address too")))
|
|
420
|
+
return "; ".join(words)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def summary_lines(out: dict) -> list[str]:
|
|
424
|
+
a, r = out["process_a"], out["readings"]
|
|
425
|
+
l2, l3 = a["level_2_salience_line"], a["level_3_memory"]
|
|
426
|
+
return [
|
|
427
|
+
f"journal {out['journal']['sha256'][:16]} entries before {out['journal']['entries_before']} phases {out['config']['phases']} refused {out['phases_refused']}",
|
|
428
|
+
f"end state: live {a['snapshot']['live']} consolidated {a['snapshot']['consolidated']} demoted {a['snapshot']['demoted']} evicted {a['snapshot']['evicted']} summaries {a['snapshot']['summaries']}",
|
|
429
|
+
f"level 1 (counts): exact at every phase = {r['level_1_exact']} (worst defect {a['level_1_counts']['worst_defect']})",
|
|
430
|
+
f"level 2 (salience line): {l2['under_threshold']}/{l2['entries']} under 1e-3; max relative defect {l2['max_relative_defect']:.3e}",
|
|
431
|
+
f"level 3 (memory): kept {l3['kept']['under_bound']}/{l3['kept']['entries']} under {l3['bound']}; max defect {l3['kept']['max_defect']}; replays {out['replays']['count']}, withdrawn {out['replays']['withdrawn']}",
|
|
432
|
+
f"level 3b (evicted address): {l3['evicted_address_reading']['still_reconstructed_under_bound']}/{l3['evicted_address_reading']['entries']} still reconstructed under the bound",
|
|
433
|
+
f"level 4 (content): holds = {r['level_4_holds']}",
|
|
434
|
+
f"two processes agree number for number: {r['two_processes_agree']}",
|
|
435
|
+
f"verdict: {out['verdict']}",
|
|
436
|
+
]
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _cli(argv=None):
|
|
440
|
+
ap = argparse.ArgumentParser(description="the reconstruction probe on the organ's layer of numbers")
|
|
441
|
+
ap.add_argument("--journal", default=None, help="a sealed journal of the record (the journal.jsonl or its directory); copied, never modified")
|
|
442
|
+
ap.add_argument("--out", default=None, help="the directory or file for the retained record")
|
|
443
|
+
ap.add_argument("--run-id", default=None, help="the run whose journal this is (names the file)")
|
|
444
|
+
ap.add_argument("--phases", type=int, default=PHASES)
|
|
445
|
+
ap.add_argument("--sentinel-seed", type=int, default=SENTINEL_SEED)
|
|
446
|
+
ap.add_argument("--session-b", default=None, help="(internal) reopen this copy in a new process and measure")
|
|
447
|
+
args = ap.parse_args(argv)
|
|
448
|
+
from cortex_c2b.crypto import key_from_env
|
|
449
|
+
key = key_from_env()
|
|
450
|
+
if key is None:
|
|
451
|
+
raise SystemExit("QUANTUM_CORTEX_JOURNAL_KEY missing")
|
|
452
|
+
if args.session_b:
|
|
453
|
+
print(json.dumps(session_b(Path(args.session_b), key, sentinel_seed=args.sentinel_seed)))
|
|
454
|
+
return
|
|
455
|
+
if not args.journal:
|
|
456
|
+
raise SystemExit("--journal is required")
|
|
457
|
+
out = run(Path(args.journal), key, phases=args.phases, sentinel_seed=args.sentinel_seed)
|
|
458
|
+
for line in summary_lines(out):
|
|
459
|
+
print(line)
|
|
460
|
+
if args.out:
|
|
461
|
+
o = Path(args.out)
|
|
462
|
+
if o.is_dir() or not o.suffix:
|
|
463
|
+
o.mkdir(parents=True, exist_ok=True)
|
|
464
|
+
o = o / f"reconstruction-probe-{args.run_id or out['journal']['sha256'][:12]}.json"
|
|
465
|
+
o.parent.mkdir(parents=True, exist_ok=True)
|
|
466
|
+
o.write_text(json.dumps(out, indent=2) + "\n", encoding="utf-8")
|
|
467
|
+
print(f"[record] retained: {o}")
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
if __name__ == "__main__":
|
|
471
|
+
_cli()
|
cortex_c2b/write_path.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""cortex_c2b.write_path — the hippocampal write path (ADR-007 D2), Slice B.
|
|
2
|
+
|
|
3
|
+
separate (DG) → associate (CA3) → compare (CA1) → gate
|
|
4
|
+
|
|
5
|
+
Each stage is a flag (default on here; ablatable to off) so that interference
|
|
6
|
+
without DG, recall without CA3, and write precision without CA1 are each a row.
|
|
7
|
+
|
|
8
|
+
DG — pattern separation: expand-then-sparsify the cue so near-duplicates
|
|
9
|
+
decorrelate before storage (the interference guard). A fixed random
|
|
10
|
+
expansion to a larger space followed by k-winners-take-all: two cues that
|
|
11
|
+
are close in input space land on mostly-different sparse codes.
|
|
12
|
+
CA3 — pattern completion: reconstruct the input from what the memory already
|
|
13
|
+
holds. Slice B uses the journal's own stored cues as the associative
|
|
14
|
+
memory (nearest stored cue = the reconstruction); Slice C swaps in the
|
|
15
|
+
C2 Hopfield/delta layer of ADR-006 (C2 *is* CA3) behind the same call.
|
|
16
|
+
CA1 — the comparator: surprise = distance(reconstruction, input). Surprise is
|
|
17
|
+
no longer a heuristic; it is a computed quantity at the memory interface
|
|
18
|
+
(RES-2 made concrete). Feeds salience and the write gate.
|
|
19
|
+
gate — the noise ruling (D4.8), TWO stages, never one detector:
|
|
20
|
+
(a) admission: reject the redundant/already-known (surprise below a
|
|
21
|
+
declared threshold) — but surprise alone is not signal: a corrupted
|
|
22
|
+
input is maximally surprising and would be journaled enthusiastically;
|
|
23
|
+
(b) the verdict is retrospective: an admitted entry that never gains
|
|
24
|
+
salience, never consolidates, never earns outcome credit is NOISE and
|
|
25
|
+
is evicted by the tribunal (`noise_tribunal`). Scheduled forgetting is
|
|
26
|
+
the judge. Slice B implements the tribunal's rule; the schedule (RES-9)
|
|
27
|
+
is Slice E.
|
|
28
|
+
|
|
29
|
+
Written from scratch (our filon). No torch here — pure numpy over the store.
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
from dataclasses import dataclass
|
|
34
|
+
|
|
35
|
+
import numpy as np
|
|
36
|
+
|
|
37
|
+
from cortex_c2b import Journal, Entry, STATE_LIVE, STATE_EVICTED, CUE_DIM
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------- #
|
|
41
|
+
# DG — pattern separation #
|
|
42
|
+
# ---------------------------------------------------------------------------- #
|
|
43
|
+
class DentateGyrus:
|
|
44
|
+
"""Expand-then-sparsify: project to a larger space with a fixed random map,
|
|
45
|
+
keep the k largest activations (k-WTA), renormalise. Near-duplicates
|
|
46
|
+
decorrelate; the separation is measurable as the drop in cosine similarity."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, in_dim: int = CUE_DIM, expand: int = 4, sparsity: float = 0.1, seed: int = 0):
|
|
49
|
+
rng = np.random.default_rng(seed)
|
|
50
|
+
self.out_dim = in_dim * expand
|
|
51
|
+
self.W = rng.standard_normal((in_dim, self.out_dim)).astype(np.float32) / np.sqrt(in_dim)
|
|
52
|
+
self.k = max(1, int(self.out_dim * sparsity))
|
|
53
|
+
|
|
54
|
+
def separate(self, cue: np.ndarray) -> np.ndarray:
|
|
55
|
+
z = cue.astype(np.float32) @ self.W
|
|
56
|
+
idx = np.argpartition(-np.abs(z), self.k)[: self.k] # k winners
|
|
57
|
+
out = np.zeros_like(z); out[idx] = z[idx]
|
|
58
|
+
n = np.linalg.norm(out)
|
|
59
|
+
return out / n if n > 0 else out
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------- #
|
|
63
|
+
# CA3 — pattern completion (Slice B: from the journal's stored cues) #
|
|
64
|
+
# ---------------------------------------------------------------------------- #
|
|
65
|
+
class CA3Completion:
|
|
66
|
+
"""Reconstruct from memory: return the stored code nearest to the input.
|
|
67
|
+
Slice C replaces this with the C2 associative layer behind the same call."""
|
|
68
|
+
|
|
69
|
+
def __init__(self):
|
|
70
|
+
self._codes: list[np.ndarray] = []
|
|
71
|
+
|
|
72
|
+
def store(self, code: np.ndarray):
|
|
73
|
+
self._codes.append(code.astype(np.float32))
|
|
74
|
+
|
|
75
|
+
def reconstruct(self, code: np.ndarray) -> np.ndarray | None:
|
|
76
|
+
if not self._codes:
|
|
77
|
+
return None
|
|
78
|
+
M = np.stack(self._codes) # (n, d)
|
|
79
|
+
sims = M @ code / (np.linalg.norm(M, axis=1) * (np.linalg.norm(code) + 1e-8) + 1e-8)
|
|
80
|
+
return M[int(np.argmax(sims))]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ---------------------------------------------------------------------------- #
|
|
84
|
+
# CA1 — the comparator: surprise as a computed quantity #
|
|
85
|
+
# ---------------------------------------------------------------------------- #
|
|
86
|
+
def ca1_surprise(reconstruction: np.ndarray | None, code: np.ndarray) -> float:
|
|
87
|
+
"""surprise = distance(reconstruction, input) in cosine terms ∈ [0, 1].
|
|
88
|
+
No memory yet → maximal surprise (everything is new)."""
|
|
89
|
+
if reconstruction is None:
|
|
90
|
+
return 1.0
|
|
91
|
+
a, b = reconstruction, code
|
|
92
|
+
cos = float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-8))
|
|
93
|
+
return float(np.clip(1.0 - cos, 0.0, 1.0))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------- #
|
|
97
|
+
# The gated write path #
|
|
98
|
+
# ---------------------------------------------------------------------------- #
|
|
99
|
+
@dataclass
|
|
100
|
+
class WriteReport:
|
|
101
|
+
admitted: bool
|
|
102
|
+
surprise: float
|
|
103
|
+
reason: str
|
|
104
|
+
entry: Entry | None = None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class WritePath:
|
|
108
|
+
"""separate → associate → compare → gate, over a Journal.
|
|
109
|
+
|
|
110
|
+
`admission_threshold`: a cue whose CA1 surprise is below it is REDUNDANT
|
|
111
|
+
(already known) and is not admitted (stage a). Admitted entries start LIVE
|
|
112
|
+
with salience = surprise. Stage (b), the retrospective verdict, is
|
|
113
|
+
`noise_tribunal()`.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(self, journal: Journal, admission_threshold: float = 0.15,
|
|
117
|
+
use_dg: bool = True, use_ca3: bool = True, use_ca1: bool = True, seed: int = 0):
|
|
118
|
+
self.j = journal
|
|
119
|
+
self.thr = admission_threshold
|
|
120
|
+
self.use_dg, self.use_ca3, self.use_ca1 = use_dg, use_ca3, use_ca1
|
|
121
|
+
self.dg = DentateGyrus(seed=seed) if use_dg else None
|
|
122
|
+
self.ca3 = CA3Completion()
|
|
123
|
+
# Decision 8 (persistence per component): CA3 is rebuilt from the journal's
|
|
124
|
+
# `ca3` events, in admission order, with the same separation. A journal
|
|
125
|
+
# written under another separation is refused, never silently re-coded.
|
|
126
|
+
self._meta = {"dg": bool(use_dg), "seed": int(seed)}
|
|
127
|
+
for eid, meta in journal.ca3_order:
|
|
128
|
+
if {"dg": meta.get("dg"), "seed": meta.get("seed")} != self._meta:
|
|
129
|
+
raise ValueError(f"CA3 rebuild: journal written with separation {meta}, this write path has {self._meta}")
|
|
130
|
+
self.ca3.store(self._code(np.asarray(journal._entries[eid].cue, dtype=np.float32)))
|
|
131
|
+
|
|
132
|
+
def _code(self, cue: np.ndarray) -> np.ndarray:
|
|
133
|
+
return self.dg.separate(cue) if self.dg else cue.astype(np.float32)
|
|
134
|
+
|
|
135
|
+
def write(self, cue, payload: bytes, schema_id: str, now: float | None = None) -> WriteReport:
|
|
136
|
+
cue = np.asarray(cue, dtype=np.float32)
|
|
137
|
+
code = self._code(cue)
|
|
138
|
+
recon = self.ca3.reconstruct(code) if self.use_ca3 else None
|
|
139
|
+
surprise = ca1_surprise(recon, code) if self.use_ca1 else 1.0 # no comparator → write everything
|
|
140
|
+
# stage (a): admission — reject the redundant / already-known
|
|
141
|
+
if surprise < self.thr:
|
|
142
|
+
return WriteReport(admitted=False, surprise=surprise, reason="redundant (below admission threshold)")
|
|
143
|
+
entry = self.j.write(cue, payload, salience=surprise, schema_id=schema_id, now=now,
|
|
144
|
+
ca3=self._meta) # one line; a failure cannot split it (D9)
|
|
145
|
+
# store the code of the cue AS JOURNALED (rounded), so a rebuild after a restart
|
|
146
|
+
# produces byte-identical CA3 contents (Decision 8)
|
|
147
|
+
self.ca3.store(self._code(np.asarray(entry.cue, dtype=np.float32)))
|
|
148
|
+
return WriteReport(admitted=True, surprise=surprise, reason="admitted", entry=entry)
|
|
149
|
+
|
|
150
|
+
# ---- outcome credit (RES-11 hook): salience earned after the fact ---------
|
|
151
|
+
def credit(self, entry_id: str, amount: float, why: str = "credit"):
|
|
152
|
+
e = self.j.get(entry_id)
|
|
153
|
+
self.j.set_salience(entry_id, e.salience + amount, why=why) # an event (Slice E, invariant 1)
|
|
154
|
+
|
|
155
|
+
# ---- stage (b): the retrospective verdict — the noise tribunal -----------
|
|
156
|
+
def noise_tribunal(self, salience_floor: float = 0.2, min_age: float = 0.0,
|
|
157
|
+
now: float | None = None) -> list[str]:
|
|
158
|
+
"""Evict LIVE entries that never earned salience above the floor and were
|
|
159
|
+
never consolidated — the D4.8 definition: noise is what never improves
|
|
160
|
+
prediction. A corrupted input (maximally surprising at admission) that
|
|
161
|
+
never earns credit is exactly what this catches. Returns evicted ids."""
|
|
162
|
+
import time as _t
|
|
163
|
+
now = _t.time() if now is None else now
|
|
164
|
+
evicted = []
|
|
165
|
+
for eid in noise_candidates(self.j, salience_floor, min_age, now):
|
|
166
|
+
self.j.transition(eid, STATE_EVICTED)
|
|
167
|
+
evicted.append(eid)
|
|
168
|
+
return evicted
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def noise_candidates(journal: Journal, salience_floor: float, min_age: float, now: float) -> list[str]:
|
|
172
|
+
"""The D4.8 rule as a predicate, shared by the tribunal above and by the
|
|
173
|
+
Slice E scheduler (which plans before it applies): LIVE entries that never
|
|
174
|
+
earned salience above the floor and are old enough. Deterministic order."""
|
|
175
|
+
return [eid for eid, e in sorted(journal._entries.items())
|
|
176
|
+
if e.state == STATE_LIVE and e.salience < salience_floor and (now - e.t_written) >= min_age]
|