From d710cbbd25f75ca50934f854cb0563c77d98d8a5 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Thu, 15 Oct 2026 18:16:07 +0100 Subject: [PATCH 1/4] insight: inference-summary@3 record shards; fit pinned @3 with migrating_from @1 (B5b-2) Reader: v3 index (record_shards: id, protocol_id, path, sha256, records) with shard files verified by sha256, header and count, assembled and validated as v1. Ingest keeps shard bytes under snapshots//, re-fetches only changed shards at the resolved commit, writes them only after the capture validates and removes orphans; offline re-verifies; --from reads shards beside the index. Board lists shards, not every record. Registry migrating_from keeps the committed @1 fit capture valid until the first @3 refresh. Co-Authored-By: Claude Opus 5.5 --- REFERENCE.md | 45 +++++++- insight/board.py | 116 ++++++++++++++++++- insight/cli.py | 20 +++- insight/ingest.py | 152 +++++++++++++++++++++++-- insight/registry.py | 28 ++++- insight/summary.py | 139 ++++++++++++++++++++++- registry.yaml | 5 +- tests/test_catalogue.py | 2 +- tests/test_sharded.py | 246 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 736 insertions(+), 17 deletions(-) create mode 100644 tests/test_sharded.py diff --git a/REFERENCE.md b/REFERENCE.md index 73e81e6..97060c9 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -9,11 +9,11 @@ There is no inference conductor and no new scientific execution engine. `registry.yaml` version `schema: 1` lists instances with `instance`, `repo`, `summary_path`, `supported_schema`, `dashboard_url`, `library_refs` and -`cortex_project`. Repository identity/location resolves from Mind's `repos.yaml`. +`cortex_project`, plus an optional `migrating_from` (below). Repository identity/location resolves from Mind's `repos.yaml`. A source is registered once. A fixture demonstrates generic reading without being registered as a real second adopter. Initial real producer: autolens_inference. -The reader supports `schema: inference-summary`, integer `version: 1` or `2`. +The reader supports `schema: inference-summary`, integer `version: 1`, `2` or `3`. Required envelope keys: project, scope, generated_at, evidence_updated_at, producer_revision, coverage, records, comparisons, comparison_policy, limitations. `project` equals the registry's Mind identity. Times are UTC; unknown evidence time @@ -191,6 +191,47 @@ lists remain a valid "no measurements" state. The producer and executor must validate model/prior/artifact provenance; the dashboard and assistant consume the same project-owned catalogue rather than independently reconstructing it. +## Version 3: record shards + +V3 is v1's envelope and record contract with the records held in shard files, so +a producer with many records never commits one large file and a refresh copies +only what changed. Live producer: `fit` (autofit_inference, 2026-10-15, B5b-2). +It does not add v2's setups; v1 and v2 remain valid for the instances pinned to +them. + +The summary file is an **index**: every v1 envelope key except `records`, which +must be absent, plus `record_shards`, a list of +`{id, protocol_id, path, sha256, records}`. `id` and `path` are unique; `path` is +a safe path relative to the index's directory; `sha256` is the lowercase hex +digest of the shard file's exact bytes; `records` is its record count. Each shard +is an object with `schema: inference-summary-records`, `version: 3`, the index's +`project`, `shard` (its index `id`), `protocol_id` and `records`. A record that +names a protocol (`protocol_id` or `scientific.protocol_id`) must name its +shard's. The producer chooses the split (autofit_inference: one shard per protocol +× dataset × search × settings × configuration, one record per line). + +The reader verifies every shard's digest, header and count, then assembles the +records in index order and validates the result exactly as v1, with record ids +unique across shards and comparisons resolved across them. A missing, altered or +mismatched shard invalidates the whole capture; no partial capture is shown. The +board lists one row per shard (protocol, records, execution, convergence and +acceptance counts, a link to the shard at the captured commit), never every +record; each shard file holds the full record provenance. + +A capture keeps the index in `snapshots/.json` (no inline records) and +each shard's bytes, unchanged, at `snapshots//`. A refresh reuses +every kept shard whose digest is unchanged and fetches only the others, at the same +resolved commit; it writes shards only after the whole capture validates and then +removes kept shards the new index no longer names. Offline reads re-verify the +kept shards. A local `--from` read takes shards from beside the index. + +**Pin migration.** A row moving to a new version may declare `migrating_from: +@`. Until the first capture under the new pin, the +committed capture made under that older schema remains valid last-good evidence, +labelled as awaiting its refresh; offline checks pass and say so. A fresh fetch +must return the pinned version (an older one is `unsupported`, and the old +capture is shown cached). Remove the field once the new capture is committed. + ## Ingest and output `bin/pyauto-insight fetch` resolves each project branch once and fetches the declared diff --git a/insight/board.py b/insight/board.py index 3d91588..cc610f7 100644 --- a/insight/board.py +++ b/insight/board.py @@ -6,6 +6,7 @@ import html import json import re +from collections import Counter from datetime import UTC, datetime from pathlib import Path @@ -29,6 +30,24 @@ def comparisons(s): return (s.doc or {}).get("comparisons", []) +def sharded(s): + return (s.doc or {}).get("version") == summary.SHARDED + + +def shard_rows(s): + """``(shard entry, its records)`` of a v3 capture: the assembled records are + the shards' records concatenated in index order with the declared counts.""" + recs, at = records(s), 0 + for entry in (s.doc or {}).get("record_shards", []): + yield entry, recs[at : at + entry["records"]] + at += entry["records"] + + +def _tally(recs, pick): + counts = Counter(pick(r) for r in recs) + return ", ".join(f"{k} {n}" for k, n in sorted(counts.items(), key=lambda kv: str(kv[0]))) + + def integrity(s): if s.source == "local": return f"local checkout {s.commit or 'unknown'} ({'dirty' if s.dirty else 'clean' if s.dirty is False else 'unknown state'})" @@ -75,7 +94,9 @@ def input_marker(views, data): "instance": vars(s.instance), "outcome": s.outcome, "source": s.source, - "doc": s.doc, + # A v3 index's sha256 digests pin every shard's records. + "doc": summary.index_of(s.doc), + "migrating": s.migrating, "commit": s.commit, "fetched_at": s.fetched_at, "errors": s.errors, @@ -149,6 +170,8 @@ def _detail(s, now=None): parts.append( '

Local preview, not a published capture. No receipt written.

' ) + if s.migrating: + parts.append('

' + e(s.migrating) + ".

") if d.get("version") == 2: nav, _ = setup_browser.render(s, now) warnings = [] @@ -169,6 +192,8 @@ def _detail(s, now=None): ] if not records(s): parts.append("

No measurements. An empty feed does not mean scientific success.

