Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHECKIN.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ may have their own maintained contract. For mixed campaign maps, completed
phases remain historical and are not reissued; deferred human gates survive.
The EP map's profiling bootstrap/scoping references route to Pulse; library
implementation phases route to Mind. Neither constitutes another organ queue.
Full-fit wall-time validation (`walltime-validation@1`, instance `lens-walltime`)
is read on the Pulse board, not here.

Mind retains bounded implementation prompts, issue/PR lifecycle and repository
claims. Reuse existing issues; a broad campaign map is never issued wholesale.
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ bin/pyauto-insight check --offline
bin/pyauto-insight census
```

The first real producer is autolens_inference. A second-producer fixture tests
interoperability; it is not a claim of a second adopter.
Two live producers are registered: autolens_inference (`lens`) and
autofit_inference (`fit`). Test fixtures exercise generic reading only and are
never registered as adopters.

### Shared board presentation

Expand Down
19 changes: 15 additions & 4 deletions REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ 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`, 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.
A source is registered once. Two live producers are registered:
autolens_inference (`lens`) and autofit_inference (`fit`). Fixtures demonstrate
generic reading and are never registered as adopters.

The reader supports `schema: inference-summary`, integer `version: 1`, `2` or `3`.
Required envelope keys: project, scope, generated_at, evidence_updated_at,
Expand Down Expand Up @@ -65,8 +66,8 @@ experiments. Its measurements are test data, not scientific recommendations.

The reader accepts both versions, but a registry row still pins **one exact
version**. Deploy this reader first, then the project producer, then change the
registry pin with its consumer. The live registry remains `inference-summary@1`
in this phase. Rendering and scientific execution are unchanged. There is no
registry pin with its consumer. The live pins are `lens` at `inference-summary@2`
and `fit` at `inference-summary@3`. Rendering and scientific execution are unchanged. There is no
automatic v1 conversion and no inferred baseline, start state or acceptance.

### Identity and ownership
Expand All @@ -76,6 +77,7 @@ automatic v1 conversion and no inferred baseline, start state or acceptance.
| Setup | `id`, `label`, `dataset_family`, `model_family`, `instrument`, `reference_record_id` | Stable navigable setup, e.g. imaging/delaunay/HST; IDs are independent of script paths. Multiple exact configurations can live under the family. |
| Prepared problem | `id`, `setup_id`, `dataset_id`, `model_id`, `priors_id`, `stage`, `baseline_record_id`, `artifacts` | Exact scientific problem shared by baseline and experiments. Model and prior IDs identify frozen definitions, not display names. |
| Run record | All v1 fields plus `setup_id`, `problem_id`, `experiment_protocol`, `initialization`, `environment`, `work` | A particular measurement with its own seed, sampler, hardware, revisions, diagnostics and clocks. |
| Run record lifecycle (optional, additive) | `setup_mode`, `setup_resolved`, `host_class`, `model_variant`, `likelihood` | Provenance of how the likelihood setup was chosen; declarations only, never identity, acceptance or ranking. `setup_mode` is `explicit` (the producer named the setup) or `auto` (the library chose it: phase 5 of the likelihood-lifecycle epic); any other value is refused. `setup_resolved` records what was used or chosen: text-or-null `backend`, `device`, `operator`, `precision`, `transformer` and integer-or-null `threads`. `host_class` is text provenance, never identity. `model_variant` is text; `likelihood` names `dataset_type` and `variant`. Absent is allowed; nothing is cross-checked against Pulse or inferred. |

`instrument` and `reference_record_id` may be null with a sibling `<field>_reason`.
Prepared problem `dataset_id`, `model_id`, `priors_id`, `stage`, and
Expand Down Expand Up @@ -309,6 +311,15 @@ Baseline manifests, archived results, comparison refusals and provenance remain
available in disclosures. Unmapped records remain reachable from the project.
Without JavaScript, setup content remains accessible as ordinary disclosures.
Images, when declared, resolve at the captured producer revision.
A record that declares lifecycle fields shows one provenance line in its
disclosure, `Setup: <setup_mode> · <backend>/<device>/<operator>/<precision> ·
<host_class> · <model_variant>`, with missing parts omitted. The line filters
and ranks nothing.

Full-fit wall-time validation is not an Insight artefact. It is a PyAutoPulse
evidence axis (`walltime-validation@1`, Pulse registry instance `lens-walltime`),
produced by autolens_inference as `dashboard/walltime_validation.json`. Insight
neither hosts nor validates it; read it on the Pulse board.

