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.
Files changed (41) hide show
  1. cortex_c2/__init__.py +161 -0
  2. cortex_c2/consolidation.py +146 -0
  3. cortex_c2/router_ablation.py +254 -0
  4. cortex_c2/router_v2.py +109 -0
  5. cortex_c2b/__init__.py +724 -0
  6. cortex_c2b/companion_demo.py +203 -0
  7. cortex_c2b/crypto.py +102 -0
  8. cortex_c2b/external_arm.py +591 -0
  9. cortex_c2b/hm_lm.py +983 -0
  10. cortex_c2b/hm_protocol.py +308 -0
  11. cortex_c2b/lifecycle.py +454 -0
  12. cortex_c2b/lm_bridge.py +338 -0
  13. cortex_c2b/organ_use.py +254 -0
  14. cortex_c2b/read_path.py +218 -0
  15. cortex_c2b/reconstruction_probe.py +471 -0
  16. cortex_c2b/write_path.py +176 -0
  17. cortex_data/__init__.py +439 -0
  18. cortex_data/relay.py +174 -0
  19. cortex_eval/__init__.py +20 -0
  20. cortex_eval/ablation_mqar.py +187 -0
  21. cortex_eval/confirmation_run.py +133 -0
  22. cortex_eval/domain_map.py +127 -0
  23. cortex_eval/external_baseline.py +148 -0
  24. cortex_eval/ingest_confirmation.py +194 -0
  25. cortex_eval/mqar.py +122 -0
  26. cortex_eval/multiseed.py +148 -0
  27. cortex_eval/recurrent_base.py +197 -0
  28. cortex_eval/recurrent_ladder.py +159 -0
  29. cortex_eval/resumable_ladder.py +231 -0
  30. cortex_eval/resumable_rb.py +207 -0
  31. cortex_eval/run_mqar.py +127 -0
  32. cortex_eval/serial_position.py +116 -0
  33. quantum_cortex-1.0.0.dist-info/METADATA +209 -0
  34. quantum_cortex-1.0.0.dist-info/RECORD +41 -0
  35. quantum_cortex-1.0.0.dist-info/WHEEL +5 -0
  36. quantum_cortex-1.0.0.dist-info/licenses/AUTHORS.md +30 -0
  37. quantum_cortex-1.0.0.dist-info/licenses/LICENSE +73 -0
  38. quantum_cortex-1.0.0.dist-info/licenses/LICENSE-DOCS.md +34 -0
  39. quantum_cortex-1.0.0.dist-info/licenses/NOTICE +50 -0
  40. quantum_cortex-1.0.0.dist-info/top_level.txt +5 -0
  41. 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()
@@ -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]