Files
ss-tools/scripts/migrate_catalog_native_result.py
busya e0b6bec875 fix(fixtures): align the baseline graph and pin with the current contracts
Follow-up to a5969af3, which gave the catalog entry the native_result the
contract requires.  Two consequences had to be carried through.

1. The declared pin now carries that native_result.  resolve_baseline_pin returns
   it for the metric entry, so the fixture's own baseline_pin - which the resolver
   oracle in test_baseline_resolver.py is built from - must declare the same block;
   otherwise the oracle asserted a pin the resolver could never produce.  The
   migration script syncs it from the entry revision rather than duplicating it.

2. _GRAPH_WITH_REF no longer carries an unbound compare_to_baseline step.  A graph
   holds its baseline references declaratively in `baselines`, and that is what
   graph_has_baseline_refs reads (baseline_resolver.py:60-63) and what needs_pin
   consults - the step is not the reference.  A v1 graph may not carry that step at
   all: runner_plan.py:144-150 refuses the shape as UNBOUND_BASELINE_GRAPH before
   the resolver runs, which is why two start_run tests in this file read as a
   regex mismatch rather than as the refusal they target.  The step becomes a
   literal assertion, so the derived plan stays non-empty (an empty plan would
   mark a walked run passed without executing anything) while the resolver still
   sees a graph with baseline refs and no explicit selector.  The test's own
   assertion graph_has_baseline_refs(...) is True keeps holding.

Measured with the Molecular CoT digest: the NATIVE_RESULT_CATALOG_INVALID EXPLORE
records that dominated this suite are gone, and nothing replaced them.

execution 3 failed / 411 passed -> 414 passed; registry 12 failed / 968 passed;
scenario 414 passed.  No production code touched.
2026-10-09 21:30:18 +03:00

232 lines
12 KiB
Python

