loki-mode 9.8.0 → 9.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -14
- package/SKILL.md +3 -2
- package/VERSION +1 -1
- package/autonomy/loki +122 -1
- package/autonomy/run.sh +49 -2
- package/dashboard/__init__.py +1 -1
- package/dashboard/api_evidence.py +411 -0
- package/dashboard/api_operator.py +283 -0
- package/dashboard/api_phases.py +262 -0
- package/dashboard/api_releases.py +242 -0
- package/dashboard/api_runs.py +477 -0
- package/dashboard/api_tests.py +444 -0
- package/dashboard/api_v2.py +47 -1
- package/dashboard/server.py +54 -0
- package/dashboard/static/index.html +246 -135
- package/docs/ARCHITECTURE-OVERVIEW.md +5 -3
- package/docs/CAPABILITY-BACKLOG.md +53 -0
- package/docs/COMPARISON.md +2 -2
- package/docs/COMPETITIVE-ANALYSIS.md +1 -1
- package/docs/COMPETITIVE-SCORECARD.md +422 -0
- package/docs/DASHBOARD-9.12-EVIDENCE.md +97 -0
- package/docs/DASHBOARD-ARCHITECTURE.md +423 -0
- package/docs/DEMOS.md +21 -23
- package/docs/HANDOFF-2026-08-03.md +439 -0
- package/docs/INSTALLATION.md +17 -10
- package/docs/OUTCOME-FRONTIER.md +536 -0
- package/docs/PROMPT-ABLATION-RESULT.md +97 -0
- package/docs/TOOLS.md +800 -0
- package/docs/alternative-installations.md +2 -3
- package/docs/audit-logging.md +44 -35
- package/docs/authentication.md +13 -2
- package/docs/authorization.md +87 -81
- package/docs/git-workflow.md +6 -3
- package/docs/metrics.md +15 -16
- package/docs/network-security.md +16 -13
- package/docs/openclaw-integration.md +36 -556
- package/docs/show-hn-post.md +2 -2
- package/docs/siem-integration.md +39 -36
- package/loki-ts/dist/loki.js +18 -18
- package/mcp/__init__.py +1 -1
- package/package.json +2 -2
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/references/confidence-routing.md +18 -1
- package/references/invariant-checks.md +13 -8
- package/references/magic-rarv-integration.md +0 -1
- package/references/multi-provider.md +27 -5
- package/skills/healing.md +4 -2
- package/tools/audit-docs.py +488 -0
- package/tools/baseline-pin.py +19 -1
- package/tools/calibration-audit.py +523 -0
- package/tools/ci-gate.py +19 -1
- package/tools/cost-forecast.py +344 -0
- package/tools/cost-guard.py +19 -1
- package/tools/cost-history.py +19 -1
- package/tools/cost-per-outcome.py +394 -0
- package/tools/estimate-run.py +19 -1
- package/tools/evidence-freshness.py +307 -0
- package/tools/gate-init.py +19 -1
- package/tools/gate-report.py +19 -1
- package/tools/gate-simulate.py +570 -0
- package/tools/gate-trend.py +354 -0
- package/tools/model-advisor.py +52 -1
- package/tools/policy-load.py +19 -1
- package/tools/prompt-cost.py +363 -0
- package/tools/prompt-diff.py +448 -0
- package/tools/prompt-lint.py +448 -0
- package/tools/receipt-bundle.py +72 -2
- package/tools/receipt-diff.py +19 -1
- package/tools/receipt-find.py +19 -1
- package/tools/receipt-stats.py +380 -0
- package/tools/receipt-timeline.py +478 -0
- package/tools/receipt-verify-batch.py +291 -0
- package/tools/run-replay.py +19 -1
- package/tools/signing-status.py +19 -1
- package/tools/token-guard.py +19 -1
- package/tools/token-tax.py +375 -0
- package/tools/tool-index.py +19 -1
- package/tools/verification-tax.py +277 -0
- package/tools/verify-chain.py +361 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
"""Read-only "test / CI results" view over the filesystem .loki/quality/ dir.
|
|
2
|
+
|
|
3
|
+
WHY THIS EXISTS. The dashboard had zero API surface over test and CI results,
|
|
4
|
+
so it could not answer the one question an operator asks first: did the last
|
|
5
|
+
run's tests pass? These are pure functions taking a loki_dir, so they are
|
|
6
|
+
testable without FastAPI and can be mounted by whoever owns server.py. Shape
|
|
7
|
+
and honesty discipline are copied from dashboard/api_runs.py; this module adds
|
|
8
|
+
no second envelope shape.
|
|
9
|
+
|
|
10
|
+
THE SOURCES, and what each can and cannot tell us. Every one is written by
|
|
11
|
+
autonomy/run.sh, and each is ONE flat record for the whole project:
|
|
12
|
+
|
|
13
|
+
.loki/quality/test-results.json the test axis. TWO writers, one schema.
|
|
14
|
+
run.sh:11563 is the no-runner writer (runner "none", pass the STRING
|
|
15
|
+
"inconclusive", status "not_run"); run.sh:11642 is the real-runner
|
|
16
|
+
writer (pass bool, status verified|failed|no_tests_run). Keys:
|
|
17
|
+
timestamp, runner, pass, summary, command, exit_code, status,
|
|
18
|
+
passed_count, failed_count, verification_gap, and min_coverage which is
|
|
19
|
+
present ONLY on the real-runner writer -- always read with .get().
|
|
20
|
+
.loki/quality/static-analysis.json run.sh:10442. Keys: timestamp,
|
|
21
|
+
files_checked, findings, summary, pass (bool).
|
|
22
|
+
.loki/quality/build-results.json run.sh:9811. Keys: timestamp, command,
|
|
23
|
+
ran, applicable, exit_code, duration_sec, status
|
|
24
|
+
(verified|failed|not_run|not_applicable).
|
|
25
|
+
.loki/quality/coverage.json run.sh:11706. Keys: measured, pct, tool,
|
|
26
|
+
runner, threshold, enforced, blocked, reason, timestamp.
|
|
27
|
+
.loki/quality/.test-results.iter the engine's own freshness marker: the
|
|
28
|
+
iteration number the test results belong to (run.sh:11570). Reported as
|
|
29
|
+
`iteration`; this module does NOT derive a staleness verdict from it.
|
|
30
|
+
|
|
31
|
+
WHAT "PER-SUITE" MEANS HERE, and the one place the writers are ambiguous. No
|
|
32
|
+
writer emits per-suite records -- pytest's individual suites are collapsed into
|
|
33
|
+
a single flat result before it is persisted, and passed_count / failed_count
|
|
34
|
+
are best-effort regex parses of the runner's summary line (null, never 0, when
|
|
35
|
+
unparseable). The only decomposition that exists on disk is the four sibling
|
|
36
|
+
AXES above, so a "suite" row here is one axis. A suite-parsing layer would have
|
|
37
|
+
to invent data no writer produces.
|
|
38
|
+
|
|
39
|
+
THE HONESTY RULE, which is the entire point of this module. A gate that did
|
|
40
|
+
not run is not a gate that passed. Three ways that fact gets destroyed, all
|
|
41
|
+
guarded:
|
|
42
|
+
|
|
43
|
+
1. `pass` is a UNION: bool true, bool false, or the string "inconclusive".
|
|
44
|
+
`if rec.get("pass")` renders "inconclusive" as passing, because a
|
|
45
|
+
non-empty string is truthy. Compared with `is True` / `is False`; anything
|
|
46
|
+
else is UNKNOWN.
|
|
47
|
+
2. An ABSENT artifact reads UNKNOWN with a stated reason, never "passed".
|
|
48
|
+
Absence of a signal is not evidence of success.
|
|
49
|
+
3. `status` carries not_run and no_tests_run, which are gaps, not passes.
|
|
50
|
+
A runner that exited 0 having executed zero tests (run.sh:11632) proves
|
|
51
|
+
nothing and must not read verified.
|
|
52
|
+
|
|
53
|
+
WHY record_is_measured() IS NOT USED HERE. autonomy/lib/efficiency_cost.py
|
|
54
|
+
holds the canonical measured-ness predicate for COST, and api_runs.py imports
|
|
55
|
+
it rather than restating it. It is the wrong rule for this domain: it inspects
|
|
56
|
+
only _MEASURED_FIELDS (cost_usd, input_tokens, output_tokens, cache_read_
|
|
57
|
+
tokens, cache_creation_tokens), which a test-results record does not have, so
|
|
58
|
+
it returns False for a genuinely green measured suite -- verified by calling
|
|
59
|
+
it against one. Routing test rows through it would silently mark every suite
|
|
60
|
+
UNKNOWN. The codebase's real definition of trustworthy test evidence is the
|
|
61
|
+
predicate at autonomy/run.sh:20846 (runner not in {"", "none"} AND pass is True
|
|
62
|
+
AND command a non-empty str AND exit_code an int == 0). It lives inside a shell
|
|
63
|
+
heredoc, so it cannot be imported; _test_evidence_is_trustworthy() below
|
|
64
|
+
mirrors it and names that line as its source.
|
|
65
|
+
|
|
66
|
+
.loki/quality/ is NOT wiped at run start -- the run-start clear at
|
|
67
|
+
autonomy/run.sh:6212 covers metrics/efficiency/iteration-*.json only. These
|
|
68
|
+
files are last-write-wins per iteration, so they describe the most recent
|
|
69
|
+
iteration that ran each gate, which is why this reader is singular: there is no
|
|
70
|
+
history on disk to make it plural.
|
|
71
|
+
|
|
72
|
+
Every returned envelope states `source` (the real paths read) and
|
|
73
|
+
`freshness_s` (age in seconds of the newest file that actually contributed,
|
|
74
|
+
None when nothing did), and carries an explicit `reason` when empty. Each row
|
|
75
|
+
carries its own `source` and `freshness_s` too, matching api_runs.py.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
from __future__ import annotations
|
|
79
|
+
|
|
80
|
+
import json
|
|
81
|
+
import os
|
|
82
|
+
import time
|
|
83
|
+
from typing import Any, Optional
|
|
84
|
+
|
|
85
|
+
__all__ = ["list_test_results", "UNKNOWN"]
|
|
86
|
+
|
|
87
|
+
# What an unmeasured value reads as. Kept as a name so callers can render it
|
|
88
|
+
# without hardcoding None-means-unknown at each call site.
|
|
89
|
+
UNKNOWN = None
|
|
90
|
+
|
|
91
|
+
_QUALITY = "quality"
|
|
92
|
+
_TEST_RESULTS = (_QUALITY, "test-results.json")
|
|
93
|
+
_STATIC_ANALYSIS = (_QUALITY, "static-analysis.json")
|
|
94
|
+
_BUILD_RESULTS = (_QUALITY, "build-results.json")
|
|
95
|
+
_COVERAGE = (_QUALITY, "coverage.json")
|
|
96
|
+
_ITER_MARKER = (_QUALITY, ".test-results.iter")
|
|
97
|
+
|
|
98
|
+
# Declared for the envelope so a caller can see exactly what was read, in the
|
|
99
|
+
# style already used by api_runs.py (real paths, not labels).
|
|
100
|
+
_SOURCE_PATHS = (
|
|
101
|
+
".loki/quality/test-results.json",
|
|
102
|
+
".loki/quality/static-analysis.json",
|
|
103
|
+
".loki/quality/build-results.json",
|
|
104
|
+
".loki/quality/coverage.json",
|
|
105
|
+
".loki/quality/.test-results.iter",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# status values the writers emit that are POSITIVELY not a pass. Everything
|
|
109
|
+
# outside this set and outside "verified" is UNKNOWN rather than assumed good.
|
|
110
|
+
_GAP_STATUSES = ("not_run", "no_tests_run", "not_applicable")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# tiny io helpers -- every one returns a default rather than raising, because a
|
|
115
|
+
# dashboard read must degrade, never 500 on a missing file.
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
def _p(loki_dir: str, *parts: str) -> str:
|
|
119
|
+
return os.path.join(loki_dir, *parts)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _read_json(path: str, default: Any = None) -> Any:
|
|
123
|
+
try:
|
|
124
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
125
|
+
return json.load(fh)
|
|
126
|
+
except Exception:
|
|
127
|
+
return default
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _mtime(path: str) -> Optional[float]:
|
|
131
|
+
try:
|
|
132
|
+
return os.path.getmtime(path)
|
|
133
|
+
except OSError:
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _freshness(mtimes: list, now: Optional[float] = None) -> Optional[int]:
|
|
138
|
+
"""Age in seconds of the NEWEST file that contributed. None if none did.
|
|
139
|
+
|
|
140
|
+
None is load-bearing: "no file contributed" is not "contributed zero
|
|
141
|
+
seconds ago". Clamped at 0 so a file written during this call (or a clock
|
|
142
|
+
that stepped) never reports a negative age.
|
|
143
|
+
"""
|
|
144
|
+
real = [m for m in mtimes if m is not None]
|
|
145
|
+
if not real:
|
|
146
|
+
return None
|
|
147
|
+
return max(0, int((now if now is not None else time.time()) - max(real)))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _iteration(loki_dir: str) -> Optional[int]:
|
|
151
|
+
"""Iteration the test results belong to, from the engine's own marker.
|
|
152
|
+
|
|
153
|
+
Reported as a fact, never turned into a staleness verdict here. Absent or
|
|
154
|
+
unparseable reads None -- not 0, which is a real iteration number.
|
|
155
|
+
"""
|
|
156
|
+
try:
|
|
157
|
+
with open(_p(loki_dir, *_ITER_MARKER), "r", encoding="utf-8") as fh:
|
|
158
|
+
return int(fh.read().strip())
|
|
159
|
+
except (OSError, ValueError):
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
# the honesty predicates
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
def _passed(value: Any) -> Optional[bool]:
|
|
168
|
+
"""Map a writer's `pass` field to True / False / UNKNOWN.
|
|
169
|
+
|
|
170
|
+
THE load-bearing function. `pass` is a union across the two writers: bool
|
|
171
|
+
true, bool false, or the string "inconclusive" (run.sh:11563 no-runner,
|
|
172
|
+
run.sh:11642 zero-tests-executed). A truth test would render that string as
|
|
173
|
+
a pass, which is the exact fake-green this module exists to prevent, so the
|
|
174
|
+
comparison is identity against the bools and everything else is UNKNOWN.
|
|
175
|
+
"""
|
|
176
|
+
if value is True:
|
|
177
|
+
return True
|
|
178
|
+
if value is False:
|
|
179
|
+
return False
|
|
180
|
+
return UNKNOWN
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _test_evidence_is_trustworthy(rec: Any) -> bool:
|
|
184
|
+
"""Mirror of the engine's own "real, passing test evidence" predicate.
|
|
185
|
+
|
|
186
|
+
Source of truth: autonomy/run.sh:20846, inside
|
|
187
|
+
_loki_supervised_completion_gates_pass. It is embedded in a shell heredoc
|
|
188
|
+
and therefore not importable, so it is mirrored here rather than imported;
|
|
189
|
+
if that predicate changes, this must follow. It is NOT the same rule as
|
|
190
|
+
efficiency_cost.record_is_measured(), which answers a cost question and
|
|
191
|
+
returns False for every test record (see the module docstring).
|
|
192
|
+
|
|
193
|
+
True only when a REAL runner ran, reported pass, and left a command and an
|
|
194
|
+
exit code of 0 behind. A record that says "tests passed" with no command
|
|
195
|
+
and no exit code is the "trust me" transcript the receipt replaces.
|
|
196
|
+
"""
|
|
197
|
+
if not isinstance(rec, dict):
|
|
198
|
+
return False
|
|
199
|
+
runner = str(rec.get("runner") or "").strip().lower()
|
|
200
|
+
command = rec.get("command")
|
|
201
|
+
exit_code = rec.get("exit_code")
|
|
202
|
+
return (
|
|
203
|
+
runner not in ("", "none")
|
|
204
|
+
and rec.get("pass") is True
|
|
205
|
+
and isinstance(command, str)
|
|
206
|
+
and bool(command.strip())
|
|
207
|
+
and isinstance(exit_code, int)
|
|
208
|
+
and not isinstance(exit_code, bool)
|
|
209
|
+
and exit_code == 0
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _status_of(rec: Any, passed: Optional[bool]) -> Any:
|
|
214
|
+
"""The writer's own status string, or a fallback derived from `pass`.
|
|
215
|
+
|
|
216
|
+
Never invents "verified". A record with no status and no usable pass reads
|
|
217
|
+
UNKNOWN, because a gate whose outcome was not recorded did not pass.
|
|
218
|
+
"""
|
|
219
|
+
if isinstance(rec, dict):
|
|
220
|
+
status = rec.get("status")
|
|
221
|
+
if isinstance(status, str) and status.strip():
|
|
222
|
+
return status
|
|
223
|
+
if passed is True:
|
|
224
|
+
return "verified"
|
|
225
|
+
if passed is False:
|
|
226
|
+
return "failed"
|
|
227
|
+
return UNKNOWN
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _row(name: str, path_label: str, path: str, rec: Any,
|
|
231
|
+
passed: Optional[bool], now: Optional[float],
|
|
232
|
+
extra: Optional[dict] = None) -> dict:
|
|
233
|
+
"""One axis row. Carries its own source and freshness, like api_runs rows.
|
|
234
|
+
|
|
235
|
+
`reason` is non-null whenever the outcome is UNKNOWN, so a caller never has
|
|
236
|
+
to guess whether a blank cell means "not run", "not recorded" or "we could
|
|
237
|
+
not read the file".
|
|
238
|
+
"""
|
|
239
|
+
present = isinstance(rec, dict)
|
|
240
|
+
if not present:
|
|
241
|
+
reason: Any = "no %s on disk; outcome was never recorded" % (path_label,)
|
|
242
|
+
elif passed is UNKNOWN:
|
|
243
|
+
reason = "%s recorded no pass/fail outcome (pass=%r)" % (
|
|
244
|
+
path_label, rec.get("pass"),
|
|
245
|
+
)
|
|
246
|
+
else:
|
|
247
|
+
reason = None
|
|
248
|
+
|
|
249
|
+
row = {
|
|
250
|
+
"suite": name,
|
|
251
|
+
"passed": passed,
|
|
252
|
+
"status": _status_of(rec, passed),
|
|
253
|
+
"recorded": present,
|
|
254
|
+
"timestamp": (rec.get("timestamp") or UNKNOWN) if present else UNKNOWN,
|
|
255
|
+
"summary": (rec.get("summary") or UNKNOWN) if present else UNKNOWN,
|
|
256
|
+
"reason": reason,
|
|
257
|
+
"source": path_label,
|
|
258
|
+
"freshness_s": _freshness([_mtime(path)], now=now),
|
|
259
|
+
}
|
|
260
|
+
if extra:
|
|
261
|
+
row.update(extra)
|
|
262
|
+
return row
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ---------------------------------------------------------------------------
|
|
266
|
+
# per-axis readers
|
|
267
|
+
# ---------------------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
def _tests_row(loki_dir: str, now: Optional[float]) -> tuple:
|
|
270
|
+
"""(row, record, path). The test axis, the reason this module exists."""
|
|
271
|
+
path = _p(loki_dir, *_TEST_RESULTS)
|
|
272
|
+
rec = _read_json(path)
|
|
273
|
+
present = isinstance(rec, dict)
|
|
274
|
+
passed = _passed(rec.get("pass")) if present else UNKNOWN
|
|
275
|
+
|
|
276
|
+
# A status the writers use for a POSITIVE gap (not_run / no_tests_run)
|
|
277
|
+
# overrides a stray pass value: a runner that exited 0 having executed zero
|
|
278
|
+
# tests proved nothing, so it must not read as passed.
|
|
279
|
+
if present and str(rec.get("status") or "") in _GAP_STATUSES:
|
|
280
|
+
passed = UNKNOWN
|
|
281
|
+
|
|
282
|
+
extra = {
|
|
283
|
+
"runner": (rec.get("runner") or UNKNOWN) if present else UNKNOWN,
|
|
284
|
+
"command": (rec.get("command") or UNKNOWN) if present else UNKNOWN,
|
|
285
|
+
# exit_code 0 is meaningful, so it is read with a presence check rather
|
|
286
|
+
# than an `or`, which would turn a real 0 into UNKNOWN.
|
|
287
|
+
"exit_code": rec.get("exit_code") if present else UNKNOWN,
|
|
288
|
+
# null, never 0, when the runner summary was unparseable -- the writer
|
|
289
|
+
# already emits null for this and the distinction must survive.
|
|
290
|
+
"passed_count": rec.get("passed_count") if present else UNKNOWN,
|
|
291
|
+
"failed_count": rec.get("failed_count") if present else UNKNOWN,
|
|
292
|
+
"verification_gap": (rec.get("verification_gap") or UNKNOWN) if present else UNKNOWN,
|
|
293
|
+
# Present only on the real-runner writer; .get() never assumes it.
|
|
294
|
+
"min_coverage": rec.get("min_coverage") if present else UNKNOWN,
|
|
295
|
+
"trustworthy": _test_evidence_is_trustworthy(rec),
|
|
296
|
+
}
|
|
297
|
+
row = _row("tests", ".loki/quality/test-results.json", path, rec, passed, now, extra)
|
|
298
|
+
if present and passed is UNKNOWN and row["reason"] is None:
|
|
299
|
+
row["reason"] = "test status %r is not an outcome" % (rec.get("status"),)
|
|
300
|
+
return row, rec, path
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _static_analysis_row(loki_dir: str, now: Optional[float]) -> tuple:
|
|
304
|
+
path = _p(loki_dir, *_STATIC_ANALYSIS)
|
|
305
|
+
rec = _read_json(path)
|
|
306
|
+
present = isinstance(rec, dict)
|
|
307
|
+
passed = _passed(rec.get("pass")) if present else UNKNOWN
|
|
308
|
+
extra = {
|
|
309
|
+
"findings": rec.get("findings") if present else UNKNOWN,
|
|
310
|
+
"files_checked": rec.get("files_checked") if present else UNKNOWN,
|
|
311
|
+
}
|
|
312
|
+
return _row("static_analysis", ".loki/quality/static-analysis.json",
|
|
313
|
+
path, rec, passed, now, extra), rec, path
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _build_row(loki_dir: str, now: Optional[float]) -> tuple:
|
|
317
|
+
"""The build axis. It has no `pass` key -- outcome comes from `status`.
|
|
318
|
+
|
|
319
|
+
The writer's three-way classification is deliberate (run.sh:9690): verified
|
|
320
|
+
and failed are outcomes, while not_run is an honest gap and not_applicable
|
|
321
|
+
means the stack positively has no build phase. Neither gap is a pass.
|
|
322
|
+
"""
|
|
323
|
+
path = _p(loki_dir, *_BUILD_RESULTS)
|
|
324
|
+
rec = _read_json(path)
|
|
325
|
+
present = isinstance(rec, dict)
|
|
326
|
+
status = str(rec.get("status") or "") if present else ""
|
|
327
|
+
if status == "verified":
|
|
328
|
+
passed: Optional[bool] = True
|
|
329
|
+
elif status == "failed":
|
|
330
|
+
passed = False
|
|
331
|
+
else:
|
|
332
|
+
passed = UNKNOWN
|
|
333
|
+
extra = {
|
|
334
|
+
"command": (rec.get("command") or UNKNOWN) if present else UNKNOWN,
|
|
335
|
+
"exit_code": rec.get("exit_code") if present else UNKNOWN,
|
|
336
|
+
"ran": rec.get("ran") if present else UNKNOWN,
|
|
337
|
+
"applicable": rec.get("applicable") if present else UNKNOWN,
|
|
338
|
+
}
|
|
339
|
+
row = _row("build", ".loki/quality/build-results.json", path, rec, passed, now, extra)
|
|
340
|
+
if present and passed is UNKNOWN:
|
|
341
|
+
row["reason"] = "build status %r is not an outcome" % (rec.get("status"),)
|
|
342
|
+
return row, rec, path
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _coverage(loki_dir: str, now: Optional[float]) -> tuple:
|
|
346
|
+
"""Coverage facts. NOT a suite row: coverage has no pass/fail outcome.
|
|
347
|
+
|
|
348
|
+
Measurement is opt-in (run.sh:11734), so the default-off record carries
|
|
349
|
+
measured=false with a reason. pct is None when unmeasured and stays None
|
|
350
|
+
here; an unmeasured coverage is never rendered as 0 percent.
|
|
351
|
+
"""
|
|
352
|
+
path = _p(loki_dir, *_COVERAGE)
|
|
353
|
+
rec = _read_json(path)
|
|
354
|
+
if not isinstance(rec, dict):
|
|
355
|
+
return {
|
|
356
|
+
"measured": False,
|
|
357
|
+
"pct": UNKNOWN,
|
|
358
|
+
"tool": UNKNOWN,
|
|
359
|
+
"threshold": UNKNOWN,
|
|
360
|
+
"enforced": UNKNOWN,
|
|
361
|
+
"blocked": UNKNOWN,
|
|
362
|
+
"reason": "no .loki/quality/coverage.json on disk; coverage was never recorded",
|
|
363
|
+
"source": ".loki/quality/coverage.json",
|
|
364
|
+
"freshness_s": None,
|
|
365
|
+
}, rec, path
|
|
366
|
+
measured = rec.get("measured") is True
|
|
367
|
+
return {
|
|
368
|
+
"measured": measured,
|
|
369
|
+
"pct": rec.get("pct") if measured else UNKNOWN,
|
|
370
|
+
"tool": rec.get("tool") or UNKNOWN,
|
|
371
|
+
"threshold": rec.get("threshold"),
|
|
372
|
+
"enforced": rec.get("enforced"),
|
|
373
|
+
"blocked": rec.get("blocked"),
|
|
374
|
+
"reason": None if measured else (rec.get("reason") or "coverage not measured"),
|
|
375
|
+
"source": ".loki/quality/coverage.json",
|
|
376
|
+
"freshness_s": _freshness([_mtime(path)], now=now),
|
|
377
|
+
}, rec, path
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
# ---------------------------------------------------------------------------
|
|
381
|
+
# public API
|
|
382
|
+
# ---------------------------------------------------------------------------
|
|
383
|
+
|
|
384
|
+
def list_test_results(loki_dir: str, now: Optional[float] = None) -> dict:
|
|
385
|
+
"""Latest test / CI results from .loki/quality/, one row per axis.
|
|
386
|
+
|
|
387
|
+
Returns an ENVELOPE, not a bare list, because the contract requires an
|
|
388
|
+
explicit reason when the result is empty and a list cannot carry one:
|
|
389
|
+
|
|
390
|
+
{"items": [...], "latest": {...}, "coverage": {...}, "iteration": int|None,
|
|
391
|
+
"source": [...], "freshness_s": int|None, "reason": None|str}
|
|
392
|
+
|
|
393
|
+
`items` is one row per axis (tests, static_analysis, build) -- see the
|
|
394
|
+
module docstring on why an axis is the finest decomposition any writer
|
|
395
|
+
produces. `latest` is the headline: the test axis, the answer to "did the
|
|
396
|
+
last run's tests pass".
|
|
397
|
+
|
|
398
|
+
THE PROPERTY THAT MATTERS: a suite whose outcome was never recorded reads
|
|
399
|
+
passed=UNKNOWN, never True and never False, and its `reason` says why. A
|
|
400
|
+
gate that did not run is not a gate that passed. This holds for an absent
|
|
401
|
+
file, for pass="inconclusive", and for status not_run / no_tests_run.
|
|
402
|
+
|
|
403
|
+
`reason` is None when at least one axis recorded an outcome. When nothing
|
|
404
|
+
on disk recorded anything it states so; rows are still returned (all
|
|
405
|
+
UNKNOWN) rather than padded with placeholders or dropped.
|
|
406
|
+
"""
|
|
407
|
+
envelope: dict = {
|
|
408
|
+
"items": [],
|
|
409
|
+
"latest": None,
|
|
410
|
+
"coverage": None,
|
|
411
|
+
"iteration": None,
|
|
412
|
+
"source": list(_SOURCE_PATHS),
|
|
413
|
+
"freshness_s": None,
|
|
414
|
+
"reason": None,
|
|
415
|
+
}
|
|
416
|
+
if not loki_dir or not os.path.isdir(loki_dir):
|
|
417
|
+
envelope["reason"] = "no .loki directory at %s" % (loki_dir,)
|
|
418
|
+
return envelope
|
|
419
|
+
|
|
420
|
+
tests_row, _tr, tests_path = _tests_row(loki_dir, now)
|
|
421
|
+
sa_row, _sa, sa_path = _static_analysis_row(loki_dir, now)
|
|
422
|
+
build_row, _br, build_path = _build_row(loki_dir, now)
|
|
423
|
+
coverage, _cv, cov_path = _coverage(loki_dir, now)
|
|
424
|
+
|
|
425
|
+
rows = [tests_row, sa_row, build_row]
|
|
426
|
+
envelope["items"] = rows
|
|
427
|
+
envelope["latest"] = tests_row
|
|
428
|
+
envelope["coverage"] = coverage
|
|
429
|
+
envelope["iteration"] = _iteration(loki_dir)
|
|
430
|
+
envelope["freshness_s"] = _freshness(
|
|
431
|
+
[_mtime(tests_path), _mtime(sa_path), _mtime(build_path), _mtime(cov_path)],
|
|
432
|
+
now=now,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
if not any(r["recorded"] for r in rows):
|
|
436
|
+
envelope["reason"] = (
|
|
437
|
+
"no quality artifacts under .loki/quality/: no gate has recorded a "
|
|
438
|
+
"result, so every suite reads UNKNOWN (not passed)"
|
|
439
|
+
)
|
|
440
|
+
elif all(r["passed"] is UNKNOWN for r in rows):
|
|
441
|
+
envelope["reason"] = (
|
|
442
|
+
"artifacts present but no suite recorded a pass/fail outcome"
|
|
443
|
+
)
|
|
444
|
+
return envelope
|
package/dashboard/api_v2.py
CHANGED
|
@@ -496,9 +496,55 @@ async def list_runs(
|
|
|
496
496
|
listing is narrowed to the caller's own projects.
|
|
497
497
|
"""
|
|
498
498
|
if tenant_ctx.is_global_admin or not tenant_ctx.auth_enabled:
|
|
499
|
-
|
|
499
|
+
_rows = await runs_mod.list_runs(
|
|
500
500
|
db, project_id=project_id, status=status, limit=limit, offset=offset,
|
|
501
501
|
)
|
|
502
|
+
# ONE CANONICAL RUN SURFACE, not two divergent APIs.
|
|
503
|
+
#
|
|
504
|
+
# These routes are real and mounted, but the SQL store behind them has
|
|
505
|
+
# NO RUNTIME WRITER: the only writers are this module and
|
|
506
|
+
# dashboard/runs.py, both API-side, while the actual run lifecycle
|
|
507
|
+
# writes to the filesystem (autonomy/run.sh, runner/council.ts,
|
|
508
|
+
# runner/checkpoint.ts). dashboard/runs.py contains zero filesystem
|
|
509
|
+
# references. So for every real `loki start` this returned [].
|
|
510
|
+
#
|
|
511
|
+
# The fix is a fallback here rather than a second /runs endpoint. A
|
|
512
|
+
# second endpoint would have produced a second surface disagreeing
|
|
513
|
+
# with this one, which is worse than an empty table.
|
|
514
|
+
#
|
|
515
|
+
# SCOPED DELIBERATELY TO THIS BRANCH. The filesystem adapter has no
|
|
516
|
+
# tenancy concept -- its signature is (loki_dir, now) and it carries
|
|
517
|
+
# zero project/tenant fields. Wiring it into the tenant-scoped paths
|
|
518
|
+
# below would let filesystem runs cross a boundary that is currently
|
|
519
|
+
# fail-closed (tenant_id is None -> []), trading a security property
|
|
520
|
+
# for a data fix. This branch is ALREADY unscoped: it is taken only
|
|
521
|
+
# when the caller is a global admin or auth is off entirely, which is
|
|
522
|
+
# exactly the local `loki start` case where the data is missing.
|
|
523
|
+
#
|
|
524
|
+
# Fallback only when SQL yields nothing, so a populated store always
|
|
525
|
+
# wins and enterprise behaviour is unchanged.
|
|
526
|
+
if not _rows and project_id is None:
|
|
527
|
+
try:
|
|
528
|
+
from . import api_runs as _fs_runs
|
|
529
|
+
|
|
530
|
+
_loki = os.environ.get("LOKI_DIR") or os.path.join(os.getcwd(), ".loki")
|
|
531
|
+
_fs = _fs_runs.list_runs(_loki)
|
|
532
|
+
# The adapter returns an envelope carrying source, freshness
|
|
533
|
+
# and an explicit reason when empty. Never fabricate a row:
|
|
534
|
+
# an empty filesystem result stays empty and the reason
|
|
535
|
+
# travels with it.
|
|
536
|
+
if _fs.get("runs"):
|
|
537
|
+
return _fs["runs"]
|
|
538
|
+
except Exception as _fs_exc: # pragma: no cover - never fatal
|
|
539
|
+
# This module defines no module-level logger, and referencing
|
|
540
|
+
# one would raise NameError inside the very handler whose job
|
|
541
|
+
# is to guarantee the fallback can never be fatal. Resolve it
|
|
542
|
+
# locally instead.
|
|
543
|
+
import logging as _logging
|
|
544
|
+
|
|
545
|
+
_logging.getLogger(__name__).warning(
|
|
546
|
+
"filesystem run fallback unavailable: %s", _fs_exc)
|
|
547
|
+
return _rows
|
|
502
548
|
|
|
503
549
|
if project_id is not None:
|
|
504
550
|
# Targeting a specific project: enforce it belongs to the caller.
|
package/dashboard/server.py
CHANGED
|
@@ -978,12 +978,66 @@ app.add_middleware(
|
|
|
978
978
|
allow_headers=["Content-Type", "Authorization", "X-Requested-With"],
|
|
979
979
|
)
|
|
980
980
|
|
|
981
|
+
# RESPONSE COMPRESSION. Measured, not assumed: the served dashboard bundle
|
|
982
|
+
# (dashboard/static/index.html) is 779,725 bytes raw and 150,341 gzipped --
|
|
983
|
+
# an 81% reduction. Until now only CORS and the collab WS auth middleware were
|
|
984
|
+
# registered, so every dashboard load shipped the full 780KB.
|
|
985
|
+
#
|
|
986
|
+
# That single fact is the most plausible cause of "the dashboard feels slow":
|
|
987
|
+
# it is not a rendering problem, it is 630KB of avoidable transfer on first
|
|
988
|
+
# paint, and it costs one middleware to fix.
|
|
989
|
+
#
|
|
990
|
+
# minimum_size=1024 leaves small JSON responses uncompressed, where the CPU
|
|
991
|
+
# round-trip outweighs the saving. GZipMiddleware is stdlib-backed and does
|
|
992
|
+
# not negotiate brotli, so it cannot fail closed on a client that only sends
|
|
993
|
+
# `Accept-Encoding: gzip` -- responses stay correct either way.
|
|
994
|
+
#
|
|
995
|
+
# Streaming endpoints are unaffected in a way that matters: Starlette's
|
|
996
|
+
# GZipMiddleware passes through responses it cannot buffer, so SSE and the
|
|
997
|
+
# WebSocket upgrade path keep their existing behaviour.
|
|
998
|
+
try:
|
|
999
|
+
from starlette.middleware.gzip import GZipMiddleware
|
|
1000
|
+
|
|
1001
|
+
app.add_middleware(GZipMiddleware, minimum_size=1024)
|
|
1002
|
+
except Exception as _gzip_exc: # pragma: no cover - starlette always ships it
|
|
1003
|
+
# Never fatal: a dashboard that starts uncompressed is strictly better
|
|
1004
|
+
# than one that does not start.
|
|
1005
|
+
logger.warning("gzip compression unavailable: %s", _gzip_exc)
|
|
1006
|
+
|
|
981
1007
|
# Static file serving is configured at the end of the file (after all API routes)
|
|
982
1008
|
|
|
983
1009
|
# Mount V2 API router
|
|
984
1010
|
from .api_v2 import router as api_v2_router
|
|
985
1011
|
app.include_router(api_v2_router)
|
|
986
1012
|
|
|
1013
|
+
# Mount the operator router: the filesystem evidence readers (run detail, gate
|
|
1014
|
+
# results, receipts, releases) reachable over HTTP. Before this they were
|
|
1015
|
+
# libraries only the test suite imported -- four readers, none of them
|
|
1016
|
+
# reachable by a user.
|
|
1017
|
+
#
|
|
1018
|
+
# THIS MOUNT FAILS CLOSED, and the first version did not. It was wrapped in a
|
|
1019
|
+
# bare `except Exception: logger.warning(...)`, which swallows a typo, a
|
|
1020
|
+
# refactor that breaks an import, or a syntax error just as happily as a
|
|
1021
|
+
# genuinely absent optional dependency. The dashboard then starts perfectly,
|
|
1022
|
+
# reports healthy, and serves 404 on every operator path -- the exact
|
|
1023
|
+
# "monitoring surface that is silently blind" failure this whole module exists
|
|
1024
|
+
# to prevent. It also makes the mount test fail on CI with no stated cause,
|
|
1025
|
+
# which is how it was found.
|
|
1026
|
+
#
|
|
1027
|
+
# A missing OPTIONAL dependency is the only tolerable degradation, so only
|
|
1028
|
+
# ImportError is caught, and even that is logged at error level rather than
|
|
1029
|
+
# warning. Every other exception propagates and takes the dashboard down,
|
|
1030
|
+
# because a dashboard that cannot show run evidence is not a dashboard that
|
|
1031
|
+
# should quietly claim to be up.
|
|
1032
|
+
try:
|
|
1033
|
+
from .api_operator import router as api_operator_router
|
|
1034
|
+
except ImportError as _operator_exc: # pragma: no cover - optional dep absent
|
|
1035
|
+
logger.error(
|
|
1036
|
+
"operator API could not be imported, /api/operator/* will 404: %s",
|
|
1037
|
+
_operator_exc)
|
|
1038
|
+
else:
|
|
1039
|
+
app.include_router(api_operator_router)
|
|
1040
|
+
|
|
987
1041
|
# Phase Merge-4: Mount Purple Lab FastAPI app under /lab/ so it appears as a
|
|
988
1042
|
# sidebar entry in Dashboard. Same `app` is also wrapped by `standalone_app`
|
|
989
1043
|
# in web-app/server.py for `loki web` (port 57375). One source of truth, no
|