loki-mode 9.53.0 → 9.54.2
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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +28 -5
- package/autonomy/council-v2.sh +13 -7
- package/autonomy/lib/done-recognition.sh +24 -16
- package/autonomy/lib/proof-check.sh +11 -3
- package/autonomy/lib/proof-generator.py +36 -5
- package/autonomy/lib/voter-agents.sh +8 -3
- package/autonomy/lib/workspace_diff.py +106 -12
- package/autonomy/prd-checklist.sh +19 -11
- package/autonomy/run.sh +503 -52
- package/dashboard/__init__.py +1 -1
- package/dashboard/api_v2.py +4 -0
- package/dashboard/audit.py +9 -1
- package/dashboard/auth.py +16 -4
- package/dashboard/registry.py +34 -12
- package/dashboard/rigour_integration.py +25 -17
- package/dashboard/server.py +252 -104
- package/dashboard/static/cost.html +55 -26
- package/dashboard/static/index.html +475 -1544
- package/dashboard/static/proofs.html +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/docs/air-gapped.md +7 -2
- package/docs/image-provenance.md +4 -2
- package/docs/v10/BACKLOG.md +82 -2
- package/docs/v10/BOARD.md +105 -0
- package/docs/v10/DECISIONS.md +71 -0
- package/docs/v10/METRICS.md +2 -1
- package/docs/v10/PROGRESS.md +60 -0
- package/docs/v10/SWARM-PROMPT.md +98 -0
- package/docs/v10/SWARM.md +237 -0
- package/loki-ts/dist/loki.js +323 -322
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/aider.sh +8 -0
- package/providers/cline.sh +8 -0
- package/providers/codex.sh +9 -0
- package/providers/opencode.sh +9 -0
- package/web-app/dist/assets/AdminPage-CZrBkzRF.js +32 -0
- package/web-app/dist/assets/Avatar-gQIp6FBI.js +11 -0
- package/web-app/dist/assets/{Badge-BF3mhT57.js → Badge-D2_fBTFY.js} +1 -1
- package/web-app/dist/assets/{Button-BDp9aJ9q.js → Button-D5qDd2ef.js} +1 -1
- package/web-app/dist/assets/CockpitPage-DVMk2RUK.js +47 -0
- package/web-app/dist/assets/ComparePage-NMLyE1xL.js +1 -0
- package/web-app/dist/assets/{ErrorBoundary-ut3wrUmu.js → ErrorBoundary-BRABI-BT.js} +1 -1
- package/web-app/dist/assets/EvidenceReceiptPanel-D2LNKqUx.js +1 -0
- package/web-app/dist/assets/GitHubIssuesPanel-BC5GzKUN.js +7 -0
- package/web-app/dist/assets/{GitHubPRsPanel-DurZgSaj.js → GitHubPRsPanel-BSlmoVTS.js} +3 -3
- package/web-app/dist/assets/HomePage-D_CDMoen.js +12 -0
- package/web-app/dist/assets/{LoginPage-HeRGJeZy.js → LoginPage-BBNA_N9t.js} +1 -1
- package/web-app/dist/assets/{MagicPage-CUKrqsZs.js → MagicPage-CnFRZCMx.js} +1 -1
- package/web-app/dist/assets/MetricsPage-CPfLMRlh.js +6 -0
- package/web-app/dist/assets/{NotFoundPage-BZDZ-B77.js → NotFoundPage-lA0XSGTz.js} +1 -1
- package/web-app/dist/assets/ProjectPage-CNAPbr9z.js +256 -0
- package/web-app/dist/assets/ProjectsPage-CMtm8goN.js +6 -0
- package/web-app/dist/assets/SettingsPage-BhIjVMYE.js +10 -0
- package/web-app/dist/assets/{ShowcasePage-BGS2jTGH.js → ShowcasePage-BndtGeYo.js} +1 -1
- package/web-app/dist/assets/SystemSettingsPage-euXI-KXE.js +1 -0
- package/web-app/dist/assets/TeamsPage-ClUKecun.js +11 -0
- package/web-app/dist/assets/TemplatesPage-BWHWVk0L.js +29 -0
- package/web-app/dist/assets/{TerminalOutput-togzv9Zl.js → TerminalOutput-Cv3N3Szf.js} +1 -1
- package/web-app/dist/assets/{activity-Dreyb_Kr.js → activity-dEhz-bXR.js} +1 -1
- package/web-app/dist/assets/{bell-DWH0drES.js → bell-B3XUMq1t.js} +1 -1
- package/web-app/dist/assets/{bot-78MoCuDl.js → bot-X-ssNlwR.js} +1 -1
- package/web-app/dist/assets/{check-ikfylPst.js → check-C_9Nx7kb.js} +1 -1
- package/web-app/dist/assets/{chevron-left-DXUqkd8J.js → chevron-left-tvNswo3A.js} +1 -1
- package/web-app/dist/assets/{circle-alert-Da74VO8P.js → circle-alert-CSB88nEs.js} +1 -1
- package/web-app/dist/assets/{clock-mlGCFq8l.js → clock-C8p2e4NI.js} +1 -1
- package/web-app/dist/assets/{cloud-B_Y5ZrIO.js → cloud-CKp2ZvhG.js} +1 -1
- package/web-app/dist/assets/{code-xml-Bbom7YWV.js → code-xml-dY70LCHD.js} +1 -1
- package/web-app/dist/assets/{database-CLcofiVp.js → database-DObn5fmn.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-C5mkjXIM.js → dollar-sign-DSu6n3K3.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-CVrkaOQP.js → file-code-corner-DpCnS73z.js} +1 -1
- package/web-app/dist/assets/{file-plus-BV9240gX.js → file-plus-D3IPKCf1.js} +1 -1
- package/web-app/dist/assets/{globe-aZ0Zso4I.js → globe-C9QXkgH_.js} +1 -1
- package/web-app/dist/assets/{hammer-EgOQNukG.js → hammer-D_FtWe1y.js} +1 -1
- package/web-app/dist/assets/index-CI54rara.css +1 -0
- package/web-app/dist/assets/{index-DvZ0Kk2x.js → index-Cz7q1bvC.js} +2 -2
- package/web-app/dist/assets/{layers-BOVKmepE.js → layers-DsW7jnJX.js} +1 -1
- package/web-app/dist/assets/{loader-circle-0K8yLgHl.js → loader-circle-B3HspjKR.js} +1 -1
- package/web-app/dist/assets/{lock-XzlDMy8M.js → lock-DSI29pct.js} +1 -1
- package/web-app/dist/assets/{package-BtXTKusI.js → package-CLNxtA2x.js} +1 -1
- package/web-app/dist/assets/{plus-PxeiD8ju.js → plus-DyI4WC4_.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-CjqzYczt.js → refresh-cw-D2dv0xA0.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-CY0os4DH.js → rotate-ccw-CBvihA-a.js} +1 -1
- package/web-app/dist/assets/scroll-text-DvoOvAl_.js +6 -0
- package/web-app/dist/assets/{server-pIIs9vNp.js → server-DenWvSUt.js} +1 -1
- package/web-app/dist/assets/{shield-alert-CsDqKXwA.js → shield-alert-40cX_FvG.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-CAh3HVzH.js → thumbs-up-BrjZ6QuV.js} +12 -7
- package/web-app/dist/assets/trash-2-C6evwkEH.js +16 -0
- package/web-app/dist/assets/{trending-down-DrtBd_C9.js → trending-down-CfbZis3O.js} +1 -1
- package/web-app/dist/assets/{trending-up-D-DbCHXm.js → trending-up-DWuEK3WC.js} +1 -1
- package/web-app/dist/assets/{upload-D4l6DCOt.js → upload-B9DFBcPG.js} +1 -1
- package/web-app/dist/assets/{usePolling-D0aoTX80.js → usePolling-BryVC-T1.js} +1 -1
- package/web-app/dist/assets/{user-BQf_XJO_.js → user-DMRXwZiw.js} +1 -1
- package/web-app/dist/index.html +2 -2
- package/web-app/server.py +122 -47
- package/web-app/dist/assets/AdminPage-y6_b2Xo4.js +0 -32
- package/web-app/dist/assets/Avatar-BFPkXhIJ.js +0 -6
- package/web-app/dist/assets/CockpitPage-5TKICCoA.js +0 -47
- package/web-app/dist/assets/ComparePage-BSuNsUHy.js +0 -1
- package/web-app/dist/assets/EvidenceReceiptPanel-CcaBwEI-.js +0 -1
- package/web-app/dist/assets/GitHubIssuesPanel-DJeYgzAa.js +0 -12
- package/web-app/dist/assets/HomePage-Bs451LAy.js +0 -28
- package/web-app/dist/assets/MetricsPage-DvY1csgz.js +0 -6
- package/web-app/dist/assets/ProjectPage-CqHkEZkl.js +0 -257
- package/web-app/dist/assets/ProjectsPage-CLvCK1Y8.js +0 -6
- package/web-app/dist/assets/SettingsPage-DP7OmdwP.js +0 -10
- package/web-app/dist/assets/SystemSettingsPage-P_rlLh0x.js +0 -1
- package/web-app/dist/assets/TeamsPage-DxtBDcBC.js +0 -11
- package/web-app/dist/assets/TemplatesPage-5e-faH3W.js +0 -29
- package/web-app/dist/assets/folder-open-D_6yV78G.js +0 -11
- package/web-app/dist/assets/index-BSgmPwC0.css +0 -1
- package/web-app/dist/assets/lightbulb-DdASgqOG.js +0 -6
- package/web-app/dist/assets/mail-COu670_W.js +0 -6
- package/web-app/dist/assets/minus-bQpUt9ff.js +0 -6
- package/web-app/dist/assets/save-v8qep5cY.js +0 -6
- package/web-app/dist/assets/trash-2-Dt6urTyY.js +0 -6
package/dashboard/__init__.py
CHANGED
package/dashboard/api_v2.py
CHANGED
|
@@ -894,6 +894,10 @@ async def verify_audit_integrity():
|
|
|
894
894
|
|
|
895
895
|
return {
|
|
896
896
|
"valid": aggregate["valid"],
|
|
897
|
+
# verified/status: whether anything was checked (valid stays True for
|
|
898
|
+
# an empty chain); see audit.verify_all_logs_in_dir.
|
|
899
|
+
"verified": aggregate["verified"],
|
|
900
|
+
"status": aggregate["status"],
|
|
897
901
|
"files_checked": aggregate["files_checked"],
|
|
898
902
|
"files_skipped": aggregate.get("files_skipped", 0),
|
|
899
903
|
"entries_checked": aggregate.get("entries_checked", 0),
|
package/dashboard/audit.py
CHANGED
|
@@ -657,7 +657,8 @@ def verify_all_logs_in_dir(audit_dir) -> dict:
|
|
|
657
657
|
if not audit_dir.exists():
|
|
658
658
|
return {"valid": True, "files_checked": 0, "files_skipped": 0,
|
|
659
659
|
"entries_checked": 0, "first_tampered_file": None,
|
|
660
|
-
"first_tampered_line": None, "genesis_file": None
|
|
660
|
+
"first_tampered_line": None, "genesis_file": None,
|
|
661
|
+
"verified": False, "status": "nothing_checked"}
|
|
661
662
|
# v7.7.15 council fix (Opus 2): rotated files have name shape
|
|
662
663
|
# `audit-YYYY-MM-DD.HHMMSS.jsonl` (from `_rotate_logs_if_needed` at
|
|
663
664
|
# line 167). Lexicographic sort puts `audit-2026-05-04.123456.jsonl`
|
|
@@ -689,8 +690,13 @@ def verify_all_logs_in_dir(audit_dir) -> dict:
|
|
|
689
690
|
"first_tampered_file": str(log_file),
|
|
690
691
|
"first_tampered_line": result.get("first_tampered_line"),
|
|
691
692
|
"genesis_file": genesis_file,
|
|
693
|
+
"verified": False,
|
|
694
|
+
"status": "tampered",
|
|
692
695
|
}
|
|
693
696
|
prev_hash = result.get("last_hash", prev_hash)
|
|
697
|
+
# `valid` means "no break found" and stays True for an empty chain (the
|
|
698
|
+
# cross-chain verifier reads it). `verified` and `status` say whether
|
|
699
|
+
# anything was actually checked: zero files checked is not a verified chain.
|
|
694
700
|
return {
|
|
695
701
|
"valid": True,
|
|
696
702
|
"files_checked": files_checked,
|
|
@@ -699,6 +705,8 @@ def verify_all_logs_in_dir(audit_dir) -> dict:
|
|
|
699
705
|
"first_tampered_file": None,
|
|
700
706
|
"first_tampered_line": None,
|
|
701
707
|
"genesis_file": genesis_file,
|
|
708
|
+
"verified": files_checked > 0,
|
|
709
|
+
"status": "verified" if files_checked > 0 else "nothing_checked",
|
|
702
710
|
}
|
|
703
711
|
|
|
704
712
|
|
package/dashboard/auth.py
CHANGED
|
@@ -343,6 +343,18 @@ def list_tokens(include_revoked: bool = False) -> list[dict]:
|
|
|
343
343
|
return result
|
|
344
344
|
|
|
345
345
|
|
|
346
|
+
def _deadline_passed(value) -> bool:
|
|
347
|
+
"""True when an ISO-8601 deadline is in the past, or cannot be read.
|
|
348
|
+
|
|
349
|
+
Fails closed: an unparseable value, or a naive timestamp that cannot be
|
|
350
|
+
ordered against an aware "now", is treated as passed.
|
|
351
|
+
"""
|
|
352
|
+
try:
|
|
353
|
+
return datetime.now(timezone.utc) > datetime.fromisoformat(str(value))
|
|
354
|
+
except (TypeError, ValueError):
|
|
355
|
+
return True
|
|
356
|
+
|
|
357
|
+
|
|
346
358
|
def validate_token(raw_token: str) -> Optional[dict]:
|
|
347
359
|
"""
|
|
348
360
|
Validate a raw token.
|
|
@@ -372,10 +384,10 @@ def validate_token(raw_token: str) -> Optional[dict]:
|
|
|
372
384
|
if matched_token.get("revoked"):
|
|
373
385
|
return None
|
|
374
386
|
|
|
375
|
-
# Check expiration
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
if
|
|
387
|
+
# Check expiration, and the grace period of a rotated-out key
|
|
388
|
+
# (api_keys.rotate_key sets rotation_expires_at on the old key).
|
|
389
|
+
for field in ("expires_at", "rotation_expires_at"):
|
|
390
|
+
if matched_token.get(field) and _deadline_passed(matched_token[field]):
|
|
379
391
|
return None
|
|
380
392
|
|
|
381
393
|
# Update last used
|
package/dashboard/registry.py
CHANGED
|
@@ -16,6 +16,8 @@ from pathlib import Path
|
|
|
16
16
|
from typing import Iterator, Optional
|
|
17
17
|
import hashlib
|
|
18
18
|
|
|
19
|
+
from .api_runs import _efficiency_module
|
|
20
|
+
|
|
19
21
|
|
|
20
22
|
# Registry file location
|
|
21
23
|
REGISTRY_DIR = Path.home() / ".loki" / "dashboard"
|
|
@@ -686,13 +688,14 @@ def _read_project_run_snapshot(path: str) -> dict:
|
|
|
686
688
|
.loki/context/tracking.json fallback (totals.total_cost_usd)
|
|
687
689
|
|
|
688
690
|
Returns a dict with phase, iteration, cost_usd, started_at, and ended_at
|
|
689
|
-
(best-effort; missing values default to None
|
|
690
|
-
|
|
691
|
+
(best-effort; missing values default to None). iteration and cost_usd are
|
|
692
|
+
None, never 0 / 0.0, when nothing recorded them. Never raises: any file problem degrades the
|
|
693
|
+
affected field to its default.
|
|
691
694
|
"""
|
|
692
695
|
snap = {
|
|
693
696
|
"phase": "",
|
|
694
|
-
"iteration":
|
|
695
|
-
"cost_usd":
|
|
697
|
+
"iteration": None,
|
|
698
|
+
"cost_usd": None,
|
|
696
699
|
"started_at": None,
|
|
697
700
|
"ended_at": None,
|
|
698
701
|
}
|
|
@@ -708,8 +711,8 @@ def _read_project_run_snapshot(path: str) -> dict:
|
|
|
708
711
|
if isinstance(state, dict):
|
|
709
712
|
_p = state.get("phase", "")
|
|
710
713
|
snap["phase"] = _p if isinstance(_p, str) else ""
|
|
711
|
-
_i = state.get("iteration"
|
|
712
|
-
snap["iteration"] = _i if isinstance(_i, int) else
|
|
714
|
+
_i = state.get("iteration")
|
|
715
|
+
snap["iteration"] = _i if isinstance(_i, int) and not isinstance(_i, bool) else None
|
|
713
716
|
except (json.JSONDecodeError, OSError, ValueError):
|
|
714
717
|
pass
|
|
715
718
|
|
|
@@ -727,6 +730,15 @@ def _read_project_run_snapshot(path: str) -> dict:
|
|
|
727
730
|
pass
|
|
728
731
|
|
|
729
732
|
# Cost: sum per-iteration efficiency files; fall back to context tracking.
|
|
733
|
+
# A present record is not a measurement: only a record that carried an
|
|
734
|
+
# observed value (the canonical record_is_measured, the same predicate as
|
|
735
|
+
# dashboard/server.py _record_is_measured) and a numeric cost counts, so an
|
|
736
|
+
# all-zero or field-less record, or the 0.0 a fresh tracking.json is seeded
|
|
737
|
+
# with, reads as unknown instead of $0.00. A measured $0.00 stays 0.0.
|
|
738
|
+
# ponytail: a measured record with tokens but no cost_usd adds nothing (the
|
|
739
|
+
# fleet does not price tokens); price it here if unpriced records appear.
|
|
740
|
+
eff_mod = _efficiency_module()
|
|
741
|
+
measured = eff_mod.record_is_measured if eff_mod is not None else (lambda _rec: False)
|
|
730
742
|
cost = 0.0
|
|
731
743
|
found_cost = False
|
|
732
744
|
eff_dir = loki_dir / "metrics" / "efficiency"
|
|
@@ -738,7 +750,7 @@ def _read_project_run_snapshot(path: str) -> dict:
|
|
|
738
750
|
if not isinstance(data, dict):
|
|
739
751
|
continue
|
|
740
752
|
c = data.get("cost_usd")
|
|
741
|
-
if isinstance(c, (int, float)):
|
|
753
|
+
if isinstance(c, (int, float)) and not isinstance(c, bool) and measured(data):
|
|
742
754
|
cost += float(c)
|
|
743
755
|
found_cost = True
|
|
744
756
|
except (json.JSONDecodeError, OSError, ValueError):
|
|
@@ -754,11 +766,15 @@ def _read_project_run_snapshot(path: str) -> dict:
|
|
|
754
766
|
totals = ctx.get("totals", {})
|
|
755
767
|
if isinstance(totals, dict):
|
|
756
768
|
tc = totals.get("total_cost_usd")
|
|
757
|
-
if isinstance(tc, (int, float))
|
|
769
|
+
if isinstance(tc, (int, float)) and not isinstance(tc, bool) and measured({
|
|
770
|
+
"cost_usd": tc,
|
|
771
|
+
"input_tokens": totals.get("total_input"),
|
|
772
|
+
"output_tokens": totals.get("total_output")}):
|
|
758
773
|
cost = float(tc)
|
|
774
|
+
found_cost = True
|
|
759
775
|
except (json.JSONDecodeError, OSError, ValueError):
|
|
760
776
|
pass
|
|
761
|
-
snap["cost_usd"] = round(cost, 6)
|
|
777
|
+
snap["cost_usd"] = round(cost, 6) if found_cost else None
|
|
762
778
|
return snap
|
|
763
779
|
|
|
764
780
|
|
|
@@ -836,7 +852,7 @@ def get_fleet_runs(include_inactive: bool = True) -> list[dict]:
|
|
|
836
852
|
"running": running,
|
|
837
853
|
"phase": snap.get("phase", ""),
|
|
838
854
|
"iteration": snap.get("iteration", 0),
|
|
839
|
-
"cost_usd": snap.get("cost_usd"
|
|
855
|
+
"cost_usd": snap.get("cost_usd"),
|
|
840
856
|
"started_at": started_at,
|
|
841
857
|
"duration_seconds": duration_seconds,
|
|
842
858
|
"port": p.get("port"),
|
|
@@ -862,16 +878,22 @@ def get_fleet_summary(include_inactive: bool = True) -> dict:
|
|
|
862
878
|
Returns counts (total / running / stopped) and a summed cost across all
|
|
863
879
|
registered projects. v1 polls the shared metadata store (see
|
|
864
880
|
get_fleet_runs); not a controller. Never raises.
|
|
881
|
+
|
|
882
|
+
total_cost_usd sums only runs that recorded a cost and is None when none
|
|
883
|
+
did; total_cost_partial is True when some runs recorded no cost, so the
|
|
884
|
+
total is a lower bound, not the whole bill.
|
|
865
885
|
"""
|
|
866
886
|
runs = get_fleet_runs(include_inactive=include_inactive)
|
|
867
887
|
total = len(runs)
|
|
868
888
|
running = sum(1 for r in runs if r.get("running"))
|
|
869
|
-
|
|
889
|
+
costs = [float(r["cost_usd"]) for r in runs
|
|
890
|
+
if isinstance(r.get("cost_usd"), (int, float)) and not isinstance(r.get("cost_usd"), bool)]
|
|
870
891
|
return {
|
|
871
892
|
"total_runs": total,
|
|
872
893
|
"running_runs": running,
|
|
873
894
|
"stopped_runs": total - running,
|
|
874
|
-
"total_cost_usd":
|
|
895
|
+
"total_cost_usd": round(sum(costs), 6) if costs else None,
|
|
896
|
+
"total_cost_partial": 0 < len(costs) < total,
|
|
875
897
|
}
|
|
876
898
|
|
|
877
899
|
|
|
@@ -245,32 +245,36 @@ class RigourIntegration:
|
|
|
245
245
|
for sev in findings:
|
|
246
246
|
findings[sev] = int(_extract_num(raw_findings, sev, 0))
|
|
247
247
|
|
|
248
|
-
# Extract category scores
|
|
248
|
+
# Extract category scores. A category Rigour did not report is None
|
|
249
|
+
# (the panel shows "--"), never a scored 0.
|
|
249
250
|
raw_categories = raw.get("categories", raw.get("scores", {}))
|
|
251
|
+
if not isinstance(raw_categories, dict):
|
|
252
|
+
raw_categories = {}
|
|
250
253
|
categories = {
|
|
251
|
-
"security": _extract_num(raw_categories, "security",
|
|
252
|
-
"code_quality": _extract_num(raw_categories, "code_quality", _extract_num(raw_categories, "codeQuality",
|
|
253
|
-
"compliance": _extract_num(raw_categories, "compliance",
|
|
254
|
-
"best_practices": _extract_num(raw_categories, "best_practices", _extract_num(raw_categories, "bestPractices",
|
|
254
|
+
"security": _extract_num(raw_categories, "security", None),
|
|
255
|
+
"code_quality": _extract_num(raw_categories, "code_quality", _extract_num(raw_categories, "codeQuality", None)),
|
|
256
|
+
"compliance": _extract_num(raw_categories, "compliance", None),
|
|
257
|
+
"best_practices": _extract_num(raw_categories, "best_practices", _extract_num(raw_categories, "bestPractices", None)),
|
|
255
258
|
}
|
|
256
259
|
|
|
257
|
-
# Compute overall score
|
|
260
|
+
# Compute overall score: the reported one, else the average of the
|
|
261
|
+
# categories that were reported, else None (no score, no grade).
|
|
258
262
|
raw_score = raw.get("score", raw.get("overall_score"))
|
|
263
|
+
score = None
|
|
259
264
|
if raw_score is not None:
|
|
260
265
|
try:
|
|
261
266
|
score = float(raw_score)
|
|
262
267
|
except (ValueError, TypeError):
|
|
263
|
-
score =
|
|
268
|
+
score = None
|
|
264
269
|
else:
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
score = round(sum(non_zero) / len(non_zero), 1) if non_zero else 0.0
|
|
270
|
+
reported = [v for v in categories.values() if v is not None]
|
|
271
|
+
score = round(sum(reported) / len(reported), 1) if reported else None
|
|
268
272
|
|
|
269
273
|
result: dict[str, Any] = {
|
|
270
274
|
"available": True,
|
|
271
|
-
"score": round(score, 1),
|
|
275
|
+
"score": round(score, 1) if score is not None else None,
|
|
272
276
|
"max_score": 100,
|
|
273
|
-
"grade": _score_to_grade(score),
|
|
277
|
+
"grade": _score_to_grade(score) if score is not None else None,
|
|
274
278
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
275
279
|
"preset": preset,
|
|
276
280
|
"findings": findings,
|
|
@@ -301,8 +305,9 @@ class RigourIntegration:
|
|
|
301
305
|
"grade": None,
|
|
302
306
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
303
307
|
"preset": preset,
|
|
304
|
-
|
|
305
|
-
"
|
|
308
|
+
# Nothing was scanned: no counts and no category scores.
|
|
309
|
+
"findings": {"critical": None, "major": None, "minor": None, "info": None},
|
|
310
|
+
"categories": {"security": None, "code_quality": None, "compliance": None, "best_practices": None},
|
|
306
311
|
"reason": reason,
|
|
307
312
|
}
|
|
308
313
|
|
|
@@ -325,9 +330,12 @@ class RigourIntegration:
|
|
|
325
330
|
}
|
|
326
331
|
|
|
327
332
|
|
|
328
|
-
def _extract_num(d: dict, key: str, default: float = 0) -> float:
|
|
329
|
-
"""Safely extract a numeric value from a dict."""
|
|
333
|
+
def _extract_num(d: dict, key: str, default: Optional[float] = 0) -> Optional[float]:
|
|
334
|
+
"""Safely extract a numeric value from a dict; `default` when absent or not numeric."""
|
|
335
|
+
v = d.get(key)
|
|
336
|
+
if v is None or isinstance(v, bool):
|
|
337
|
+
return default
|
|
330
338
|
try:
|
|
331
|
-
return float(
|
|
339
|
+
return float(v)
|
|
332
340
|
except (ValueError, TypeError):
|
|
333
341
|
return default
|