") + if sharded(s): + return "".join(parts + [_shard_html(s)]) + "" for archived, label in [(False, "Current results"), (True, "Historical project results")]: rows = [] for r in records(s): @@ -263,6 +288,62 @@ def _detail(s, now=None): return "".join(parts) + "" +def _shard_html(s): + """A v3 capture, one row per record shard (a protocol × leg): its counts and a + link to the shard at the captured commit. Records are not listed one by one; + each shard file holds their full provenance.""" + inst = s.instance + rows = [] + for entry, recs in shard_rows(s): + where = summary.shard_location(inst.summary_path, entry["path"]) + rows.append( + [ + e(entry["protocol_id"]), + e(entry["id"]), + e(len(recs)), + e(_tally(recs, lambda r: str(r["execution"]["status"]).split(":", 1)[0])), + e(_tally(recs, lambda r: r["scientific"]["convergence"])), + e(_tally(recs, lambda r: r["scientific"]["acceptance"])), + f'records', + ] + ) + return ( + "

Record shards

inference-summary@3: the records are held in one file per " + "protocol and leg, each verified against the index's sha256 at the captured commit. " + "Counts are the producer's assertions, shown without ranking; each shard file " + "holds every record's diagnostics, timings and provenance.

" + + table( + [ + "Protocol", + "Shard", + "Records", + "Execution", + "Convergence", + "Acceptance", + "Evidence", + ], + rows, + ) + + "

Comparisons and parity groups

" + + ( + "

No certified comparison pairs supplied by this producer.

" + if not comparisons(s) + else "".join( + "

" + + e(c["id"]) + + ": " + + e( + "refused: " + "; ".join(reasons) + if (reasons := summary.comparison_refusals(s.doc, c)) + else "producer-declared protocol " + c["protocol"] + ) + + "

" + for c in comparisons(s) + ) + ) + ) + + def _captured_at(views): """Oldest successful observation of displayed inputs; failed or missing inputs stay unknown. @@ -345,8 +426,41 @@ def render_markdown(views, now=None, campaign_data=None): ] if s.cached: out.append("Cached: original evidence and capture times retained.") + if s.migrating: + out.append(s.migrating[0].upper() + s.migrating[1:] + ".") if not s.doc: continue + if sharded(s): + out += [ + "", + "Coverage: " + _dump(s.doc["coverage"]), + *s.doc["limitations"], + "", + "| Protocol | Shard | Records | Execution | Convergence | Acceptance | Evidence |", + "|---|---|---:|---|---|---|---|", + ] + for entry, recs in shard_rows(s): + where = summary.shard_location(s.instance.summary_path, entry["path"]) + vals = [ + entry["protocol_id"], + entry["id"], + len(recs), + _tally(recs, lambda r: str(r["execution"]["status"]).split(":", 1)[0]), + _tally(recs, lambda r: r["scientific"]["convergence"]), + _tally(recs, lambda r: r["scientific"]["acceptance"]), + ] + out.append( + "| " + + " | ".join(md(v) for v in vals) + + f" | [records]({s.instance.blob_url(s.commit, where)}) |" + ) + for c in comparisons(s): + out.append( + f"Comparison {md(c['id'])}: " + + md(summary.comparison_refusals(s.doc, c) or c["protocol"]) + ) + out.append("") + continue if s.doc.get("version") == 2: for setup in s.doc["setups"]: out.append( diff --git a/insight/cli.py b/insight/cli.py index 1e09aa3..420e307 100644 --- a/insight/cli.py +++ b/insight/cli.py @@ -89,6 +89,11 @@ def cmd_fetch(args) -> int: f"{inst.instance}: {receipt.get('outcome')} at " f"{(receipt.get('resolved_commit') or 'unresolved')[:12]} " f"(receipt {ingest.receipt_path(args.out, inst.instance)})" + + ( + f"; {s.shards_fetched} record shard(s) fetched, the rest unchanged" + if s.shards_fetched is not None + else "" + ) ) if s.failed: print(f" {'; '.join(s.errors)}", file=sys.stderr) @@ -198,9 +203,13 @@ def cmd_check(args) -> int: problems.append(f"{key}: receipt does not record the snapshot commit") print(f"FAIL {key}: receipt does not record the snapshot commit") continue - # The committed snapshot must still validate under this reader. + # The committed snapshot must still validate under this reader (under the + # registry's migrating_from schema while a pin migration awaits its refresh). outcome, errors = summary_mod.classify( - s.doc, (s.instance.schema_name, s.instance.schema_version) + s.doc, + s.instance.previous_schema + if s.migrating + else (s.instance.schema_name, s.instance.schema_version), ) if outcome != "ok": problems.append(f"{key}: snapshot {outcome}: {'; '.join(errors)}") @@ -210,7 +219,14 @@ def cmd_check(args) -> int: f"ok {key}: {s.doc['schema']}@{s.doc['version']} valid, " f"{len(board.records(s))} records, {len(board.comparisons(s))} comparisons at " f"{(s.commit or 'unknown')[:12]} ({mode if s.source == 'remote' else 'local read'})" + + ( + f" in {len(s.doc['record_shards'])} shards" + if s.doc.get("version") == summary_mod.SHARDED + else "" + ) ) + if s.migrating: + print(f"note {key}: {s.migrating}") out = Path(args.out) md, page = out / "dashboard.md", out / "dashboard.html" if not md.is_file() or not page.is_file(): diff --git a/insight/ingest.py b/insight/ingest.py index dfafd29..af9d6a2 100644 --- a/insight/ingest.py +++ b/insight/ingest.py @@ -18,10 +18,22 @@ ``--from`` read reports the checkout's revision and dirty state, is labelled "local checkout", and writes no receipt or snapshot: it cannot masquerade as a published capture. + +An ``inference-summary@3`` summary is an index of record shards. Each shard is +read at the same resolved commit, verified against the index's sha256 and kept +beside the snapshot (``snapshots//``, bytes unchanged); a +refresh reuses every kept shard whose sha256 is unchanged and fetches only the +rest. Shards are written only after the whole capture validates, and shards the +new index no longer names are then removed. + +While a registry row declares ``migrating_from``, a committed capture made +under that older schema stays valid as last-good evidence, labelled as awaiting +its first refresh under the new pin; a fresh fetch must return the pinned schema. """ from __future__ import annotations +import hashlib import json import os import re @@ -113,6 +125,8 @@ class Snapshot: checkout: str | None = None source_branch: str | None = None refreshed_at: str | None = None # latest successful observation, independent of capture + migrating: str | None = None # shown capture predates the pin (registry migrating_from) + shards_fetched: int | None = None # v3 online ingest: shards fetched (rest reused) @property def failed(self) -> bool: @@ -127,6 +141,40 @@ def snapshot_path(out_dir: Path, name: str) -> Path: return Path(out_dir) / "snapshots" / f"{name}.json" +def shard_dir(out_dir: Path, name: str) -> Path: + """Where a v3 capture's shards are kept: ``snapshots//``.""" + return Path(out_dir) / "snapshots" / name + + +def _kept_shards(out_dir: Path, name: str, index: dict): + """``read(path) -> bytes`` over the kept shards of a v3 capture.""" + base = shard_dir(out_dir, name) + return lambda path: (base / path).read_bytes() + + +def _cached_classify(doc, inst): + """Classify a committed capture: under the pin, or, while the row declares + ``migrating_from``, under that older schema. Returns ``(outcome, errors, + migrating note or None)``.""" + outcome, errors = summary_mod.classify(doc, (inst.schema_name, inst.schema_version)) + previous = inst.previous_schema + if ( + outcome == "unsupported" + and previous + and isinstance(doc, dict) + and (doc.get("schema"), doc.get("version")) == previous + ): + outcome, errors = summary_mod.classify(doc, previous) + if outcome == "ok": + return ( + "ok", + [], + f"captured under {inst.migrating_from}; the registry pins " + f"{inst.supported_schema}, awaiting the first refresh under it", + ) + return outcome, errors, None + + def _read_json(path: Path) -> dict | None: try: doc = json.loads(path.read_text()) @@ -207,9 +255,16 @@ def _from_files(inst: Instance, out_dir: Path) -> Snapshot: view.outcome = "invalid" view.errors += problems if snap is not None: - outcome, errors = summary_mod.classify( - snap["summary"], (inst.schema_name, inst.schema_version) - ) + doc, errors = snap["summary"], [] + if doc.get("version") == summary_mod.SHARDED: + doc, errors = summary_mod.load_sharded( + doc, _kept_shards(out_dir, inst.instance, snap["summary"]) + ) + errors = [f"cached {e}" for e in errors] + if errors: + view.outcome, view.errors = "invalid", errors + return view + outcome, errors, migrating = _cached_classify(doc, inst) if snap.get("repo") != inst.repo or snap["summary"].get("project") != inst.repo: errors.append("cached project identity differs from registry") if snap.get("github") != inst.github or snap.get("summary_path") != inst.summary_path: @@ -221,7 +276,8 @@ def _from_files(inst: Instance, out_dir: Path) -> Snapshot: if outcome != "ok" or errors: view.outcome, view.errors = "invalid", errors return view - view.doc = snap["summary"] + view.doc = doc + view.migrating = migrating view.commit = snap.get("commit") view.fetched_at = snap.get("fetched_at") view.source = "remote" @@ -253,6 +309,7 @@ def ingest( now = now or utc_now() commit, doc, errors, outcome = None, None, [], "ok" + index, shard_bytes, fetched = None, {}, None try: commit = resolve_commit(inst.github, inst.branch) raw = fetch_at(inst.github, commit, inst.summary_path) @@ -261,13 +318,25 @@ def ingest( except ValueError as exc: outcome, errors = "invalid", [f"{inst.summary_path} is not JSON ({exc})"] else: - outcome, errors = summary_mod.classify(doc, wanted) + why = summary_mod.supported(doc, wanted) + if why is None and doc.get("version") == summary_mod.SHARDED: + index = doc + shard_bytes, fetched, errors = _fetch_shards(inst, out_dir, commit, index) + if errors: + outcome, doc = "invalid", None + else: + doc, errors = summary_mod.load_sharded(index, shard_bytes.__getitem__) + outcome = "invalid" if errors else "ok" + if outcome == "ok": + outcome, errors = summary_mod.classify(doc, wanted) if outcome == "ok" and doc.get("project") != inst.repo: outcome, errors = "invalid", ["project identity differs from registry"] except FetchError as exc: outcome, errors = "unavailable", [str(exc)] snap_file = snapshot_path(out_dir, inst.instance) + if outcome == "ok" and index is not None: + _keep_shards(out_dir, inst.instance, shard_bytes) if outcome == "ok": _carry( snap_file, @@ -278,7 +347,7 @@ def ingest( "summary_path": inst.summary_path, "commit": commit, "fetched_at": now, - "summary": doc, + "summary": summary_mod.index_of(doc), }, ) last_good = read_snapshot(out_dir, inst.instance) @@ -297,6 +366,11 @@ def ingest( "record_count": len(doc["records"]) if isinstance(doc, dict) and isinstance(doc.get("records"), list) else None, + **( + {"record_shards": len(index["record_shards"])} + if outcome == "ok" and index is not None + else {} + ), "errors": errors, "cached_from": last_good.get("fetched_at") if last_good and outcome != "ok" else None, } @@ -307,7 +381,60 @@ def ingest( receipt["fetched_at"] = prior["fetched_at"] receipt["refreshed_at"] = now if outcome == "ok" else None _carry(receipt_path(out_dir, inst.instance), receipt, keys=()) - return _from_files(inst, out_dir) + view = _from_files(inst, out_dir) + view.shards_fetched = fetched + return view + + +def _fetch_shards(inst: Instance, out_dir: Path, commit: str, index: dict): + """``({shard path: bytes}, fetched count, errors)`` for a v3 index at + ``commit``: a kept shard whose bytes match the declared sha256 is reused, + every other shard is fetched at the same commit. Raises FetchError when a + shard cannot be fetched (the capture is then unavailable).""" + errors = summary_mod.validate_shard_index(index) + if "records" in index: + errors.append("a v3 index carries its records in record_shards, never inline") + if errors: + return {}, None, errors + base = shard_dir(out_dir, inst.instance) + out, fetched = {}, 0 + for entry in index["record_shards"]: + kept = base / entry["path"] + try: + body = kept.read_bytes() + except OSError: + body = None + if body is None or hashlib.sha256(body).hexdigest() != entry["sha256"]: + body = fetch_at( + inst.github, commit, summary_mod.shard_location(inst.summary_path, entry["path"]) + ) + fetched += 1 + out[entry["path"]] = body + return out, fetched, [] + + +def _keep_shards(out_dir: Path, name: str, shards: dict[str, bytes]) -> None: + """Write a validated capture's shards (only those that changed) and remove + kept shards its index no longer names.""" + base = shard_dir(out_dir, name) + for path, body in shards.items(): + target = base / path + try: + if target.read_bytes() == body: + continue + except OSError: + pass + target.parent.mkdir(parents=True, exist_ok=True) + tmp = target.with_name(target.name + ".tmp") + tmp.write_bytes(body) + tmp.replace(target) + if base.is_dir(): + wanted = {(base / p).resolve() for p in shards} + for path in sorted(base.rglob("*"), reverse=True): + if path.is_file() and path.resolve() not in wanted: + path.unlink() + elif path.is_dir() and not any(path.iterdir()): + path.rmdir() def _git(checkout: Path, *args: str) -> str | None: @@ -342,6 +469,17 @@ def _ingest_local(inst: Instance, path: Path, wanted) -> Snapshot: view.outcome, view.errors = "invalid", [f"{file} is not JSON ({exc})"] view.doc = None return view + if ( + summary_mod.supported(view.doc, wanted) is None + and view.doc.get("version") == summary_mod.SHARDED + ): + base = file.parent + view.doc, errors = summary_mod.load_sharded( + view.doc, lambda shard: (base / shard).read_bytes() + ) + if errors: + view.outcome, view.errors, view.doc = "invalid", errors, None + return view view.outcome, view.errors = summary_mod.classify(view.doc, wanted) if view.outcome == "ok" and view.doc.get("project") != inst.repo: view.outcome, view.errors = "invalid", ["project identity differs from registry"] diff --git a/insight/registry.py b/insight/registry.py index 6bdbb1a..560fbff 100644 --- a/insight/registry.py +++ b/insight/registry.py @@ -26,7 +26,11 @@ BRANCH = "main" # the branch every ingest resolves to one commit REQUIRED = ("instance", "repo", "summary_path", "supported_schema", "dashboard_url") -FIELDS = (*REQUIRED, "library_refs", "cortex_project") +#: ``migrating_from``: the schema an instance's pin moved from. While it is set, a +#: committed last-good capture made under that older schema is still shown (as a +#: capture awaiting its first refresh under the new pin) instead of being invalid; a +#: fresh fetch must still return the pinned schema. +FIELDS = (*REQUIRED, "library_refs", "cortex_project", "migrating_from") _KEY = re.compile(r"^[a-z][a-z0-9_-]*$") _GITHUB = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$") @@ -47,6 +51,7 @@ class Instance: dashboard_url: str library_refs: tuple[str, ...] = () cortex_project: str | None = None + migrating_from: str | None = None # Resolved from the body map, never written in registry.yaml. path: str = "" github: str = "" @@ -68,6 +73,14 @@ def schema_name(self) -> str: def schema_version(self) -> int: return int(_SCHEMA.match(self.supported_schema)[2]) + @property + def previous_schema(self) -> tuple[str, int] | None: + """``(name, version)`` of ``migrating_from``, or None.""" + if not self.migrating_from: + return None + m = _SCHEMA.match(self.migrating_from) + return (m[1], int(m[2])) + def blob_url(self, commit: str | None, path: str) -> str: """The GitHub page of ``path`` at ``commit`` (the branch when unknown).""" return f"{self.github_url}/blob/{quote(commit or self.branch, safe='')}/{quote(path, safe='/')}" @@ -202,6 +215,18 @@ def validate(data, body_map: dict[str, dict] | None = None) -> list[str]: schema = row.get("supported_schema") if isinstance(schema, str) and not _SCHEMA.match(schema): problems.append(f"{where}: supported_schema must be @") + previous = row.get("migrating_from") + if previous is not None: + m, cur = ( + _SCHEMA.match(previous) if isinstance(previous, str) else None, + _SCHEMA.match(schema) if isinstance(schema, str) else None, + ) + if not m: + problems.append(f"{where}: migrating_from must be @") + elif cur and (m[1] != cur[1] or int(m[2]) >= int(cur[2])): + problems.append( + f"{where}: migrating_from must be an older version of supported_schema" + ) url = row.get("dashboard_url") if isinstance(url, str) and not _https_url(url): problems.append(f"{where}: dashboard_url must be an https:// URL") @@ -266,6 +291,7 @@ def load( dashboard_url=row["dashboard_url"], library_refs=tuple(row.get("library_refs") or ()), cortex_project=row.get("cortex_project"), + migrating_from=row.get("migrating_from"), path=str(entry.get("path") or row["repo"]), github=str(entry["github"]), ) diff --git a/insight/summary.py b/insight/summary.py index ec90976..44ba6a4 100644 --- a/insight/summary.py +++ b/insight/summary.py @@ -1,13 +1,28 @@ -"""Validate inference-summary v1/v2 without interpreting scientific quality.""" +"""Validate inference-summary v1/v2/v3 without interpreting scientific quality. + +v3 is v1's envelope and record contract with the records held in shard files: the +summary file is an index whose ``record_shards`` name each shard's path (relative to +the index's directory), sha256 and record count. :func:`load_sharded` reads and +verifies the shards and returns the assembled document (index fields plus the +concatenated ``records``), which :func:`validate` then checks exactly like v1. +""" from __future__ import annotations +import hashlib +import json import math +import posixpath import re from datetime import datetime from urllib.parse import unquote SHA = re.compile(r"^[0-9a-f]{40}$") +SHA256 = re.compile(r"^[0-9a-f]{64}$") +VERSIONS = (1, 2, 3) +#: The schema of a v3 shard file. +SHARD_SCHEMA = "inference-summary-records" +SHARDED = 3 def parse_utc(value): @@ -50,7 +65,7 @@ def supported(doc, wanted=None): if not isinstance(doc, dict): return "summary is not an object" got = doc.get("schema"), doc.get("version") - if got[0] != "inference-summary" or type(got[1]) is not int or got[1] not in (1, 2): + if got[0] != "inference-summary" or type(got[1]) is not int or got[1] not in VERSIONS: return f"unsupported schema {got!r}" if wanted is not None and got != tuple(wanted): return "summary schema differs from registry" @@ -306,9 +321,129 @@ def validate(doc): from insight import catalogue errors.extend(catalogue.validate(doc)) + if doc.get("version") == SHARDED: + errors.extend(validate_shard_index(doc)) + shards = doc.get("record_shards") + if isinstance(shards, list) and all(isinstance(x, dict) for x in shards): + declared = sum(x.get("records") for x in shards if type(x.get("records")) is int) + if declared != len(recs): + errors.append( + f"record_shards declare {declared} records but {len(recs)} were assembled" + ) + return errors + + +def validate_shard_index(doc): + """The v3 ``record_shards`` list: unique ids and paths, safe relative paths, + sha256 digests and nonnegative record counts. Index-only: no shard is read.""" + shards = doc.get("record_shards") + if not isinstance(shards, list): + return ["record_shards must be a list"] + errors, ids, paths = [], set(), set() + for i, x in enumerate(shards): + if not isinstance(x, dict): + errors.append(f"record_shards[{i}] is not an object") + continue + sid = x.get("id") + if not _text(sid): + errors.append(f"record_shards[{i}] needs id") + elif sid in ids: + errors.append(f"duplicate record shard id {sid}") + ids.add(sid) + if not _text(x.get("protocol_id")): + errors.append(f"record shard {sid}: protocol_id must be nonempty text") + path = x.get("path") + if not safe_relative_path(path): + errors.append(f"record shard {sid}: unsafe or missing path") + elif path in paths: + errors.append(f"record shard {sid}: duplicate path") + paths.add(path) + if not isinstance(x.get("sha256"), str) or not SHA256.fullmatch(x["sha256"]): + errors.append(f"record shard {sid}: sha256 must be 64 lowercase hex") + if type(x.get("records")) is not int or x["records"] < 0: + errors.append(f"record shard {sid}: records must be a nonnegative integer") return errors +def shard_location(summary_path, shard_path): + """A shard's path in the producer repository: relative to the index's directory.""" + return posixpath.join(posixpath.dirname(summary_path), shard_path) + + +def load_sharded(index, read): + """Assemble a v3 index with its shards. + + ``read(shard_path) -> bytes`` returns a shard's bytes (``shard_path`` as the + index declares it). Each shard must match its declared sha256, be a + ``inference-summary-records`` v3 object of this project and declare the + index entry's shard id, protocol id and record count; a record that names a + protocol must name the shard's. Returns ``(assembled document or None, + errors)``; the assembled document still has to pass :func:`validate`. + """ + if not isinstance(index, dict) or index.get("version") != SHARDED: + return None, ["not an inference-summary v3 index"] + if "records" in index: + return None, ["a v3 index carries its records in record_shards, never inline"] + errors = validate_shard_index(index) + if errors: + return None, errors + records = [] + for x in index["record_shards"]: + sid = x["id"] + try: + body = read(x["path"]) + except Exception as exc: # noqa: BLE001 - any read failure is reported, never raised + errors.append(f"record shard {sid}: unreadable ({exc})") + continue + if hashlib.sha256(body).hexdigest() != x["sha256"]: + errors.append(f"record shard {sid}: sha256 differs from the index") + continue + try: + shard = json.loads(body) + except ValueError as exc: + errors.append(f"record shard {sid}: not JSON ({exc})") + continue + if not isinstance(shard, dict): + errors.append(f"record shard {sid}: not an object") + continue + for key, want in ( + ("schema", SHARD_SCHEMA), + ("version", SHARDED), + ("project", index.get("project")), + ("shard", sid), + ("protocol_id", x["protocol_id"]), + ): + if shard.get(key) != want: + errors.append(f"record shard {sid}: {key} {shard.get(key)!r} != {want!r}") + recs = shard.get("records") + if not isinstance(recs, list): + errors.append(f"record shard {sid}: records must be a list") + continue + if len(recs) != x["records"]: + errors.append(f"record shard {sid}: {len(recs)} records, index declares {x['records']}") + for r in recs: + if isinstance(r, dict): + named = {r.get("protocol_id"), (r.get("scientific") or {}).get("protocol_id")} + named.discard(None) + if named and named != {x["protocol_id"]}: + errors.append( + f"record shard {sid}: record {r.get('id')} names protocol " + f"{sorted(named)}, not the shard's" + ) + records.extend(recs) + if errors: + return None, errors + return {**index, "records": records}, [] + + +def index_of(doc): + """A v3 document without its assembled records (what the producer published + as the summary file); other versions unchanged.""" + if isinstance(doc, dict) and doc.get("version") == SHARDED: + return {k: v for k, v in doc.items() if k != "records"} + return doc + + def classify(doc, wanted=None): why = supported(doc, wanted) if why: diff --git a/registry.yaml b/registry.yaml index 7e2f2c7..c130108 100644 --- a/registry.yaml +++ b/registry.yaml @@ -10,7 +10,10 @@ instances: - instance: fit repo: autofit_inference summary_path: dashboard/summary.json - supported_schema: inference-summary@1 + supported_schema: inference-summary@3 + # Until the first @3 capture: the committed @1 snapshot stays valid last-good + # evidence. Remove once snapshots/fit.json is @3 (B5b-2, autofit_inference#12). + migrating_from: inference-summary@1 dashboard_url: https://github.com/PyAutoLabs/autofit_inference#readme library_refs: [PyAutoFit] cortex_project: autofit_inference diff --git a/tests/test_catalogue.py b/tests/test_catalogue.py index 36bd018..09924b0 100644 --- a/tests/test_catalogue.py +++ b/tests/test_catalogue.py @@ -21,7 +21,7 @@ def test_baseline_cold_warm_and_resume_are_valid_but_not_automatically_accepted( assert doc["records"][0]["timings"]["initialization_s"] is None -@pytest.mark.parametrize("version", [True, 0, 3, "2", None]) +@pytest.mark.parametrize("version", [True, 0, 4, "2", None]) def test_unsupported_versions(version): doc = document() doc["version"] = version diff --git a/tests/test_sharded.py b/tests/test_sharded.py new file mode 100644 index 0000000..e9d80fd --- /dev/null +++ b/tests/test_sharded.py @@ -0,0 +1,246 @@ +"""inference-summary@3: record shards, sha256-verified, refreshed shard by shard.""" + +import copy +import hashlib +import json + +import pytest +import yaml +from conftest import LENS_ROW, SHA_A, SHA_B, fixture_doc + +from insight import board, ingest, registry, summary + +GH = "PyAutoLabs/autolens_inference" +PATH = "dashboard/summary.json" + + +def shard_text(project, sid, protocol, records): + head = { + "schema": summary.SHARD_SCHEMA, + "version": 3, + "project": project, + "shard": sid, + "protocol_id": protocol, + "records": records, + } + return json.dumps(head, sort_keys=True).encode() + + +def make_v3(records_per_shard=(1, 2), tweak=None): + """``(index, {shard path: bytes})`` from the v1 lens fixture's record.""" + v1 = fixture_doc("lens_summary_v1.json") + (base,) = v1["records"] + index = {k: v for k, v in v1.items() if k != "records"} + index["version"] = 3 + entries, files, n = [], {}, 0 + for i, count in enumerate(records_per_shard): + protocol = f"proto@{i + 1}" + recs = [] + for _ in range(count): + r = copy.deepcopy(base) + r["id"] = f"{base['id']}#{n}" + r["scientific"]["protocol_id"] = protocol + n += 1 + recs.append(r) + path = f"records/{protocol}/leg{i}.json" + body = shard_text(index["project"], f"{protocol}/leg{i}", protocol, recs) + files[path] = body + entries.append( + { + "id": f"{protocol}/leg{i}", + "protocol_id": protocol, + "path": path, + "records": count, + "sha256": hashlib.sha256(body).hexdigest(), + } + ) + index["record_shards"] = entries + if tweak: + tweak(index, files) + return index, files + + +def publish(web, sha, index, files): + web.publish(GH, sha, PATH, index) + for path, body in files.items(): + web.files[(GH, sha, f"dashboard/{path}")] = body + + +@pytest.fixture +def lens3(tmp_path, fake_mind): + path = tmp_path / "registry3.yaml" + row = dict(LENS_ROW, supported_schema="inference-summary@3") + path.write_text(yaml.safe_dump({"schema": 1, "instances": [row]})) + return registry.get(registry.load(path, mind=fake_mind), "lens") + + +def test_assembled_v3_validates_like_v1(): + index, files = make_v3() + doc, errors = summary.load_sharded(index, files.__getitem__) + assert errors == [] and len(doc["records"]) == 3 + assert summary.classify(doc, ("inference-summary", 3)) == ("ok", []) + assert "records" not in summary.index_of(doc) + # the index alone is not a summary: its records live in the shards + assert summary.classify(index, ("inference-summary", 3))[0] == "invalid" + + +@pytest.mark.parametrize( + "tweak, needle", + [ + (lambda i, f: f.update({"records/proto@1/leg0.json": b"{}"}), "sha256 differs"), + (lambda i, f: i["record_shards"][1].update(records=5), "index declares 5"), + (lambda i, f: i["record_shards"][1].update(path="../x.json"), "unsafe"), + (lambda i, f: i["record_shards"][1].update(id="proto@1/leg0"), "duplicate record shard"), + (lambda i, f: i.update(records=[]), "never inline"), + ], +) +def test_shard_problems_are_refused(tweak, needle): + index, files = make_v3(tweak=tweak) + doc, errors = summary.load_sharded(index, files.__getitem__) + assert doc is None and any(needle in e for e in errors), errors + + +def test_a_record_naming_another_protocol_is_refused(): + index, files = make_v3() + path = "records/proto@2/leg1.json" + shard = json.loads(files[path]) + shard["records"][0]["scientific"]["protocol_id"] = "proto@1" + files[path] = json.dumps(shard).encode() + index["record_shards"][1]["sha256"] = hashlib.sha256(files[path]).hexdigest() + doc, errors = summary.load_sharded(index, files.__getitem__) + assert doc is None and "not the shard's" in errors[0] + + +def test_ingest_keeps_shards_and_refetches_only_what_changed(lens3, web, tmp_path): + index, files = make_v3() + publish(web, SHA_A, index, files) + s = ingest.ingest(lens3, tmp_path, now="2026-10-02T00:00:00Z") + assert s.outcome == "ok" and len(s.doc["records"]) == 3 and s.shards_fetched == 2 + kept = ingest.shard_dir(tmp_path, "lens") + assert sorted(p.relative_to(kept).as_posix() for p in kept.rglob("*.json")) == sorted(files) + snap = ingest.read_snapshot(tmp_path, "lens") + assert "records" not in snap["summary"] and snap["summary"]["record_shards"] + receipt = ingest.read_receipt(tmp_path, "lens") + assert receipt["record_count"] == 3 and receipt["record_shards"] == 2 + + # a new commit that changes one shard and drops the other: one fetch, one removal + def change(i, f): + body = shard_text( + i["project"], + "proto@2/leg1", + "proto@2", + json.loads(f["records/proto@2/leg1.json"])["records"][:1], + ) + f["records/proto@2/leg1.json"] = body + i["record_shards"][1].update(records=1, sha256=hashlib.sha256(body).hexdigest()) + + index2, files2 = make_v3(tweak=change) + publish(web, SHA_B, index2, files2) + web.requests.clear() + s = ingest.ingest(lens3, tmp_path, now="2026-10-03T00:00:00Z") + assert s.outcome == "ok" and s.shards_fetched == 1 and len(s.doc["records"]) == 2 + raw = [r.full_url for r in web.requests if "raw.githubusercontent" in r.full_url] + assert raw[-1].endswith("/dashboard/records/proto@2/leg1.json") + + del index2["record_shards"][0] + publish(web, SHA_A, index2, {"records/proto@2/leg1.json": files2["records/proto@2/leg1.json"]}) + s = ingest.ingest(lens3, tmp_path, now="2026-10-04T00:00:00Z") + assert s.outcome == "ok" and s.shards_fetched == 0 + assert not (kept / "records/proto@1/leg0.json").exists() + assert not (kept / "records/proto@1").exists() + + offline = ingest.ingest(lens3, tmp_path, offline=True) + assert offline.outcome == "ok" and len(offline.doc["records"]) == 1 + + +def test_a_failed_shard_keeps_the_whole_last_good_capture(lens3, web, tmp_path): + index, files = make_v3() + publish(web, SHA_A, index, files) + ingest.ingest(lens3, tmp_path, now="2026-10-02T00:00:00Z") + before = {p: p.read_bytes() for p in ingest.shard_dir(tmp_path, "lens").rglob("*.json")} + bad_index, bad_files = make_v3(records_per_shard=(2, 2)) + bad_files["records/proto@2/leg1.json"] = b"tampered" + publish(web, SHA_B, bad_index, bad_files) + s = ingest.ingest(lens3, tmp_path, now="2026-10-03T00:00:00Z") + assert s.outcome == "invalid" and s.cached and s.commit == SHA_A + after = {p: p.read_bytes() for p in ingest.shard_dir(tmp_path, "lens").rglob("*.json")} + assert after == before + + +def test_a_missing_shard_is_unavailable(lens3, web, tmp_path): + index, files = make_v3() + files.pop("records/proto@2/leg1.json") + publish(web, SHA_A, index, files) + s = ingest.ingest(lens3, tmp_path, now="2026-10-02T00:00:00Z") + assert s.outcome == "unavailable" and s.doc is None + + +def test_offline_detects_a_corrupted_kept_shard(lens3, web, tmp_path): + index, files = make_v3() + publish(web, SHA_A, index, files) + ingest.ingest(lens3, tmp_path, now="2026-10-02T00:00:00Z") + (ingest.shard_dir(tmp_path, "lens") / "records/proto@1/leg0.json").write_text("{}") + s = ingest.ingest(lens3, tmp_path, offline=True) + assert s.outcome == "invalid" and "sha256" in s.errors[0] + + +def test_a_local_read_assembles_shards_beside_the_index(lens3, tmp_path): + index, files = make_v3() + checkout = tmp_path / "autolens_inference" + (checkout / "dashboard").mkdir(parents=True) + (checkout / PATH).write_text(json.dumps(index)) + for path, body in files.items(): + (checkout / "dashboard" / path).parent.mkdir(parents=True, exist_ok=True) + (checkout / "dashboard" / path).write_bytes(body) + s = ingest.ingest(lens3, tmp_path / "organ", local_path=checkout) + assert s.outcome == "ok" and s.source == "local" and len(s.doc["records"]) == 3 + + +def test_migrating_from_keeps_an_older_capture_valid_until_refreshed(tmp_path, fake_mind, web): + v1_reg = tmp_path / "r1.yaml" + v1_reg.write_text(yaml.safe_dump({"schema": 1, "instances": [dict(LENS_ROW)]})) + (old,) = registry.load(v1_reg, mind=fake_mind) + web.publish(GH, SHA_A, PATH, fixture_doc("lens_summary_v1.json")) + ingest.ingest(old, tmp_path, now="2026-10-02T00:00:00Z") + row = dict(LENS_ROW, supported_schema="inference-summary@3") + strict, moving = tmp_path / "r3.yaml", tmp_path / "r3m.yaml" + strict.write_text(yaml.safe_dump({"schema": 1, "instances": [row]})) + moving.write_text( + yaml.safe_dump( + {"schema": 1, "instances": [dict(row, migrating_from="inference-summary@1")]} + ) + ) + (pinned,) = registry.load(strict, mind=fake_mind) + assert ingest.ingest(pinned, tmp_path, offline=True).outcome == "invalid" + (migrating,) = registry.load(moving, mind=fake_mind) + s = ingest.ingest(migrating, tmp_path, offline=True) + assert s.outcome == "ok" and "awaiting the first refresh" in s.migrating + # a fresh fetch must return the pinned schema: the old capture is shown cached + s = ingest.ingest(migrating, tmp_path, now="2026-10-03T00:00:00Z") + assert s.outcome == "unsupported" and s.cached + index, files = make_v3() + publish(web, SHA_B, index, files) + s = ingest.ingest(migrating, tmp_path, now="2026-10-04T00:00:00Z") + assert s.outcome == "ok" and s.migrating is None and s.commit == SHA_B + + +@pytest.mark.parametrize( + "value", ["inference-summary@3", "inference-summary@4", "other@1", "nonsense"] +) +def test_migrating_from_must_be_an_older_version_of_the_pin(body_map, value): + row = dict(LENS_ROW, supported_schema="inference-summary@3", migrating_from=value) + problems = registry.validate({"schema": 1, "instances": [row]}, body_map) + assert any("migrating_from" in p for p in problems) + + +def test_board_lists_shards_not_every_record(lens3, web, tmp_path): + index, files = make_v3() + publish(web, SHA_A, index, files) + s = ingest.ingest(lens3, tmp_path, now="2026-10-02T00:00:00Z") + page = board.render_html([s], campaign_data={"campaigns": [], "tasks": []}) + text = board.render_markdown([s], campaign_data={"campaigns": [], "tasks": []}) + for out in (page, text): + assert "proto@1/leg0" in out and "proto@2/leg1" in out + assert "Provenance:" not in out + assert "dashboard/records/proto%402/leg1.json" in text # blob URL, quoted at the commit + assert board.render_state([s])["headline"].startswith("1 projects · 3 records") From bfc532d115a83149fb6dbbd62f4c0aa2cc927f6c Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Thu, 15 Oct 2026 18:16:07 +0100 Subject: [PATCH 2/4] =?UTF-8?q?insight:=20gaussian=5Fx3=20wave=202=20close?= =?UTF-8?q?d=20(B5b-2)=20=E2=80=94=20task=20file,=20campaign=20next;=20boa?= =?UTF-8?q?rd=20--offline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- campaigns.yaml | 27 +++++++++++++-------------- dashboard.html | 2 +- dashboard.md | 9 +++++---- tasks/gaussian_x3_search_wave2.md | 30 ++++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 19 deletions(-) diff --git a/campaigns.yaml b/campaigns.yaml index 666be96..f6a75d2 100644 --- a/campaigns.yaml +++ b/campaigns.yaml @@ -93,17 +93,16 @@ campaigns: title: PyAutoFit search benchmark on gaussian_x3 (autofit_inference) status: running updated: '2026-10-15' - next: B5b-2 — when RAL job 409930 (the 980-run probe-first remainder, Zeus JAX + NSS 50-step) finishes, pull - and judge it under gaussian_x3@2 to close wave 2, refresh the fit snapshot from main, and apply the human's - choice of form for the wave-2 summary records (autofit_inference#10). - recent_progress: Scored wave 2 pulled and judged under gaussian_x3@2 (B5b, 2026-10-15, autofit_inference#10) - — 11,770 rows admitted at the pin df6d218f10cd; all 20 per-realisation references complete; catalogue - gaussian_x3@2 section orders legs within one task and one dataset; probe-first remainder submitted as 409930. - blockers: the wave-2 summary records are withheld on size (~84 MB) pending a human choice of form - (autofit_inference#10); the probe-first cells stay open until 409930 is pulled. - job_status: unknown — live scheduler not queried; the Cortex ledger records 409930 submitted 2026-10-15 on - ral - evidence: https://github.com/PyAutoLabs/autofit_inference/issues/10 + next: After the B5b-2 PRs merge, capture the fit snapshot from main under inference-summary@3, run + pyauto-insight check online and drop the registry's migrating_from; then B5c (a second model family). + recent_progress: Wave 2 closed (B5b-2, 2026-10-15, autofit_inference#12) — RAL 409930 pulled, 12,750 rows + admitted at the pin df6d218f10cd with 0 missing; NSS 50-step leads both evidence rankings; the catalogue + ranks only legs with success rate >= 50 % over >= 20 attempts (post-hoc presentation convention); fit + records published as inference-summary@3 (101 record shards). + blockers: none recorded; the fit snapshot stays the committed @1 capture until the first @3 refresh. + job_status: unknown — live scheduler not queried; the Cortex ledger records 409930 COMPLETED (all 20 + tasks, 2026-10-15) + evidence: https://github.com/PyAutoLabs/autofit_inference/issues/12 project: autofit_inference tasks: - id: drop_1e3_prior_centring_after_631 @@ -183,6 +182,6 @@ tasks: priority: high path: tasks/gaussian_x3_search_wave2.md updated: '2026-10-15' - next: B5b judged the scored arrays under gaussian_x3@2 and submitted the probe-first remainder (409930); B5b-2 - pulls and judges it, then refreshes the fit snapshot from main. - evidence: https://github.com/PyAutoLabs/autofit_inference/issues/10 + next: Wave 2 is closed and judged (B5b-2); the fit snapshot is captured from main under inference-summary@3 + after the B5b-2 PRs merge. + evidence: https://github.com/PyAutoLabs/autofit_inference/issues/12 diff --git a/dashboard.html b/dashboard.html index f44e86d..082d5d6 100644 --- a/dashboard.html +++ b/dashboard.html @@ -369,7 +369,7 @@ - PyAutoInsight: https://github.com/PyAutoLabs/PyAutoInsight - autolens_inference: https://github.com/PyAutoLabs/autolens_inference - autofit_inference: https://github.com/PyAutoLabs/autofit_inference
Last updated 2026-10-15 16:02:00 UTC
↻ Update

