memtrust-cli 0.3.0__py3-none-any.whl
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.
- memtrust/__init__.py +11 -0
- memtrust/adapters/__init__.py +62 -0
- memtrust/adapters/base.py +2020 -0
- memtrust/adapters/mem0_adapter.py +456 -0
- memtrust/adapters/mem0_direct_adapter.py +1217 -0
- memtrust/adapters/mempalace_adapter.py +1166 -0
- memtrust/adapters/openviking_adapter.py +570 -0
- memtrust/adapters/zep_graphiti_adapter.py +181 -0
- memtrust/adapters/zep_graphiti_selfhosted_adapter.py +882 -0
- memtrust/cli.py +1201 -0
- memtrust/evals/__init__.py +5 -0
- memtrust/evals/compression.py +235 -0
- memtrust/evals/contradiction.py +451 -0
- memtrust/evals/crash_recovery.py +290 -0
- memtrust/evals/embedder_cost.py +163 -0
- memtrust/evals/embedding_drift.py +273 -0
- memtrust/evals/episode_temporal_leak.py +182 -0
- memtrust/evals/extraction_quality.py +373 -0
- memtrust/evals/filter_injection.py +371 -0
- memtrust/evals/language_degradation.py +189 -0
- memtrust/evals/lock_contention.py +263 -0
- memtrust/evals/locomo.py +392 -0
- memtrust/evals/longmemeval.py +249 -0
- memtrust/evals/mempalace_metadata_scale.py +494 -0
- memtrust/evals/migration_rollback.py +276 -0
- memtrust/evals/orphan_cleanup.py +235 -0
- memtrust/evals/ranking_quality.py +303 -0
- memtrust/evals/resource_sync_safety.py +336 -0
- memtrust/evals/result_consistency.py +239 -0
- memtrust/evals/scale_fixtures.py +189 -0
- memtrust/evals/scale_stress.py +502 -0
- memtrust/evals/stats_accuracy.py +240 -0
- memtrust/evals/temporal_kg_boundary.py +281 -0
- memtrust/receipt.py +318 -0
- memtrust/scoring/__init__.py +1 -0
- memtrust/scoring/cost_tracker.py +199 -0
- memtrust/scoring/llm_judge.py +161 -0
- memtrust_cli-0.3.0.dist-info/METADATA +624 -0
- memtrust_cli-0.3.0.dist-info/RECORD +42 -0
- memtrust_cli-0.3.0.dist-info/WHEEL +4 -0
- memtrust_cli-0.3.0.dist-info/entry_points.txt +2 -0
- memtrust_cli-0.3.0.dist-info/licenses/LICENSE +202 -0
memtrust/cli.py
ADDED
|
@@ -0,0 +1,1201 @@
|
|
|
1
|
+
"""`memtrust run` and `memtrust report` -- the CLI entry points.
|
|
2
|
+
|
|
3
|
+
`memtrust run` executes the requested eval suite against whichever
|
|
4
|
+
requested backends actually have credentials configured. An unconfigured
|
|
5
|
+
backend prints SKIPPED and the run continues -- this command never raises
|
|
6
|
+
on missing credentials, which is what lets it run in a fresh clone or in
|
|
7
|
+
CI with zero vendor API keys. See adapters/base.py's
|
|
8
|
+
BackendNotConfiguredError contract.
|
|
9
|
+
|
|
10
|
+
`memtrust report` reads a prior run's JSON output and prints a formatted
|
|
11
|
+
summary -- it never re-runs anything or re-derives a score, it only
|
|
12
|
+
reformats what a `memtrust run` invocation already produced and wrote to
|
|
13
|
+
disk.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import sys
|
|
20
|
+
from dataclasses import asdict
|
|
21
|
+
from datetime import UTC, datetime
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
import click
|
|
26
|
+
from rich.console import Console
|
|
27
|
+
from rich.table import Table
|
|
28
|
+
|
|
29
|
+
from memtrust import __version__
|
|
30
|
+
from memtrust.adapters import ADAPTER_REGISTRY
|
|
31
|
+
from memtrust.adapters.base import BackendNotConfiguredError, MemoryBackendAdapter
|
|
32
|
+
from memtrust.evals.compression import CompressionEvalResult, run_compression_eval
|
|
33
|
+
from memtrust.evals.contradiction import ContradictionEvalResult, run_contradiction_eval
|
|
34
|
+
from memtrust.evals.crash_recovery import CrashRecoveryEvalResult, run_crash_recovery_eval
|
|
35
|
+
from memtrust.evals.embedding_drift import EmbeddingDriftEvalResult, run_embedding_drift_eval
|
|
36
|
+
from memtrust.evals.extraction_quality import (
|
|
37
|
+
ExtractionQualityEvalResult,
|
|
38
|
+
run_extraction_quality_eval,
|
|
39
|
+
)
|
|
40
|
+
from memtrust.evals.filter_injection import FilterInjectionEvalResult, run_filter_injection_eval
|
|
41
|
+
from memtrust.evals.lock_contention import LockContentionEvalResult, run_lock_contention_eval
|
|
42
|
+
from memtrust.evals.locomo import LoCoMoResult, load_exclude_question_ids, run_locomo
|
|
43
|
+
from memtrust.evals.longmemeval import LongMemEvalResult, run_longmemeval
|
|
44
|
+
from memtrust.evals.migration_rollback import (
|
|
45
|
+
MigrationRollbackEvalResult,
|
|
46
|
+
run_migration_rollback_eval,
|
|
47
|
+
)
|
|
48
|
+
from memtrust.evals.orphan_cleanup import OrphanCleanupEvalResult, run_orphan_cleanup_eval
|
|
49
|
+
from memtrust.evals.ranking_quality import RankingQualityEvalResult, run_ranking_quality_eval
|
|
50
|
+
from memtrust.evals.resource_sync_safety import ResourceSyncEvalResult, run_resource_sync_eval
|
|
51
|
+
from memtrust.evals.result_consistency import ConsistencyEvalResult, run_result_consistency_eval
|
|
52
|
+
from memtrust.evals.scale_stress import (
|
|
53
|
+
DEFAULT_N_RECORDS,
|
|
54
|
+
ScaleTestResult,
|
|
55
|
+
run_scale_stress_eval,
|
|
56
|
+
)
|
|
57
|
+
from memtrust.evals.stats_accuracy import StatsAccuracyEvalResult, run_stats_accuracy_eval
|
|
58
|
+
from memtrust.receipt import (
|
|
59
|
+
PUBLIC_KEY_ENV_VAR,
|
|
60
|
+
ReceiptError,
|
|
61
|
+
receipt_path_for,
|
|
62
|
+
sign_report_with_keyfile,
|
|
63
|
+
verify_receipt_file,
|
|
64
|
+
write_keypair,
|
|
65
|
+
)
|
|
66
|
+
from memtrust.scoring.cost_tracker import CostTracker
|
|
67
|
+
from memtrust.scoring.llm_judge import LLMJudge
|
|
68
|
+
|
|
69
|
+
#: Explicit width rather than relying on terminal auto-detection -- with 16
|
|
70
|
+
#: evals now registered, the `report` table has 18 columns; under a
|
|
71
|
+
#: non-tty runner (tests, CI logs) rich's default-width fallback wraps
|
|
72
|
+
#: cell text across lines, which is cosmetic in a real terminal but breaks
|
|
73
|
+
#: substring assertions on rendered output. A fixed wide width keeps
|
|
74
|
+
#: rendering deterministic in both contexts.
|
|
75
|
+
console = Console(width=400)
|
|
76
|
+
|
|
77
|
+
#: The 4 canonical, non-aliased backend names v0.1 tracks at full eval
|
|
78
|
+
#: depth. "zep" and "graphiti" both resolve to the same adapter in
|
|
79
|
+
#: ADAPTER_REGISTRY; "all" expands to this list, not to every registry key,
|
|
80
|
+
#: so a backend is never silently evaluated twice under two names.
|
|
81
|
+
ALL_BACKENDS = ["mempalace", "mem0", "zep", "openviking"]
|
|
82
|
+
ALL_EVALS = [
|
|
83
|
+
"longmemeval",
|
|
84
|
+
"locomo",
|
|
85
|
+
"contradiction",
|
|
86
|
+
"resource_sync_safety",
|
|
87
|
+
"compression",
|
|
88
|
+
"ranking_quality",
|
|
89
|
+
"scale_stress",
|
|
90
|
+
"embedding_drift",
|
|
91
|
+
"crash_recovery",
|
|
92
|
+
"extraction_quality",
|
|
93
|
+
"migration_rollback",
|
|
94
|
+
"filter_injection",
|
|
95
|
+
"lock_contention",
|
|
96
|
+
"stats_accuracy",
|
|
97
|
+
"orphan_cleanup",
|
|
98
|
+
"result_consistency",
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _resolve_backend_names(backends_arg: str) -> list[str]:
|
|
103
|
+
if backends_arg.strip().lower() == "all":
|
|
104
|
+
return list(ALL_BACKENDS)
|
|
105
|
+
names = [n.strip().lower() for n in backends_arg.split(",") if n.strip()]
|
|
106
|
+
unknown = [n for n in names if n not in ADAPTER_REGISTRY]
|
|
107
|
+
if unknown:
|
|
108
|
+
raise click.BadParameter(
|
|
109
|
+
f"unknown backend(s): {', '.join(unknown)}. "
|
|
110
|
+
f"Known backends: {', '.join(sorted(set(ALL_BACKENDS)))}"
|
|
111
|
+
)
|
|
112
|
+
return names
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _resolve_eval_names(eval_arg: str) -> list[str]:
|
|
116
|
+
if eval_arg.strip().lower() == "all":
|
|
117
|
+
return list(ALL_EVALS)
|
|
118
|
+
names = [n.strip().lower() for n in eval_arg.split(",") if n.strip()]
|
|
119
|
+
unknown = [n for n in names if n not in ALL_EVALS]
|
|
120
|
+
if unknown:
|
|
121
|
+
raise click.BadParameter(
|
|
122
|
+
f"unknown eval(s): {', '.join(unknown)}. Known evals: {', '.join(ALL_EVALS)}"
|
|
123
|
+
)
|
|
124
|
+
return names
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _serialize_eval_result(result: object) -> dict[str, Any]:
|
|
128
|
+
"""Best-effort dataclass -> plain dict for JSON output. Nested
|
|
129
|
+
dataclasses (case results) are converted the same way via asdict.
|
|
130
|
+
"""
|
|
131
|
+
if isinstance(result, LongMemEvalResult):
|
|
132
|
+
return {
|
|
133
|
+
"backend": result.backend_name,
|
|
134
|
+
"dataset_path": result.dataset_path,
|
|
135
|
+
"accuracy": result.accuracy,
|
|
136
|
+
"judge_unavailable": result.judge_unavailable,
|
|
137
|
+
"n_cases": len(result.case_results),
|
|
138
|
+
"n_graded": len(result.graded_cases),
|
|
139
|
+
"n_records_empty": result.n_records_empty,
|
|
140
|
+
"cases": [asdict(c) for c in result.case_results],
|
|
141
|
+
}
|
|
142
|
+
if isinstance(result, LoCoMoResult):
|
|
143
|
+
return {
|
|
144
|
+
"backend": result.backend_name,
|
|
145
|
+
"dataset_path": result.dataset_path,
|
|
146
|
+
"accuracy": result.accuracy,
|
|
147
|
+
"non_adversarial_accuracy": result.non_adversarial_accuracy,
|
|
148
|
+
"accuracy_by_category": result.accuracy_by_category(),
|
|
149
|
+
"n_cases": len(result.case_results),
|
|
150
|
+
"n_graded": len(result.graded_cases),
|
|
151
|
+
"n_records_empty": result.n_records_empty,
|
|
152
|
+
"n_excluded_ground_truth": result.n_excluded_ground_truth,
|
|
153
|
+
"cases": [asdict(c) for c in result.case_results],
|
|
154
|
+
}
|
|
155
|
+
if isinstance(result, ContradictionEvalResult):
|
|
156
|
+
return {
|
|
157
|
+
"backend": result.backend_name,
|
|
158
|
+
"dataset_path": result.dataset_path,
|
|
159
|
+
"flagged_rate": result.flagged_rate,
|
|
160
|
+
"silent_overwrite_rate": result.silent_overwrite_rate,
|
|
161
|
+
"served_stale_rate": result.served_stale_rate,
|
|
162
|
+
"not_applicable_rate": result.not_applicable_rate,
|
|
163
|
+
"empty_or_lost_rate": result.empty_or_lost_rate,
|
|
164
|
+
"n_cases": len(result.case_results),
|
|
165
|
+
"cases": [
|
|
166
|
+
{
|
|
167
|
+
"case_id": c.case.case_id,
|
|
168
|
+
"subject": c.case.subject,
|
|
169
|
+
"signal": str(c.signal),
|
|
170
|
+
"adapter_reported_signal": (
|
|
171
|
+
str(c.adapter_reported_signal) if c.adapter_reported_signal else None
|
|
172
|
+
),
|
|
173
|
+
"error": c.error,
|
|
174
|
+
}
|
|
175
|
+
for c in result.case_results
|
|
176
|
+
],
|
|
177
|
+
}
|
|
178
|
+
if isinstance(result, ResourceSyncEvalResult):
|
|
179
|
+
return {
|
|
180
|
+
"backend": result.backend_name,
|
|
181
|
+
"dataset_path": result.dataset_path,
|
|
182
|
+
"skipped": result.skipped,
|
|
183
|
+
"skip_reason": result.skip_reason,
|
|
184
|
+
"user_file_deletion_rate": result.user_file_deletion_rate,
|
|
185
|
+
"preserved_rate": result.preserved_rate,
|
|
186
|
+
"overwritten_unchanged_rate": result.overwritten_unchanged_rate,
|
|
187
|
+
"nested_content_unindexed_rate": result.nested_content_unindexed_rate,
|
|
188
|
+
"n_files": len(result.file_results),
|
|
189
|
+
"files": [
|
|
190
|
+
{
|
|
191
|
+
"case_id": f.case_id,
|
|
192
|
+
"path_suffix": f.path_suffix,
|
|
193
|
+
"origin": f.origin,
|
|
194
|
+
"signal": str(f.signal),
|
|
195
|
+
"indexed_after_resync": f.indexed_after_resync,
|
|
196
|
+
"error": f.error,
|
|
197
|
+
}
|
|
198
|
+
for f in result.file_results
|
|
199
|
+
],
|
|
200
|
+
}
|
|
201
|
+
if isinstance(result, RankingQualityEvalResult):
|
|
202
|
+
return {
|
|
203
|
+
"backend": result.backend_name,
|
|
204
|
+
"dataset_path": result.dataset_path,
|
|
205
|
+
"signal_driven_rate": result.signal_driven_rate,
|
|
206
|
+
"missing_ordering_key_rate": result.missing_ordering_key_rate,
|
|
207
|
+
"order_inconsistent_rate": result.order_inconsistent_rate,
|
|
208
|
+
"not_applicable_rate": result.not_applicable_rate,
|
|
209
|
+
"n_cases": len(result.case_results),
|
|
210
|
+
"cases": [
|
|
211
|
+
{
|
|
212
|
+
"case_id": c.case.case_id,
|
|
213
|
+
"ranking_field": c.case.ranking_field,
|
|
214
|
+
"signal": str(c.signal),
|
|
215
|
+
"adapter_reported_signal": (
|
|
216
|
+
str(c.adapter_reported_signal) if c.adapter_reported_signal else None
|
|
217
|
+
),
|
|
218
|
+
"matches_insertion_order": c.matches_insertion_order,
|
|
219
|
+
"error": c.error,
|
|
220
|
+
}
|
|
221
|
+
for c in result.case_results
|
|
222
|
+
],
|
|
223
|
+
}
|
|
224
|
+
if isinstance(result, CrashRecoveryEvalResult):
|
|
225
|
+
return {
|
|
226
|
+
"backend": result.backend_name,
|
|
227
|
+
"dataset_path": result.dataset_path,
|
|
228
|
+
"skipped": result.skipped,
|
|
229
|
+
"skip_reason": result.skip_reason,
|
|
230
|
+
"recovered_rate": result.recovered_rate,
|
|
231
|
+
"index_lost_data_survived_rate": result.index_lost_data_survived_rate,
|
|
232
|
+
"data_lost_rate": result.data_lost_rate,
|
|
233
|
+
"n_cases": len(result.case_results),
|
|
234
|
+
"cases": [
|
|
235
|
+
{
|
|
236
|
+
"case_id": c.case.case_id,
|
|
237
|
+
"signal": str(c.signal),
|
|
238
|
+
"present_before_crash": c.present_before_crash,
|
|
239
|
+
"queryable_after_crash": c.queryable_after_crash,
|
|
240
|
+
"raw_store_contains_after_crash": c.raw_store_contains_after_crash,
|
|
241
|
+
"error": c.error,
|
|
242
|
+
}
|
|
243
|
+
for c in result.case_results
|
|
244
|
+
],
|
|
245
|
+
}
|
|
246
|
+
if isinstance(result, MigrationRollbackEvalResult):
|
|
247
|
+
return {
|
|
248
|
+
"backend": result.backend_name,
|
|
249
|
+
"dataset_path": result.dataset_path,
|
|
250
|
+
"skipped": result.skipped,
|
|
251
|
+
"skip_reason": result.skip_reason,
|
|
252
|
+
"restored_rate": result.restored_rate,
|
|
253
|
+
"data_lost_rate": result.data_lost_rate,
|
|
254
|
+
"n_cases": len(result.case_results),
|
|
255
|
+
"cases": [
|
|
256
|
+
{
|
|
257
|
+
"case_id": c.case.case_id,
|
|
258
|
+
"signal": str(c.signal),
|
|
259
|
+
"original_data_recoverable": c.original_data_recoverable,
|
|
260
|
+
"adapter_reported_recoverable": c.adapter_reported_recoverable,
|
|
261
|
+
"error": c.error,
|
|
262
|
+
}
|
|
263
|
+
for c in result.case_results
|
|
264
|
+
],
|
|
265
|
+
}
|
|
266
|
+
if isinstance(result, ExtractionQualityEvalResult):
|
|
267
|
+
return {
|
|
268
|
+
"backend": result.backend_name,
|
|
269
|
+
"dataset_path": result.dataset_path,
|
|
270
|
+
"junk_retained_rate": result.junk_retained_rate,
|
|
271
|
+
"junk_rejected_rate": result.junk_rejected_rate,
|
|
272
|
+
"valid_retained_rate": result.valid_retained_rate,
|
|
273
|
+
"valid_lost_rate": result.valid_lost_rate,
|
|
274
|
+
"feedback_loop_duplicate_rate": result.feedback_loop_duplicate_rate,
|
|
275
|
+
"n_cases": len(result.case_results),
|
|
276
|
+
"n_feedback_loop_cases": len(result.feedback_loop_results),
|
|
277
|
+
"cases": [
|
|
278
|
+
{
|
|
279
|
+
"case_id": c.case.case_id,
|
|
280
|
+
"category": c.case.category,
|
|
281
|
+
"should_be_stored": c.case.should_be_stored,
|
|
282
|
+
"signal": str(c.signal),
|
|
283
|
+
"retrieved": c.retrieved,
|
|
284
|
+
"error": c.error,
|
|
285
|
+
}
|
|
286
|
+
for c in result.case_results
|
|
287
|
+
],
|
|
288
|
+
"feedback_loop_cases": [
|
|
289
|
+
{
|
|
290
|
+
"case_id": c.case.case_id,
|
|
291
|
+
"signal": str(c.signal),
|
|
292
|
+
"records_after_first_store": c.records_after_first_store,
|
|
293
|
+
"records_after_second_store": c.records_after_second_store,
|
|
294
|
+
"error": c.error,
|
|
295
|
+
}
|
|
296
|
+
for c in result.feedback_loop_results
|
|
297
|
+
],
|
|
298
|
+
}
|
|
299
|
+
if isinstance(result, FilterInjectionEvalResult):
|
|
300
|
+
return {
|
|
301
|
+
"backend": result.backend_name,
|
|
302
|
+
"dataset_path": result.dataset_path,
|
|
303
|
+
"skipped": result.skipped,
|
|
304
|
+
"skip_reason": result.skip_reason,
|
|
305
|
+
"injection_succeeded_rate": result.injection_succeeded_rate,
|
|
306
|
+
"malicious_rejected_rate": result.malicious_rejected_rate,
|
|
307
|
+
"benign_accepted_rate": result.benign_accepted_rate,
|
|
308
|
+
"benign_false_positive_rate": result.benign_false_positive_rate,
|
|
309
|
+
"n_cases": len(result.case_results),
|
|
310
|
+
"cases": [
|
|
311
|
+
{
|
|
312
|
+
"case_id": c.case.case_id,
|
|
313
|
+
"malicious": c.case.malicious,
|
|
314
|
+
"filter_key": c.case.filter_key,
|
|
315
|
+
"signal": str(c.signal),
|
|
316
|
+
"probe_accepted": c.probe_accepted,
|
|
317
|
+
"error": c.error,
|
|
318
|
+
}
|
|
319
|
+
for c in result.case_results
|
|
320
|
+
],
|
|
321
|
+
}
|
|
322
|
+
if isinstance(result, CompressionEvalResult):
|
|
323
|
+
return {
|
|
324
|
+
"backend": result.backend_name,
|
|
325
|
+
"dataset_path": result.dataset_path,
|
|
326
|
+
"modes": result.modes,
|
|
327
|
+
"mean_fidelity_by_mode": result.mean_fidelity_by_mode(),
|
|
328
|
+
"fidelity_drop_pp": result.fidelity_drop_pp,
|
|
329
|
+
"mode_results": {
|
|
330
|
+
mode: {
|
|
331
|
+
"mean_fidelity": mode_result.mean_fidelity,
|
|
332
|
+
"n_cases": len(mode_result.case_results),
|
|
333
|
+
"n_scored": len(mode_result.scored_cases),
|
|
334
|
+
"cases": [asdict(c) for c in mode_result.case_results],
|
|
335
|
+
}
|
|
336
|
+
for mode, mode_result in result.mode_results.items()
|
|
337
|
+
},
|
|
338
|
+
}
|
|
339
|
+
if isinstance(result, ScaleTestResult):
|
|
340
|
+
return {
|
|
341
|
+
"backend": result.backend_name,
|
|
342
|
+
"n_records_requested": result.n_records_requested,
|
|
343
|
+
"seed": result.seed,
|
|
344
|
+
"signal": str(result.signal),
|
|
345
|
+
"records_stored": result.records_stored,
|
|
346
|
+
"records_store_errors": result.records_store_errors,
|
|
347
|
+
"records_checked": result.records_checked,
|
|
348
|
+
"records_recoverable": result.records_recoverable,
|
|
349
|
+
"recall_degradation_pct": result.recall_degradation_pct,
|
|
350
|
+
"anchor_lost_at_n": result.anchor_lost_at_n,
|
|
351
|
+
"latency_p99_ms": result.latency_p99_ms,
|
|
352
|
+
"error": result.error,
|
|
353
|
+
"checkpoints": [
|
|
354
|
+
{
|
|
355
|
+
"checkpoint_n": c.checkpoint_n,
|
|
356
|
+
"records_stored_so_far": c.records_stored_so_far,
|
|
357
|
+
"recall_rate": c.recall_rate,
|
|
358
|
+
"anchor_recall": c.anchor_recall,
|
|
359
|
+
"latency_p50_ms": c.latency_p50_ms,
|
|
360
|
+
"latency_p99_ms": c.latency_p99_ms,
|
|
361
|
+
"n_needle_queries": len(c.needle_queries),
|
|
362
|
+
}
|
|
363
|
+
for c in result.checkpoints
|
|
364
|
+
],
|
|
365
|
+
}
|
|
366
|
+
if isinstance(result, EmbeddingDriftEvalResult):
|
|
367
|
+
return {
|
|
368
|
+
"backend": result.backend_name,
|
|
369
|
+
"dataset_path": result.dataset_path,
|
|
370
|
+
"drift_rate": result.drift_rate,
|
|
371
|
+
"clean_rate": result.clean_rate,
|
|
372
|
+
"not_applicable_rate": result.not_applicable_rate,
|
|
373
|
+
"n_records": len(result.record_results),
|
|
374
|
+
"records": [
|
|
375
|
+
{
|
|
376
|
+
"case_id": r.case_id,
|
|
377
|
+
"content": r.content,
|
|
378
|
+
"model_a_label": r.model_a_label,
|
|
379
|
+
"model_b_label": r.model_b_label,
|
|
380
|
+
"signal": str(r.signal),
|
|
381
|
+
"error": r.error,
|
|
382
|
+
}
|
|
383
|
+
for r in result.record_results
|
|
384
|
+
],
|
|
385
|
+
}
|
|
386
|
+
if isinstance(result, LockContentionEvalResult):
|
|
387
|
+
return {
|
|
388
|
+
"backend": result.backend_name,
|
|
389
|
+
"resource_path": result.resource_path,
|
|
390
|
+
"budget_ms": result.budget_ms,
|
|
391
|
+
"n_concurrent": result.n_concurrent,
|
|
392
|
+
"skipped": result.skipped,
|
|
393
|
+
"skip_reason": result.skip_reason,
|
|
394
|
+
"signal": str(result.signal),
|
|
395
|
+
"stalled_count": result.stalled_count,
|
|
396
|
+
"max_latency_ms": result.max_latency_ms,
|
|
397
|
+
"requests": [
|
|
398
|
+
{
|
|
399
|
+
"worker_index": r.worker_index,
|
|
400
|
+
"completed": r.completed,
|
|
401
|
+
"succeeded": r.succeeded,
|
|
402
|
+
"latency_ms": r.latency_ms,
|
|
403
|
+
"error": r.error,
|
|
404
|
+
}
|
|
405
|
+
for r in result.requests
|
|
406
|
+
],
|
|
407
|
+
}
|
|
408
|
+
if isinstance(result, StatsAccuracyEvalResult):
|
|
409
|
+
return {
|
|
410
|
+
"backend": result.backend_name,
|
|
411
|
+
"n_records_requested": result.n_records_requested,
|
|
412
|
+
"records_stored": result.records_stored,
|
|
413
|
+
"skipped": result.skipped,
|
|
414
|
+
"skip_reason": result.skip_reason,
|
|
415
|
+
"signal": str(result.signal),
|
|
416
|
+
"verified_count": result.verified_count,
|
|
417
|
+
"reported_count": result.reported_count,
|
|
418
|
+
"undercount_gap": result.undercount_gap,
|
|
419
|
+
"error": result.error,
|
|
420
|
+
}
|
|
421
|
+
if isinstance(result, OrphanCleanupEvalResult):
|
|
422
|
+
return {
|
|
423
|
+
"backend": result.backend_name,
|
|
424
|
+
"dataset_path": result.dataset_path,
|
|
425
|
+
"skipped": result.skipped,
|
|
426
|
+
"skip_reason": result.skip_reason,
|
|
427
|
+
"orphaned_vector_entry_rate": result.orphaned_vector_entry_rate,
|
|
428
|
+
"clean_rate": result.clean_rate,
|
|
429
|
+
"n_files": len(result.file_results),
|
|
430
|
+
"files": [
|
|
431
|
+
{
|
|
432
|
+
"case_id": f.case_id,
|
|
433
|
+
"path_suffix": f.path_suffix,
|
|
434
|
+
"signal": str(f.signal),
|
|
435
|
+
"present_after_delete": f.present_after_delete,
|
|
436
|
+
"queryable_after_delete": f.queryable_after_delete,
|
|
437
|
+
"error": f.error,
|
|
438
|
+
}
|
|
439
|
+
for f in result.file_results
|
|
440
|
+
],
|
|
441
|
+
}
|
|
442
|
+
if isinstance(result, ConsistencyEvalResult):
|
|
443
|
+
return {
|
|
444
|
+
"backend": result.backend_name,
|
|
445
|
+
"dataset_path": result.dataset_path,
|
|
446
|
+
"consistent_rate": result.consistent_rate,
|
|
447
|
+
"inconsistent_rate": result.inconsistent_rate,
|
|
448
|
+
"n_cases": len(result.case_results),
|
|
449
|
+
"cases": [
|
|
450
|
+
{
|
|
451
|
+
"case_id": c.case.case_id,
|
|
452
|
+
"signal": str(c.signal),
|
|
453
|
+
"average_jaccard": c.average_jaccard,
|
|
454
|
+
"n_successful_runs": c.n_successful_runs,
|
|
455
|
+
"error": c.error,
|
|
456
|
+
}
|
|
457
|
+
for c in result.case_results
|
|
458
|
+
],
|
|
459
|
+
}
|
|
460
|
+
raise TypeError(f"no serializer for {type(result)!r}")
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
@click.group()
|
|
464
|
+
@click.version_option(version=__version__, prog_name="memtrust")
|
|
465
|
+
def main() -> None:
|
|
466
|
+
"""memtrust: an independent, reproducible benchmark harness for agent-memory backends."""
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
@main.command()
|
|
470
|
+
@click.option(
|
|
471
|
+
"--backends", default="all", show_default=True, help="Comma-separated backend list, or 'all'."
|
|
472
|
+
)
|
|
473
|
+
@click.option(
|
|
474
|
+
"--eval",
|
|
475
|
+
"eval_arg",
|
|
476
|
+
default="all",
|
|
477
|
+
show_default=True,
|
|
478
|
+
help=(
|
|
479
|
+
"Comma-separated eval list (longmemeval,locomo,contradiction,"
|
|
480
|
+
"resource_sync_safety,compression,ranking_quality,scale_stress,"
|
|
481
|
+
"embedding_drift,crash_recovery,extraction_quality,"
|
|
482
|
+
"migration_rollback,filter_injection,lock_contention,stats_accuracy,"
|
|
483
|
+
"orphan_cleanup,result_consistency), or 'all'."
|
|
484
|
+
),
|
|
485
|
+
)
|
|
486
|
+
@click.option(
|
|
487
|
+
"--output",
|
|
488
|
+
"output_path",
|
|
489
|
+
default=None,
|
|
490
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
491
|
+
help="Path to write the JSON report. Defaults to ./memtrust-report-<date>.json",
|
|
492
|
+
)
|
|
493
|
+
@click.option(
|
|
494
|
+
"--locomo-exclude-question-ids-file",
|
|
495
|
+
"locomo_exclude_ids_path",
|
|
496
|
+
default=None,
|
|
497
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
498
|
+
help=(
|
|
499
|
+
"Path to a file of known-bad-ground-truth LoCoMo question IDs to exclude from "
|
|
500
|
+
"scoring (JSON array, or one ID per line). See evals/locomo.py's "
|
|
501
|
+
"load_exclude_question_ids() and docs/methodology.md for the ID shape and how "
|
|
502
|
+
"a corrected list (e.g. derived from a published ground-truth audit) plugs in. "
|
|
503
|
+
"No such list ships with memtrust by default."
|
|
504
|
+
),
|
|
505
|
+
)
|
|
506
|
+
@click.option(
|
|
507
|
+
"--locomo-dataset-path",
|
|
508
|
+
"locomo_dataset_path",
|
|
509
|
+
default=None,
|
|
510
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
511
|
+
help=(
|
|
512
|
+
"Path to a real locomo10.json (download it yourself from "
|
|
513
|
+
"https://github.com/snap-research/locomo -- memtrust does not bundle or "
|
|
514
|
+
"auto-fetch it) to run the locomo eval against, instead of the bundled "
|
|
515
|
+
"synthetic tests/fixtures/locomo_sample.json. See docs/methodology.md's "
|
|
516
|
+
'"LoCoMo" section for the schema and download link. Ignored if --eval does '
|
|
517
|
+
"not include locomo."
|
|
518
|
+
),
|
|
519
|
+
)
|
|
520
|
+
@click.option(
|
|
521
|
+
"--scale-stress-n-records",
|
|
522
|
+
"scale_stress_n_records",
|
|
523
|
+
default=DEFAULT_N_RECORDS,
|
|
524
|
+
show_default=True,
|
|
525
|
+
type=int,
|
|
526
|
+
help=(
|
|
527
|
+
"How many synthetic records the scale_stress eval stores and re-queries. "
|
|
528
|
+
"Kept small by default so `memtrust run` stays fast in CI -- pass a much larger "
|
|
529
|
+
"value (e.g. 10000) to actually reach the corpus size volcengine/OpenViking#2850 "
|
|
530
|
+
"and getzep/graphiti#1275 manifest at, against a real configured backend. "
|
|
531
|
+
"See evals/scale_stress.py and docs/methodology.md."
|
|
532
|
+
),
|
|
533
|
+
)
|
|
534
|
+
@click.option(
|
|
535
|
+
"--sign",
|
|
536
|
+
"sign_key_path",
|
|
537
|
+
default=None,
|
|
538
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
539
|
+
help=(
|
|
540
|
+
"Path to an Ed25519 private key PEM file (see `memtrust keygen`). When given, "
|
|
541
|
+
"writes a signed receipt alongside the normal JSON report -- "
|
|
542
|
+
"<output>.receipt.json -- proving the report was produced by the holder of "
|
|
543
|
+
"this key and has not been altered since. Omit for plain, unsigned JSON "
|
|
544
|
+
"output (the default, unchanged from before this flag existed)."
|
|
545
|
+
),
|
|
546
|
+
)
|
|
547
|
+
def run(
|
|
548
|
+
backends: str,
|
|
549
|
+
eval_arg: str,
|
|
550
|
+
output_path: Path | None,
|
|
551
|
+
locomo_exclude_ids_path: Path | None,
|
|
552
|
+
locomo_dataset_path: Path | None,
|
|
553
|
+
scale_stress_n_records: int,
|
|
554
|
+
sign_key_path: Path | None,
|
|
555
|
+
) -> None:
|
|
556
|
+
"""Run the eval suite against the requested backends.
|
|
557
|
+
|
|
558
|
+
Backends without a configured credential env var print SKIPPED and
|
|
559
|
+
the run continues -- this command never crashes on missing
|
|
560
|
+
credentials.
|
|
561
|
+
"""
|
|
562
|
+
backend_names = _resolve_backend_names(backends)
|
|
563
|
+
eval_names = _resolve_eval_names(eval_arg)
|
|
564
|
+
locomo_exclude_ids = (
|
|
565
|
+
load_exclude_question_ids(locomo_exclude_ids_path)
|
|
566
|
+
if locomo_exclude_ids_path is not None
|
|
567
|
+
else None
|
|
568
|
+
)
|
|
569
|
+
cost_tracker = CostTracker()
|
|
570
|
+
judge = LLMJudge(cost_tracker=cost_tracker)
|
|
571
|
+
|
|
572
|
+
run_id = datetime.now(UTC).strftime("mt_%Y-%m-%dT%H%M%SZ")
|
|
573
|
+
report: dict[str, Any] = {
|
|
574
|
+
"run_id": run_id,
|
|
575
|
+
"memtrust_version": __version__,
|
|
576
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
|
577
|
+
"backends_requested": backend_names,
|
|
578
|
+
"evals_requested": eval_names,
|
|
579
|
+
"results": {},
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
console.print(f"[bold]memtrust {__version__}[/bold] -- run_id={run_id}")
|
|
583
|
+
console.print(f"Backends: {', '.join(backend_names)} Evals: {', '.join(eval_names)}\n")
|
|
584
|
+
|
|
585
|
+
for backend_name in backend_names:
|
|
586
|
+
adapter_cls = ADAPTER_REGISTRY[backend_name]
|
|
587
|
+
try:
|
|
588
|
+
adapter: MemoryBackendAdapter = adapter_cls()
|
|
589
|
+
except BackendNotConfiguredError as exc:
|
|
590
|
+
console.print(f"[yellow]{backend_name}: SKIPPED (not configured)[/yellow] -- {exc}")
|
|
591
|
+
report["results"][backend_name] = {
|
|
592
|
+
"status": "skipped",
|
|
593
|
+
"reason": str(exc),
|
|
594
|
+
"missing_env_var": exc.missing_env_var,
|
|
595
|
+
}
|
|
596
|
+
continue
|
|
597
|
+
|
|
598
|
+
console.print(f"[green]{backend_name}: configured[/green], running evals...")
|
|
599
|
+
backend_report: dict[str, Any] = {"status": "configured", "evals": {}}
|
|
600
|
+
|
|
601
|
+
if "longmemeval" in eval_names:
|
|
602
|
+
console.print(f" Running LongMemEval against {backend_name}...")
|
|
603
|
+
lme_result = run_longmemeval(adapter, judge)
|
|
604
|
+
backend_report["evals"]["longmemeval"] = _serialize_eval_result(lme_result)
|
|
605
|
+
acc = lme_result.accuracy
|
|
606
|
+
if acc is not None:
|
|
607
|
+
console.print(f" accuracy: {acc:.1%}")
|
|
608
|
+
else:
|
|
609
|
+
console.print(" accuracy: N/A (judge not configured)")
|
|
610
|
+
if lme_result.n_records_empty:
|
|
611
|
+
console.print(
|
|
612
|
+
f" [yellow]records_empty: {lme_result.n_records_empty}/"
|
|
613
|
+
f"{len(lme_result.case_results)}[/yellow] "
|
|
614
|
+
"(backend call succeeded but returned nothing)"
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
if "locomo" in eval_names:
|
|
618
|
+
console.print(f" Running LoCoMo against {backend_name}...")
|
|
619
|
+
locomo_run_kwargs: dict[str, Any] = {"exclude_question_ids": locomo_exclude_ids}
|
|
620
|
+
if locomo_dataset_path is not None:
|
|
621
|
+
locomo_run_kwargs["dataset_path"] = locomo_dataset_path
|
|
622
|
+
locomo_result = run_locomo(adapter, judge, **locomo_run_kwargs)
|
|
623
|
+
backend_report["evals"]["locomo"] = _serialize_eval_result(locomo_result)
|
|
624
|
+
acc = locomo_result.accuracy
|
|
625
|
+
non_adv_acc = locomo_result.non_adversarial_accuracy
|
|
626
|
+
if acc is not None:
|
|
627
|
+
console.print(f" accuracy (all categories, incl. adversarial): {acc:.1%}")
|
|
628
|
+
else:
|
|
629
|
+
console.print(" accuracy: N/A (judge not configured)")
|
|
630
|
+
if non_adv_acc is not None:
|
|
631
|
+
console.print(
|
|
632
|
+
f" non_adversarial_accuracy (excludes category 5): {non_adv_acc:.1%}"
|
|
633
|
+
)
|
|
634
|
+
else:
|
|
635
|
+
console.print(" non_adversarial_accuracy: N/A (judge not configured)")
|
|
636
|
+
if locomo_result.n_records_empty:
|
|
637
|
+
console.print(
|
|
638
|
+
f" [yellow]records_empty: {locomo_result.n_records_empty}/"
|
|
639
|
+
f"{len(locomo_result.case_results)}[/yellow] "
|
|
640
|
+
"(backend call succeeded but returned nothing)"
|
|
641
|
+
)
|
|
642
|
+
if locomo_result.n_excluded_ground_truth:
|
|
643
|
+
console.print(
|
|
644
|
+
f" excluded_ground_truth: {locomo_result.n_excluded_ground_truth}/"
|
|
645
|
+
f"{len(locomo_result.case_results)} "
|
|
646
|
+
"(known-bad ground truth, excluded via --locomo-exclude-question-ids-file)"
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
if "contradiction" in eval_names:
|
|
650
|
+
console.print(f" Running Contradiction-Detection against {backend_name}...")
|
|
651
|
+
contra_result = run_contradiction_eval(adapter)
|
|
652
|
+
backend_report["evals"]["contradiction"] = _serialize_eval_result(contra_result)
|
|
653
|
+
fr = contra_result.flagged_rate
|
|
654
|
+
so = contra_result.silent_overwrite_rate
|
|
655
|
+
ss = contra_result.served_stale_rate
|
|
656
|
+
eol = contra_result.empty_or_lost_rate
|
|
657
|
+
if fr is not None:
|
|
658
|
+
console.print(
|
|
659
|
+
f" flagged: {fr:.1%} silent-overwrite: {so:.1%} served-stale: {ss:.1%}"
|
|
660
|
+
f" empty-or-lost: {eol:.1%}"
|
|
661
|
+
)
|
|
662
|
+
else:
|
|
663
|
+
console.print(" N/A (no scoreable cases)")
|
|
664
|
+
|
|
665
|
+
if "resource_sync_safety" in eval_names:
|
|
666
|
+
console.print(f" Running Resource-Sync Safety against {backend_name}...")
|
|
667
|
+
rss_result = run_resource_sync_eval(adapter)
|
|
668
|
+
backend_report["evals"]["resource_sync_safety"] = _serialize_eval_result(rss_result)
|
|
669
|
+
if rss_result.skipped:
|
|
670
|
+
console.print(f" SKIPPED: {rss_result.skip_reason}")
|
|
671
|
+
else:
|
|
672
|
+
dr = rss_result.user_file_deletion_rate
|
|
673
|
+
ndr = rss_result.nested_content_unindexed_rate
|
|
674
|
+
if dr is not None:
|
|
675
|
+
ndr_str = f"{ndr:.1%}" if ndr is not None else "N/A"
|
|
676
|
+
console.print(
|
|
677
|
+
f" user-file deletion rate: {dr:.1%}"
|
|
678
|
+
f" nested-content-unindexed rate: {ndr_str}"
|
|
679
|
+
)
|
|
680
|
+
else:
|
|
681
|
+
console.print(" N/A (no scoreable files)")
|
|
682
|
+
|
|
683
|
+
if "compression" in eval_names:
|
|
684
|
+
console.print(f" Running Compression/Round-Trip-Fidelity against {backend_name}...")
|
|
685
|
+
compression_result = run_compression_eval(adapter)
|
|
686
|
+
backend_report["evals"]["compression"] = _serialize_eval_result(compression_result)
|
|
687
|
+
means = compression_result.mean_fidelity_by_mode()
|
|
688
|
+
if means:
|
|
689
|
+
rendered = " ".join(
|
|
690
|
+
f"{mode}: {value:.1%}" if value is not None else f"{mode}: N/A"
|
|
691
|
+
for mode, value in means.items()
|
|
692
|
+
)
|
|
693
|
+
console.print(f" fidelity by mode -- {rendered}")
|
|
694
|
+
else:
|
|
695
|
+
console.print(" N/A (no scoreable cases)")
|
|
696
|
+
|
|
697
|
+
if "ranking_quality" in eval_names:
|
|
698
|
+
console.print(f" Running Ranking-Quality against {backend_name}...")
|
|
699
|
+
ranking_result = run_ranking_quality_eval(adapter)
|
|
700
|
+
backend_report["evals"]["ranking_quality"] = _serialize_eval_result(ranking_result)
|
|
701
|
+
mok = ranking_result.missing_ordering_key_rate
|
|
702
|
+
sdr = ranking_result.signal_driven_rate
|
|
703
|
+
oir = ranking_result.order_inconsistent_rate
|
|
704
|
+
if mok is not None:
|
|
705
|
+
console.print(
|
|
706
|
+
f" missing-ordering-key: {mok:.1%} signal-driven: {sdr:.1%}"
|
|
707
|
+
f" order-inconsistent: {oir:.1%}"
|
|
708
|
+
)
|
|
709
|
+
else:
|
|
710
|
+
console.print(" N/A (no scoreable cases)")
|
|
711
|
+
|
|
712
|
+
if "scale_stress" in eval_names:
|
|
713
|
+
console.print(
|
|
714
|
+
f" Running Scale/Volume Stress ({scale_stress_n_records} records) "
|
|
715
|
+
f"against {backend_name}..."
|
|
716
|
+
)
|
|
717
|
+
scale_result = run_scale_stress_eval(adapter, n_records=scale_stress_n_records)
|
|
718
|
+
backend_report["evals"]["scale_stress"] = _serialize_eval_result(scale_result)
|
|
719
|
+
console.print(f" signal: {scale_result.signal}")
|
|
720
|
+
console.print(
|
|
721
|
+
f" records_stored: {scale_result.records_stored}/"
|
|
722
|
+
f"{scale_result.n_records_requested}"
|
|
723
|
+
f" records_recoverable: {scale_result.records_recoverable}/"
|
|
724
|
+
f"{scale_result.records_checked}"
|
|
725
|
+
)
|
|
726
|
+
if scale_result.recall_degradation_pct is not None:
|
|
727
|
+
console.print(
|
|
728
|
+
f" recall_degradation: {scale_result.recall_degradation_pct:.1f}pp"
|
|
729
|
+
)
|
|
730
|
+
if scale_result.anchor_lost_at_n is not None:
|
|
731
|
+
console.print(
|
|
732
|
+
f" [yellow]anchor record lost at n={scale_result.anchor_lost_at_n}"
|
|
733
|
+
"[/yellow] (earliest-stored content became unrecoverable as volume grew)"
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
if "embedding_drift" in eval_names:
|
|
737
|
+
console.print(f" Running Embedding-Drift/Consistency against {backend_name}...")
|
|
738
|
+
drift_result = run_embedding_drift_eval(adapter)
|
|
739
|
+
backend_report["evals"]["embedding_drift"] = _serialize_eval_result(drift_result)
|
|
740
|
+
dr = drift_result.drift_rate
|
|
741
|
+
if dr is not None:
|
|
742
|
+
console.print(
|
|
743
|
+
f" drift-rate: {dr:.1%} clean-rate: {drift_result.clean_rate:.1%}"
|
|
744
|
+
)
|
|
745
|
+
else:
|
|
746
|
+
console.print(" N/A (no scoreable records)")
|
|
747
|
+
|
|
748
|
+
if "crash_recovery" in eval_names:
|
|
749
|
+
console.print(f" Running Crash-Recovery against {backend_name}...")
|
|
750
|
+
crash_result = run_crash_recovery_eval(adapter)
|
|
751
|
+
backend_report["evals"]["crash_recovery"] = _serialize_eval_result(crash_result)
|
|
752
|
+
if crash_result.skipped:
|
|
753
|
+
console.print(f" SKIPPED: {crash_result.skip_reason}")
|
|
754
|
+
else:
|
|
755
|
+
ilds = crash_result.index_lost_data_survived_rate
|
|
756
|
+
rec = crash_result.recovered_rate
|
|
757
|
+
if ilds is not None:
|
|
758
|
+
console.print(f" index-lost-data-survived: {ilds:.1%} recovered: {rec:.1%}")
|
|
759
|
+
else:
|
|
760
|
+
console.print(" N/A (no scoreable cases)")
|
|
761
|
+
|
|
762
|
+
if "extraction_quality" in eval_names:
|
|
763
|
+
console.print(f" Running Extraction-Quality against {backend_name}...")
|
|
764
|
+
extraction_result = run_extraction_quality_eval(adapter)
|
|
765
|
+
backend_report["evals"]["extraction_quality"] = _serialize_eval_result(
|
|
766
|
+
extraction_result
|
|
767
|
+
)
|
|
768
|
+
jr = extraction_result.junk_retained_rate
|
|
769
|
+
vl = extraction_result.valid_lost_rate
|
|
770
|
+
fld = extraction_result.feedback_loop_duplicate_rate
|
|
771
|
+
if jr is not None:
|
|
772
|
+
vl_str = f"{vl:.1%}" if vl is not None else "N/A"
|
|
773
|
+
console.print(f" junk-retained: {jr:.1%} valid-lost: {vl_str}")
|
|
774
|
+
else:
|
|
775
|
+
console.print(" junk-retained: N/A (no scoreable cases)")
|
|
776
|
+
if fld is not None:
|
|
777
|
+
console.print(f" feedback-loop-duplicate: {fld:.1%}")
|
|
778
|
+
else:
|
|
779
|
+
console.print(" feedback-loop-duplicate: N/A (no scoreable feedback-loop cases)")
|
|
780
|
+
|
|
781
|
+
if "migration_rollback" in eval_names:
|
|
782
|
+
console.print(f" Running Migration-Rollback against {backend_name}...")
|
|
783
|
+
migration_result = run_migration_rollback_eval(adapter)
|
|
784
|
+
backend_report["evals"]["migration_rollback"] = _serialize_eval_result(migration_result)
|
|
785
|
+
if migration_result.skipped:
|
|
786
|
+
console.print(f" SKIPPED: {migration_result.skip_reason}")
|
|
787
|
+
else:
|
|
788
|
+
rr = migration_result.restored_rate
|
|
789
|
+
dl = migration_result.data_lost_rate
|
|
790
|
+
if rr is not None:
|
|
791
|
+
console.print(f" restored: {rr:.1%} data-lost: {dl:.1%}")
|
|
792
|
+
else:
|
|
793
|
+
console.print(" N/A (no scoreable cases)")
|
|
794
|
+
|
|
795
|
+
if "filter_injection" in eval_names:
|
|
796
|
+
console.print(f" Running Filter-Injection against {backend_name}...")
|
|
797
|
+
filter_injection_result = run_filter_injection_eval(adapter)
|
|
798
|
+
backend_report["evals"]["filter_injection"] = _serialize_eval_result(
|
|
799
|
+
filter_injection_result
|
|
800
|
+
)
|
|
801
|
+
if filter_injection_result.skipped:
|
|
802
|
+
console.print(f" SKIPPED: {filter_injection_result.skip_reason}")
|
|
803
|
+
else:
|
|
804
|
+
isr = filter_injection_result.injection_succeeded_rate
|
|
805
|
+
bfr = filter_injection_result.benign_false_positive_rate
|
|
806
|
+
bfr_str = f"{bfr:.1%}" if bfr is not None else "N/A"
|
|
807
|
+
if isr is not None:
|
|
808
|
+
console.print(
|
|
809
|
+
f" injection-succeeded: {isr:.1%} benign-false-positive: {bfr_str}"
|
|
810
|
+
)
|
|
811
|
+
else:
|
|
812
|
+
console.print(" N/A (no scoreable cases)")
|
|
813
|
+
|
|
814
|
+
if "lock_contention" in eval_names:
|
|
815
|
+
console.print(f" Running Lock-Contention/Hang-Detection against {backend_name}...")
|
|
816
|
+
lock_contention_result = run_lock_contention_eval(adapter)
|
|
817
|
+
backend_report["evals"]["lock_contention"] = _serialize_eval_result(
|
|
818
|
+
lock_contention_result
|
|
819
|
+
)
|
|
820
|
+
if lock_contention_result.skipped:
|
|
821
|
+
console.print(f" SKIPPED: {lock_contention_result.skip_reason}")
|
|
822
|
+
else:
|
|
823
|
+
console.print(
|
|
824
|
+
f" signal: {lock_contention_result.signal}"
|
|
825
|
+
f" stalled: {lock_contention_result.stalled_count}/"
|
|
826
|
+
f"{lock_contention_result.n_concurrent}"
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
if "stats_accuracy" in eval_names:
|
|
830
|
+
console.print(f" Running Stats/Dashboard-Accuracy against {backend_name}...")
|
|
831
|
+
stats_result = run_stats_accuracy_eval(adapter)
|
|
832
|
+
backend_report["evals"]["stats_accuracy"] = _serialize_eval_result(stats_result)
|
|
833
|
+
if stats_result.skipped:
|
|
834
|
+
console.print(f" SKIPPED: {stats_result.skip_reason}")
|
|
835
|
+
elif stats_result.verified_count is not None:
|
|
836
|
+
console.print(
|
|
837
|
+
f" signal: {stats_result.signal}"
|
|
838
|
+
f" verified: {stats_result.verified_count}"
|
|
839
|
+
f" reported: {stats_result.reported_count}"
|
|
840
|
+
)
|
|
841
|
+
else:
|
|
842
|
+
console.print(" N/A (no scoreable records)")
|
|
843
|
+
|
|
844
|
+
if "orphan_cleanup" in eval_names:
|
|
845
|
+
console.print(f" Running Orphan-Cleanup against {backend_name}...")
|
|
846
|
+
orphan_result = run_orphan_cleanup_eval(adapter)
|
|
847
|
+
backend_report["evals"]["orphan_cleanup"] = _serialize_eval_result(orphan_result)
|
|
848
|
+
if orphan_result.skipped:
|
|
849
|
+
console.print(f" SKIPPED: {orphan_result.skip_reason}")
|
|
850
|
+
else:
|
|
851
|
+
ovr = orphan_result.orphaned_vector_entry_rate
|
|
852
|
+
if ovr is not None:
|
|
853
|
+
console.print(f" orphaned-vector-entry rate: {ovr:.1%}")
|
|
854
|
+
else:
|
|
855
|
+
console.print(" N/A (no scoreable files)")
|
|
856
|
+
|
|
857
|
+
if "result_consistency" in eval_names:
|
|
858
|
+
console.print(f" Running Result-Consistency against {backend_name}...")
|
|
859
|
+
consistency_result = run_result_consistency_eval(adapter)
|
|
860
|
+
backend_report["evals"]["result_consistency"] = _serialize_eval_result(
|
|
861
|
+
consistency_result
|
|
862
|
+
)
|
|
863
|
+
ir = consistency_result.inconsistent_rate
|
|
864
|
+
if ir is not None:
|
|
865
|
+
console.print(
|
|
866
|
+
f" inconsistent rate: {ir:.1%}"
|
|
867
|
+
f" consistent rate: {consistency_result.consistent_rate:.1%}"
|
|
868
|
+
)
|
|
869
|
+
else:
|
|
870
|
+
console.print(" N/A (no scoreable cases)")
|
|
871
|
+
|
|
872
|
+
report["results"][backend_name] = backend_report
|
|
873
|
+
close = getattr(adapter, "close", None)
|
|
874
|
+
if callable(close):
|
|
875
|
+
close()
|
|
876
|
+
|
|
877
|
+
judge.close()
|
|
878
|
+
report["cost"] = {
|
|
879
|
+
"total_usd": cost_tracker.total_cost_usd,
|
|
880
|
+
"total_input_tokens": cost_tracker.total_input_tokens,
|
|
881
|
+
"total_output_tokens": cost_tracker.total_output_tokens,
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
console.print()
|
|
885
|
+
for line in cost_tracker.summary_lines():
|
|
886
|
+
console.print(line)
|
|
887
|
+
|
|
888
|
+
out_path = output_path or Path(f"memtrust-report-{datetime.now(UTC).strftime('%Y-%m-%d')}.json")
|
|
889
|
+
out_path.write_text(json.dumps(report, indent=2, default=str))
|
|
890
|
+
console.print(f"\nFull report: {out_path}")
|
|
891
|
+
|
|
892
|
+
if sign_key_path is not None:
|
|
893
|
+
try:
|
|
894
|
+
receipt = sign_report_with_keyfile(report, sign_key_path)
|
|
895
|
+
except ReceiptError as exc:
|
|
896
|
+
console.print(f"[red]Could not sign report: {exc}[/red]")
|
|
897
|
+
sys.exit(1)
|
|
898
|
+
receipt_path = receipt_path_for(out_path)
|
|
899
|
+
receipt_path.write_text(json.dumps(receipt, indent=2))
|
|
900
|
+
console.print(f"Signed receipt: {receipt_path} (public key: {receipt['public_key']})")
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
@main.command()
|
|
904
|
+
@click.argument("report_path", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
|
905
|
+
def report(report_path: Path) -> None:
|
|
906
|
+
"""Read a prior `memtrust run` JSON report and print a formatted summary."""
|
|
907
|
+
try:
|
|
908
|
+
data = json.loads(report_path.read_text())
|
|
909
|
+
except json.JSONDecodeError as exc:
|
|
910
|
+
console.print(f"[red]Could not parse {report_path} as JSON: {exc}[/red]")
|
|
911
|
+
sys.exit(1)
|
|
912
|
+
|
|
913
|
+
console.print(f"[bold]memtrust report[/bold] -- run_id={data.get('run_id', 'unknown')}")
|
|
914
|
+
console.print(f"Generated: {data.get('timestamp', 'unknown')}\n")
|
|
915
|
+
|
|
916
|
+
table = Table(title="Backend results")
|
|
917
|
+
table.add_column("Backend")
|
|
918
|
+
table.add_column("Status")
|
|
919
|
+
table.add_column("LongMemEval")
|
|
920
|
+
table.add_column("LoCoMo (all cats / non-adversarial)")
|
|
921
|
+
table.add_column("Contradiction (flagged/overwrite/stale/empty-or-lost)")
|
|
922
|
+
table.add_column("Resource-Sync (user-file deletion / nested-content-unindexed)")
|
|
923
|
+
table.add_column("Compression fidelity by mode")
|
|
924
|
+
table.add_column("Ranking Quality (missing-ordering-key rate)")
|
|
925
|
+
table.add_column("Scale/Volume Stress (signal, recall degradation)")
|
|
926
|
+
table.add_column("Embedding Drift (drift rate)")
|
|
927
|
+
table.add_column("Crash-Recovery (index-lost-data-survived rate)")
|
|
928
|
+
table.add_column("Extraction Quality (junk-retained / valid-lost / feedback-loop-dup)")
|
|
929
|
+
table.add_column("Migration-Rollback (restored / data-lost)")
|
|
930
|
+
table.add_column("Filter Injection (injection-succeeded / benign-false-positive)")
|
|
931
|
+
table.add_column("Lock Contention (signal, stalled/n)")
|
|
932
|
+
table.add_column("Stats Accuracy (signal, verified/reported)")
|
|
933
|
+
table.add_column("Orphan-Cleanup (orphaned-vector-entry rate)")
|
|
934
|
+
table.add_column("Result-Consistency (inconsistent rate)")
|
|
935
|
+
|
|
936
|
+
for backend_name, backend_data in data.get("results", {}).items():
|
|
937
|
+
if backend_data.get("status") == "skipped":
|
|
938
|
+
table.add_row(
|
|
939
|
+
backend_name,
|
|
940
|
+
"SKIPPED",
|
|
941
|
+
"-",
|
|
942
|
+
"-",
|
|
943
|
+
"-",
|
|
944
|
+
"-",
|
|
945
|
+
"-",
|
|
946
|
+
"-",
|
|
947
|
+
"-",
|
|
948
|
+
"-",
|
|
949
|
+
"-",
|
|
950
|
+
"-",
|
|
951
|
+
"-",
|
|
952
|
+
"-",
|
|
953
|
+
"-",
|
|
954
|
+
"-",
|
|
955
|
+
"-",
|
|
956
|
+
"-",
|
|
957
|
+
)
|
|
958
|
+
continue
|
|
959
|
+
|
|
960
|
+
evals = backend_data.get("evals", {})
|
|
961
|
+
lme = evals.get("longmemeval", {})
|
|
962
|
+
locomo = evals.get("locomo", {})
|
|
963
|
+
contra = evals.get("contradiction", {})
|
|
964
|
+
rss = evals.get("resource_sync_safety", {})
|
|
965
|
+
compression = evals.get("compression", {})
|
|
966
|
+
ranking = evals.get("ranking_quality", {})
|
|
967
|
+
scale_stress = evals.get("scale_stress", {})
|
|
968
|
+
drift = evals.get("embedding_drift", {})
|
|
969
|
+
crash_recovery = evals.get("crash_recovery", {})
|
|
970
|
+
extraction = evals.get("extraction_quality", {})
|
|
971
|
+
migration_rollback = evals.get("migration_rollback", {})
|
|
972
|
+
filter_injection = evals.get("filter_injection", {})
|
|
973
|
+
lock_contention = evals.get("lock_contention", {})
|
|
974
|
+
stats_accuracy = evals.get("stats_accuracy", {})
|
|
975
|
+
orphan_cleanup = evals.get("orphan_cleanup", {})
|
|
976
|
+
result_consistency = evals.get("result_consistency", {})
|
|
977
|
+
|
|
978
|
+
def _fmt_pct(value: float | None) -> str:
|
|
979
|
+
return f"{value:.1%}" if value is not None else "N/A"
|
|
980
|
+
|
|
981
|
+
contra_str = (
|
|
982
|
+
f"{_fmt_pct(contra.get('flagged_rate'))} / "
|
|
983
|
+
f"{_fmt_pct(contra.get('silent_overwrite_rate'))} / "
|
|
984
|
+
f"{_fmt_pct(contra.get('served_stale_rate'))} / "
|
|
985
|
+
f"{_fmt_pct(contra.get('empty_or_lost_rate'))}"
|
|
986
|
+
if contra
|
|
987
|
+
else "-"
|
|
988
|
+
)
|
|
989
|
+
if rss.get("skipped"):
|
|
990
|
+
rss_str = "SKIPPED (unsupported)"
|
|
991
|
+
elif rss:
|
|
992
|
+
rss_str = (
|
|
993
|
+
f"{_fmt_pct(rss.get('user_file_deletion_rate'))} / "
|
|
994
|
+
f"{_fmt_pct(rss.get('nested_content_unindexed_rate'))}"
|
|
995
|
+
)
|
|
996
|
+
else:
|
|
997
|
+
rss_str = "-"
|
|
998
|
+
compression_str = (
|
|
999
|
+
" ".join(
|
|
1000
|
+
f"{mode}: {_fmt_pct(value)}"
|
|
1001
|
+
for mode, value in compression.get("mean_fidelity_by_mode", {}).items()
|
|
1002
|
+
)
|
|
1003
|
+
if compression
|
|
1004
|
+
else "-"
|
|
1005
|
+
)
|
|
1006
|
+
locomo_acc_str = _fmt_pct(locomo.get("accuracy"))
|
|
1007
|
+
locomo_non_adv_str = _fmt_pct(locomo.get("non_adversarial_accuracy"))
|
|
1008
|
+
locomo_str = f"{locomo_acc_str} / {locomo_non_adv_str}" if locomo else "-"
|
|
1009
|
+
ranking_str = _fmt_pct(ranking.get("missing_ordering_key_rate")) if ranking else "-"
|
|
1010
|
+
if scale_stress:
|
|
1011
|
+
degradation = scale_stress.get("recall_degradation_pct")
|
|
1012
|
+
degradation_str = f"{degradation:.1f}pp" if degradation is not None else "N/A"
|
|
1013
|
+
scale_stress_str = f"{scale_stress.get('signal', 'unknown')} ({degradation_str})"
|
|
1014
|
+
else:
|
|
1015
|
+
scale_stress_str = "-"
|
|
1016
|
+
drift_str = _fmt_pct(drift.get("drift_rate")) if drift else "-"
|
|
1017
|
+
if crash_recovery.get("skipped"):
|
|
1018
|
+
crash_recovery_str = "SKIPPED (unsupported)"
|
|
1019
|
+
elif crash_recovery:
|
|
1020
|
+
crash_recovery_str = _fmt_pct(crash_recovery.get("index_lost_data_survived_rate"))
|
|
1021
|
+
else:
|
|
1022
|
+
crash_recovery_str = "-"
|
|
1023
|
+
extraction_str = (
|
|
1024
|
+
f"{_fmt_pct(extraction.get('junk_retained_rate'))} / "
|
|
1025
|
+
f"{_fmt_pct(extraction.get('valid_lost_rate'))} / "
|
|
1026
|
+
f"{_fmt_pct(extraction.get('feedback_loop_duplicate_rate'))}"
|
|
1027
|
+
if extraction
|
|
1028
|
+
else "-"
|
|
1029
|
+
)
|
|
1030
|
+
if migration_rollback.get("skipped"):
|
|
1031
|
+
migration_rollback_str = "SKIPPED (unsupported)"
|
|
1032
|
+
elif migration_rollback:
|
|
1033
|
+
migration_rollback_str = (
|
|
1034
|
+
f"{_fmt_pct(migration_rollback.get('restored_rate'))} / "
|
|
1035
|
+
f"{_fmt_pct(migration_rollback.get('data_lost_rate'))}"
|
|
1036
|
+
)
|
|
1037
|
+
else:
|
|
1038
|
+
migration_rollback_str = "-"
|
|
1039
|
+
if filter_injection.get("skipped"):
|
|
1040
|
+
filter_injection_str = "SKIPPED (unsupported)"
|
|
1041
|
+
elif filter_injection:
|
|
1042
|
+
filter_injection_str = (
|
|
1043
|
+
f"{_fmt_pct(filter_injection.get('injection_succeeded_rate'))} / "
|
|
1044
|
+
f"{_fmt_pct(filter_injection.get('benign_false_positive_rate'))}"
|
|
1045
|
+
)
|
|
1046
|
+
else:
|
|
1047
|
+
filter_injection_str = "-"
|
|
1048
|
+
if lock_contention.get("skipped"):
|
|
1049
|
+
lock_contention_str = "SKIPPED (unsupported)"
|
|
1050
|
+
elif lock_contention:
|
|
1051
|
+
lock_contention_str = (
|
|
1052
|
+
f"{lock_contention.get('signal', 'unknown')} "
|
|
1053
|
+
f"({lock_contention.get('stalled_count', 0)}/"
|
|
1054
|
+
f"{lock_contention.get('n_concurrent', 0)})"
|
|
1055
|
+
)
|
|
1056
|
+
else:
|
|
1057
|
+
lock_contention_str = "-"
|
|
1058
|
+
if stats_accuracy.get("skipped"):
|
|
1059
|
+
stats_accuracy_str = "SKIPPED (unsupported)"
|
|
1060
|
+
elif stats_accuracy and stats_accuracy.get("verified_count") is not None:
|
|
1061
|
+
stats_accuracy_str = (
|
|
1062
|
+
f"{stats_accuracy.get('signal', 'unknown')} "
|
|
1063
|
+
f"({stats_accuracy.get('verified_count')}/"
|
|
1064
|
+
f"{stats_accuracy.get('reported_count')})"
|
|
1065
|
+
)
|
|
1066
|
+
elif stats_accuracy:
|
|
1067
|
+
stats_accuracy_str = "N/A"
|
|
1068
|
+
else:
|
|
1069
|
+
stats_accuracy_str = "-"
|
|
1070
|
+
if orphan_cleanup.get("skipped"):
|
|
1071
|
+
orphan_cleanup_str = "SKIPPED (unsupported)"
|
|
1072
|
+
elif orphan_cleanup:
|
|
1073
|
+
orphan_cleanup_str = _fmt_pct(orphan_cleanup.get("orphaned_vector_entry_rate"))
|
|
1074
|
+
else:
|
|
1075
|
+
orphan_cleanup_str = "-"
|
|
1076
|
+
result_consistency_str = (
|
|
1077
|
+
_fmt_pct(result_consistency.get("inconsistent_rate")) if result_consistency else "-"
|
|
1078
|
+
)
|
|
1079
|
+
table.add_row(
|
|
1080
|
+
backend_name,
|
|
1081
|
+
"configured",
|
|
1082
|
+
_fmt_pct(lme.get("accuracy")) if lme else "-",
|
|
1083
|
+
locomo_str,
|
|
1084
|
+
contra_str,
|
|
1085
|
+
rss_str,
|
|
1086
|
+
compression_str or "-",
|
|
1087
|
+
ranking_str,
|
|
1088
|
+
scale_stress_str,
|
|
1089
|
+
drift_str,
|
|
1090
|
+
crash_recovery_str,
|
|
1091
|
+
extraction_str,
|
|
1092
|
+
migration_rollback_str,
|
|
1093
|
+
filter_injection_str,
|
|
1094
|
+
lock_contention_str,
|
|
1095
|
+
stats_accuracy_str,
|
|
1096
|
+
orphan_cleanup_str,
|
|
1097
|
+
result_consistency_str,
|
|
1098
|
+
)
|
|
1099
|
+
|
|
1100
|
+
console.print(table)
|
|
1101
|
+
|
|
1102
|
+
cost = data.get("cost", {})
|
|
1103
|
+
if cost:
|
|
1104
|
+
console.print(f"\nEstimated cost: ${cost.get('total_usd', 0):.4f}")
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
@main.command()
|
|
1108
|
+
@click.option(
|
|
1109
|
+
"--private-key-out",
|
|
1110
|
+
"private_key_path",
|
|
1111
|
+
default=Path("memtrust-key.pem"),
|
|
1112
|
+
show_default=True,
|
|
1113
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
1114
|
+
help="Where to write the new Ed25519 private key (PEM, unencrypted). Keep this secret.",
|
|
1115
|
+
)
|
|
1116
|
+
@click.option(
|
|
1117
|
+
"--public-key-out",
|
|
1118
|
+
"public_key_path",
|
|
1119
|
+
default=Path("memtrust-key.pub"),
|
|
1120
|
+
show_default=True,
|
|
1121
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
1122
|
+
help="Where to write the matching public key (PEM). Safe to publish/share.",
|
|
1123
|
+
)
|
|
1124
|
+
@click.option(
|
|
1125
|
+
"--force",
|
|
1126
|
+
is_flag=True,
|
|
1127
|
+
default=False,
|
|
1128
|
+
help="Overwrite the output files if they already exist.",
|
|
1129
|
+
)
|
|
1130
|
+
def keygen(private_key_path: Path, public_key_path: Path, force: bool) -> None:
|
|
1131
|
+
"""Generate a new Ed25519 keypair for signing `memtrust run` receipts.
|
|
1132
|
+
|
|
1133
|
+
Equivalent to:
|
|
1134
|
+
|
|
1135
|
+
openssl genpkey -algorithm ed25519 -out <private-key-out>
|
|
1136
|
+
openssl pkey -in <private-key-out> -pubout -out <public-key-out>
|
|
1137
|
+
|
|
1138
|
+
Publish the public key file (or its contents via MEMTRUST_RECEIPT_PUBLIC_KEY)
|
|
1139
|
+
so others can run `memtrust verify` against your signed receipts. Never
|
|
1140
|
+
share the private key.
|
|
1141
|
+
"""
|
|
1142
|
+
try:
|
|
1143
|
+
write_keypair(private_key_path, public_key_path, overwrite=force)
|
|
1144
|
+
except ReceiptError as exc:
|
|
1145
|
+
console.print(f"[red]{exc}[/red]")
|
|
1146
|
+
sys.exit(1)
|
|
1147
|
+
console.print(f"[green]Wrote private key:[/green] {private_key_path} (keep secret)")
|
|
1148
|
+
console.print(f"[green]Wrote public key:[/green] {public_key_path} (safe to publish)")
|
|
1149
|
+
console.print(
|
|
1150
|
+
"\nSign a run with: memtrust run --sign "
|
|
1151
|
+
f"{private_key_path} ...\n"
|
|
1152
|
+
"Verify a receipt with: memtrust verify <receipt.json> --public-key "
|
|
1153
|
+
f"{public_key_path}"
|
|
1154
|
+
)
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
@main.command()
|
|
1158
|
+
@click.argument("receipt_path", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
|
1159
|
+
@click.option(
|
|
1160
|
+
"--public-key",
|
|
1161
|
+
"public_key_path",
|
|
1162
|
+
default=None,
|
|
1163
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
1164
|
+
help=(
|
|
1165
|
+
f"Path to the trusted Ed25519 public key (PEM). Falls back to the "
|
|
1166
|
+
f"{PUBLIC_KEY_ENV_VAR} env var if omitted -- one of the two is required. "
|
|
1167
|
+
"Never taken from the receipt file itself: a receipt cannot vouch for its own key."
|
|
1168
|
+
),
|
|
1169
|
+
)
|
|
1170
|
+
def verify(receipt_path: Path, public_key_path: Path | None) -> None:
|
|
1171
|
+
"""Verify a signed receipt produced by `memtrust run --sign`.
|
|
1172
|
+
|
|
1173
|
+
Proves exactly two things when valid: the receipt's payload has not
|
|
1174
|
+
been altered since it was signed, and it was signed by the holder of
|
|
1175
|
+
the supplied public key. It does NOT prove the benchmark numbers
|
|
1176
|
+
inside the payload are accurate -- see docs/methodology.md.
|
|
1177
|
+
|
|
1178
|
+
Exits 0 and prints "valid: True" on success; exits 1 on any failure
|
|
1179
|
+
(bad signature, tampered payload, missing/wrong public key, malformed
|
|
1180
|
+
receipt).
|
|
1181
|
+
"""
|
|
1182
|
+
try:
|
|
1183
|
+
result = verify_receipt_file(receipt_path, public_key_path=public_key_path)
|
|
1184
|
+
except ReceiptError as exc:
|
|
1185
|
+
console.print(f"[red]Could not verify: {exc}[/red]")
|
|
1186
|
+
sys.exit(1)
|
|
1187
|
+
|
|
1188
|
+
color = "green" if result.valid else "red"
|
|
1189
|
+
console.print(f"[{color}]valid: {result.valid}[/{color}]")
|
|
1190
|
+
console.print(result.reason)
|
|
1191
|
+
if result.embedded_key_matches_trusted_key is False:
|
|
1192
|
+
console.print(
|
|
1193
|
+
"[yellow]Note: the public key embedded in the receipt does not match "
|
|
1194
|
+
"the trusted public key you supplied.[/yellow]"
|
|
1195
|
+
)
|
|
1196
|
+
if not result.valid:
|
|
1197
|
+
sys.exit(1)
|
|
1198
|
+
|
|
1199
|
+
|
|
1200
|
+
if __name__ == "__main__":
|
|
1201
|
+
main()
|