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,439 @@
1
+ """cortex_data — the data composition layer (ADR-005, Slice 1).
2
+
3
+ A data mix is a DAG of typed, composable operators. Each operator is a
4
+ lazy, pull-based iterator over byte tokens (uint16 in 0..255 plus reserved
5
+ oracle ids); nothing intermediate is materialised. Every node has a
6
+ deterministic hash; the sink hash is the mix_hash — two runs are comparable
7
+ iff they share it (ADR-005 D2). The engine is written from scratch (D8) and
8
+ orchestrates licensed sources via credited recipes.
9
+
10
+ Slice 1 implements exactly what N2 needs: the operator interface, the hashed
11
+ DAG manifest, lazy `source` / `decontaminate` / `mix`, compilation to a
12
+ uint16 .bin the trainer already reads, and the five-invariant harness (tests).
13
+ Memoisation, Bloom-at-scale and the data-β estimator are later slices.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import hashlib
18
+ import json
19
+ from dataclasses import dataclass, field
20
+ from pathlib import Path
21
+ from typing import Iterator, Sequence
22
+
23
+ import numpy as np
24
+
25
+ TOKEN_DTYPE = np.uint16 # matches train.py's memmap
26
+ _CHUNK = 1 << 16 # streaming granularity (tokens)
27
+
28
+
29
+ def _hash_obj(obj) -> str:
30
+ """Deterministic short hash of a JSON-able object."""
31
+ return hashlib.sha256(
32
+ json.dumps(obj, sort_keys=True, separators=(",", ":")).encode()
33
+ ).hexdigest()[:16]
34
+
35
+
36
+ # --------------------------------------------------------------------------- #
37
+ # Operator base #
38
+ # --------------------------------------------------------------------------- #
39
+ class Op:
40
+ """A typed, composable, lazy operator over a stream of uint16 tokens.
41
+
42
+ Subclasses implement `stream()` (a fresh iterator over token chunks,
43
+ each a 1-D uint16 ndarray) and `_spec()` (the JSON-able description used
44
+ for hashing). `node_hash` is f(spec, input hashes) — content addressing.
45
+ """
46
+
47
+ op_type: str = "op"
48
+ inputs: Sequence["Op"] = ()
49
+
50
+ def _spec(self) -> dict:
51
+ raise NotImplementedError
52
+
53
+ def stream(self, *, seed: int) -> Iterator[np.ndarray]:
54
+ raise NotImplementedError
55
+
56
+ # -- hashing -------------------------------------------------------------
57
+ def node_hash(self) -> str:
58
+ return _hash_obj(
59
+ {
60
+ "op": self.op_type,
61
+ "spec": self._spec(),
62
+ "inputs": [c.node_hash() for c in self.inputs],
63
+ }
64
+ )
65
+
66
+ def manifest(self) -> dict:
67
+ """The DAG rooted at this node, as a JSON-able recipe (ADR-005 D2)."""
68
+ return {
69
+ "op": self.op_type,
70
+ "spec": self._spec(),
71
+ "hash": self.node_hash(),
72
+ "inputs": [c.manifest() for c in self.inputs],
73
+ }
74
+
75
+ # -- materialisation -----------------------------------------------------
76
+ def to_bin(self, path: str | Path, *, seed: int, max_tokens: int | None = None) -> int:
77
+ """Stream this node to a uint16 .bin the trainer reads. Returns token count.
78
+
79
+ Only the sink is materialised (ADR-005 D3: no intermediate files).
80
+ """
81
+ path = Path(path)
82
+ path.parent.mkdir(parents=True, exist_ok=True)
83
+ written = 0
84
+ with open(path, "wb") as fh:
85
+ for chunk in self.stream(seed=seed):
86
+ if max_tokens is not None and written + len(chunk) > max_tokens:
87
+ chunk = chunk[: max_tokens - written]
88
+ if len(chunk):
89
+ fh.write(np.ascontiguousarray(chunk, dtype=TOKEN_DTYPE).tobytes())
90
+ written += len(chunk)
91
+ if max_tokens is not None and written >= max_tokens:
92
+ break
93
+ return written
94
+
95
+
96
+ # --------------------------------------------------------------------------- #
97
+ # Sources #
98
+ # --------------------------------------------------------------------------- #
99
+ @dataclass
100
+ class BinSource(Op):
101
+ """A licensed corpus already compiled to a uint16 .bin, content-addressed.
102
+
103
+ `dataset_id` and `license` are declared for provenance/attribution (D8).
104
+ `content_sha` pins the exact bytes; if absent it is computed on first use.
105
+ """
106
+
107
+ path: str
108
+ dataset_id: str
109
+ license: str
110
+ slice: str = ""
111
+ content_sha: str | None = None
112
+ op_type: str = field(default="source", init=False)
113
+
114
+ def _resolve(self) -> Path:
115
+ return Path(self.path)
116
+
117
+ def _sha(self) -> str:
118
+ if self.content_sha:
119
+ return self.content_sha
120
+ p = self._resolve()
121
+ if not p.exists():
122
+ # hash the spec instead so the DAG is still deterministic offline
123
+ return "unresolved:" + _hash_obj({"path": self.path, "id": self.dataset_id})
124
+ h = hashlib.sha256()
125
+ with open(p, "rb") as fh:
126
+ for block in iter(lambda: fh.read(1 << 20), b""):
127
+ h.update(block)
128
+ self.content_sha = h.hexdigest()[:16]
129
+ return self.content_sha
130
+
131
+ def _spec(self) -> dict:
132
+ return {
133
+ "dataset_id": self.dataset_id,
134
+ "license": self.license,
135
+ "slice": self.slice,
136
+ "content_sha": self._sha(),
137
+ }
138
+
139
+ def stream(self, *, seed: int) -> Iterator[np.ndarray]:
140
+ p = self._resolve()
141
+ if not p.exists():
142
+ raise FileNotFoundError(f"source .bin missing: {p} ({self.dataset_id})")
143
+ arr = np.memmap(p, dtype=TOKEN_DTYPE, mode="r")
144
+ for i in range(0, len(arr), _CHUNK):
145
+ yield np.asarray(arr[i : i + _CHUNK])
146
+
147
+
148
+ @dataclass
149
+ class ArraySource(Op):
150
+ """An in-memory token array — used by tests and small deterministic inputs."""
151
+
152
+ tokens: np.ndarray
153
+ name: str = "array"
154
+ op_type: str = field(default="source", init=False)
155
+
156
+ def _spec(self) -> dict:
157
+ return {"name": self.name, "content_sha": _hash_obj(self.tokens.tolist())}
158
+
159
+ def stream(self, *, seed: int) -> Iterator[np.ndarray]:
160
+ arr = np.asarray(self.tokens, dtype=TOKEN_DTYPE)
161
+ for i in range(0, len(arr), _CHUNK):
162
+ yield arr[i : i + _CHUNK]
163
+
164
+
165
+ # --------------------------------------------------------------------------- #
166
+ # Combinators #
167
+ # --------------------------------------------------------------------------- #
168
+ @dataclass
169
+ class Mix(Op):
170
+ """Proportional sampling WITHOUT replacement (ADR-005 D6).
171
+
172
+ Each source contributes a fraction of the final stream proportional to its
173
+ weight, drawn without replacement from that source's tokens. This makes the
174
+ identity and duplication invariants (D7) hold, and is the seam a future
175
+ data-β resolver (RES-14) slots into: weights are *resolved* values here.
176
+
177
+ Determinism: block order and per-source sampling are driven by `seed`.
178
+ Weight resolution is a constant today (D5); the resolver seam is the
179
+ `_resolved_weights()` method, overridable later without touching callers.
180
+ """
181
+
182
+ sources: Sequence[Op]
183
+ weights: Sequence[float]
184
+ block: int = 4096 # interleave granularity (tokens per draw)
185
+ total: int | None = None # target output budget; default = weighted mean of
186
+ # source lengths (so mixing does not inflate size)
187
+ op_type: str = field(default="mix", init=False)
188
+
189
+ def __post_init__(self):
190
+ if len(self.sources) != len(self.weights):
191
+ raise ValueError("mix: sources and weights length mismatch")
192
+ if not self.sources:
193
+ raise ValueError("mix: at least one source required")
194
+ self.inputs = tuple(self.sources)
195
+
196
+ def _resolved_weights(self) -> list[float]:
197
+ """The RES-14 seam. Today: normalise the declared constants."""
198
+ w = np.asarray(self.weights, dtype=np.float64)
199
+ if (w < 0).any():
200
+ raise ValueError("mix: weights must be non-negative")
201
+ s = w.sum()
202
+ if s <= 0:
203
+ raise ValueError("mix: weights sum to zero")
204
+ return (w / s).tolist()
205
+
206
+ def _spec(self) -> dict:
207
+ return {"weights": self._resolved_weights(), "block": self.block}
208
+
209
+ def _materialise_inputs(self, seed: int) -> list[np.ndarray]:
210
+ out = []
211
+ for c in self.sources:
212
+ parts = list(c.stream(seed=seed))
213
+ out.append(
214
+ np.concatenate(parts) if parts else np.empty(0, dtype=TOKEN_DTYPE)
215
+ )
216
+ return out
217
+
218
+ def stream(self, *, seed: int) -> Iterator[np.ndarray]:
219
+ weights = self._resolved_weights()
220
+ pools = self._materialise_inputs(seed)
221
+ n = len(pools)
222
+
223
+ # single-source identity fast-path: mix([S]) is exactly S (D7 identity)
224
+ if n == 1:
225
+ for i in range(0, len(pools[0]), _CHUNK):
226
+ yield pools[0][i : i + _CHUNK]
227
+ return
228
+
229
+ # --- signature-aware redundancy folding (the bit-vector insight) ------
230
+ # Two sources with the same content signature are the SAME data. We fold
231
+ # them by content hash and combine their weights, so a budget is spread
232
+ # over the REAL union of tokens, never over overlapping re-draws. This is
233
+ # what makes mix([S,S], budget=|S|) a multiset-equivalent of S: the
234
+ # duplicate is recognised, not re-sampled. (ADR-005 D6; RES-15 phase-1
235
+ # provenance / RES-16 content-addressed consolidation.)
236
+ sigs: dict[str, int] = {}
237
+ folded_pools: list[np.ndarray] = []
238
+ folded_w: list[float] = []
239
+ for pool, w in zip(pools, weights):
240
+ sig = _hash_obj(pool.tolist())
241
+ if sig in sigs:
242
+ folded_w[sigs[sig]] += w
243
+ else:
244
+ sigs[sig] = len(folded_pools)
245
+ folded_pools.append(pool)
246
+ folded_w.append(w)
247
+ pools, weights = folded_pools, folded_w
248
+ m = len(pools)
249
+ s = sum(weights)
250
+ weights = [w / s for w in weights]
251
+
252
+ if m == 1: # everything folded to one source → it IS that source
253
+ budget = int(self.total) if self.total is not None else len(pools[0])
254
+ budget = min(budget, len(pools[0]))
255
+ for i in range(0, budget, _CHUNK):
256
+ yield pools[0][i : min(i + _CHUNK, budget)]
257
+ return
258
+
259
+ # Output budget (D6): mixing redistributes a budget, it does not inflate
260
+ # size. Default = weighted mean of (deduplicated) source lengths.
261
+ if self.total is not None:
262
+ budget = int(self.total)
263
+ else:
264
+ budget = int(round(sum(w * len(p) for w, p in zip(weights, pools))))
265
+
266
+ # Per-source target, uniform WITHOUT replacement over that source's real
267
+ # tokens (uniform stride, not front-loaded blocks — so a source is
268
+ # covered evenly, and identical sources already folded above).
269
+ targets = [min(int(round(w * budget)), len(p)) for w, p in zip(weights, pools)]
270
+ drift = budget - sum(targets)
271
+ # push leftover onto the largest-capacity source, capped
272
+ order = sorted(range(m), key=lambda i: len(pools[i]) - targets[i], reverse=True)
273
+ k = 0
274
+ while drift > 0 and k < m:
275
+ i = order[k]
276
+ room = len(pools[i]) - targets[i]
277
+ add = min(drift, room)
278
+ targets[i] += add
279
+ drift -= add
280
+ k += 1
281
+
282
+ # deterministic interleave of evenly-strided samples from each source
283
+ picks = []
284
+ for i, t in enumerate(targets):
285
+ if t <= 0:
286
+ continue
287
+ idx = np.linspace(0, len(pools[i]) - 1, num=t, dtype=np.int64)
288
+ picks.append((i, idx))
289
+ order_rng = np.random.default_rng(seed ^ 0x9E3779B9)
290
+ cursors = {i: 0 for i, _ in picks}
291
+ remaining = {i: len(idx) for i, idx in picks}
292
+ idx_of = {i: idx for i, idx in picks}
293
+ buf: list[np.ndarray] = []
294
+ buf_len = 0
295
+ total_out = sum(remaining.values())
296
+ produced = 0
297
+ while produced < total_out and any(r > 0 for r in remaining.values()):
298
+ live = [i for i in remaining if remaining[i] > 0]
299
+ probs = np.asarray([remaining[i] for i in live], dtype=np.float64)
300
+ probs /= probs.sum()
301
+ pick = live[int(order_rng.choice(len(live), p=probs))]
302
+ take = min(self.block, remaining[pick])
303
+ sel = idx_of[pick][cursors[pick] : cursors[pick] + take]
304
+ seg = pools[pick][sel]
305
+ cursors[pick] += take
306
+ remaining[pick] -= take
307
+ produced += take
308
+ buf.append(seg)
309
+ buf_len += take
310
+ if buf_len >= _CHUNK:
311
+ yield np.concatenate(buf)
312
+ buf, buf_len = [], 0
313
+ if buf:
314
+ yield np.concatenate(buf)
315
+
316
+
317
+ # --------------------------------------------------------------------------- #
318
+ # Transforms #
319
+ # --------------------------------------------------------------------------- #
320
+ @dataclass
321
+ class Decontaminate(Op):
322
+ """First-class decontamination operator (ADR-005 D4).
323
+
324
+ Slice 1: exact n-gram membership against provided eval-set token sequences.
325
+ Emits a contamination report (`last_report`) and, in `strict` mode, raises
326
+ if any eval n-gram is found in the source. The Bloom-filter algebra
327
+ (OR/AND/XOR at scale) is a later slice; the interface here is stable.
328
+ """
329
+
330
+ source: Op
331
+ eval_ngrams: Sequence[Sequence[int]] = ()
332
+ n: int = 13
333
+ strict: bool = True
334
+ op_type: str = field(default="decontaminate", init=False)
335
+ last_report: dict = field(default_factory=dict, init=False, compare=False)
336
+
337
+ def __post_init__(self):
338
+ self.inputs = (self.source,)
339
+
340
+ def _spec(self) -> dict:
341
+ # hash the eval-set signature and policy, not the raw grams
342
+ sig = _hash_obj([list(g) for g in self.eval_ngrams])
343
+ return {"n": self.n, "strict": self.strict, "eval_sig": sig}
344
+
345
+ def _eval_set(self) -> set[bytes]:
346
+ out = set()
347
+ for g in self.eval_ngrams:
348
+ arr = np.asarray(g, dtype=TOKEN_DTYPE)
349
+ out.add(arr.tobytes())
350
+ return out
351
+
352
+ def stream(self, *, seed: int) -> Iterator[np.ndarray]:
353
+ evals = self._eval_set()
354
+ hits = 0
355
+ checked = 0
356
+ # stream through; check rolling n-grams across chunk boundaries
357
+ carry = np.empty(0, dtype=TOKEN_DTYPE)
358
+ for chunk in self.source.stream(seed=seed):
359
+ buf = np.concatenate([carry, chunk]) if len(carry) else chunk
360
+ if evals and len(buf) >= self.n:
361
+ for i in range(len(buf) - self.n + 1):
362
+ checked += 1
363
+ if buf[i : i + self.n].tobytes() in evals:
364
+ hits += 1
365
+ # keep the last n-1 tokens to catch grams spanning the boundary
366
+ carry = buf[-(self.n - 1):] if self.n > 1 else np.empty(0, dtype=TOKEN_DTYPE)
367
+ yield chunk # decontaminate is pass-through in Slice 1 (report-only)
368
+ self.last_report = {"n": self.n, "eval_ngrams": len(evals),
369
+ "checked": checked, "hits": hits}
370
+ if self.strict and hits > 0:
371
+ raise ValueError(
372
+ f"decontamination failed: {hits} eval n-gram(s) present in source "
373
+ f"(n={self.n}). This mix is not comparable until cleaned."
374
+ )
375
+
376
+
377
+ # --------------------------------------------------------------------------- #
378
+ # Manifest I/O #
379
+ # --------------------------------------------------------------------------- #
380
+ def write_manifest(root: Op, path: str | Path) -> str:
381
+ """Write the DAG manifest (the reproducible recipe) and return mix_hash."""
382
+ path = Path(path)
383
+ path.parent.mkdir(parents=True, exist_ok=True)
384
+ mh = root.node_hash()
385
+ doc = {"mix_hash": mh, "dag": root.manifest()}
386
+ path.write_text(json.dumps(doc, indent=2, sort_keys=True) + "\n", encoding="utf-8")
387
+ return mh
388
+
389
+
390
+ def mix_hash(root: Op) -> str:
391
+ """The sink hash — two runs are comparable iff equal (ADR-005 D2)."""
392
+ return root.node_hash()
393
+
394
+
395
+ # --------------------------------------------------------------------------- #
396
+ # Manifest -> DAG (build an Op tree from a JSON recipe, ADR-005 D2) #
397
+ # --------------------------------------------------------------------------- #
398
+ def from_manifest(doc: dict) -> "Op":
399
+ """Build an operator DAG from a mix manifest (the reproducible recipe).
400
+
401
+ Schema (minimal, Slice 1):
402
+ {"op": "source", "path": "...", "dataset_id": "...", "license": "...",
403
+ "slice": "...", "content_sha": "..."?}
404
+ {"op": "decontaminate", "input": <node>, "n": 13, "strict": true,
405
+ "eval_ngrams": [[...], ...]?}
406
+ {"op": "mix", "inputs": [<node>, ...], "weights": [...], "block": 4096,
407
+ "total": null?}
408
+ A bare {"dag": <node>, ...} wrapper is unwrapped.
409
+ """
410
+ if "dag" in doc and "op" not in doc:
411
+ doc = doc["dag"]
412
+ op = doc["op"]
413
+ if op == "source":
414
+ return BinSource(
415
+ path=doc["path"], dataset_id=doc["dataset_id"],
416
+ license=doc.get("license", "unspecified"),
417
+ slice=doc.get("slice", ""), content_sha=doc.get("content_sha"),
418
+ )
419
+ if op == "decontaminate":
420
+ return Decontaminate(
421
+ from_manifest(doc["input"]),
422
+ eval_ngrams=[list(g) for g in doc.get("eval_ngrams", [])],
423
+ n=int(doc.get("n", 13)), strict=bool(doc.get("strict", True)),
424
+ )
425
+ if op == "mix":
426
+ return Mix(
427
+ [from_manifest(c) for c in doc["inputs"]],
428
+ list(doc["weights"]),
429
+ block=int(doc.get("block", 4096)),
430
+ total=doc.get("total"),
431
+ )
432
+ raise ValueError(f"from_manifest: unknown op '{op}'")
433
+
434
+
435
+ def load_manifest(path: str | Path) -> tuple["Op", str]:
436
+ """Load a mix manifest file → (dag, mix_hash)."""
437
+ doc = json.loads(Path(path).read_text())
438
+ dag = from_manifest(doc)
439
+ return dag, dag.node_hash()
cortex_data/relay.py ADDED
@@ -0,0 +1,174 @@
1
+ """The checkpoint relay (2026-09-16): a store that survives the container.
2
+
3
+ Kaggle relaunches an interrupted session in a FRESH container: `/kaggle/working` is empty,
4
+ the runs directory kept outside the clone (the resume of 2026-09-13) is gone, and a run of
5
+ several hours restarts from step 0 (v7, run `7e343011489f`, relaunched at step 1740). The only
6
+ stores that outlive a container are a version's Output, written at the END of a session, and
7
+ Kaggle datasets, which the API can update from inside a running session.
8
+
9
+ This module pushes a directory (a run's checkpoint and sealed journal, or a ladder's unit
10
+ files) to a PRIVATE Kaggle dataset after every checkpoint or unit, as ONE tar file inside a
11
+ staging directory (no directory mode subtleties), and pulls the latest version back before a
12
+ resume. Everything is optional: without the two secrets (`KAGGLE_USERNAME`, `KAGGLE_KEY`) or
13
+ without the `kaggle` command, `available()` is False and the callers run as before.
14
+
15
+ relay.pull("quantum-cortex-relay-journal-n1-v7", "runs/journal-n1-v7") # before the resume lookup
16
+ relay.push_async("runs/journal-n1-v7", "quantum-cortex-relay-journal-n1-v7", "step 3000") # after a checkpoint
17
+
18
+ Pushes run in a background process and never block training; a push that is still running
19
+ when the next checkpoint lands is left to finish and the new one is skipped (the next
20
+ checkpoint will push). A dataset that does not exist yet is created on the first push
21
+ (private, CC0 metadata on a private store is irrelevant). One dataset per run or per ladder
22
+ directory, named after it, so two runs never overwrite each other.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import json
27
+ import os
28
+ import shutil
29
+ import subprocess
30
+ import sys
31
+ import tarfile
32
+ import tempfile
33
+ import time
34
+ from pathlib import Path
35
+
36
+ ARCHIVE = "relay.tar"
37
+ _inflight: dict[str, subprocess.Popen] = {}
38
+
39
+
40
+ def kaggle_cmd() -> list[str] | None:
41
+ """The kaggle command, or None when it is not installed."""
42
+ exe = shutil.which("kaggle")
43
+ if exe:
44
+ return [exe]
45
+ try: # the module without the console script on PATH
46
+ import kaggle # noqa: F401
47
+ return [sys.executable, "-m", "kaggle.cli"]
48
+ except Exception:
49
+ return None
50
+
51
+
52
+ def available() -> bool:
53
+ return bool(os.environ.get("KAGGLE_USERNAME")) and bool(os.environ.get("KAGGLE_KEY")) and kaggle_cmd() is not None
54
+
55
+
56
+ def dataset_id(slug: str) -> str:
57
+ return f"{os.environ['KAGGLE_USERNAME']}/{slug}"
58
+
59
+
60
+ def _stage(src: Path, slug: str, message: str) -> Path:
61
+ """A staging directory with the metadata and ONE tar of `src`."""
62
+ stage = Path(tempfile.mkdtemp(prefix="relay-"))
63
+ with tarfile.open(stage / ARCHIVE, "w") as tar:
64
+ tar.add(src, arcname=src.name)
65
+ (stage / "dataset-metadata.json").write_text(json.dumps({
66
+ "title": f"quantum-cortex relay: {slug}"[:50], "id": dataset_id(slug),
67
+ "licenses": [{"name": "CC0-1.0"}],
68
+ "description": f"checkpoint relay for {src.name}; last push: {message}",
69
+ }, indent=2), encoding="utf-8")
70
+ return stage
71
+
72
+
73
+ def exists(slug: str) -> bool:
74
+ cmd = kaggle_cmd()
75
+ if cmd is None:
76
+ return False
77
+ r = subprocess.run(cmd + ["datasets", "status", dataset_id(slug)], capture_output=True, text=True)
78
+ return r.returncode == 0 and "ready" in (r.stdout + r.stderr).lower()
79
+
80
+
81
+ def push_command(stage: Path, slug: str, message: str, create: bool) -> list[str]:
82
+ cmd = kaggle_cmd() or ["kaggle"]
83
+ if create:
84
+ return cmd + ["datasets", "create", "-p", str(stage), "-q"]
85
+ return cmd + ["datasets", "version", "-p", str(stage), "-m", message[:80], "-q"]
86
+
87
+
88
+ def push(src, slug: str, message: str = "") -> bool:
89
+ """Push `src` now (blocking). Returns True when the command returned 0."""
90
+ if not available():
91
+ return False
92
+ src = Path(src)
93
+ if not src.is_dir():
94
+ return False
95
+ stage = _stage(src, slug, message)
96
+ r = subprocess.run(push_command(stage, slug, message, create=not exists(slug)), capture_output=True, text=True)
97
+ shutil.rmtree(stage, ignore_errors=True)
98
+ if r.returncode != 0:
99
+ print(f"[relay] push failed ({slug}): {(r.stdout + r.stderr).strip()[-300:]}")
100
+ else:
101
+ print(f"[relay] pushed {slug} ({message}): {(r.stdout + r.stderr).strip()[-120:] or 'ok'}")
102
+ return r.returncode == 0
103
+
104
+
105
+ def push_async(src, slug: str, message: str = "") -> bool:
106
+ """Push in a background process; skipped (False) when the previous push of this slug is
107
+ still running, when the relay is not available, or when `src` is not a directory."""
108
+ if not available():
109
+ return False
110
+ src = Path(src)
111
+ if not src.is_dir():
112
+ return False
113
+ prev = _inflight.get(slug)
114
+ if prev is not None and prev.poll() is None:
115
+ return False
116
+ stage = _stage(src, slug, message)
117
+ cmd = push_command(stage, slug, message, create=not exists(slug))
118
+ _inflight[slug] = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
119
+ return True
120
+
121
+
122
+ def wait(timeout: float = 600.0) -> None:
123
+ """Wait for the in flight pushes (called at the end of a run so the last checkpoint lands)."""
124
+ t0 = time.time()
125
+ for p in list(_inflight.values()):
126
+ remaining = max(0.0, timeout - (time.time() - t0))
127
+ try:
128
+ p.wait(timeout=remaining)
129
+ except subprocess.TimeoutExpired:
130
+ pass
131
+
132
+
133
+ def pull(slug: str, dest) -> bool:
134
+ """Download the latest version of the relay dataset and unpack its tar so that `dest`
135
+ holds what was pushed. Returns True when something landed; False (and nothing touched)
136
+ when the relay is unavailable, the dataset does not exist, or the download failed."""
137
+ if not available() or not exists(slug):
138
+ return False
139
+ dest = Path(dest)
140
+ tmp = Path(tempfile.mkdtemp(prefix="relay-pull-"))
141
+ cmd = kaggle_cmd() or ["kaggle"]
142
+ r = subprocess.run(cmd + ["datasets", "download", "-d", dataset_id(slug), "-p", str(tmp), "--unzip", "--force", "-q"],
143
+ capture_output=True, text=True)
144
+ tar_path = next(iter(tmp.rglob(ARCHIVE)), None)
145
+ if r.returncode != 0 or tar_path is None:
146
+ landed = sorted(str(q.relative_to(tmp)) for q in tmp.rglob("*") if q.is_file())[:8]
147
+ shutil.rmtree(tmp, ignore_errors=True)
148
+ print(f"[relay] pull found nothing for {slug}: download rc={r.returncode}, "
149
+ f"archive {'absent' if tar_path is None else 'present'}, files landed {landed}, "
150
+ f"output {(r.stdout + r.stderr).strip()[-200:]!r}")
151
+ return False
152
+ with tarfile.open(tar_path) as tar:
153
+ members = tar.getmembers()
154
+ root = members[0].name.split("/")[0] if members else None
155
+ tar.extractall(tmp / "x", filter="data")
156
+ unpacked = tmp / "x" / root if root else None
157
+ if unpacked is None or not unpacked.exists():
158
+ shutil.rmtree(tmp, ignore_errors=True)
159
+ return False
160
+ dest.parent.mkdir(parents=True, exist_ok=True)
161
+ if dest.exists():
162
+ shutil.rmtree(dest)
163
+ shutil.move(str(unpacked), str(dest))
164
+ shutil.rmtree(tmp, ignore_errors=True)
165
+ print(f"[relay] pulled {slug} -> {dest}")
166
+ return True
167
+
168
+
169
+ def slug_for(name: str) -> str:
170
+ """A dataset slug from a run or directory name: lowercase, hyphens, under Kaggle's limits."""
171
+ s = "".join(ch if ch.isalnum() else "-" for ch in name.lower()).strip("-")
172
+ while "--" in s:
173
+ s = s.replace("--", "-")
174
+ return ("quantum-cortex-relay-" + s)[:50].rstrip("-")
@@ -0,0 +1,20 @@
1
+ """cortex_eval — evaluation harnesses for the C2 ablation protocol (ADR-006).
2
+
3
+ Slice B: MQAR (associative recall, by difficulty tiers) and the serial-position
4
+ curve (primacy/recency). Evaluation-only; validated on the control before any
5
+ ablation run. The founder's principle: never measure at a single point that may
6
+ be flat — sweep a scale and read the curve.
7
+ """
8
+ from cortex_eval.mqar import (
9
+ MQARTier, MQARResult, make_batch, standard_curriculum, accuracy, SEP,
10
+ )
11
+ from cortex_eval.serial_position import (
12
+ SerialPositionTask, SerialPositionResult, aggregate_by_position,
13
+ make_batch as make_serial_batch, PROBE,
14
+ )
15
+
16
+ __all__ = [
17
+ "MQARTier", "MQARResult", "make_batch", "standard_curriculum", "accuracy", "SEP",
18
+ "SerialPositionTask", "SerialPositionResult", "aggregate_by_position",
19
+ "make_serial_batch", "PROBE",
20
+ ]