`sampler_candidates.json` is a manually reviewed public handoff from literature
curation. Each entry records primary paper and official code URLs, review date,
Expand Down
23 changes: 22 additions & 1 deletion insight/setup_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,27 @@ def seconds(value):
return f"{value:,.3g} s" if value < 60 else f"{value / 60:,.3g} min ({value:,.3g} s)"


def provenance(row):
"""The record's declared setup as one line; empty when nothing is declared.

Provenance only: the line shows what the producer declared (or, under
``auto``, what the library chose) and never filters or ranks records.
"""
resolved = row.get("setup_resolved")
resolved = resolved if isinstance(resolved, dict) else {}
tuple_ = "/".join(
str(resolved[k])
for k in ("backend", "device", "operator", "precision")
if resolved.get(k) is not None
)
parts = [
str(x)
for x in (row.get("setup_mode"), tuple_, row.get("host_class"), row.get("model_variant"))
if x
]
return f'<p class="setup-provenance">Setup: {e(" · ".join(parts))}</p>' if parts else ""


def record(snapshot, row):
init = row.get("initialization", {})
env = row.get("environment", {})
Expand All @@ -90,7 +111,7 @@ def record(snapshot, row):
"archived" if row["archived"] else row["execution"]["status"],
)
)
body = metrics(
body = provenance(row) + metrics(
[
("Sampling", seconds(timing["sampling_s"])),
("Fit call", seconds(timing["total_s"])),
Expand Down
42 changes: 42 additions & 0 deletions insight/summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,47 @@ def supported(doc, wanted=None):
return None


#: ``setup_mode`` values: ``explicit`` = the producer named the setup;
#: ``auto`` = the library chose it, and ``setup_resolved`` records what it chose.
SETUP_MODES = ("auto", "explicit")
#: Text-or-null members of a record's ``setup_resolved`` object.
SETUP_RESOLVED_TEXT = ("backend", "device", "operator", "precision", "transformer")


def lifecycle_errors(r, rid):
"""Optional additive lifecycle fields, validated as declarations only.

Absent is allowed (pre-lifecycle producers). Nothing is cross-checked against
Pulse, an ``auto`` resolution is never inferred, and none of these fields
feeds acceptance, ranking or setup selection. ``host_class`` is provenance,
never identity.
"""
errors = []
if "setup_mode" in r and r["setup_mode"] not in SETUP_MODES:
errors.append(f"{rid}: setup_mode must be auto or explicit")
if "setup_resolved" in r:
resolved = r["setup_resolved"]
if not isinstance(resolved, dict):
errors.append(f"{rid}: setup_resolved must be an object")
else:
for key in SETUP_RESOLVED_TEXT:
if resolved.get(key) is not None and not _text(resolved[key]):
errors.append(f"{rid}: setup_resolved.{key} must be text or null")
threads = resolved.get("threads")
if threads is not None and (type(threads) is not int or threads < 0):
errors.append(f"{rid}: setup_resolved.threads must be integer or null")
for key in ("host_class", "model_variant"):
if r.get(key) is not None and not _text(r[key]):
errors.append(f"{rid}: {key} must be text or null")
if r.get("likelihood") is not None:
likelihood = r["likelihood"]
if not isinstance(likelihood, dict) or not all(
_text(likelihood.get(k)) for k in ("dataset_type", "variant")
):
errors.append(f"{rid}: likelihood must name dataset_type and variant as text")
return errors


def validate(doc):
if not isinstance(doc, dict):
return ["summary is not an object"]
Expand Down Expand Up @@ -284,6 +325,7 @@ def validate(doc):
or not isinstance(diagnostics.get("values"), dict)
):
errors.append(f"{rid}: diagnostics require status and values")
errors += lifecycle_errors(r, rid)
by_id = {r["id"]: r for r in recs if isinstance(r, dict) and _text(r.get("id"))}
for r in by_id.values():
parent = r.get("parent_run_id")
Expand Down
15 changes: 15 additions & 0 deletions tests/fixtures/lens_summary_v2.json
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,21 @@
"retained_samples": "Stored posterior samples after warmup",
"effective_sample_size": "Sampler-reported posterior ESS"
}
},
"setup_mode": "auto",
"setup_resolved": {
"backend": "jax",
"device": "cpu",
"operator": "dense",
"precision": "fp64",
"transformer": "na",
"threads": null
},
"host_class": "local_laptop",
"model_variant": "default",
"likelihood": {
"dataset_type": "imaging",
"variant": "delaunay"
}
},
{
Expand Down
39 changes: 39 additions & 0 deletions tests/test_catalogue.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,3 +271,42 @@ def test_v2_empty_and_unmapped_legacy_records_are_valid():
assert summary.classify(doc) == ("ok", [])
doc["records"] = []
assert summary.classify(doc) == ("ok", [])


def test_lifecycle_setup_fields_are_accepted_as_declarations():
doc = document()
auto = next(r for r in doc["records"] if r["id"] == "experiment-cold")
assert auto["setup_mode"] == "auto" and auto["setup_resolved"]["backend"] == "jax"
assert summary.classify(doc) == ("ok", [])
# Absent fields stay valid: a pre-lifecycle record declares none of them.
assert "setup_mode" not in doc["records"][0]
# Declarations never become acceptance.
assert summary.qualified_records(doc) == 0


@pytest.mark.parametrize("mode", ["automatic", "", None, 1])
def test_setup_mode_outside_auto_and_explicit_is_refused_by_name(mode):
doc = document()
doc["records"][2]["setup_mode"] = mode
status, errors = summary.classify(doc)
assert status == "invalid"
assert "experiment-cold: setup_mode must be auto or explicit" in errors


@pytest.mark.parametrize(
"key, value, needle",
[
("setup_resolved", "jax/cpu", "setup_resolved must be an object"),
("setup_resolved", {"backend": 3}, "setup_resolved.backend must be text or null"),
("setup_resolved", {"threads": "8"}, "setup_resolved.threads must be integer or null"),
("setup_resolved", {"threads": True}, "setup_resolved.threads must be integer or null"),
("host_class", "", "host_class must be text or null"),
("model_variant", 7, "model_variant must be text or null"),
("likelihood", {"dataset_type": "imaging"}, "likelihood must name dataset_type"),
],
)
def test_malformed_lifecycle_fields_are_refused(key, value, needle):
doc = document()
doc["records"][2][key] = value
status, errors = summary.classify(doc)
assert status == "invalid" and any(needle in e for e in errors), errors
15 changes: 15 additions & 0 deletions tests/test_setup_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,18 @@ def test_home_keeps_actionable_warnings_outside_collapsed_provenance(registry_fi
assert "past its declared freshness" in prefix
assert "Integrity:" not in prefix
assert '<h2 id="lens">' not in detail


def test_record_disclosure_shows_declared_setup_as_provenance(registry_file, fake_mind):
snapshot = view(registry_file, fake_mind, "lens_summary_v2.json")
_, pages = setup_browser.render(snapshot)
assert "Setup: auto · jax/cpu/dense/fp64 · local_laptop · default" in pages
# A record without lifecycle fields renders no provenance line.
assert setup_browser.provenance(snapshot.doc["records"][0]) == ""


def test_provenance_line_omits_missing_parts():
row = {"setup_mode": "explicit", "setup_resolved": {"backend": "numba", "device": None}}
assert setup_browser.provenance(row) == (
'<p class="setup-provenance">Setup: explicit · numba</p>'
)
36 changes: 36 additions & 0 deletions tests/test_sharded.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,42 @@ def test_assembled_v3_validates_like_v1():
assert summary.classify(index, ("inference-summary", 3))[0] == "invalid"


def test_v3_shard_records_accept_lifecycle_fields_and_refuse_a_bad_mode():
def declare(mode):
def tweak(index, files):
path = "records/proto@1/leg0.json"
shard = json.loads(files[path])
shard["records"][0].update(
setup_mode=mode,
setup_resolved={
"backend": "jax",
"device": "gpu",
"operator": "dense",
"precision": "fp64",
"transformer": "na",
"threads": None,
},
host_class="hpc_a100",
model_variant="light_fixed",
likelihood={"dataset_type": "imaging", "variant": "rectangular"},
)
body = json.dumps(shard, sort_keys=True).encode()
files[path] = body
index["record_shards"][0]["sha256"] = hashlib.sha256(body).hexdigest()

return tweak

index, files = make_v3(tweak=declare("auto"))
doc, errors = summary.load_sharded(index, files.__getitem__)
assert errors == []
assert summary.classify(doc, ("inference-summary", 3)) == ("ok", [])
index, files = make_v3(tweak=declare("chosen"))
doc, errors = summary.load_sharded(index, files.__getitem__)
status, errors = summary.classify(doc, ("inference-summary", 3))
assert status == "invalid"
assert f"{doc['records'][0]['id']}: setup_mode must be auto or explicit" in errors


@pytest.mark.parametrize(
"tweak, needle",
[
Expand Down
Loading