From 96fe2e0bc3334f842b4f5005474910183510d2c1 Mon Sep 17 00:00:00 2001 From: stroblme Date: Wed, 16 Sep 2026 16:43:52 +0200 Subject: [PATCH] Leave a dotted key out of the runs export header _leaves named a column for a key like `-1.0` that _dig can never read back, so the export showed an always-empty column. Skip such a key. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01QgG6TJw5qBZzaLghQSRVrq --- backend/fluksio/api/routes/runs.py | 6 ++++++ backend/tests/api/routes/test_runs.py | 5 +++++ docs/code/api.md | 3 ++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/backend/fluksio/api/routes/runs.py b/backend/fluksio/api/routes/runs.py index e0ef98e..03ebe12 100644 --- a/backend/fluksio/api/routes/runs.py +++ b/backend/fluksio/api/routes/runs.py @@ -441,9 +441,15 @@ def _leaves(value: Any, prefix: str = "") -> Iterator[tuple[str, Any]]: the numbers arrive inside `final_metrics` — and a record in one cell is not a column anybody can compare. Lists are left whole: a curve belongs in the long table, not in a cell of this one. + + A key with a dot in its own name — a frequency written `-1.0` — is left + out, and whatever sits under it with it: `_dig` cannot read such a path + back, so naming it would promise a column that is always empty. """ if isinstance(value, dict): for key, inner in value.items(): + if "." in str(key): + continue yield from _leaves(inner, f"{prefix}.{key}" if prefix else str(key)) else: yield prefix, value diff --git a/backend/tests/api/routes/test_runs.py b/backend/tests/api/routes/test_runs.py index 8178f7f..4f2f6e8 100644 --- a/backend/tests/api/routes/test_runs.py +++ b/backend/tests/api/routes/test_runs.py @@ -1121,6 +1121,9 @@ def exported(): "note": "n/a", "final_metrics": {"train_loss": acc * 2}, "test_metrics": {"known": {"perfect": 1.0}}, + # Keyed by a number written with its decimal point, + # which no dotted path can address. + "spectrum": {"-1.0": acc, "0.25": {"mean": acc}}, }, created_at=made + timedelta(seconds=index), ) @@ -1199,6 +1202,8 @@ def test_an_exported_run_row_carries_every_recorded_input( assert rows[0]["metric.final_metrics.train_loss"] == 0.5 assert rows[0]["metric.test_metrics.known.perfect"] == 1.0 assert "metric.note" not in rows[0] + # A key holding the separator cannot be read back, so it is not a column. + assert not any(key.startswith("metric.spectrum") for key in rows[0]) named = _lines(export(format="jsonl", metrics="test_metrics.known.perfect")) assert list(named[0])[-1] == "metric.test_metrics.known.perfect" diff --git a/docs/code/api.md b/docs/code/api.md index cecbb54..c2fbac9 100644 --- a/docs/code/api.md +++ b/docs/code/api.md @@ -134,7 +134,8 @@ the inputs that *vary* across the selection in `param.` columns (the sweep axis) unless `?params=` names them, and the run's numbers in `metric.` columns. Both are dotted paths into whatever a node returned, so `?metrics=final_metrics.train_loss` selects a field of a record and the -defaults reach every number inside one. The run id is on every row of both, which is what makes an exported +defaults reach every number inside one. A key with a dot in its own name, such +as `-1.0`, cannot be addressed that way and gets no column. The run id is on every row of both, which is what makes an exported file a join back to the run rather than a loose table. `compare` answers in the same shape a chart widget draws, so three training