diff --git a/AGENTS.md b/AGENTS.md
index 9316cc3..31a772f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -10,8 +10,15 @@ release?" gate.
The organs, boundaries and the `Brain → Heart (gate) → Hands (execute)` call
chain are defined once in `PyAutoBrain/ORGANISM.md`. Heart's side of it:
-**observer only** — it reads and emits the authoritative green/yellow/red
-verdict, never writes into other repos, and never triggers Build.
+**observer only** — it reads and emits the authoritative
+green/stale/yellow/red verdict, never writes into other repos, and never
+triggers Build. **STALE** is the freshness tier: nothing known-bad, but some
+evidence is missing or expired — the remedy is re-running a check, never
+fixing code, and evidence whose last known result was adverse stays
+yellow/red (`heart/readiness.py` docstring is the canonical definition).
+Releases still require GREEN; the dev-ship gate (PyAutoBrain `AUTONOMY.md`
+leg 4) treats STALE as passing because an evidence gap is organism-scope,
+not branch-scope.
For the release-**validation** rehearsal specifically (build-and-exercise the
exact source about to ship, before promoting to PyPI — see
diff --git a/heart/dashboard.py b/heart/dashboard.py
index cea3957..58d2e39 100644
--- a/heart/dashboard.py
+++ b/heart/dashboard.py
@@ -93,22 +93,25 @@ class Section:
class Board:
"""The whole board as data — every surface is a projection of this."""
- verdict: str # green | yellow | red
+ verdict: str # green | stale | yellow | red
score: int
ts: str # snapshot timestamp
age_seconds: float | None
- stale: bool
+ stale: bool # board-tick staleness (snapshot age), NOT the verdict tier
red_reasons: list[str]
yellow_reasons: list[str]
sections: list[Section]
+ # readiness freshness tier: evidence missing/expired, nothing known-bad
+ # (heart/readiness.py) — distinct from the tick-age `stale` bool above.
+ stale_reasons: list[str] = field(default_factory=list)
# --- verdict/state → glyph & colour maps ------------------------------------
-_VERDICT_STATE = {"red": FAIL, "yellow": WARN, "green": OK}
-_VERDICT_WORD = {"red": "RED", "yellow": "YELLOW", "green": "GREEN"}
+_VERDICT_STATE = {"red": FAIL, "yellow": WARN, "stale": INFO, "green": OK}
+_VERDICT_WORD = {"red": "RED", "yellow": "YELLOW", "stale": "STALE", "green": "GREEN"}
_STATE_MD = {OK: "🟢", WARN: "🟡", FAIL: "🔴", UNOBS: "⚪", INFO: "🔵"}
_STATE_HTML = {OK: "ok", WARN: "warn", FAIL: "fail", UNOBS: "unobs", INFO: "info"}
-_BADGE_COLOR = {"red": "red", "yellow": "yellow", "green": "brightgreen"}
+_BADGE_COLOR = {"red": "red", "yellow": "yellow", "stale": "blue", "green": "brightgreen"}
def _colour(state: str, text: str) -> str:
@@ -303,6 +306,7 @@ def build_board(
score = _as_int(verdict.get("score", 0))
red = list(verdict.get("red_reasons") or [])
yellow = list(verdict.get("yellow_reasons") or [])
+ stale_reasons = list(verdict.get("stale_reasons") or [])
sections: list[Section] = []
@@ -476,6 +480,7 @@ def build_board(
red_reasons=red,
yellow_reasons=yellow,
sections=sections,
+ stale_reasons=stale_reasons,
)
@@ -493,6 +498,7 @@ def render_readiness_block(verdict: dict[str, Any], *, quiet: bool = False) -> l
lines = [f"{c_info('RELEASE READINESS')} {_glyph(state)} {word} {c_meta(f'score {score}')}"]
reds = verdict.get("red_reasons") or []
yellows = verdict.get("yellow_reasons") or []
+ stales = verdict.get("stale_reasons") or []
limit = 1 if quiet else 6
shown = 0
for r in reds:
@@ -503,6 +509,10 @@ def render_readiness_block(verdict: dict[str, Any], *, quiet: bool = False) -> l
if shown < limit:
for y in yellows[: limit - shown]:
lines.append(" " + c_warn(f"! {y}"))
+ shown += 1
+ if shown < limit:
+ for s in stales[: limit - shown]:
+ lines.append(" " + c_info(f"? {s}"))
return lines
@@ -533,6 +543,8 @@ def _render_oneline(board: Board) -> str:
tail = f"{len(board.red_reasons)} blockers"
elif board.verdict == "yellow":
tail = f"{len(board.yellow_reasons)} warnings"
+ elif board.verdict == "stale":
+ tail = f"{len(board.stale_reasons)} evidence gap(s)"
else:
tail = "all green"
age = format_age(board.age_seconds, stale=board.stale)
@@ -557,6 +569,10 @@ def _render_md(board: Board) -> str:
elif board.yellow_reasons:
lines.append("**Warnings:** " + "; ".join(board.yellow_reasons[:6]))
lines.append("")
+ elif board.stale_reasons:
+ lines.append("**Evidence gaps (re-run, don't fix):** "
+ + "; ".join(board.stale_reasons[:6]))
+ lines.append("")
lines += ["| | Check | Status |", "|--|--|--|"]
for sec in board.sections:
em = _STATE_MD[sec.state]
@@ -587,9 +603,11 @@ def _render_html(board: Board) -> str:
f"
{_html.escape(sec.summary)}{details} | "
)
reasons_html = ""
- reasons = board.red_reasons or board.yellow_reasons
+ reasons = board.red_reasons or board.yellow_reasons or board.stale_reasons
if reasons:
- label = "Blockers" if board.red_reasons else "Warnings"
+ label = ("Blockers" if board.red_reasons
+ else "Warnings" if board.yellow_reasons
+ else "Evidence gaps")
items = "".join(f"{_html.escape(r)}" for r in reasons[:8])
reasons_html = f""
stale_html = (
diff --git a/heart/readiness.py b/heart/readiness.py
index 96dd97c..d6aff54 100644
--- a/heart/readiness.py
+++ b/heart/readiness.py
@@ -25,12 +25,29 @@
under a profile other than ``release``), and — crucially — any *unknown*
(missing test-run report, a library absent from the snapshot). An unknown is
never silently treated as green and never escalated to red.
+- **STALE** (an evidence gap, the freshness tier) when nothing is known-bad but
+ some evidence is *missing or expired*: a check that was never run, a
+ passing-but-aged report, a rehearsal whose ``commit_shas`` no longer match
+ ``main``, an unknown repo/version status. The remedy for a stale reason is to
+ **re-run the check**, never to fix code — which is exactly what separates it
+ from yellow. The tier is not a skip lever: evidence whose *last known result
+ was adverse* stays yellow/red until a fresh run says otherwise; only
+ unknown or passed-but-expired evidence lands here.
- **GREEN** otherwise — which now REQUIRES a fresh, passing release-validation
report matching the current source under the ``release`` profile, not just an
absence of red signals.
-Red dominates yellow structurally: reasons are collected into separate lists
-and ``verdict = red if red_reasons else yellow if yellow_reasons else green``.
+Consumer semantics: the **release gate needs GREEN** — STALE blocks a release
+exactly like YELLOW, but prescribes refreshing evidence rather than a human
+acknowledgement. The **dev-ship gate** (PyAutoBrain ``AUTONOMY.md`` leg 4)
+treats STALE as passing: an evidence gap is organism-scope, not branch-scope,
+and the other three legs gate the branch itself. This is what makes GREEN
+reachable in steady state and keeps YELLOW meaning "something is actually
+wrong" instead of training ack-fatigue.
+
+Red dominates yellow, which dominates stale, structurally: reasons are
+collected into separate lists and
+``verdict = red if red_reasons else yellow if yellow_reasons else stale if stale_reasons else green``.
A ``score`` (0–100, weighted penalties) is advisory/sortable only — the colour,
not the number, is the gate. ``compute`` is a pure function of the snapshot for
easy testing and never raises on partial/malformed data.
@@ -179,6 +196,10 @@ def compute(
red: list[str] = []
yellow: list[str] = []
+ # The freshness tier: nothing known-bad, but evidence missing/expired —
+ # remedy is re-running a check. Never receives a reason whose last known
+ # result was adverse (those stay red/yellow); see the module docstring.
+ stale: list[str] = []
counts: dict[str, int] = {}
def hit(key: str, n: int = 1) -> None:
@@ -188,7 +209,7 @@ def hit(key: str, n: int = 1) -> None:
for lib in libs:
body = repos.get(lib)
if not isinstance(body, dict) or not body:
- yellow.append(f"{lib}: status unknown")
+ stale.append(f"{lib}: status unknown")
hit("lib_unknown")
continue
ci = body.get("ci_status", {}) or {}
@@ -254,12 +275,14 @@ def hit(key: str, n: int = 1) -> None:
)
hit("test_failing")
elif ready is True:
+ # Passing-but-expired evidence is a freshness gap, not a warning —
+ # the last known result was good.
age = _age_days(test_run.get("ts"), ref)
if age is not None and age > TEST_STALE_DAYS:
- yellow.append(f"test run stale ({int(age)}d old)")
+ stale.append(f"test run stale ({int(age)}d old)")
hit("test_stale")
else:
- yellow.append("test run status unknown")
+ stale.append("test run status unknown")
hit("test_unknown")
# parked staleness (YELLOW)
parked = _as_int(test_run.get("parked_stale_count", 0))
@@ -267,7 +290,7 @@ def hit(key: str, n: int = 1) -> None:
yellow.append(f"{parked} stale parked script(s)")
hit("parked")
else:
- yellow.append("test run status unknown (no report.json)")
+ stale.append("test run status unknown (no report.json)")
hit("test_unknown")
# --- version skew (RED ahead / YELLOW behind) ---
@@ -296,7 +319,7 @@ def hit(key: str, n: int = 1) -> None:
yellow.append(f"{w.get('workspace')}: pinned BEHIND installed {w.get('installed')}")
hit("skew_behind")
elif status == "UNKNOWN":
- yellow.append(f"{w.get('workspace')}: installed {w.get('library')} version unknown")
+ stale.append(f"{w.get('workspace')}: installed {w.get('library')} version unknown")
hit("skew_unknown")
# --- manifest drift (YELLOW — identity hygiene vs PyAutoMind/repos.yaml) ---
@@ -324,13 +347,13 @@ def hit(key: str, n: int = 1) -> None:
else:
age = _age_days(vi.get("ts"), ref)
if age is None or age > INSTALL_STALE_DAYS:
- yellow.append(
+ stale.append(
"install verification stale "
+ ("(age unknown)" if age is None else f"({int(age)}d old)")
)
hit("install_stale")
else:
- yellow.append("install verification not run")
+ stale.append("install verification not run")
hit("install_unknown")
# --- release-validation gate (hard) -------------------------------------
@@ -375,45 +398,45 @@ def hit(key: str, n: int = 1) -> None:
mismatched.append(lib)
profile = str(vr.get("profile") or "").strip().lower()
if not commit_shas:
- yellow.append("release validation source unconfirmed (no commit_shas)")
+ stale.append("release validation source unconfirmed (no commit_shas)")
hit("validation_unknown")
elif mismatched:
- yellow.append(
+ stale.append(
"release validation stale: source moved since rehearsal ("
+ ", ".join(mismatched) + ")"
)
hit("validation_stale_sha")
elif confirmed == 0:
- yellow.append("release validation source unconfirmed (current HEADs unknown)")
+ stale.append("release validation source unconfirmed (current HEADs unknown)")
hit("validation_unknown")
elif unconfirmed:
# Some libs matched, but at least one gated repo's SHA could not
# be confirmed either way — an unknown must never be silently
# treated as green (same principle every other gate here follows).
- yellow.append(
+ stale.append(
"release validation partially unconfirmed (repo(s) with unknown "
"current HEAD: " + ", ".join(unconfirmed) + ")"
)
hit("validation_unknown")
elif profile != "release":
- yellow.append(
+ stale.append(
f"release validation profile '{vr.get('profile') or '?'}' is not 'release'"
)
hit("validation_profile")
else:
age = _age_days(vr.get("ts"), ref)
if age is None or age > VALIDATION_STALE_DAYS:
- yellow.append(
+ stale.append(
"release validation stale "
+ ("(age unknown)" if age is None else f"({int(age)}d old)")
)
hit("validation_stale")
# else: fresh, passing, matching, release profile → GREEN-eligible
else:
- yellow.append("release validation status unknown")
+ stale.append("release validation status unknown")
hit("validation_unknown")
else:
- yellow.append("no release validation for current source")
+ stale.append("no release validation for current source")
hit("validation_absent")
# --- script timing (YELLOW) ---
@@ -462,13 +485,14 @@ def hit(key: str, n: int = 1) -> None:
score -= min(n * w, cap)
score = max(0, min(100, score))
- verdict = "red" if red else ("yellow" if yellow else "green")
+ verdict = "red" if red else ("yellow" if yellow else ("stale" if stale else "green"))
return {
"verdict": verdict,
"score": score,
- "reasons": red + yellow,
+ "reasons": red + yellow + stale,
"red_reasons": red,
"yellow_reasons": yellow,
+ "stale_reasons": stale,
"ts": snapshot.get("ts") or datetime.datetime.now(datetime.timezone.utc).isoformat(),
}
diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py
index 48b34f3..e3d21d4 100644
--- a/tests/test_dashboard.py
+++ b/tests/test_dashboard.py
@@ -200,7 +200,7 @@ def test_unknown_fmt_raises():
# --- badge -------------------------------------------------------------------
@pytest.mark.parametrize("verdict,color", [("green", "brightgreen"), ("yellow", "yellow"),
- ("red", "red")])
+ ("stale", "blue"), ("red", "red")])
def test_badge_endpoint_colour(verdict, color):
board = dashboard.build_board(make_snapshot(), make_verdict(verdict, 50), now=FRESH_NOW)
b = dashboard.badge_endpoint(board)
diff --git a/tests/test_readiness.py b/tests/test_readiness.py
index 9c3ad94..76a28d7 100644
--- a/tests/test_readiness.py
+++ b/tests/test_readiness.py
@@ -81,13 +81,14 @@ def test_one_library_ci_failing_is_red():
assert v["score"] == 70
-def test_test_run_stale_is_yellow():
- # ready but ~31 days before the snapshot ts → stale caution, not a blocker.
+def test_test_run_stale_is_stale_tier():
+ # ready but ~31 days before the snapshot ts → passing-but-expired evidence:
+ # the freshness tier, not a warning (last known result was good).
snap = make_snapshot(test_run={"ready": True, "ts": "2026-05-01T00:00:00+00:00",
"parked_stale_count": 0})
v = compute(snap)
- assert v["verdict"] == "yellow"
- assert any("test run stale" in r for r in v["yellow_reasons"])
+ assert v["verdict"] == "stale"
+ assert any("test run stale" in r for r in v["stale_reasons"])
def test_test_run_fresh_ready_is_green():
@@ -138,14 +139,14 @@ def test_version_skew_bad_is_red():
assert any("unparseable" in r for r in v["red_reasons"])
-def test_version_skew_unknown_is_yellow():
+def test_version_skew_unknown_is_stale_tier():
snap = make_snapshot(version_skew={"workspaces": [
{"workspace": "autolens_workspace", "library": "PyAutoLens",
"pinned": "2026.6.1.1", "installed": None, "status": "UNKNOWN"}
]})
v = compute(snap)
- assert v["verdict"] == "yellow"
- assert any("version unknown" in r for r in v["yellow_reasons"])
+ assert v["verdict"] == "stale"
+ assert any("version unknown" in r for r in v["stale_reasons"])
def test_install_verification_failed_is_red():
@@ -159,22 +160,22 @@ def test_install_verification_failed_is_red():
assert v["score"] == 60
-def test_install_verification_stale_is_yellow():
+def test_install_verification_stale_is_stale_tier():
snap = make_snapshot(verify_install={
"ready": True, "ts": "2026-05-01T00:00:00+00:00", # ~31d before snapshot ts
"checks": [],
})
v = compute(snap)
- assert v["verdict"] == "yellow"
- assert any("install verification stale" in r for r in v["yellow_reasons"])
+ assert v["verdict"] == "stale"
+ assert any("install verification stale" in r for r in v["stale_reasons"])
-def test_install_verification_not_run_is_yellow():
+def test_install_verification_not_run_is_stale_tier():
snap = make_snapshot()
snap.pop("verify_install")
v = compute(snap)
- assert v["verdict"] == "yellow"
- assert any("install verification not run" in r for r in v["yellow_reasons"])
+ assert v["verdict"] == "stale"
+ assert any("install verification not run" in r for r in v["stale_reasons"])
def test_install_verification_fresh_pass_is_green():
@@ -194,19 +195,19 @@ def test_validation_fresh_pass_matching_source_is_green():
assert not any("release validation" in r for r in v["reasons"])
-def test_validation_absent_is_yellow():
+def test_validation_absent_is_stale_tier():
snap = make_snapshot()
del snap["validation_report"]
v = compute(snap)
- assert v["verdict"] == "yellow"
+ assert v["verdict"] == "stale"
assert not v["red_reasons"]
- assert any("no release validation for current source" in r for r in v["yellow_reasons"])
+ assert any("no release validation for current source" in r for r in v["stale_reasons"])
-def test_validation_empty_dict_is_yellow():
+def test_validation_empty_dict_is_stale_tier():
v = compute(make_snapshot(validation_report={}))
- assert v["verdict"] == "yellow"
- assert any("no release validation" in r for r in v["yellow_reasons"])
+ assert v["verdict"] == "stale"
+ assert any("no release validation" in r for r in v["stale_reasons"])
def test_validation_failed_is_red():
@@ -219,41 +220,42 @@ def test_validation_failed_is_red():
assert v["score"] == 60 # validation_failed penalty 40
-def test_validation_stale_by_sha_is_yellow():
- # A report whose commit_shas no longer match the current main HEADs is stale:
- # the source moved on since the rehearsal → caution, not a blocker, not green.
+def test_validation_stale_by_sha_is_stale_tier():
+ # A report whose commit_shas no longer match the current main HEADs is
+ # expired evidence: the source moved on since the rehearsal → freshness
+ # tier, not a blocker, not green.
snap = make_snapshot()
snap["repos"]["PyAutoLens"]["ci_status"]["head_sha"] = "f" * 40 # HEAD moved
v = compute(snap)
- assert v["verdict"] == "yellow"
+ assert v["verdict"] == "stale"
assert not v["red_reasons"]
assert any("source moved since rehearsal" in r and "PyAutoLens" in r
- for r in v["yellow_reasons"])
+ for r in v["stale_reasons"])
-def test_validation_wrong_profile_is_yellow():
+def test_validation_wrong_profile_is_stale_tier():
report = _green_validation_report()
report["profile"] = "smoke"
report["stages"]["integrate"] = {"status": "pass", "profile": "smoke"}
v = compute(make_snapshot(validation_report=report))
- assert v["verdict"] == "yellow"
- assert any("profile 'smoke' is not 'release'" in r for r in v["yellow_reasons"])
+ assert v["verdict"] == "stale"
+ assert any("profile 'smoke' is not 'release'" in r for r in v["stale_reasons"])
-def test_validation_stale_by_age_is_yellow():
+def test_validation_stale_by_age_is_stale_tier():
# passing + matching + release profile, but the rehearsal is >7d old.
report = _green_validation_report(ts="2026-05-01T00:00:00+00:00") # ~31d before snap ts
v = compute(make_snapshot(validation_report=report))
- assert v["verdict"] == "yellow"
- assert any("release validation stale" in r for r in v["yellow_reasons"])
+ assert v["verdict"] == "stale"
+ assert any("release validation stale" in r for r in v["stale_reasons"])
-def test_validation_no_commit_shas_is_yellow():
+def test_validation_no_commit_shas_is_stale_tier():
report = _green_validation_report()
report["commit_shas"] = {}
v = compute(make_snapshot(validation_report=report))
- assert v["verdict"] == "yellow"
- assert any("source unconfirmed" in r for r in v["yellow_reasons"])
+ assert v["verdict"] == "stale"
+ assert any("source unconfirmed" in r for r in v["stale_reasons"])
def test_validation_missing_ts_is_yellow_not_silently_green():
@@ -264,8 +266,8 @@ def test_validation_missing_ts_is_yellow_not_silently_green():
report = _green_validation_report()
del report["ts"]
v = compute(make_snapshot(validation_report=report))
- assert v["verdict"] == "yellow"
- assert any("release validation stale" in r and "unknown" in r for r in v["yellow_reasons"])
+ assert v["verdict"] == "stale"
+ assert any("release validation stale" in r and "unknown" in r for r in v["stale_reasons"])
def test_validation_partial_sha_confirmation_is_yellow_not_green():
@@ -277,16 +279,16 @@ def test_validation_partial_sha_confirmation_is_yellow_not_green():
snap = make_snapshot(validation_report=report)
del snap["repos"]["PyAutoLens"]["ci_status"]["head_sha"]
v = compute(snap)
- assert v["verdict"] == "yellow"
- assert any("partially unconfirmed" in r and "PyAutoLens" in r for r in v["yellow_reasons"])
+ assert v["verdict"] == "stale"
+ assert any("partially unconfirmed" in r and "PyAutoLens" in r for r in v["stale_reasons"])
-def test_validation_ready_unknown_is_yellow():
+def test_validation_ready_unknown_is_stale_tier():
report = _green_validation_report()
report["release_ready"] = None
v = compute(make_snapshot(validation_report=report))
- assert v["verdict"] == "yellow"
- assert any("release validation status unknown" in r for r in v["yellow_reasons"])
+ assert v["verdict"] == "stale"
+ assert any("release validation status unknown" in r for r in v["stale_reasons"])
def test_validation_failed_dominates_and_reds_first():
@@ -352,12 +354,12 @@ def test_parked_stale_is_yellow():
assert any("parked" in r for r in v["yellow_reasons"])
-def test_missing_test_run_is_yellow_unknown_not_crash():
+def test_missing_test_run_is_stale_unknown_not_crash():
snap = make_snapshot()
del snap["test_run"]
v = compute(snap)
- assert v["verdict"] == "yellow"
- assert any("unknown" in r for r in v["yellow_reasons"])
+ assert v["verdict"] == "stale"
+ assert any("unknown" in r for r in v["stale_reasons"])
assert v["score"] == 90
@@ -478,21 +480,57 @@ def test_red_dominates_yellow():
assert v["reasons"][0] in v["red_reasons"]
-def test_missing_library_is_yellow_unknown():
+def test_missing_library_is_stale_unknown():
snap = make_snapshot()
del snap["repos"]["PyAutoConf"]
v = compute(snap)
- assert v["verdict"] == "yellow"
- assert any("PyAutoConf" in r and "unknown" in r for r in v["yellow_reasons"])
+ assert v["verdict"] == "stale"
+ assert any("PyAutoConf" in r and "unknown" in r for r in v["stale_reasons"])
def test_empty_snapshot_not_green_no_crash():
v = readiness.compute({}, libraries=LIBS)
- assert v["verdict"] == "yellow" # unknowns, never green on no data
+ assert v["verdict"] == "stale" # all-unknown evidence gaps, never green on no data
+ assert v["stale_reasons"] and not v["red_reasons"] and not v["yellow_reasons"]
assert v["score"] < 100
json.dumps(v)
+# --- the freshness tier's own semantics --------------------------------------
+
+
+def test_yellow_dominates_stale_and_reason_order():
+ # A genuine warning (timing regression) + an evidence gap (verify_install
+ # never run) → YELLOW, with reasons ordered red + yellow + stale.
+ snap = make_snapshot(script_timing={"red_count": 1})
+ snap.pop("verify_install")
+ v = compute(snap)
+ assert v["verdict"] == "yellow"
+ assert v["yellow_reasons"] and v["stale_reasons"]
+ assert v["reasons"] == v["red_reasons"] + v["yellow_reasons"] + v["stale_reasons"]
+
+
+def test_stale_never_masks_last_known_bad():
+ # An old FAILING validation report stays YELLOW — expiry only applies to
+ # passing evidence. The freshness tier must never be a skip lever.
+ snap = make_snapshot(test_run={"ready": False, "failed": 5,
+ "run_label": "old", "ts": "2026-05-01T00:00:00+00:00"})
+ v = compute(snap)
+ assert v["verdict"] == "yellow"
+ assert any("workspace validation not passing" in r for r in v["yellow_reasons"])
+ assert not any("test run" in r for r in v["stale_reasons"])
+
+
+def test_multiple_evidence_gaps_is_single_stale_verdict():
+ snap = make_snapshot()
+ snap.pop("verify_install")
+ del snap["validation_report"]
+ v = compute(snap)
+ assert v["verdict"] == "stale"
+ assert len(v["stale_reasons"]) == 2
+ assert not v["yellow_reasons"] and not v["red_reasons"]
+
+
def test_score_clamped_to_zero_floor():
snap = make_snapshot(test_run={"ready": False})
for lib in LIBS:
@@ -553,7 +591,8 @@ def test_run_with_no_state_cache_still_writes(tmp_path, monkeypatch):
importlib.reload(r_mod)
v = r_mod.run()
assert (tmp_path / "release_ready.json").is_file()
- assert v["verdict"] == "yellow"
+ # empty state = all-unknown evidence gaps -> the freshness tier
+ assert v["verdict"] == "stale"
importlib.reload(state_mod)
importlib.reload(r_mod)