';
@@ -12770,11 +12770,11 @@ def validate_configuration():
path = os.path.join(WORKSPACE, f)
if os.path.exists(path):
found_files.append(f)
-
+
if not found_files:
warnings.append(f"[warn] No OpenClaw workspace files found in {WORKSPACE}")
tips.append("[tip] Create SOUL.md, AGENTS.md, or MEMORY.md to set up your agent workspace")
-
+
# Check if log directory exists and has recent logs
if not os.path.exists(LOG_DIR):
warnings.append(f"[warn] Log directory doesn't exist: {LOG_DIR}")
@@ -12782,17 +12782,17 @@ def validate_configuration():
else:
# Check for recent log files
log_pattern = os.path.join(LOG_DIR, "*claw*.log")
- recent_logs = [f for f in glob.glob(log_pattern)
+ recent_logs = [f for f in glob.glob(log_pattern)
if os.path.getmtime(f) > time.time() - 86400] # Last 24h
if not recent_logs:
warnings.append(f"[warn] No recent log files found in {LOG_DIR}")
tips.append("[tip] Start your OpenClaw agent to see real-time data")
-
+
# Check if sessions directory exists
if not SESSIONS_DIR or not os.path.exists(SESSIONS_DIR):
warnings.append(f"[warn] Sessions directory not found: {SESSIONS_DIR}")
tips.append("[tip] Sessions will appear when your agent starts conversations")
-
+
return warnings, tips
@@ -12870,13 +12870,14 @@ def detect_config(args=None):
else:
# Auto-detect: check common locations including Docker volumes
data_dir = _auto_detect_data_dir()
-
+
if data_dir and os.path.isdir(data_dir):
# Auto-set workspace, sessions, crons from data dir
ws = os.path.join(data_dir, 'workspace')
if os.path.isdir(ws) and not (args and args.workspace):
if not args:
- import argparse; args = argparse.Namespace()
+ import argparse
+ args = argparse.Namespace()
args.workspace = ws
sess = os.path.join(data_dir, 'agents', 'main', 'sessions')
if os.path.isdir(sess) and not (args and getattr(args, 'sessions_dir', None)):
diff --git a/docs/blueprints/quality-cloud-parity.md b/docs/blueprints/quality-cloud-parity.md
new file mode 100644
index 0000000000..ac1dd19e42
--- /dev/null
+++ b/docs/blueprints/quality-cloud-parity.md
@@ -0,0 +1,200 @@
+# Feature Blueprint: Quality Grade Cloud Parity
+
+> Local mirror of the Software Factory Feature Blueprint of the same name.
+> The hosted record is the system of record; this file exists so drift-bot,
+> PR review, and headless agents can read the same spec without a factory
+> login. When the hosted blueprint is created or updated, keep this file in
+> step (FLYWHEEL 1f).
+
+## Feature Summary
+
+A hosted ClawMetry user viewing the Quality tab should see the same grade
+that the local dashboard shows for their machine. Before this feature, the
+hosted container answered quality requests from its own DuckDB — which is
+present but empty — and reported a working machine as having produced nothing.
+The grade now travels in the encrypted snapshot the daemon emits on each
+sync cycle, and the hosted process refuses to answer from its own store.
+
+The root cause was two coupled faults: the quality grade never had a snapshot
+slice, so the cloud had nothing to render; and the hosted endpoint fell back
+to reading its own empty store rather than returning an honest "not available"
+response. Both faults produced the same screen — "Nothing to grade yet" — for
+opposite reasons.
+
+## Component Blueprint Composition
+
+This feature extends two existing components without redefining them.
+
+`#CloudSnapshotSync` in `clawmetry/sync.py` bakes the encrypted snapshot the
+hosted dashboard renders. Per the cloud-parity hard gate, any data surface
+that a hosted trial user can click must be served by a `cm-cloud-*`
+interceptor over a snapshot slice, or must show an honest locked state. The
+Quality tab inherited that obligation but had no slice; this feature adds one.
+
+`#QualityRoutes` in `routes/quality.py` serves `GET /api/quality/report-card`
+and owns the composition logic for the report card payload. This feature
+extracts the composition into a shared callable (`compose_report_card`) so
+both the request handler and the daemon build identical payloads from the same
+code path, eliminating the divergence that produced a different answer on
+screen depending on where the request was served.
+
+## Feature-Specific Components
+
+```component
+name: QualitySnapshotBuilder
+container: ClawMetry Sync Daemon
+responsibilities:
+ - Reading the node's quality sessions exactly three times per snapshot cycle:
+ the current window, the prior window, and a 30-day calibration history
+ - Calling compose_report_card once for the node-wide card and once per
+ runtime seen in either the current window or the 30-day history
+ - Hoisting calibration thresholds to the slice root rather than embedding
+ them in each per-runtime card
+ - Returning an empty dict {} on any exception so the snapshot never fails
+ to build over one optional slice
+```
+
+`#QualitySnapshotBuilder` is implemented as `_build_quality_snapshot()` in
+`clawmetry/sync.py`, called from `sync_system_snapshot()`. It imports
+`compose_report_card` from `routes.quality` at call time (late import, same
+pattern as the other snapshot builders that use route-layer helpers).
+
+The three-read constraint is load-bearing: the daemon runs this builder on
+every snapshot cycle, and emitting one card per runtime must not multiply the
+store reads proportionally. Rows are grouped in Python after the three fetches;
+no additional queries are made regardless of how many runtimes are present.
+
+The quiet-runtime rule is also load-bearing: a runtime that ran sessions in
+the last 30 days but was quiet this week must still appear in `byRuntime` with
+an honest "nothing to grade this week" card. Without it, the hosted tab would
+fall back to the node-wide card when a runtime filter was selected and show
+another runtime's grade under the wrong runtime's name.
+
+## System Contracts
+
+### Key Contracts
+
+The `quality` snapshot slice has the shape
+`{window_hours, all, byRuntime, thresholds}`. `all` is the node-wide report
+card. `byRuntime` is a dict keyed by runtime identifier; each value is the
+report card for that runtime and never contains sessions belonging to a
+different runtime. `thresholds` is the calibration map, hoisted once to the
+slice root and absent from each per-runtime card (it is byte-identical across
+cards and carrying it inline multiplied the slice size by the runtime count).
+
+`compose_report_card` in `routes/quality.py` is the single canonical builder
+for the report card payload. Both the sync daemon (`_build_quality_snapshot`)
+and the request handler (`quality_report_card`) call it. No other code path
+may build a report card payload; duplicating the composition logic is the
+drift pattern this feature closes.
+
+The store is read exactly three times per `_build_quality_snapshot()` call:
+once for the current window (the `window_hours` parameter, default 168h),
+once for the prior window of the same length (for week-over-week comparison),
+and once for the 30-day calibration history. Emitting one card per runtime
+must not add queries: rows are split in Python, and `_assess_rows` is called
+once over the node's full window rows with its assessment map reused across
+every per-runtime card.
+
+Every runtime that appears in either the current window rows or the 30-day
+history rows gets its own entry in `byRuntime`. A runtime that was quiet this
+week but active in the last 30 days gets a card stating it has nothing to
+grade this week, in its own name. Absence from the current window is not
+grounds for omission from `byRuntime`.
+
+The `quality` slice returns `{}` on any exception and must never propagate an
+exception to `sync_system_snapshot`. A misconfiguration, an import failure, or
+a store error in the quality builder must not prevent the rest of the snapshot
+from being emitted.
+
+### Integration Contracts
+
+`GET /api/quality/report-card` when served by the hosted dashboard
+(`CLAWMETRY_CLOUD=1`) always returns `store_available: false` with a message
+explaining that the grade lives on the user's machine. It never reads the
+hosted container's DuckDB for this response. The distinction matters because
+an empty store answers queries successfully with zero rows, which produces
+"Nothing to grade yet" — a false statement about a machine that is grading
+normally. `store_available: false` is an honest "I cannot see your machine"
+and must not say "Nothing to grade".
+
+`GET /api/quality/report-card` when served by a local dashboard
+(`CLAWMETRY_CLOUD` unset) reads from the daemon query path and returns
+`store_available: true` when the store answers (even with zero rows).
+An empty local store is a true statement that this machine has no graded runs
+this week; the response may say "Nothing to grade yet" in that case.
+
+An unreachable store — `_store_via_daemon_or_direct` returning `None` rather
+than `[]` — is not an empty grade. The local handler returns
+`store_available: false` in that case, using the same message as the hosted
+process. The distinction between `None` (store unreachable) and `[]` (store
+answered with nothing) is preserved through the entire call chain.
+
+### Integration Boundaries
+
+`compose_report_card` is importable from `routes.quality` by both the sync
+daemon and the request handler. Its signature takes rows as arguments rather
+than a fetch callable: the daemon fetches once and passes sub-lists; the
+request handler passes the rows it has already fetched. Neither path is
+special-cased inside the function.
+
+The sync daemon never calls `_store_via_daemon_or_direct`. It holds the writer
+lock and reads from `local_store.get_store()` directly. The request handler
+uses `_store_via_daemon_or_direct` to read through the daemon's query server.
+The two paths are separated at the module boundary: `_build_quality_snapshot`
+is in `sync.py`; `quality_report_card` is in `routes/quality.py`.
+
+The `CLAWMETRY_CLOUD` environment variable is the only signal the request
+handler reads to decide whether to serve from the snapshot or refuse. It is
+not threaded through as a function argument; `os.environ.get` is called at
+request time.
+
+## Architecture Decision Records
+
+### ADR-001: Extract compose_report_card rather than duplicate composition
+
+Context: the request handler in `routes/quality.py` built the report card
+payload inline. The sync daemon needed to build the same payload for the
+snapshot. Duplicating the inline logic would have been the path of least
+resistance but would have produced two code paths that could diverge silently.
+
+Decision: extract the composition into `compose_report_card`, make it accept
+rows as arguments (rather than fetching internally), and have both callers
+pass their already-fetched rows to it.
+
+Consequences: one code path for the report card payload. The daemon's
+per-runtime cards are provably identical in structure to what the local request
+handler would return, because they use the same function. The test
+`test_precomputed_assessments_match_the_request_path` pins this.
+
+### ADR-002: CLAWMETRY_CLOUD gates the endpoint; no runtime argument
+
+Context: the hosted dashboard could signal its nature through a function
+argument, a constructor parameter, or an environment variable. The environment
+variable already existed (`CLAWMETRY_CLOUD`) and was already read by other
+parts of the hosted process to suppress local-only behavior.
+
+Decision: read `os.environ.get('CLAWMETRY_CLOUD')` at request time in
+`quality_report_card`. No new argument is added to the function.
+
+Consequences: the behavior is consistent with how other endpoints handle the
+hosted/local distinction, the environment variable stays the single source of
+truth, and tests can toggle the behavior with `monkeypatch.setenv` without
+touching function signatures.
+
+### ADR-003: Calibration thresholds hoisted to slice root, not per-card
+
+Context: the first version of the snapshot slice embedded the full calibration
+thresholds in every per-runtime card. At 30 runtimes the thresholds were
+repeated 14 times; each copy was byte-identical. The thresholds made up
+roughly a quarter of the slice by size.
+
+Decision: hoist `thresholds` to the slice root as a single key, and strip it
+from every per-runtime card before returning. The cloud interceptor reattaches
+it per card when composing the response.
+
+Consequences: slice size scales with the number of runtimes for the `byRuntime`
+keys (unavoidable), but not for the calibration data (avoided). The per-card
+strip is visible in `_build_quality_snapshot` after the per-runtime loop.
+The test `test_calibration_is_carried_once_not_per_card` pins the absence of
+`thresholds` in every individual card.
diff --git a/routes/quality.py b/routes/quality.py
index 395041d66e..85a2789a41 100644
--- a/routes/quality.py
+++ b/routes/quality.py
@@ -30,6 +30,7 @@
from __future__ import annotations
+import os
from datetime import datetime, timedelta, timezone
from flask import Blueprint, jsonify, request
@@ -91,72 +92,49 @@ def _iso_cutoff(hours: int) -> str:
return (datetime.now(timezone.utc) - timedelta(hours=hours)).isoformat()
-@bp_quality.route("/api/quality/report-card", methods=["GET"])
-def quality_report_card():
- """Everything the Quality tab renders, in one fetch.
-
- ``?window=7d`` (default), ``?runtime=`` (optional scope). No auth gate
- — this is the free-tier home for the "is my agent OK?" answer, same
- rationale as /api/evaluators and /api/evals/metrics.
+def compose_report_card(
+ rows: list,
+ prior_rows: list,
+ hist_rows: list,
+ *,
+ window_hours: int = 168,
+ runtime: str | None = None,
+ assessments: dict | None = None,
+ prior_assessments: dict | None = None,
+) -> dict:
+ """Build the Quality payload from rows already read out of the store.
+
+ Split out of the request handler so the sync daemon can build the SAME
+ payload for the cloud snapshot without re-querying per runtime (founder
+ live-hit 2026-08-22: the hosted Quality tab said "Nothing to grade yet"
+ for a machine showing an A and 119 graded runs locally — the hosted
+ container answered from its own empty DuckDB, because nothing ever put
+ quality in the snapshot).
+
+ Taking rows as arguments rather than a fetch callable is deliberate: the
+ daemon reads the node's sessions ONCE and groups them in Python, so
+ emitting a card per runtime costs no extra queries.
"""
from clawmetry import quality as _q
from clawmetry import quality_thresholds as _qt
- window_hours = _parse_window(request.args.get("window", "7d"))
- runtime = (request.args.get("runtime") or "").strip() or None
- if runtime == "all":
- runtime = None
-
- since = _iso_cutoff(window_hours)
- prior_since = _iso_cutoff(window_hours * 2)
-
- raw_rows = _store_via_daemon_or_direct(
- "query_quality_sessions",
- runtime=runtime, since=since, limit=400,
- )
- # None means the store could not be reached at all (the hosted dashboard
- # has no local DuckDB); [] means it answered and there is nothing there.
- # Collapsing the two would tell a cloud user "nothing to grade yet" about
- # a machine that is in fact working fine — a wrong answer dressed as an
- # empty state. FLYWHEEL cloud-parity gate: be honestly unavailable.
- if raw_rows is None:
- payload = _q.compute_report_card([], {})
- payload.update({
- "window_hours": window_hours,
- "runtime": runtime or "all",
- "store_available": False,
- "headline": "Quality is graded on your own machine.",
- "subline": (
- "This view reads the run history stored locally by the "
- "collector, which isn't reachable from here right now. "
- "Open ClawMetry on the machine your agents run on, or start "
- "the collector there, and the grade appears."
- ),
- })
- return jsonify(payload)
- rows = raw_rows
- prior_rows = _store_via_daemon_or_direct(
- "query_quality_sessions",
- runtime=runtime, since=prior_since, until=since, limit=400,
- ) or []
-
- # Calibrate per runtime off a 30-day history of the SAME runtime, so
- # "rough" means unusual for this runtime on this install.
- hist = _store_via_daemon_or_direct(
- "query_quality_sessions",
- runtime=runtime, since=_iso_cutoff(24 * 30), limit=1500,
- ) or []
- by_runtime_hist: dict[str, list[dict]] = {}
- for h in hist:
+ by_runtime_hist: dict = {}
+ for h in hist_rows or []:
by_runtime_hist.setdefault(h.get("runtime") or "openclaw", []).append(h)
thresholds = _qt.calibrate_all(by_runtime_hist)
- assessments = _assess_rows(rows, thresholds)
- prior_assessments = _assess_rows(prior_rows, thresholds, deep_limit=0)
+ # Pre-computed assessments are how the daemon emits a card PER RUNTIME
+ # without paying for a deep scan per runtime: it assesses the node's rows
+ # once and hands the same map to every card, which then reads only the
+ # sessions it contains. Passing none keeps the request-path behaviour.
+ if assessments is None:
+ assessments = _assess_rows(rows, thresholds)
+ if prior_assessments is None:
+ prior_assessments = _assess_rows(prior_rows or [], thresholds, deep_limit=0)
payload = _q.compute_report_card(
rows, assessments,
- prior_rows=prior_rows, prior_assessments=prior_assessments,
+ prior_rows=prior_rows or [], prior_assessments=prior_assessments,
)
payload["window_hours"] = window_hours
payload["runtime"] = runtime or "all"
@@ -180,7 +158,85 @@ def quality_report_card():
}
payload["benign_filter"] = _benign_filter_state()
payload["store_available"] = True
- return jsonify(payload)
+ return payload
+
+
+def unavailable_report_card(window_hours: int = 168, runtime: str | None = None) -> dict:
+ """The honest answer when this process cannot see the run history.
+
+ Never an empty grade: "nothing to grade" and "I cannot see your machine
+ from here" look identical on screen and mean opposite things. The hosted
+ dashboard hit exactly that — it has a DuckDB file, it is simply empty, so
+ the store answered [] and the tab reported a working machine as having
+ produced nothing.
+ """
+ from clawmetry import quality as _q
+
+ payload = _q.compute_report_card([], {})
+ payload.update({
+ "window_hours": window_hours,
+ "runtime": runtime or "all",
+ "store_available": False,
+ "headline": "Quality is graded on your own machine.",
+ "subline": (
+ "This view reads the run history stored locally by the "
+ "collector, which isn't reachable from here right now. "
+ "Open ClawMetry on the machine your agents run on, or start "
+ "the collector there, and the grade appears."
+ ),
+ })
+ return payload
+
+
+@bp_quality.route("/api/quality/report-card", methods=["GET"])
+def quality_report_card():
+ """Everything the Quality tab renders, in one fetch.
+
+ ``?window=7d`` (default), ``?runtime=`` (optional scope). No auth gate
+ — this is the free-tier home for the "is my agent OK?" answer, same
+ rationale as /api/evaluators and /api/evals/metrics.
+ """
+ window_hours = _parse_window(request.args.get("window", "7d"))
+ runtime = (request.args.get("runtime") or "").strip() or None
+ if runtime == "all":
+ runtime = None
+
+ # The hosted dashboard has no run history of its own — it ships with an
+ # EMPTY DuckDB, which answers queries rather than failing them. Reading it
+ # here produced "Nothing to grade yet" for machines that were grading
+ # fine, so the hosted process refuses to answer from it at all. The real
+ # grade reaches the cloud through the daemon's encrypted snapshot; when
+ # that slice is missing (older daemon) this honest message is what shows.
+ if os.environ.get("CLAWMETRY_CLOUD", "").strip():
+ return jsonify(unavailable_report_card(window_hours, runtime))
+
+ since = _iso_cutoff(window_hours)
+ prior_since = _iso_cutoff(window_hours * 2)
+
+ raw_rows = _store_via_daemon_or_direct(
+ "query_quality_sessions",
+ runtime=runtime, since=since, limit=400,
+ )
+ # None means the store could not be reached at all; [] means it answered
+ # and there is nothing there. Collapsing the two would tell a user
+ # "nothing to grade yet" about a machine that is in fact working fine.
+ if raw_rows is None:
+ return jsonify(unavailable_report_card(window_hours, runtime))
+
+ prior_rows = _store_via_daemon_or_direct(
+ "query_quality_sessions",
+ runtime=runtime, since=prior_since, until=since, limit=400,
+ ) or []
+ # Calibrate per runtime off a 30-day history of the SAME runtime, so
+ # "rough" means unusual for this runtime on this install.
+ hist = _store_via_daemon_or_direct(
+ "query_quality_sessions",
+ runtime=runtime, since=_iso_cutoff(24 * 30), limit=1500,
+ ) or []
+ return jsonify(compose_report_card(
+ raw_rows, prior_rows, hist,
+ window_hours=window_hours, runtime=runtime,
+ ))
def _assess_rows(
diff --git a/tests/test_quality_cloud_parity.py b/tests/test_quality_cloud_parity.py
new file mode 100644
index 0000000000..7c88794d45
--- /dev/null
+++ b/tests/test_quality_cloud_parity.py
@@ -0,0 +1,263 @@
+"""Quality reaches the hosted dashboard, and never lies when it cannot.
+
+Founder live-hit 2026-08-22: the hosted Quality tab said "Nothing to grade
+yet" for a machine whose own dashboard showed an A over 119 graded runs, same
+node, same runtime. Two faults, one screen:
+
+ 1. The grade never rode the encrypted snapshot, so the cloud had nothing to
+ render -- the cloud-parity gate every data card is supposed to pass.
+ 2. The hosted container answered anyway, from its OWN DuckDB. That file
+ exists and is empty, so the query succeeded with zero rows and the tab
+ reported a working machine as having produced nothing. "Nothing to
+ grade" and "I cannot see your machine" look identical and mean opposite
+ things.
+
+These tests pin both: the daemon emits the slice (node-wide and per runtime),
+and the hosted process refuses to answer from its own empty store.
+
+Blueprint: docs/blueprints/quality-cloud-parity.md
+"""
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+
+import pytest
+from flask import Flask
+
+sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
+
+
+def _row(sid, runtime, cost=1.0, tool_err=0.0):
+ return {
+ "session_id": sid,
+ "runtime": runtime,
+ "cost_usd": cost,
+ "message_count": 12,
+ "toolErrorPct": tool_err,
+ "maxIdleGapSec": 5,
+ "status": "completed",
+ "title": f"task {sid}",
+ "started_at": "2026-08-20T10:00:00+00:00",
+ "ended_at": "2026-08-20T10:20:00+00:00",
+ "last_active_at": "2026-08-20T10:20:00+00:00",
+ "metadata": {"quality": {"measurable": True, "rough": False,
+ "signals": {}, "verdicts": []}},
+ }
+
+
+class _FakeStore:
+ """Answers query_quality_sessions the way the daemon's own handle does."""
+
+ def __init__(self, window_rows, hist_rows=None):
+ self.window_rows = window_rows
+ self.hist_rows = hist_rows if hist_rows is not None else window_rows
+ self.calls = 0
+
+ def query_quality_sessions(self, since=None, until=None, limit=None, runtime=None):
+ self.calls += 1
+ if until: # prior window
+ return []
+ if limit and limit > 1000: # the 30-day calibration read
+ return list(self.hist_rows)
+ return list(self.window_rows)
+
+
+@pytest.fixture
+def daemon_store(monkeypatch):
+ def _install(window_rows, hist_rows=None):
+ from clawmetry import local_store as ls
+ store = _FakeStore(window_rows, hist_rows)
+ monkeypatch.setattr(ls, "get_store", lambda *a, **k: store)
+ return store
+ return _install
+
+
+# -- the daemon emits it -------------------------------------------------------
+# Blueprint contract: QualitySnapshotBuilder responsibilities + Key Contracts
+
+def test_snapshot_carries_node_wide_and_per_runtime_cards(daemon_store):
+ """The quality slice carries node-wide and per-runtime cards.
+
+ Contract: docs/blueprints/quality-cloud-parity.md > Key Contracts >
+ quality slice shape {window_hours, all, byRuntime, thresholds}.
+ """
+ from clawmetry import sync
+
+ daemon_store([_row("s1", "claude_code"), _row("s2", "claude_code"),
+ _row("s3", "openclaw")])
+ slice_ = sync._build_quality_snapshot()
+
+ assert slice_["window_hours"] == 168
+ assert slice_["all"]["total_runs"] == 3
+ assert set(slice_["byRuntime"]) == {"claude_code", "openclaw"}
+ assert slice_["byRuntime"]["claude_code"]["total_runs"] == 2
+ assert slice_["byRuntime"]["openclaw"]["total_runs"] == 1
+
+
+def test_per_runtime_card_never_carries_another_runtimes_runs(daemon_store):
+ """A per-runtime card is scoped exclusively to its own runtime.
+
+ Contract: docs/blueprints/quality-cloud-parity.md > Key Contracts >
+ byRuntime values never contain sessions belonging to a different runtime.
+ """
+ from clawmetry import sync
+
+ daemon_store([_row("a", "claude_code"), _row("b", "codex"), _row("c", "codex")])
+ cards = sync._build_quality_snapshot()["byRuntime"]
+ assert cards["claude_code"]["total_runs"] == 1
+ assert cards["codex"]["total_runs"] == 2
+ assert cards["claude_code"]["runtime"] == "claude_code"
+
+
+def test_runtime_quiet_this_week_still_gets_its_own_card(daemon_store):
+ """A runtime active in the last 30 days gets its own card even if quiet now.
+
+ Contract: docs/blueprints/quality-cloud-parity.md > Key Contracts >
+ Every runtime that appears in either the current window rows or the 30-day
+ history rows gets its own entry in byRuntime.
+ """
+ from clawmetry import sync
+
+ daemon_store([_row("a", "claude_code")],
+ hist_rows=[_row("a", "claude_code"), _row("old", "cursor")])
+ cards = sync._build_quality_snapshot()["byRuntime"]
+ assert "cursor" in cards
+ assert cards["cursor"]["total_runs"] == 0
+ assert "nothing to grade" in cards["cursor"]["headline"].lower()
+
+
+def test_calibration_is_carried_once_not_per_card(daemon_store):
+ """Thresholds are hoisted to the slice root, absent from every per-card.
+
+ Contract: docs/blueprints/quality-cloud-parity.md > ADR-003 and
+ Key Contracts > thresholds hoisted once to the slice root.
+ """
+ from clawmetry import sync
+
+ daemon_store([_row("a", "claude_code"), _row("b", "codex")])
+ slice_ = sync._build_quality_snapshot()
+ assert "thresholds" in slice_
+ assert "thresholds" not in slice_["all"]
+ for card in slice_["byRuntime"].values():
+ assert "thresholds" not in card
+
+
+def test_store_is_read_once_regardless_of_runtime_count(daemon_store):
+ """The daemon reads the store exactly 3 times per snapshot cycle.
+
+ Contract: docs/blueprints/quality-cloud-parity.md > Key Contracts >
+ The store is read exactly three times per _build_quality_snapshot() call.
+ """
+ from clawmetry import sync
+
+ store = daemon_store([_row(f"s{i}", rt) for i, rt in
+ enumerate(["claude_code", "codex", "cursor",
+ "goose", "openclaw", "pi"])])
+ sync._build_quality_snapshot()
+ assert store.calls == 3, (
+ f"expected 3 reads (window, prior, history), got {store.calls}"
+ )
+
+
+def test_snapshot_slice_never_raises(monkeypatch):
+ """A broken store must not prevent the snapshot from being emitted.
+
+ Contract: docs/blueprints/quality-cloud-parity.md > Key Contracts >
+ The quality slice returns {} on any exception.
+ """
+ from clawmetry import local_store as ls
+ from clawmetry import sync
+
+ def boom(*a, **k):
+ raise RuntimeError("store on fire")
+
+ monkeypatch.setattr(ls, "get_store", boom)
+ assert sync._build_quality_snapshot() == {}
+
+
+# -- the hosted process refuses to answer from its own empty store -------------
+# Blueprint contract: Integration Contracts
+
+def _app():
+ from routes.quality import bp_quality
+ app = Flask(__name__)
+ app.register_blueprint(bp_quality)
+ return app
+
+
+def test_hosted_dashboard_says_where_the_grade_lives(monkeypatch):
+ """The hosted process returns store_available:false, not an empty grade.
+
+ Contract: docs/blueprints/quality-cloud-parity.md > Integration Contracts >
+ GET /api/quality/report-card with CLAWMETRY_CLOUD=1 always returns
+ store_available:false. store_available:false must not say 'Nothing to grade'.
+ """
+ from routes import quality as qmod
+
+ monkeypatch.setenv("CLAWMETRY_CLOUD", "1")
+ # An empty-but-working store, exactly what the hosted container has.
+ monkeypatch.setattr(qmod, "_store_via_daemon_or_direct", lambda *a, **k: [])
+ with _app().test_client() as c:
+ body = c.get("/api/quality/report-card?window=7d&runtime=claude_code").get_json()
+ assert body["store_available"] is False
+ assert "your own machine" in body["headline"].lower()
+ assert "nothing to grade" not in body["headline"].lower(), (
+ "the hosted tab must not report a working machine as having produced "
+ "nothing -- that is the bug this closes"
+ )
+
+
+def test_local_dashboard_still_reports_a_real_empty_week(monkeypatch):
+ """Off the hosted dashboard, an empty store IS the answer.
+
+ Contract: docs/blueprints/quality-cloud-parity.md > Integration Contracts >
+ GET /api/quality/report-card without CLAWMETRY_CLOUD reads from the daemon
+ query path and returns store_available:true when the store answers.
+ """
+ from routes import quality as qmod
+
+ monkeypatch.delenv("CLAWMETRY_CLOUD", raising=False)
+ monkeypatch.setattr(qmod, "_store_via_daemon_or_direct", lambda *a, **k: [])
+ with _app().test_client() as c:
+ body = c.get("/api/quality/report-card?window=7d").get_json()
+ assert body["store_available"] is True
+ assert "nothing to grade" in body["headline"].lower()
+
+
+def test_unreachable_store_is_not_an_empty_grade(monkeypatch):
+ """An unreachable store (None) is distinct from an empty store ([]).
+
+ Contract: docs/blueprints/quality-cloud-parity.md > Integration Contracts >
+ An unreachable store is not an empty grade -- store_available:false.
+ """
+ from routes import quality as qmod
+
+ monkeypatch.delenv("CLAWMETRY_CLOUD", raising=False)
+ monkeypatch.setattr(qmod, "_store_via_daemon_or_direct", lambda *a, **k: None)
+ with _app().test_client() as c:
+ body = c.get("/api/quality/report-card").get_json()
+ assert body["store_available"] is False
+ assert "your own machine" in body["headline"].lower()
+
+
+def test_precomputed_assessments_match_the_request_path(daemon_store):
+ """Shared assessment map gives identical grade to a fresh per-request assessment.
+
+ Contract: docs/blueprints/quality-cloud-parity.md > ADR-001 >
+ compose_report_card is the single canonical builder; the daemon's
+ precomputed-assessment path must produce the same grade as the request path.
+ """
+ from routes.quality import _assess_rows, compose_report_card
+ from clawmetry import quality_thresholds as qt
+
+ rows = [_row("a", "claude_code", cost=3.0), _row("b", "claude_code")]
+ thresholds = qt.calibrate_all({"claude_code": rows})
+ shared = _assess_rows(rows, thresholds)
+
+ fresh = compose_report_card(rows, [], rows, window_hours=168, runtime="claude_code")
+ reused = compose_report_card(rows, [], rows, window_hours=168, runtime="claude_code",
+ assessments=shared, prior_assessments={})
+ assert fresh["grade"] == reused["grade"]
+ assert fresh["total_runs"] == reused["total_runs"]
+ assert fresh["graded_runs"] == reused["graded_runs"]