# #region Tooling.CatalogNativeResultFixtureMigration [C:3] [TYPE Module] [SEMANTICS test,fixture,catalog,native,result,migration]
# @BRIEF Bring the published-catalog fixture's table entry up to the runtime DTO + native_result contract.
# @INVARIANT Only fields the contract requires are added and only contract-forbidden shapes are removed;
# every value the entry already carried is preserved or re-derived by its production owner.
# @INVARIANT The fixture's outer identity tokens (entry_digest, coordinate_hash, catalog_digest,
# baseline_family) are placeholders compared by equality, so they are proven unchanged
# rather than recomputed.
# @RATIONALE require_entry_native_result refuses a table entry without native_result
# (native_result_catalog.py:30-32) and require_runtime_value_policy refuses the canonical
# YAML shape (native_result_catalog.py:83-87), which together produced every
# NATIVE_RESULT_CATALOG_INVALID in the execution and registry runs.
# @REJECTED Adding only a native_result block was rejected: the entry also carried an outlawed
# `policy` key, no `comparison_policy`, expected.canonical instead of canonical_value,
# non-string canonical cells and provenance fields the contract does not declare.
"""Bring the table entry in the published-catalog fixture up to the runtime DTO contract.
Why: require_entry_native_result refuses an entry whose expected.kind is 'table' but which carries
no native_result block (native_result_catalog.py:30-32). That refusal is the NATIVE_RESULT_CATALOG_INVALID
seen in the execution and registry runs. Adding the block alone is not enough: the entry is in the
canonical YAML shape, and require_runtime_value_policy (native_result_catalog.py:83-87) refuses an
entry that has 'policy', lacks 'comparison_policy', lacks expected.canonical_value, or still carries
expected.canonical. BaselineEntry also requires label, dataset_id and updated_at, and forbids extras.
Every derived value comes from the production helpers that define it, following the existing owner
tests/services/dashboard_testing/execution/native_pin_literal_fixture.py. The fixture's outer identity
tokens (entry_digest, coordinate_hash, catalog_digest, baseline_family) are placeholders compared by
equality, not recomputed from the entry, so they are deliberately left alone - the round-trip check
below proves that claim rather than assuming it.
Run with --write to apply; without it the script only reports.
"""
from __future__ import annotations
import hashlib
import json
from pathlib import Path
import sys
BACKEND = Path(__file__).resolve().parents[0] / ".." / "backend"
sys.path.insert(0, str(BACKEND))
from src.schemas.dashboard_testing import NormalizedValue # noqa: E402
from src.schemas.dashboard_testing.catalog import BaselineEntry # noqa: E402
from src.services.dashboard_testing.native_reference_policy import native_policy_snapshot # noqa: E402
from src.services.dashboard_testing.native_result_catalog import ( # noqa: E402
require_entry_native_result,
require_native_revision,
)
from src.services.dashboard_testing.result_values import ( # noqa: E402
canonical_json,
descriptor_sha256,
require_result_descriptor,
result_identity_sha256,
validate_result_value,
)
PATH = Path(__file__).resolve().parents[0] / ".." / "specs" / "044-dashboard-scenario-execution" / "fixtures" / "production-contract-refresh.json"
TABLE_INDEX = 1
def sync_pin_native_result(data: dict) -> int:
"""The pin declares what resolution returns; the metric entry now carries a native_result."""
by_baseline = {wrapper["baseline_id"]: wrapper["entry"]
for wrapper in data["catalog_revision"]["entry_revisions"]}
synced = 0
for pin_entry in data["baseline_pin"].get("entries", []):
entry = by_baseline.get(pin_entry.get("baseline_id"))
block = (entry or {}).get("native_result")
if block is not None and pin_entry.get("native_result") != block:
pin_entry["native_result"] = json.loads(json.dumps(block))
synced += 1
return synced
def canonical_holders(data: dict, entry: dict):
"""Every canonical table the fixture repeats: the entry's own and both comparison sides."""
yield entry["expected"]["canonical"]
for comparison in data.get("result", {}).get("comparisons", []):
for side in ("actual", "expected"):
holder = comparison.get(side, {}).get("canonical")
if isinstance(holder, dict):
yield holder
def build_descriptor(entry: dict, dataset_id: int, run_id: str, key_columns: list[str]):
"""Descriptor derived from the entry's own canonical table, not invented.
`selected_metric` must appear among the descriptor's column names
(result_descriptor.py:77), and the fixture's canonical data is the only place that named the
metric column `revenue` while the entry's result_key, the evaluation criterion and the
comparisons all call it `revenue_by_region`; the caller renames it before this runs.
"""
canonical = entry["expected"]["canonical"]
columns = canonical["columns"]
codes = {name: (1 if name in key_columns else 0) for name in columns}
request_sha = hashlib.sha256(canonical_json({"entry": entry["baseline_id"]})).hexdigest()
return require_result_descriptor({
"schema_version": 1,
"canonicalization_version": 1,
"kind": "table",
"scope": "native_query_result",
"chart_id": entry["chart_id"],
"dataset_id": dataset_id,
"selected_metric": entry["result_key"],
"query_semantics_sha256": "c" * 64,
"columns": [{"name": name, "generic_type_code": codes[name], "nullable": False} for name in columns],
"key_columns": list(key_columns),
"native_request": {"content_ref": f"draft:{run_id}:{request_sha}", "sha256": request_sha},
})
def main() -> int:
data = json.loads(PATH.read_text(encoding="utf-8"))
revision = data["catalog_revision"]
wrapper = revision["entry_revisions"][TABLE_INDEX]
entry = wrapper["entry"]
already = "native_result" in entry and "canonical" not in entry.get("expected", {})
if already:
# Idempotent second half: the declared pin must return what the resolver now produces from
# this entry, which includes the native_result block the pin oracle did not carry before.
synced = sync_pin_native_result(data)
print(f"entry already migrated; pin entries synced: {synced}")
if "--write" in sys.argv:
PATH.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
print(f"written: {PATH}")
return 0
entry.setdefault("dataset_id", 4)
canonical = entry["expected"]["canonical"]
report: dict[str, object] = {
"baseline_id": entry["baseline_id"],
"chart_id": entry["chart_id"],
"dataset_id": entry["dataset_id"],
"result_key": entry["result_key"],
"columns": canonical["columns"],
"rows": len(canonical["rows"]),
"had_policy_key": "policy" in entry,
"had_canonical_key": "canonical" in entry["expected"],
"outer_entry_digest": wrapper["entry_digest"],
"outer_catalog_digest": revision["catalog_digest"],
}
run_id = entry["provenance"].get("source_artifact_id") or wrapper["capture_artifact_id"]
key_columns = list(entry.get("policy", {}).get("key_columns", []))
# The metric column is the one name the fixture got wrong: result_key, the evaluation criterion
# and both comparison sides call it revenue_by_region, only the canonical data says "revenue".
# A descriptor requires selected_metric to be a column, so the canonical data follows its own
# declared metric everywhere it repeats the column list.
renamed = 0
for holder in canonical_holders(data, entry):
if holder.get("columns") and entry["result_key"] not in holder["columns"]:
holder["columns"] = [entry["result_key"] if name == "revenue" else name
for name in holder["columns"]]
renamed += 1
# Canonical table cells are strings by contract: require_cell refuses a non-str cell
# (result_values.py:71-72), and numeric columns must be decimal notation.
holder["rows"] = [[None if cell is None else str(cell) for cell in row]
for row in holder.get("rows", [])]
report["canonical_column_lists_renamed"] = renamed
descriptor = build_descriptor(entry, entry["dataset_id"], run_id, key_columns)
value = {"kind": "table", "canonical_value": json.dumps(canonical, separators=(",", ":"))}
response_sha = entry["source_response_hash"]
record = {
"schema_version": 1,
"descriptor": descriptor.model_dump(mode="json"),
"descriptor_sha256": descriptor_sha256(descriptor),
"result_identity_sha256": result_identity_sha256(descriptor),
"capture_artifact_id": wrapper["capture_artifact_id"],
"capture_content_ref": f"draft:{run_id}:{response_sha}",
"source_response_hash": response_sha,
"canonical_value_digest": validate_result_value(descriptor, NormalizedValue.model_validate(value)),
"context_sha256": "b" * 64,
}
policy = native_policy_snapshot(descriptor)
block = {
"schema_version": 1,
"authority_record": record,
"authority_record_sha256": hashlib.sha256(canonical_json(record)).hexdigest(),
"policy_snapshot": policy,
"policy_sha256": hashlib.sha256(canonical_json(policy)).hexdigest(),
}
# Rewrite the entry into the runtime DTO shape the catalog contract declares.
entry.pop("policy", None)
entry["expected"] = value
entry["comparison_policy"] = policy["comparison_policy"]
entry["native_result"] = block
entry.setdefault("label", "Revenue by region")
entry.setdefault("updated_at", entry["created_at"])
# Provenance allows exactly environment/actor/agent_run_id; the captured capture-binding fields
# the fixture carried there are not part of that contract, and the block now owns the capture
# identity (capture_artifact_id, capture_content_ref, source_response_hash).
dropped = sorted(set(entry["provenance"]) - {"environment", "actor", "agent_run_id"})
entry["provenance"] = {k: v for k, v in entry["provenance"].items()
if k in {"environment", "actor", "agent_run_id"}}
report["provenance_fields_dropped"] = dropped
# Proof, not diff review: the same validators the production path runs.
require_entry_native_result(entry)
BaselineEntry.model_validate_json(canonical_json(entry), strict=True)
require_native_revision(revision)
report["require_entry_native_result"] = "PASS"
report["baseline_entry_strict"] = "PASS"
report["require_native_revision"] = "PASS"
# Prove the outer placeholder identities were not silently recomputed.
report["entry_digest_unchanged"] = wrapper["entry_digest"] == report["outer_entry_digest"]
report["catalog_digest_unchanged"] = revision["catalog_digest"] == report["outer_catalog_digest"]
report["pin_unchanged"] = data["baseline_pin"]["catalog_digest"] == report["outer_catalog_digest"]
for key, value in report.items():
print(f"{key}: {value}")
must_hold = ("entry_digest_unchanged", "catalog_digest_unchanged", "pin_unchanged")
failed = [k for k in must_hold if report[k] is not True]
if failed:
raise SystemExit(f"verification failed: {failed}")
if "--write" in sys.argv:
PATH.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
print(f"written: {PATH}")
else:
print("dry run (pass --write to apply)")
return 0
if __name__ == "__main__":
raise SystemExit(main())
# #endregion Tooling.CatalogNativeResultFixtureMigration