-

Active campaigns

CampaignRecent progressNext steps
HST SLaM base backend parityProject and Cortex ledgers retain the 24 September rerun submissions; published rows require coverage reconciliation.Reconcile all seeds and stages with current project rows and Cortex before proposing another run.
HST SLaM Delaunay variantDelaunay is a separate target/recipe; pre-speedup runs are archived, reruns recorded on 24 September.Retrieve or account for every rerun, failed seed and missing stage; preserve the archived comparison context.
Point-source sampler admission2 October project record reports five Nautilus seeds; no gradient-sampler admission measurement.Assess a bounded gradient-sampler admission proposal with the human; no new job is authorized.
Graphical and EP inference coordinationMind campaign map and two scoping contracts preserved verbatim; implementation work remains in Mind.Reconcile current Cortex observations and shipped fixes, then select one bounded phase.
Analytic Gaussian ensemble and control calibrationCortex records accepted wave-1 baseline and 30 September independent-review caveats.Use Cortex planned diagnostic on passing, edge and worst seeds; retain the human conclusion there.
N=50 NUTS versus three EP repeatsCortex records failed wave 1 (EMFILE/OOM), wave-2 preparation and no submission.Present the existing wave-2 proposal with current dependency evidence for decision.
Hierarchical slope recovery at scaleCortex retains graphical baseline, post-processing OOM and stale EP hierarchical factor; Mind records later moments fix.Verify the moments fix release and project mirror; recover missing summaries from existing samples before a proposed rerun.
IC50 EP and graphical scale ladderCortex records N=5/10/25 results, failed N=50 EP rung and width-interpretation caveat.Check implementation close-outs, then propose a seeded ladder with appropriate output and mean-field diagnostics.
PyAutoFit search benchmark on gaussian_x3 (autofit_inference)Scored wave 2 pulled and judged under gaussian_x3@2 (B5b, 2026-10-15, autofit_inference#10) — 11,770 rows admitted at the pin df6d218f10cd; all 20 per-realisation references complete; catalogue gaussian_x3@2 section orders legs within one task and one dataset; probe-first remainder submitted as 409930.B5b-2 — when RAL job 409930 (the 980-run probe-first remainder, Zeus JAX + NSS 50-step) finishes, pull and judge it under gaussian_x3@2 to close wave 2, refresh the fit snapshot from main, and apply the human's choice of form for the wave-2 summary records (autofit_inference#10).

Active tasks

TaskCampaignStatusPriorityNext steps
Drop the 1e-3 prior centring for shear / multipole / ell_comps in the benchmarkslam-hst-baseblockednormalVerify the release containing Galaxy#634 and Lens#754 before changing priors; retain zero-centre gradient witness.
EP campaign — phase map for the 2026 Q3 graphical/EP pushgraphical-epneeds-decisionhighReconcile current Cortex records and human gates; this campaign map is never issued or bulk scheduled.
Expectation Propagation Scale-Up — Scopinggraphical-epneeds-slicinghighReconcile completed wrapper work and Pulse profiling ownership before selecting a bounded remaining inference phase.
Graphical Model Scale-Up — Scopinggraphical-epneeds-slicinghighReconcile current Cortex scale results and library dependencies; select one bounded inference phase with a witness.
NUTS/HMC trial on validated likelihoodspoint-source-admissionneeds-decisionnormalAn unscoped idea, not permission to submit: reconcile point-source admission with imaging and weak-lensing candidate targets.
gaussian_x3 search benchmark — wave 1 (the pilot)gaussian-x3-search-benchmarkblockednormalPilot stopped at 223 of 520 runs and is not restarted; freeze gaussian_x3@2 from its 17-row calibration before any scored run; the deferred cells move to scored wave 2 (after A2 and A4).
gaussian_x3 search benchmark — wave 2 (scored)gaussian-x3-search-benchmarkrunninghighB5b judged the scored arrays under gaussian_x3@2 and submitted the probe-first remainder (409930); B5b-2 pulls and judges it, then refreshes the fit snapshot from main.

Ledger

Inference Results

PyAutoFit

Integrity: ok · Freshness: freshness policy unspecified · evidence 2026-10-08T18:53:07Z · Scientific qualification: 35/223 accepted by producer; remaining assessments shown below

Project dashboard · Ingest receipt

Current results

TargetStageSamplerConfigurationSeedExecutionConvergence / acceptanceSampler clock (s)Fit call (s)DiagnosticsSamplesEvidence
gaussian_x3_blend/data_seed1/seed0single searchbfgslocal_jax_cpu_fp640failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / not_assessed13.60236716270446813.88767320000261availableunknownresult
gaussian_x3_blend/data_seed1/seed1single searchbfgslocal_jax_cpu_fp641failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / not_assessed14.8931858539581315.443632700000308availableunknownresult
gaussian_x3_blend/data_seed1/seed2single searchbfgslocal_jax_cpu_fp642failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / not_assessed12.52607297897338913.512804099998903availableunknownresult
gaussian_x3_blend/data_seed1/seed3single searchbfgslocal_jax_cpu_fp643completenot_assessed / not_assessed14.95915532112121649.19529500000135availableunknownresult
gaussian_x3_blend/data_seed1/seed4single searchbfgslocal_jax_cpu_fp644failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / not_assessed13.58659744262695314.05181679999805availableunknownresult
gaussian_x3_blend/data_seed1/seed5single searchbfgslocal_jax_cpu_fp645failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / not_assessed17.4019801616668718.110931600000185availableunknownresult
gaussian_x3_blend/data_seed1/seed6single searchbfgslocal_jax_cpu_fp646failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / not_assessed14.23751783370971714.935181099997862availableunknownresult
gaussian_x3_blend/data_seed1/seed7single searchbfgslocal_jax_cpu_fp647failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / not_assessed14.59865355491638215.270657600001869availableunknownresult
gaussian_x3_blend/data_seed1/seed8single searchbfgslocal_jax_cpu_fp648completenot_assessed / not_assessed10.69766545295715340.309002100002544availableunknownresult
gaussian_x3_blend/data_seed1/seed9single searchbfgslocal_jax_cpu_fp649failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / not_assessed11.59244132041931212.118992199997592availableunknownresult
gaussian_x3_blend/data_seed1/seed0single searchbfgslocal_numpy_fp640failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / rejected7.0074257850646977.736843099999533availableunknownresult
gaussian_x3_blend/data_seed1/seed1single searchbfgslocal_numpy_fp641failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / rejected7.5578756332397468.733155400001124availableunknownresult
gaussian_x3_blend/data_seed1/seed2single searchbfgslocal_numpy_fp642failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / rejected7.5922012329101569.3094577999982availableunknownresult
gaussian_x3_blend/data_seed1/seed3single searchbfgslocal_numpy_fp643failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / rejected8.89510560035705610.495542200002092availableunknownresult
gaussian_x3_blend/data_seed1/seed4single searchbfgslocal_numpy_fp644failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / rejected7.0470001697540287.982328100002633availableunknownresult
gaussian_x3_blend/data_seed1/seed5single searchbfgslocal_numpy_fp645failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / rejected6.6906182765960698.159760599999572availableunknownresult
gaussian_x3_blend/data_seed1/seed6single searchbfgslocal_numpy_fp646failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / rejected8.791458606719979.973385900000721availableunknownresult
gaussian_x3_blend/data_seed1/seed7single searchbfgslocal_numpy_fp647failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / rejected6.41832447052001957.853619100002106availableunknownresult
gaussian_x3_blend/data_seed1/seed8single searchbfgslocal_numpy_fp648failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / rejected8.5289349555969249.719483400000172availableunknownresult
gaussian_x3_blend/data_seed1/seed9single searchbfgslocal_numpy_fp649failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / rejected8.3923377990722669.515777500000695availableunknownresult
gaussian_x3_blend/data_seed1/seed0single searchblackjax_nutslocal_jax_cpu_fp640failed: TracerBoolConversionError: Attempted boolean conversion of traced array with shape bool[]. +

Active campaigns

CampaignRecent progressNext steps
HST SLaM base backend parityProject and Cortex ledgers retain the 24 September rerun submissions; published rows require coverage reconciliation.Reconcile all seeds and stages with current project rows and Cortex before proposing another run.
HST SLaM Delaunay variantDelaunay is a separate target/recipe; pre-speedup runs are archived, reruns recorded on 24 September.Retrieve or account for every rerun, failed seed and missing stage; preserve the archived comparison context.
Point-source sampler admission2 October project record reports five Nautilus seeds; no gradient-sampler admission measurement.Assess a bounded gradient-sampler admission proposal with the human; no new job is authorized.
Graphical and EP inference coordinationMind campaign map and two scoping contracts preserved verbatim; implementation work remains in Mind.Reconcile current Cortex observations and shipped fixes, then select one bounded phase.
Analytic Gaussian ensemble and control calibrationCortex records accepted wave-1 baseline and 30 September independent-review caveats.Use Cortex planned diagnostic on passing, edge and worst seeds; retain the human conclusion there.
N=50 NUTS versus three EP repeatsCortex records failed wave 1 (EMFILE/OOM), wave-2 preparation and no submission.Present the existing wave-2 proposal with current dependency evidence for decision.
Hierarchical slope recovery at scaleCortex retains graphical baseline, post-processing OOM and stale EP hierarchical factor; Mind records later moments fix.Verify the moments fix release and project mirror; recover missing summaries from existing samples before a proposed rerun.
IC50 EP and graphical scale ladderCortex records N=5/10/25 results, failed N=50 EP rung and width-interpretation caveat.Check implementation close-outs, then propose a seeded ladder with appropriate output and mean-field diagnostics.
PyAutoFit search benchmark on gaussian_x3 (autofit_inference)Wave 2 closed (B5b-2, 2026-10-15, autofit_inference#12) — RAL 409930 pulled, 12,750 rows admitted at the pin df6d218f10cd with 0 missing; NSS 50-step leads both evidence rankings; the catalogue ranks only legs with success rate >= 50 % over >= 20 attempts (post-hoc presentation convention); fit records published as inference-summary@3 (101 record shards).After the B5b-2 PRs merge, capture the fit snapshot from main under inference-summary@3, run pyauto-insight check online and drop the registry's migrating_from; then B5c (a second model family).

Active tasks

TaskCampaignStatusPriorityNext steps
Drop the 1e-3 prior centring for shear / multipole / ell_comps in the benchmarkslam-hst-baseblockednormalVerify the release containing Galaxy#634 and Lens#754 before changing priors; retain zero-centre gradient witness.
EP campaign — phase map for the 2026 Q3 graphical/EP pushgraphical-epneeds-decisionhighReconcile current Cortex records and human gates; this campaign map is never issued or bulk scheduled.
Expectation Propagation Scale-Up — Scopinggraphical-epneeds-slicinghighReconcile completed wrapper work and Pulse profiling ownership before selecting a bounded remaining inference phase.
Graphical Model Scale-Up — Scopinggraphical-epneeds-slicinghighReconcile current Cortex scale results and library dependencies; select one bounded inference phase with a witness.
NUTS/HMC trial on validated likelihoodspoint-source-admissionneeds-decisionnormalAn unscoped idea, not permission to submit: reconcile point-source admission with imaging and weak-lensing candidate targets.
gaussian_x3 search benchmark — wave 1 (the pilot)gaussian-x3-search-benchmarkblockednormalPilot stopped at 223 of 520 runs and is not restarted; freeze gaussian_x3@2 from its 17-row calibration before any scored run; the deferred cells move to scored wave 2 (after A2 and A4).
gaussian_x3 search benchmark — wave 2 (scored)gaussian-x3-search-benchmarkrunninghighWave 2 is closed and judged (B5b-2); the fit snapshot is captured from main under inference-summary@3 after the B5b-2 PRs merge.

Ledger

Inference Results

PyAutoFit

Integrity: ok · Freshness: freshness policy unspecified · evidence 2026-10-08T18:53:07Z · Scientific qualification: 35/223 accepted by producer; remaining assessments shown below

captured under inference-summary@1; the registry pins inference-summary@3, awaiting the first refresh under it.

Project dashboard · Ingest receipt

Current results

TargetStageSamplerConfigurationSeedExecutionConvergence / acceptanceSampler clock (s)Fit call (s)DiagnosticsSamplesEvidence
gaussian_x3_blend/data_seed1/seed0single searchbfgslocal_jax_cpu_fp640failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / not_assessed13.60236716270446813.88767320000261availableunknownresult
gaussian_x3_blend/data_seed1/seed1single searchbfgslocal_jax_cpu_fp641failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / not_assessed14.8931858539581315.443632700000308availableunknownresult
gaussian_x3_blend/data_seed1/seed2single searchbfgslocal_jax_cpu_fp642failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / not_assessed12.52607297897338913.512804099998903availableunknownresult
gaussian_x3_blend/data_seed1/seed3single searchbfgslocal_jax_cpu_fp643completenot_assessed / not_assessed14.95915532112121649.19529500000135availableunknownresult
gaussian_x3_blend/data_seed1/seed4single searchbfgslocal_jax_cpu_fp644failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / not_assessed13.58659744262695314.05181679999805availableunknownresult
gaussian_x3_blend/data_seed1/seed5single searchbfgslocal_jax_cpu_fp645failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / not_assessed17.4019801616668718.110931600000185availableunknownresult
gaussian_x3_blend/data_seed1/seed6single searchbfgslocal_jax_cpu_fp646failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / not_assessed14.23751783370971714.935181099997862availableunknownresult
gaussian_x3_blend/data_seed1/seed7single searchbfgslocal_jax_cpu_fp647failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / not_assessed14.59865355491638215.270657600001869availableunknownresult
gaussian_x3_blend/data_seed1/seed8single searchbfgslocal_jax_cpu_fp648completenot_assessed / not_assessed10.69766545295715340.309002100002544availableunknownresult
gaussian_x3_blend/data_seed1/seed9single searchbfgslocal_jax_cpu_fp649failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / not_assessed11.59244132041931212.118992199997592availableunknownresult
gaussian_x3_blend/data_seed1/seed0single searchbfgslocal_numpy_fp640failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / rejected7.0074257850646977.736843099999533availableunknownresult
gaussian_x3_blend/data_seed1/seed1single searchbfgslocal_numpy_fp641failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / rejected7.5578756332397468.733155400001124availableunknownresult
gaussian_x3_blend/data_seed1/seed2single searchbfgslocal_numpy_fp642failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / rejected7.5922012329101569.3094577999982availableunknownresult
gaussian_x3_blend/data_seed1/seed3single searchbfgslocal_numpy_fp643failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / rejected8.89510560035705610.495542200002092availableunknownresult
gaussian_x3_blend/data_seed1/seed4single searchbfgslocal_numpy_fp644failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / rejected7.0470001697540287.982328100002633availableunknownresult
gaussian_x3_blend/data_seed1/seed5single searchbfgslocal_numpy_fp645failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / rejected6.6906182765960698.159760599999572availableunknownresult
gaussian_x3_blend/data_seed1/seed6single searchbfgslocal_numpy_fp646failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / rejected8.791458606719979.973385900000721availableunknownresult
gaussian_x3_blend/data_seed1/seed7single searchbfgslocal_numpy_fp647failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / rejected6.41832447052001957.853619100002106availableunknownresult
gaussian_x3_blend/data_seed1/seed8single searchbfgslocal_numpy_fp648failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / rejected8.5289349555969249.719483400000172availableunknownresult
gaussian_x3_blend/data_seed1/seed9single searchbfgslocal_numpy_fp649failed: non-finite result (max_log_likelihood=inf, max_log_posterior=inf)not_assessed / rejected8.3923377990722669.515777500000695availableunknownresult
gaussian_x3_blend/data_seed1/seed0single searchblackjax_nutslocal_jax_cpu_fp640failed: TracerBoolConversionError: Attempted boolean conversion of traced array with shape bool[]. The error occurred while tracing the function log_l at <workspace>/PyAutoFit/autofit/non_linear/search/mcmc/blackjax/nuts/search.py:730 for jit. This concrete value was not available in Python because it depends on the value of the argument params. See https://docs.jax.dev/en/latest/errors.html#jax.errors.TracerBoolConversionErrornot_converged / not_assessedunknown42.179321799998434availableunknownresult
gaussian_x3_blend/data_seed1/seed1single searchblackjax_nutslocal_jax_cpu_fp641failed: TracerBoolConversionError: Attempted boolean conversion of traced array with shape bool[]. The error occurred while tracing the function log_l at <workspace>/PyAutoFit/autofit/non_linear/search/mcmc/blackjax/nuts/search.py:730 for jit. This concrete value was not available in Python because it depends on the value of the argument params. diff --git a/dashboard.md b/dashboard.md index e162a1d..0e03208 100644 --- a/dashboard.md +++ b/dashboard.md @@ -1,6 +1,6 @@ # PyAutoInsight — inference control room - + | Where | Count | |---|---:| @@ -10,7 +10,7 @@ | [Cached](#evidence) | 0 | | [Failed](#evidence) | 0 | - + ## Check in on all inference work Copy this into one chat. Add a campaign focus or idea before or after it, or leave it unchanged. @@ -46,7 +46,7 @@ After taking action, report what changed, what the evidence supports and what re | [N=50 NUTS versus three EP repeats](https://github.com/PyAutoLabs/PyAutoCortex/blob/d45f198804615c090e6b6694cf725ddb0be2354e/projects/ep_toy_gaussian.md) | Cortex records failed wave 1 (EMFILE/OOM), wave-2 preparation and no submission. | Present the existing wave-2 proposal with current dependency evidence for decision. | | [Hierarchical slope recovery at scale](https://github.com/PyAutoLabs/PyAutoCortex/blob/d45f198804615c090e6b6694cf725ddb0be2354e/projects/slope_hierarchy_scale.md) | Cortex retains graphical baseline, post-processing OOM and stale EP hierarchical factor; Mind records later moments fix. | Verify the moments fix release and project mirror; recover missing summaries from existing samples before a proposed rerun. | | [IC50 EP and graphical scale ladder](https://github.com/PyAutoLabs/PyAutoCortex/blob/d45f198804615c090e6b6694cf725ddb0be2354e/projects/ic50_workspace.md) | Cortex records N=5/10/25 results, failed N=50 EP rung and width-interpretation caveat. | Check implementation close-outs, then propose a seeded ladder with appropriate output and mean-field diagnostics. | -| [PyAutoFit search benchmark on gaussian_x3 (autofit_inference)](https://github.com/PyAutoLabs/autofit_inference/issues/10) | Scored wave 2 pulled and judged under gaussian_x3@2 (B5b, 2026-10-15, autofit_inference#10) — 11,770 rows admitted at the pin df6d218f10cd; all 20 per-realisation references complete; catalogue gaussian_x3@2 section orders legs within one task and one dataset; probe-first remainder submitted as 409930. | B5b-2 — when RAL job 409930 (the 980-run probe-first remainder, Zeus JAX + NSS 50-step) finishes, pull and judge it under gaussian_x3@2 to close wave 2, refresh the fit snapshot from main, and apply the human's choice of form for the wave-2 summary records (autofit_inference#10). | +| [PyAutoFit search benchmark on gaussian_x3 (autofit_inference)](https://github.com/PyAutoLabs/autofit_inference/issues/12) | Wave 2 closed (B5b-2, 2026-10-15, autofit_inference#12) — RAL 409930 pulled, 12,750 rows admitted at the pin df6d218f10cd with 0 missing; NSS 50-step leads both evidence rankings; the catalogue ranks only legs with success rate >= 50 % over >= 20 attempts (post-hoc presentation convention); fit records published as inference-summary@3 (101 record shards). | After the B5b-2 PRs merge, capture the fit snapshot from main under inference-summary@3, run pyauto-insight check online and drop the registry's migrating_from; then B5c (a second model family). | ## Active tasks @@ -58,7 +58,7 @@ After taking action, report what changed, what the evidence supports and what re | [Graphical Model Scale-Up — Scoping](https://github.com/PyAutoLabs/PyAutoInsight/blob/main/tasks/graphical_scoping.md) | graphical-ep | needs-slicing | high | Reconcile current Cortex scale results and library dependencies; select one bounded inference phase with a witness. | | [NUTS/HMC trial on validated likelihoods](https://github.com/PyAutoLabs/PyAutoInsight/blob/main/tasks/gradient_sampler_validated_likelihoods.md) | point-source-admission | needs-decision | normal | An unscoped idea, not permission to submit: reconcile point-source admission with imaging and weak-lensing candidate targets. | | [gaussian_x3 search benchmark — wave 1 (the pilot)](https://github.com/PyAutoLabs/PyAutoInsight/blob/main/tasks/gaussian_x3_search_wave1.md) | gaussian-x3-search-benchmark | blocked | normal | Pilot stopped at 223 of 520 runs and is not restarted; freeze gaussian_x3@2 from its 17-row calibration before any scored run; the deferred cells move to scored wave 2 (after A2 and A4). | -| [gaussian_x3 search benchmark — wave 2 (scored)](https://github.com/PyAutoLabs/PyAutoInsight/blob/main/tasks/gaussian_x3_search_wave2.md) | gaussian-x3-search-benchmark | running | high | B5b judged the scored arrays under gaussian_x3@2 and submitted the probe-first remainder (409930); B5b-2 pulls and judges it, then refreshes the fit snapshot from main. | +| [gaussian_x3 search benchmark — wave 2 (scored)](https://github.com/PyAutoLabs/PyAutoInsight/blob/main/tasks/gaussian_x3_search_wave2.md) | gaussian-x3-search-benchmark | running | high | Wave 2 is closed and judged (B5b-2); the fit snapshot is captured from main under inference-summary@3 after the B5b-2 PRs merge. | [Ledger](https://github.com/PyAutoLabs/PyAutoInsight/blob/main/campaigns.yaml) @@ -12133,6 +12133,7 @@ Archived rows belong to this project's September 2026 runs and are historical, n ## autofit_inference Integrity: ok. Freshness: freshness policy unspecified · evidence 2026-10-08T18:53:07Z. Qualification: 35/223 accepted by producer; remaining assessments shown below. Capture source branch: main; revision: e60dc01f9f7a3f338c60946bcf4f48a1b2c48497; fetched 2026-10-15T16:02:01Z; latest attempt 2026-10-15T16:02:01Z. +Captured under inference-summary@1; the registry pins inference-summary@3, awaiting the first refresh under it.
Full captured evidence and provenance diff --git a/tasks/gaussian_x3_search_wave2.md b/tasks/gaussian_x3_search_wave2.md index 9b9ff18..da61fef 100644 --- a/tasks/gaussian_x3_search_wave2.md +++ b/tasks/gaussian_x3_search_wave2.md @@ -87,3 +87,33 @@ When 409930 finishes: 1. Pull the remainder and judge the Zeus JAX and NSS cells. This closes the wave. 2. Refresh the `fit` snapshot from `main`. 3. Apply the human's choice of summary form for the wave-2 records. + +## Remainder judged; records published (B5b-2, 2026-10-15, autofit_inference#12) + +- **Remainder.** RAL job 409930 finished: all 20 array tasks `COMPLETED`, 16:08–17:38 + UTC. Its 980 rows (and the 20 probe rows, byte-identical) were pulled by the B5b tar + pattern. All 12,750 wave-2 rows are admitted at the pin `df6d218f10cd`, with 0 refused + and 0 missing, so every leg of the wave is closed (250 blend warm-NUTS runs stay + deferred). +- **Probe-first legs.** NSS 50-step: 158/250 on the blend and 210/250 on the separated + control; it leads both evidence rankings. Zeus JAX: 0/250 on the blend and 190/250 on + the control. The verdicts and ranks are in the `gaussian_x3@2` section of + `catalogue/search_catalogue.json`; this task only records them. +- **Ordering convention.** The catalogue ranks only legs with a success rate ≥ 50 % over + ≥ 20 attempts, and lists the rest as unreliable with their numbers. This is a post-hoc + presentation convention (protocol §8 note), not a verdict. +- **Summary records.** `fit` moves to **`inference-summary@3`**: the index carries + `record_shards`, and 101 per-protocol, per-leg record files hold the 12,973 records + (largest 1.6 MB). The ruling said `@2`, but `@2` is already the setups contract + (`lens`), so the split took the next version. The registry pins `@3` with + `migrating_from: inference-summary@1`, so the committed `@1` snapshot stays valid + until the first `@3` capture. +- **Insight snapshot.** It is not refreshed in this task. A local read of the B5b-2 + branch (`check --from fit=`) validates all 12,973 records in 101 shards. + +## Next + +1. After autofit_inference#12's PR and this repository's PR merge, the push-triggered + Dashboard Refresh (or `bin/pyauto-insight board` with a commit) captures `fit` under + `@3` from `main`. Then run `bin/pyauto-insight check` online and drop `migrating_from`. +2. B5c, a second model family. From 2a805c5add25e4367c16f0e71b78c448dd77d219 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Thu, 15 Oct 2026 18:28:44 +0100 Subject: [PATCH 3/4] review: shard prune after the index, missing-shard wording, stale migrating_from note, v3 parity disclaimer (B5b-2) Co-Authored-By: Claude Opus 5.5 --- .gitignore | 1 + REFERENCE.md | 15 ++++++++++----- insight/board.py | 2 +- insight/cli.py | 5 +++++ insight/ingest.py | 20 ++++++++++++++------ 5 files changed, 31 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index ace699e..81561f2 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ _mind/ # Half-written receipt/snapshot files (written then renamed into place) receipts/*.tmp snapshots/*.tmp +snapshots/**/*.tmp diff --git a/REFERENCE.md b/REFERENCE.md index 97060c9..44336c4 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -195,7 +195,8 @@ the same project-owned catalogue rather than independently reconstructing it. V3 is v1's envelope and record contract with the records held in shard files, so a producer with many records never commits one large file and a refresh copies -only what changed. Live producer: `fit` (autofit_inference, 2026-10-15, B5b-2). +only what changed. First producer: `fit` (autofit_inference, B5b-2; pinned 2026-10-15, +first capture pending the producer's merge). It does not add v2's setups; v1 and v2 remain valid for the instances pinned to them. @@ -212,8 +213,10 @@ shard's. The producer chooses the split (autofit_inference: one shard per protoc The reader verifies every shard's digest, header and count, then assembles the records in index order and validates the result exactly as v1, with record ids -unique across shards and comparisons resolved across them. A missing, altered or -mismatched shard invalidates the whole capture; no partial capture is shown. The +unique across shards and comparisons resolved across them. An altered or +mismatched shard makes the capture `invalid` and a shard that cannot be fetched +makes it `unavailable`; either way the whole capture is refused and the last +good one stays shown. No partial capture is shown. The board lists one row per shard (protocol, records, execution, convergence and acceptance counts, a link to the shard at the captured commit), never every record; each shard file holds the full record provenance. @@ -221,8 +224,10 @@ record; each shard file holds the full record provenance. A capture keeps the index in `snapshots/.json` (no inline records) and each shard's bytes, unchanged, at `snapshots//`. A refresh reuses every kept shard whose digest is unchanged and fetches only the others, at the same -resolved commit; it writes shards only after the whole capture validates and then -removes kept shards the new index no longer names. Offline reads re-verify the +resolved commit; it writes shards only after the whole capture validates, then +the index, then removes kept shards the new index no longer names. An interrupted +write leaves digests that no longer match (offline: `invalid`), and the next +refresh re-fetches the mismatched shards. Offline reads re-verify the kept shards. A local `--from` read takes shards from beside the index. **Pin migration.** A row moving to a new version may declare `migrating_from: diff --git a/insight/board.py b/insight/board.py index cc610f7..a8de7ab 100644 --- a/insight/board.py +++ b/insight/board.py @@ -324,7 +324,7 @@ def _shard_html(s): ], rows, ) - + "

Comparisons and parity groups

" + + "

Comparisons and parity groups

Targets, seeds and stages are retained. No best-seed selection, cross-target ranking or automatic convergence threshold. Declared groups do not themselves certify parity.

" + ( "

No certified comparison pairs supplied by this producer.

" if not comparisons(s) diff --git a/insight/cli.py b/insight/cli.py index 420e307..a29914e 100644 --- a/insight/cli.py +++ b/insight/cli.py @@ -227,6 +227,11 @@ def cmd_check(args) -> int: ) if s.migrating: print(f"note {key}: {s.migrating}") + elif s.instance.migrating_from and s.source == "remote": + print( + f"note {key}: the capture is already {s.instance.supported_schema}; " + "drop migrating_from from registry.yaml" + ) out = Path(args.out) md, page = out / "dashboard.md", out / "dashboard.html" if not md.is_file() or not page.is_file(): diff --git a/insight/ingest.py b/insight/ingest.py index af9d6a2..9db78f3 100644 --- a/insight/ingest.py +++ b/insight/ingest.py @@ -146,7 +146,7 @@ def shard_dir(out_dir: Path, name: str) -> Path: return Path(out_dir) / "snapshots" / name -def _kept_shards(out_dir: Path, name: str, index: dict): +def _kept_shards(out_dir: Path, name: str): """``read(path) -> bytes`` over the kept shards of a v3 capture.""" base = shard_dir(out_dir, name) return lambda path: (base / path).read_bytes() @@ -257,9 +257,7 @@ def _from_files(inst: Instance, out_dir: Path) -> Snapshot: if snap is not None: doc, errors = snap["summary"], [] if doc.get("version") == summary_mod.SHARDED: - doc, errors = summary_mod.load_sharded( - doc, _kept_shards(out_dir, inst.instance, snap["summary"]) - ) + doc, errors = summary_mod.load_sharded(doc, _kept_shards(out_dir, inst.instance)) errors = [f"cached {e}" for e in errors] if errors: view.outcome, view.errors = "invalid", errors @@ -335,6 +333,10 @@ def ingest( outcome, errors = "unavailable", [str(exc)] snap_file = snapshot_path(out_dir, inst.instance) + # Order: changed shards, then the index, then the pruning of shards the new index + # no longer names. An interrupted refresh leaves a capture whose digests no longer + # match (offline: invalid, never a silently mixed capture); the next refresh + # re-fetches every mismatched shard and repairs it. if outcome == "ok" and index is not None: _keep_shards(out_dir, inst.instance, shard_bytes) if outcome == "ok": @@ -350,6 +352,8 @@ def ingest( "summary": summary_mod.index_of(doc), }, ) + if outcome == "ok" and index is not None: + _prune_shards(out_dir, inst.instance, shard_bytes) last_good = read_snapshot(out_dir, inst.instance) receipt = { "instance": inst.instance, @@ -414,8 +418,7 @@ def _fetch_shards(inst: Instance, out_dir: Path, commit: str, index: dict): def _keep_shards(out_dir: Path, name: str, shards: dict[str, bytes]) -> None: - """Write a validated capture's shards (only those that changed) and remove - kept shards its index no longer names.""" + """Write a validated capture's shards (only those that changed).""" base = shard_dir(out_dir, name) for path, body in shards.items(): target = base / path @@ -428,6 +431,11 @@ def _keep_shards(out_dir: Path, name: str, shards: dict[str, bytes]) -> None: tmp = target.with_name(target.name + ".tmp") tmp.write_bytes(body) tmp.replace(target) + + +def _prune_shards(out_dir: Path, name: str, shards: dict[str, bytes]) -> None: + """Remove kept shards (and emptied folders) the current index no longer names.""" + base = shard_dir(out_dir, name) if base.is_dir(): wanted = {(base / p).resolve() for p in shards} for path in sorted(base.rglob("*"), reverse=True): From f0d76fb8fa81ae3ebdc8c2c8e5d5f625f418b96d Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Thu, 15 Oct 2026 18:38:01 +0100 Subject: [PATCH 4/4] review 2: fetch prints shard counts only for a kept capture; local-read wording (B5b-2) Co-Authored-By: Claude Opus 5.5 --- insight/cli.py | 2 +- tasks/gaussian_x3_search_wave2.md | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/insight/cli.py b/insight/cli.py index a29914e..6a679ec 100644 --- a/insight/cli.py +++ b/insight/cli.py @@ -91,7 +91,7 @@ def cmd_fetch(args) -> int: f"(receipt {ingest.receipt_path(args.out, inst.instance)})" + ( f"; {s.shards_fetched} record shard(s) fetched, the rest unchanged" - if s.shards_fetched is not None + if s.shards_fetched is not None and not s.failed else "" ) ) diff --git a/tasks/gaussian_x3_search_wave2.md b/tasks/gaussian_x3_search_wave2.md index da61fef..8caada6 100644 --- a/tasks/gaussian_x3_search_wave2.md +++ b/tasks/gaussian_x3_search_wave2.md @@ -108,8 +108,10 @@ When 409930 finishes: (`lens`), so the split took the next version. The registry pins `@3` with `migrating_from: inference-summary@1`, so the committed `@1` snapshot stays valid until the first `@3` capture. -- **Insight snapshot.** It is not refreshed in this task. A local read of the B5b-2 - branch (`check --from fit=`) validates all 12,973 records in 101 shards. +- **Insight snapshot.** It is not refreshed in this task. In a local read of the B5b-2 + branch (`check --from fit=`), the `fit` line validates all 12,973 records in + 101 shards. The overall result is still `FAIL`, because any `--from` read changes the + dashboard input marker; that is expected for a local read. ## Next