diff --git a/src/openultrasast/learn/examples.py b/src/openultrasast/learn/examples.py index f9378d0..a97a819 100644 --- a/src/openultrasast/learn/examples.py +++ b/src/openultrasast/learn/examples.py @@ -208,7 +208,7 @@ def build_examples( "frameworks": list(label["frameworks"]) if label.get("frameworks") is not None else None, "unit": unit, "direction": label.get("direction"), "pin_role": label.get("pin_role", "vulnerable"), "weight": float(label.get("weight", 1.0)), "license": (license_of(label) if license_of else "") or "", - "x": dict(record["x"]), "instruments": dict(record["instruments"]), + "x": dict(record["x"]), "instruments": {k: dict(v) for k, v in record["instruments"].items() if v is not None}, "roles": list(roles_of(label)) if roles_of else [], "excerpt_sha": shown.sha, "label_set_digest": digest, } ) # fmt: skip @@ -238,12 +238,14 @@ class Example: license: str = "" @classmethod + # An instrument that is not part of the row's profile may be stored as null (feature records carry every + # instrument name); it is not a table and is skipped, both when a row is written and when it is read. def from_row(cls, row: Mapping[str, Any]) -> Example: return cls( id=str(row["id"]), group=str(row.get("group") or ""), source=str(row["source"]), frameworks=tuple(row.get("frameworks") or ()), family=str(row["family"]), label=int(row["label"]), language=str(row.get("language") or "other"), profile=str(row.get("profile") or "static"), unit=str(row.get("unit") or "pin"), x=dict(row["x"]), - instruments={k: dict(v) for k, v in row["instruments"].items()}, roles=tuple(row.get("roles") or ()), + instruments={k: dict(v) for k, v in row["instruments"].items() if v is not None}, roles=tuple(row.get("roles") or ()), excerpt_sha=str(row["excerpt_sha"]), license=str(row.get("license") or ""), ) # fmt: skip diff --git a/tests/test_learn_examples.py b/tests/test_learn_examples.py index f3495dd..49f3e00 100644 --- a/tests/test_learn_examples.py +++ b/tests/test_learn_examples.py @@ -212,3 +212,20 @@ def test_side_reader_reads_every_pair_kind_source(tmp_path: Path, monkeypatch: p lab = label(source="pairs", source_ref=name, pin="") assert reader(lab, "vulnerable") == ["def handler(request):", " return 1"] assert reader.license(lab) == "MIT" + + +def test_null_instruments_are_skipped_when_reading_and_writing() -> None: + """Static-profile feature records carry the plane-only instruments as null; an example row must load (the live + store had 937 unreadable rows on 2026-10-02) and never write such a null itself.""" + from openultrasast.learn.examples import Example + + row = { + "id": "x", "kind": "example", "repo": "o/r", "pin": "p", "run": "learn-memory", "task": "build", "population": "pairs", + "split": "train", "image": "host", "candidate": "a.py::f", "family": "injection", "language": "python", + "profile": "static", "label": 1, "source": "pairs", "group": "o/r", "frameworks": None, "unit": "pin", + "direction": None, "pin_role": "vulnerable", "weight": 1.0, "license": "MIT", "x": {}, + "instruments": {"language": {"state": "ran"}, "verify": None, "agree": None, "model_sinks": None}, + "roles": [], "excerpt_sha": "0" * 64, "label_set_digest": "1" * 64, + } # fmt: skip + example = Example.from_row(row) + assert set(example.instruments) == {"language"}