fix(vision): NaN/inf/bool di uno strumento non diventano più una quota (#16)
runner.run_graph esclude bool e NaN/inf dagli output numerici, con il motivo in RunOutcome.invalid_outputs. vision_service tratta una quota mappata con valore None/assente/non finito come "non prodotta" (422), invece di arrivare a float(None) e sollevare TypeError. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+19
-2
@@ -6,6 +6,7 @@ database, no filesystem beyond reading the pinned submodule's commit.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
@@ -34,6 +35,12 @@ class RunOutcome:
|
||||
failures: list[ToolFailure] = field(default_factory=list)
|
||||
engine_version: str = ""
|
||||
duration_ms: int = 0
|
||||
# A tool that "succeeded" but handed back NaN/inf (vs-measure emits NaN on
|
||||
# an empty slice - see caliper.py:112) or a bool (a subclass of int, so
|
||||
# `isinstance(x, (int, float))` alone would let it through as a quote).
|
||||
# Kept apart from `failures`: the tool did not raise, it just produced a
|
||||
# number the caller must not measure a part by. Value is the reason.
|
||||
invalid_outputs: dict[str, str] = field(default_factory=dict)
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
@@ -109,6 +116,7 @@ def run_graph(
|
||||
|
||||
outputs: dict[str, float] = {}
|
||||
failures: list[ToolFailure] = []
|
||||
invalid_outputs: dict[str, str] = {}
|
||||
for result in report:
|
||||
if not result.ok:
|
||||
failures.append(ToolFailure(
|
||||
@@ -118,12 +126,21 @@ def run_graph(
|
||||
))
|
||||
continue
|
||||
for name, value in result.outputs.items():
|
||||
if isinstance(value, (int, float)):
|
||||
outputs[f"{result.tool_id}.{name}"] = float(value)
|
||||
key = f"{result.tool_id}.{name}"
|
||||
# bool is a subclass of int - a tool that outputs a flag must not
|
||||
# be read as a quote.
|
||||
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
||||
continue
|
||||
value = float(value)
|
||||
if not math.isfinite(value):
|
||||
invalid_outputs[key] = "nan" if value != value else "infinite"
|
||||
continue
|
||||
outputs[key] = value
|
||||
|
||||
return RunOutcome(
|
||||
outputs=outputs,
|
||||
failures=failures,
|
||||
engine_version=engine_version(),
|
||||
duration_ms=elapsed_ms,
|
||||
invalid_outputs=invalid_outputs,
|
||||
)
|
||||
|
||||
@@ -72,6 +72,53 @@ def test_a_broken_tool_does_not_sink_the_others():
|
||||
assert any(key.endswith(".diameter") for key in outcome.outputs)
|
||||
|
||||
|
||||
class _FakeResult:
|
||||
"""Stands in for a vs-task tool result: only what run_graph touches."""
|
||||
|
||||
def __init__(self, tool_id, tool_name, ok, outputs=None, error=None):
|
||||
self.tool_id = tool_id
|
||||
self.tool_name = tool_name
|
||||
self.ok = ok
|
||||
self.outputs = outputs or {}
|
||||
self.error = error
|
||||
|
||||
|
||||
def test_a_nan_output_is_dropped_not_measured(monkeypatch):
|
||||
"""vs-measure emits NaN on an empty slice (caliper.py:112 per the
|
||||
revisione) - a NaN must not become a quote's value (#16)."""
|
||||
def fake_run(self, context=None):
|
||||
return [_FakeResult("t1", "diametro", True, outputs={"diameter": float("nan")})]
|
||||
monkeypatch.setattr(runner.Task, "run", fake_run)
|
||||
|
||||
outcome = run_graph(a_disc(), a_circle_graph())
|
||||
|
||||
assert outcome.outputs == {}
|
||||
assert outcome.invalid_outputs == {"t1.diameter": "nan"}
|
||||
|
||||
|
||||
def test_an_infinite_output_is_dropped_not_measured(monkeypatch):
|
||||
def fake_run(self, context=None):
|
||||
return [_FakeResult("t1", "diametro", True, outputs={"diameter": float("inf")})]
|
||||
monkeypatch.setattr(runner.Task, "run", fake_run)
|
||||
|
||||
outcome = run_graph(a_disc(), a_circle_graph())
|
||||
|
||||
assert outcome.outputs == {}
|
||||
assert outcome.invalid_outputs == {"t1.diameter": "infinite"}
|
||||
|
||||
|
||||
def test_a_bool_output_is_not_treated_as_a_measurable_number(monkeypatch):
|
||||
"""bool is a subclass of int - a flag output must not become a quote."""
|
||||
def fake_run(self, context=None):
|
||||
return [_FakeResult("t1", "flag", True, outputs={"present": True})]
|
||||
monkeypatch.setattr(runner.Task, "run", fake_run)
|
||||
|
||||
outcome = run_graph(a_disc(), a_circle_graph())
|
||||
|
||||
assert outcome.outputs == {}
|
||||
assert outcome.invalid_outputs == {}
|
||||
|
||||
|
||||
def test_the_engine_says_which_version_it_is():
|
||||
version = engine_version()
|
||||
assert len(version) == 40 # a full git SHA
|
||||
|
||||
Reference in New Issue
Block a user