loki-mode 7.129.5 → 8.0.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 +81 -5
- package/SKILL.md +66 -5
- package/VERSION +1 -1
- package/autonomy/app-runner.sh +37 -0
- package/autonomy/completion-council.sh +862 -28
- package/autonomy/council-v2.sh +39 -0
- package/autonomy/crash.sh +3 -2
- package/autonomy/grill.sh +42 -0
- package/autonomy/hooks/validate-bash.sh +301 -4
- package/autonomy/lib/cockpit-render.sh +8 -2
- package/autonomy/lib/cr-rematerialize.py +101 -4
- package/autonomy/lib/deadline.py +957 -0
- package/autonomy/lib/dependency-setup.sh +205 -0
- package/autonomy/lib/done-recognition.sh +45 -0
- package/autonomy/lib/efficiency_cost.py +13 -6
- package/autonomy/lib/no_mock_scan.py +793 -0
- package/autonomy/lib/prd-enrich.sh +42 -0
- package/autonomy/lib/proof-analytics-props.py +69 -0
- package/autonomy/lib/proof-generator.py +282 -95
- package/autonomy/lib/proof-template.html +15 -12
- package/autonomy/lib/proof-verify.py +151 -102
- package/autonomy/lib/requirements_contract.py +848 -0
- package/autonomy/lib/sdk-mode.sh +103 -0
- package/autonomy/lib/secret-scan.sh +139 -0
- package/autonomy/lib/spec-expand.sh +208 -0
- package/autonomy/lib/tree_digest.py +266 -0
- package/autonomy/lib/voter-agents.sh +58 -8
- package/autonomy/lib/workspace_diff.py +124 -0
- package/autonomy/loki +189 -17
- package/autonomy/playwright-verify.sh +501 -0
- package/autonomy/prd-checklist.sh +17 -8
- package/autonomy/provider-offer.sh +111 -0
- package/autonomy/run.sh +4417 -676
- package/autonomy/sandbox.sh +42 -0
- package/autonomy/spec-interrogation.sh +148 -5
- package/autonomy/spec.sh +118 -1
- package/autonomy/telemetry.sh +133 -7
- package/autonomy/verify.sh +107 -0
- package/bin/loki +110 -3
- package/completions/_loki +10 -0
- package/completions/loki.bash +1 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/audit.py +96 -7
- package/dashboard/build_supervisor.py +1619 -0
- package/dashboard/control.py +8 -0
- package/dashboard/prompt_optimizer.py +7 -0
- package/dashboard/server.py +577 -22
- package/dashboard/static/trust.html +1 -1
- package/docs/ARCHITECTURE-OVERVIEW.md +207 -0
- package/docs/AUTONOMI-ECOSYSTEM.md +107 -0
- package/docs/INSTALLATION.md +19 -2
- package/docs/MODEL-EQUIVALENCE-HARNESS-PLAN.md +70 -0
- package/docs/PLANS-INDEX.md +33 -0
- package/docs/PRIVACY.md +29 -1
- package/docs/SIGNED-RECEIPTS.md +102 -0
- package/docs/SONNET5-DEFAULT-PLAN.md +1 -1
- package/docs/V8-ACCEPTANCE-TRIAGE-2026-07-24.md +114 -0
- package/docs/V8-AGENT-SDK-PLAN.md +692 -0
- package/docs/V8-COMPLEXITY-AUDIT-2026-07-24.md +45 -0
- package/docs/V8-MAJOR-RELEASE-PLAN.md +137 -0
- package/docs/V8-OVERNIGHT-PLAN-2026-07-25.md +82 -0
- package/docs/V8-RUNTIME-TRUTH-2026-07-25.md +232 -0
- package/docs/V8-SDK-RESEARCH-RAW.md +129 -0
- package/docs/alternative-installations.md +4 -4
- package/loki-ts/dist/loki.js +674 -285
- package/loki-ts/package.json +2 -0
- package/mcp/__init__.py +1 -1
- package/package.json +7 -6
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/claude.sh +75 -0
- package/providers/codex.sh +85 -13
- package/references/sdk-mode.md +106 -0
- package/skills/model-selection.md +1 -1
- package/skills/quality-gates.md +22 -0
|
@@ -20,9 +20,8 @@
|
|
|
20
20
|
the literal token anywhere else (this comment deliberately spells it out
|
|
21
21
|
instead of repeating it, and the renderer detects an un-substituted
|
|
22
22
|
blob structurally rather than by string-matching the token).
|
|
23
|
-
4.
|
|
24
|
-
|
|
25
|
-
parity (one string replace, one template).
|
|
23
|
+
4. The generator also substitutes the receipt title, social description,
|
|
24
|
+
and share-button setting before embedding the parsed blob.
|
|
26
25
|
|
|
27
26
|
The renderer below is defensive: every nullable / optional schema field is
|
|
28
27
|
guarded (deployed_url, public_url, diffs, council.enabled false, empty
|
|
@@ -42,10 +41,10 @@
|
|
|
42
41
|
<head>
|
|
43
42
|
<meta charset="utf-8">
|
|
44
43
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
45
|
-
<title>Loki Mode
|
|
44
|
+
<title>__PROOF_RECEIPT_TITLE__ - Loki Mode Evidence Receipt</title>
|
|
46
45
|
<!-- Static text-only social meta. No og:image (would break self-containment). -->
|
|
47
46
|
<meta property="og:type" content="website">
|
|
48
|
-
<meta property="og:title" content="
|
|
47
|
+
<meta property="og:title" content="__PROOF_RECEIPT_TITLE__ - Loki Mode Evidence Receipt">
|
|
49
48
|
<!-- og:description and twitter:description are filled at generation time with
|
|
50
49
|
the real measured cost + files-changed + council ratio (cost-free variant
|
|
51
50
|
when cost was not collected). The generator replaces the og token below in
|
|
@@ -589,7 +588,7 @@
|
|
|
589
588
|
function honestyVariant(headline) {
|
|
590
589
|
var h = String(headline || "").toUpperCase();
|
|
591
590
|
if (h === "VERIFIED") return { cls: "is-verified", mark: "OK",
|
|
592
|
-
sub: "
|
|
591
|
+
sub: "The recorded tests passed and this receipt contains a non-empty source change. Review the evidence below for the exact scope." };
|
|
593
592
|
if (h.indexOf("GAPS") !== -1) return { cls: "is-gaps", mark: "!",
|
|
594
593
|
sub: "Some evidence is verified, but the items listed below were not. A gap is never hidden: read them before trusting this run." };
|
|
595
594
|
return { cls: "is-failed", mark: "X",
|
|
@@ -776,7 +775,7 @@
|
|
|
776
775
|
// fmtUsd: a skeptic seeing a zero-dollar total assumes the artifact is fake.
|
|
777
776
|
var usdRaw = g(p, "cost.usd", null);
|
|
778
777
|
var fc = num(g(p, "files_changed.count", 0));
|
|
779
|
-
var parts = [
|
|
778
|
+
var parts = [__PROOF_RECEIPT_TITLE_JSON__];
|
|
780
779
|
if (usdRaw !== null) parts.push(fmtUsd(usdRaw));
|
|
781
780
|
else parts.push("cost not recorded");
|
|
782
781
|
parts.push(fc + " file" + (fc === 1 ? "" : "s") + " changed");
|
|
@@ -929,11 +928,15 @@
|
|
|
929
928
|
function renderCost(p) {
|
|
930
929
|
var card = document.getElementById("costCard");
|
|
931
930
|
var usdRaw = g(p, "cost.usd", null);
|
|
931
|
+
function tokenValue(path) {
|
|
932
|
+
var raw = g(p, path, null);
|
|
933
|
+
return raw === null ? "not recorded" : fmtInt(raw);
|
|
934
|
+
}
|
|
932
935
|
var cells = [
|
|
933
|
-
["Input tokens",
|
|
934
|
-
["Output tokens",
|
|
935
|
-
["Cache read",
|
|
936
|
-
["Cache write",
|
|
936
|
+
["Input tokens", tokenValue("cost.input_tokens")],
|
|
937
|
+
["Output tokens", tokenValue("cost.output_tokens")],
|
|
938
|
+
["Cache read", tokenValue("cost.cache_read_tokens")],
|
|
939
|
+
["Cache write", tokenValue("cost.cache_creation_tokens")]
|
|
937
940
|
];
|
|
938
941
|
var grid = "";
|
|
939
942
|
for (var i = 0; i < cells.length; i++) {
|
|
@@ -1249,7 +1252,7 @@
|
|
|
1249
1252
|
function shareHook() {
|
|
1250
1253
|
var m = document.querySelector('meta[property="og:description"]');
|
|
1251
1254
|
var c = m ? (m.getAttribute("content") || "") : "";
|
|
1252
|
-
return c ||
|
|
1255
|
+
return c || (__PROOF_RECEIPT_TITLE_JSON__ + " - Loki Mode Evidence Receipt");
|
|
1253
1256
|
}
|
|
1254
1257
|
document.addEventListener("click", function (e) {
|
|
1255
1258
|
var t = e.target && e.target.closest ? e.target.closest("[data-share]") : null;
|
|
@@ -30,10 +30,11 @@ Three checks (mirrors dashboard/audit.py verify-CLI style):
|
|
|
30
30
|
ensure_ascii setting), sha256, compare to the recorded verification.hash.
|
|
31
31
|
Any mismatch means the JSON was edited after signing.
|
|
32
32
|
|
|
33
|
-
2. DRIFT CHECK (diff_drift): from the recorded git base sha, re-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
2. DRIFT CHECK (diff_drift): from the recorded git base sha, re-derive the
|
|
34
|
+
final committed, staged, unstaged, deleted, and untracked workspace diff.
|
|
35
|
+
Compare its stat and diff_sha256 to the receipt. When tree_sha256 is
|
|
36
|
+
present, also bind exact final file content so equal line counts cannot
|
|
37
|
+
hide content drift.
|
|
37
38
|
|
|
38
39
|
3. GPG (gpg_ok): if a detached signature is present and gpg is available,
|
|
39
40
|
verify it over the canonical bytes. Otherwise "n/a".
|
|
@@ -67,6 +68,13 @@ import os
|
|
|
67
68
|
import subprocess
|
|
68
69
|
import sys
|
|
69
70
|
|
|
71
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
72
|
+
if _HERE not in sys.path:
|
|
73
|
+
sys.path.insert(0, _HERE)
|
|
74
|
+
|
|
75
|
+
from tree_digest import compute_tree_digest
|
|
76
|
+
from workspace_diff import collect_workspace_diff
|
|
77
|
+
|
|
70
78
|
|
|
71
79
|
# ---------------------------------------------------------------------------
|
|
72
80
|
# canonicalization (MUST match proof-generator._canonical exactly)
|
|
@@ -113,42 +121,13 @@ def _rev_resolvable(repo_dir, ref):
|
|
|
113
121
|
|
|
114
122
|
|
|
115
123
|
def _numstat(repo_dir, base, head):
|
|
116
|
-
"""Return
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
proof falsely reported drift. BUG-DIFFSHA.)"""
|
|
124
|
-
raw = _git(repo_dir, ["diff", "--numstat", str(base), str(head)])
|
|
125
|
-
if raw is None:
|
|
126
|
-
return None
|
|
127
|
-
files = []
|
|
128
|
-
ins_total = 0
|
|
129
|
-
del_total = 0
|
|
130
|
-
for line in raw.splitlines():
|
|
131
|
-
parts = line.split("\t")
|
|
132
|
-
if len(parts) < 3:
|
|
133
|
-
continue
|
|
134
|
-
ins_s, del_s, path = parts[0], parts[1], parts[2]
|
|
135
|
-
# binary files show "-" for both columns; count the file, add 0.
|
|
136
|
-
ins = 0 if ins_s == "-" else _to_int(ins_s)
|
|
137
|
-
dele = 0 if del_s == "-" else _to_int(del_s)
|
|
138
|
-
ins_total += ins
|
|
139
|
-
del_total += dele
|
|
140
|
-
files.append({
|
|
141
|
-
"path": path,
|
|
142
|
-
"insertions": ins,
|
|
143
|
-
"deletions": dele,
|
|
144
|
-
"status": "binary" if ins_s == "-" else "modified",
|
|
145
|
-
})
|
|
146
|
-
return {
|
|
147
|
-
"count": len(files),
|
|
148
|
-
"insertions": ins_total,
|
|
149
|
-
"deletions": del_total,
|
|
150
|
-
"files": files,
|
|
151
|
-
}
|
|
124
|
+
"""Return final-worktree stats relative to ``base``.
|
|
125
|
+
|
|
126
|
+
``head`` remains in the signature for installed-layout compatibility. The
|
|
127
|
+
live working tree is the proof target, not only its current commit.
|
|
128
|
+
"""
|
|
129
|
+
stat, _ = collect_workspace_diff(repo_dir, str(base), False)
|
|
130
|
+
return stat
|
|
152
131
|
|
|
153
132
|
|
|
154
133
|
def _diff_sha256_from_stat(files_changed):
|
|
@@ -205,12 +184,42 @@ def _compute_headline(facts, degraded):
|
|
|
205
184
|
|
|
206
185
|
sec = facts.get("security") or {}
|
|
207
186
|
sec_high = bool(sec.get("ran") and (sec.get("high_active") or 0) > 0)
|
|
187
|
+
fn = facts.get("functional") or {}
|
|
188
|
+
fn_failed = bool(
|
|
189
|
+
os.environ.get("LOKI_FV_GATE") == "1"
|
|
190
|
+
and fn.get("ran")
|
|
191
|
+
and fn.get("functional_status") == "failed"
|
|
192
|
+
)
|
|
193
|
+
execution = facts.get("execution") or {}
|
|
194
|
+
execution_outcome = str(execution.get("outcome") or "").lower()
|
|
195
|
+
failed_run_statuses = {
|
|
196
|
+
"failed",
|
|
197
|
+
"force_stopped",
|
|
198
|
+
"inconclusive_spec_contradiction",
|
|
199
|
+
"interrupted",
|
|
200
|
+
"max_iterations_reached",
|
|
201
|
+
"max_retries_exceeded",
|
|
202
|
+
"paused",
|
|
203
|
+
"policy_blocked",
|
|
204
|
+
"provider_deadline_partial_mutation",
|
|
205
|
+
}
|
|
206
|
+
execution_failed = bool(
|
|
207
|
+
execution.get("terminated")
|
|
208
|
+
or execution.get("exit_code") not in (None, 0)
|
|
209
|
+
or str(execution.get("run_status") or "").lower() in failed_run_statuses
|
|
210
|
+
or (
|
|
211
|
+
execution_outcome
|
|
212
|
+
and execution_outcome not in ("complete", "completed", "success")
|
|
213
|
+
)
|
|
214
|
+
)
|
|
208
215
|
any_failed = (
|
|
209
|
-
|
|
216
|
+
execution_failed
|
|
217
|
+
or tests.get("status") == "failed"
|
|
210
218
|
or build.get("status") == "failed"
|
|
211
219
|
or any(g.get("status") == "failed"
|
|
212
220
|
for g in (facts.get("quality_gates") or []))
|
|
213
221
|
or sec_high
|
|
222
|
+
or fn_failed
|
|
214
223
|
)
|
|
215
224
|
if any_failed:
|
|
216
225
|
return "NOT VERIFIED"
|
|
@@ -294,6 +303,16 @@ def _recorded_diff_sha256(proof):
|
|
|
294
303
|
return None
|
|
295
304
|
|
|
296
305
|
|
|
306
|
+
def _recorded_tree_sha256(proof):
|
|
307
|
+
facts = proof.get("facts")
|
|
308
|
+
if isinstance(facts, dict):
|
|
309
|
+
git = facts.get("git")
|
|
310
|
+
if isinstance(git, dict) and git.get("tree_sha256"):
|
|
311
|
+
return str(git["tree_sha256"])
|
|
312
|
+
value = proof.get("tree_sha256")
|
|
313
|
+
return str(value) if value else None
|
|
314
|
+
|
|
315
|
+
|
|
297
316
|
def _recorded_degraded(proof):
|
|
298
317
|
"""Return the honesty.degraded list the generator disclosed, or []."""
|
|
299
318
|
honesty = proof.get("honesty")
|
|
@@ -404,6 +423,75 @@ def _load_proof(proof_path):
|
|
|
404
423
|
return data
|
|
405
424
|
|
|
406
425
|
|
|
426
|
+
def verify_integrity(proof):
|
|
427
|
+
"""Verify the receipt hash, signature, and recorded headline.
|
|
428
|
+
|
|
429
|
+
This is the canonical in-memory integrity check shared by the CLI verifier
|
|
430
|
+
and supervised execution binding. Repository drift is intentionally left to
|
|
431
|
+
verify(), whose caller supplies the repository path.
|
|
432
|
+
"""
|
|
433
|
+
result = {
|
|
434
|
+
"hash_ok": False,
|
|
435
|
+
"gpg_ok": "n/a",
|
|
436
|
+
"generator_trusted": True,
|
|
437
|
+
"headline_consistent": None,
|
|
438
|
+
"degraded": _recorded_degraded(proof) if isinstance(proof, dict) else [],
|
|
439
|
+
"reason": "",
|
|
440
|
+
"ok": False,
|
|
441
|
+
}
|
|
442
|
+
if not isinstance(proof, dict):
|
|
443
|
+
result["reason"] = "proof root is not a JSON object"
|
|
444
|
+
return result
|
|
445
|
+
|
|
446
|
+
verification = proof.get("verification")
|
|
447
|
+
if not isinstance(verification, dict) or not verification.get("hash"):
|
|
448
|
+
result["reason"] = "no verification.hash recorded; cannot prove integrity"
|
|
449
|
+
return result
|
|
450
|
+
|
|
451
|
+
unsigned = dict(proof)
|
|
452
|
+
unsigned.pop("verification", None)
|
|
453
|
+
canonical_bytes = _canonical(unsigned).encode("utf-8")
|
|
454
|
+
recorded_hash = str(verification.get("hash"))
|
|
455
|
+
recomputed = hashlib.sha256(canonical_bytes).hexdigest()
|
|
456
|
+
result["hash_ok"] = recomputed == recorded_hash
|
|
457
|
+
if not result["hash_ok"]:
|
|
458
|
+
result["reason"] = (
|
|
459
|
+
"integrity hash mismatch (proof.json was edited after signing)"
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
result["gpg_ok"] = _verify_gpg(
|
|
463
|
+
canonical_bytes, verification.get("gpg_signature")
|
|
464
|
+
)
|
|
465
|
+
result["generator_trusted"] = result["gpg_ok"] is not True
|
|
466
|
+
|
|
467
|
+
recorded_headline = _recorded_headline(proof)
|
|
468
|
+
facts = proof.get("facts")
|
|
469
|
+
if recorded_headline is not None and isinstance(facts, dict):
|
|
470
|
+
derived = _compute_headline(facts, _recorded_degraded_raw(proof))
|
|
471
|
+
result["headline_consistent"] = derived == recorded_headline
|
|
472
|
+
if not result["headline_consistent"] and not result["reason"]:
|
|
473
|
+
result["reason"] = (
|
|
474
|
+
"honesty.headline (%r) disagrees with the headline re-derived "
|
|
475
|
+
"from the recorded facts (%r); the headline was edited to "
|
|
476
|
+
"misrepresent the facts" % (recorded_headline, derived)
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
result["ok"] = bool(
|
|
480
|
+
result["hash_ok"]
|
|
481
|
+
and result["gpg_ok"] in (True, "n/a")
|
|
482
|
+
and result["headline_consistent"] is not False
|
|
483
|
+
)
|
|
484
|
+
if result["ok"]:
|
|
485
|
+
result["reason"] = ""
|
|
486
|
+
elif not result["reason"]:
|
|
487
|
+
result["reason"] = (
|
|
488
|
+
"gpg signature verification failed"
|
|
489
|
+
if result["gpg_ok"] is False
|
|
490
|
+
else "verification failed"
|
|
491
|
+
)
|
|
492
|
+
return result
|
|
493
|
+
|
|
494
|
+
|
|
407
495
|
def verify(proof_path, repo_dir="."):
|
|
408
496
|
"""Re-verify a proof.json against the repo.
|
|
409
497
|
|
|
@@ -444,47 +532,21 @@ def verify(proof_path, repo_dir="."):
|
|
|
444
532
|
"""
|
|
445
533
|
proof = _load_proof(proof_path)
|
|
446
534
|
|
|
535
|
+
integrity = verify_integrity(proof)
|
|
447
536
|
result = {
|
|
448
|
-
|
|
537
|
+
**integrity,
|
|
449
538
|
"diff_drift": None,
|
|
450
539
|
"diff_recheck": {"recorded": None, "current": None},
|
|
451
|
-
"
|
|
452
|
-
"
|
|
453
|
-
"headline_consistent": None,
|
|
454
|
-
"degraded": _recorded_degraded(proof),
|
|
455
|
-
"reason": "",
|
|
540
|
+
"tree_drift": None,
|
|
541
|
+
"tree_recheck": {"recorded": None, "current": None},
|
|
456
542
|
"ok": False,
|
|
457
543
|
}
|
|
458
544
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
result["reason"] = "no verification.hash recorded; cannot prove integrity"
|
|
545
|
+
if not integrity["hash_ok"] and not (
|
|
546
|
+
isinstance(proof.get("verification"), dict)
|
|
547
|
+
and proof["verification"].get("hash")
|
|
548
|
+
):
|
|
464
549
|
return result
|
|
465
|
-
recorded_hash = str(verification.get("hash"))
|
|
466
|
-
|
|
467
|
-
# Recompute over the canonical form with verification REMOVED, exactly as
|
|
468
|
-
# the generator hashed it (hash computed before verification was attached).
|
|
469
|
-
unsigned = dict(proof)
|
|
470
|
-
unsigned.pop("verification", None)
|
|
471
|
-
canonical_str = _canonical(unsigned)
|
|
472
|
-
canonical_bytes = canonical_str.encode("utf-8")
|
|
473
|
-
recomputed = hashlib.sha256(canonical_bytes).hexdigest()
|
|
474
|
-
result["hash_ok"] = (recomputed == recorded_hash)
|
|
475
|
-
if not result["hash_ok"]:
|
|
476
|
-
result["reason"] = "integrity hash mismatch (proof.json was edited after signing)"
|
|
477
|
-
# Continue to gather drift/gpg signals for the report, but ok stays False.
|
|
478
|
-
|
|
479
|
-
# ----- 3. GPG (compute before returning so the report is complete) -----
|
|
480
|
-
gpg_sig = verification.get("gpg_signature")
|
|
481
|
-
result["gpg_ok"] = _verify_gpg(canonical_bytes, gpg_sig)
|
|
482
|
-
|
|
483
|
-
# generator_trusted: only a VALID signature (gpg_ok is True) means the
|
|
484
|
-
# generator is NOT trusted (neutral non-forgeability). "n/a" or a bad sig
|
|
485
|
-
# leaves the generator trusted -- the facts are taken at face value and a
|
|
486
|
-
# consistent forger is NOT caught. Stated so ok=True still discloses it.
|
|
487
|
-
result["generator_trusted"] = (result["gpg_ok"] is not True)
|
|
488
550
|
|
|
489
551
|
# ----- 2. DRIFT CHECK --------------------------------------------------
|
|
490
552
|
recorded_stat = _recorded_diff_stat(proof)
|
|
@@ -559,37 +621,24 @@ def verify(proof_path, repo_dir="."):
|
|
|
559
621
|
if drift and not result["reason"]:
|
|
560
622
|
result["reason"] = "recorded diff no longer matches the repo (drift detected)"
|
|
561
623
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
if recorded_headline is not None and isinstance(facts, dict):
|
|
575
|
-
derived = _compute_headline(facts, _recorded_degraded_raw(proof))
|
|
576
|
-
result["headline_consistent"] = (derived == recorded_headline)
|
|
577
|
-
if not result["headline_consistent"] and not result["reason"]:
|
|
578
|
-
result["reason"] = (
|
|
579
|
-
"honesty.headline (%r) disagrees with the headline re-derived "
|
|
580
|
-
"from the recorded facts (%r); the headline was edited to "
|
|
581
|
-
"misrepresent the facts" % (recorded_headline, derived)
|
|
582
|
-
)
|
|
583
|
-
else:
|
|
584
|
-
# No recorded headline, or no facts to re-derive from: cannot check.
|
|
585
|
-
result["headline_consistent"] = None
|
|
624
|
+
recorded_tree = _recorded_tree_sha256(proof)
|
|
625
|
+
result["tree_recheck"]["recorded"] = recorded_tree
|
|
626
|
+
if recorded_tree:
|
|
627
|
+
current_tree = compute_tree_digest(repo_dir)
|
|
628
|
+
result["tree_recheck"]["current"] = current_tree or None
|
|
629
|
+
if not current_tree:
|
|
630
|
+
if not result["reason"]:
|
|
631
|
+
result["reason"] = "final workspace tree could not be re-derived"
|
|
632
|
+
else:
|
|
633
|
+
result["tree_drift"] = current_tree != recorded_tree
|
|
634
|
+
if result["tree_drift"] and not result["reason"]:
|
|
635
|
+
result["reason"] = "recorded final workspace tree no longer matches the repo"
|
|
586
636
|
|
|
587
637
|
# ----- overall verdict -------------------------------------------------
|
|
588
638
|
result["ok"] = bool(
|
|
589
|
-
|
|
639
|
+
integrity["ok"]
|
|
590
640
|
and result["diff_drift"] is False
|
|
591
|
-
and result["
|
|
592
|
-
and result["headline_consistent"] is not False
|
|
641
|
+
and (not recorded_tree or result["tree_drift"] is False)
|
|
593
642
|
)
|
|
594
643
|
if result["ok"]:
|
|
595
644
|
result["reason"] = ""
|