llm-redact-proxy 1.0.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.
- llm_redact/__init__.py +3 -0
- llm_redact/__main__.py +4 -0
- llm_redact/audit.py +135 -0
- llm_redact/audit_s3.py +97 -0
- llm_redact/bench/__init__.py +6 -0
- llm_redact/bench/__main__.py +110 -0
- llm_redact/bench/corpus.py +968 -0
- llm_redact/bench/fp_scan.py +129 -0
- llm_redact/bench/latency.py +365 -0
- llm_redact/bench/metrics.py +124 -0
- llm_redact/cli.py +753 -0
- llm_redact/cloud_detect.py +92 -0
- llm_redact/completions.py +117 -0
- llm_redact/config.py +1023 -0
- llm_redact/config_write.py +314 -0
- llm_redact/dashboard.html +1218 -0
- llm_redact/detection/__init__.py +4 -0
- llm_redact/detection/base.py +24 -0
- llm_redact/detection/deny.py +57 -0
- llm_redact/detection/engine.py +345 -0
- llm_redact/detection/gliner_ner.py +73 -0
- llm_redact/detection/hf_ner.py +85 -0
- llm_redact/detection/ner.py +76 -0
- llm_redact/detection/presidio_ner.py +123 -0
- llm_redact/detection/regex_rules.py +1647 -0
- llm_redact/detection/stanza_ner.py +76 -0
- llm_redact/detection/validators.py +114 -0
- llm_redact/detection/wallet_checksums.py +264 -0
- llm_redact/doctor_cli.py +687 -0
- llm_redact/eventstream.py +216 -0
- llm_redact/fips.py +77 -0
- llm_redact/free_defaults.py +80 -0
- llm_redact/init_cli.py +147 -0
- llm_redact/jsonwalk.py +82 -0
- llm_redact/license_cli.py +94 -0
- llm_redact/licensing.py +133 -0
- llm_redact/log.py +47 -0
- llm_redact/metrics.py +154 -0
- llm_redact/multipart.py +116 -0
- llm_redact/ndjson.py +25 -0
- llm_redact/placeholders.py +96 -0
- llm_redact/plugin_api.py +92 -0
- llm_redact/plugin_assets.py +398 -0
- llm_redact/plugin_cli.py +298 -0
- llm_redact/providers/__init__.py +46 -0
- llm_redact/providers/anthropic.py +238 -0
- llm_redact/providers/azure_openai.py +83 -0
- llm_redact/providers/base.py +290 -0
- llm_redact/providers/bedrock.py +269 -0
- llm_redact/providers/claude_vertex.py +52 -0
- llm_redact/providers/cohere.py +204 -0
- llm_redact/providers/custom.py +105 -0
- llm_redact/providers/gemini.py +255 -0
- llm_redact/providers/ollama.py +129 -0
- llm_redact/providers/openai.py +371 -0
- llm_redact/providers/openai_responses.py +271 -0
- llm_redact/providers/vertex.py +50 -0
- llm_redact/proxy.py +2398 -0
- llm_redact/py.typed +0 -0
- llm_redact/realtime.py +861 -0
- llm_redact/redactor.py +124 -0
- llm_redact/registry.py +142 -0
- llm_redact/rehydrate.py +270 -0
- llm_redact/run_cli.py +215 -0
- llm_redact/service_cli.py +199 -0
- llm_redact/sessions.py +72 -0
- llm_redact/sse.py +100 -0
- llm_redact/user_guide.html +73 -0
- llm_redact/user_guide.md +124 -0
- llm_redact/users.py +162 -0
- llm_redact/vault.py +715 -0
- llm_redact/vault_cli.py +673 -0
- llm_redact/vault_crypto.py +104 -0
- llm_redact/vault_rdbms.py +891 -0
- llm_redact_proxy-1.0.0.dist-info/METADATA +526 -0
- llm_redact_proxy-1.0.0.dist-info/RECORD +79 -0
- llm_redact_proxy-1.0.0.dist-info/WHEEL +4 -0
- llm_redact_proxy-1.0.0.dist-info/entry_points.txt +2 -0
- llm_redact_proxy-1.0.0.dist-info/licenses/LICENSE +661 -0
llm_redact/__init__.py
ADDED
llm_redact/__main__.py
ADDED
llm_redact/audit.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Audit-log contract (the Free side of the open-core split).
|
|
2
|
+
|
|
3
|
+
The audit log records metadata only — detector types and counts, paths,
|
|
4
|
+
timestamps, durations. Never values, never placeholder ids, never headers or
|
|
5
|
+
bodies. The concrete SQLite log, its tamper-evident hash chain, and the
|
|
6
|
+
off-machine sinks are a paid subsystem (``llm_redact_pro.audit`` /
|
|
7
|
+
``llm_redact_pro.audit_s3``). This module holds only what the Free core needs
|
|
8
|
+
at the seam:
|
|
9
|
+
|
|
10
|
+
- ``AuditRecord`` — the metadata row ``record_request`` builds and hands to the
|
|
11
|
+
log (kept here so ``proxy.py`` is unchanged),
|
|
12
|
+
- the ``AuditLog`` **Protocol** the proxy calls,
|
|
13
|
+
- the env-var name + key-resolution helper ``doctor`` uses for its posture
|
|
14
|
+
checks (generic env-reading glue, no paid secrecy value), and
|
|
15
|
+
- the fail-closed ``build_audit`` default.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import hashlib
|
|
21
|
+
import os
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
25
|
+
|
|
26
|
+
from .config import ConfigError
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from .config import AuditConfig
|
|
30
|
+
|
|
31
|
+
AUDIT_HMAC_ENV = "LLM_REDACT_AUDIT_HMAC_KEY"
|
|
32
|
+
|
|
33
|
+
_PRO_HINT = "install the llm-redact-pro package to enable it"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class AuditRecord:
|
|
38
|
+
ts: str
|
|
39
|
+
session: str
|
|
40
|
+
provider: str | None
|
|
41
|
+
method: str
|
|
42
|
+
path: str
|
|
43
|
+
status: int | None
|
|
44
|
+
duration_ms: float
|
|
45
|
+
streamed: bool
|
|
46
|
+
detections: dict[str, int] = field(default_factory=dict)
|
|
47
|
+
rehydrations: dict[str, int] = field(default_factory=dict)
|
|
48
|
+
# Named-user attribution (2.0): the user NAME only, never keys. None on
|
|
49
|
+
# single-user deployments and rows recorded before 2.0.
|
|
50
|
+
user: str | None = None
|
|
51
|
+
# Warn-mode hits attributed to this request (3.3): types+counts of values
|
|
52
|
+
# FORWARDED upstream unredacted. None when nothing warned and on rows
|
|
53
|
+
# recorded before the column existed.
|
|
54
|
+
warned: dict[str, int] | None = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class AuditWriteError(RuntimeError):
|
|
58
|
+
"""A required-mode audit row could not be durably committed.
|
|
59
|
+
|
|
60
|
+
Raised by write-ahead audit implementations (``[audit] required = true``,
|
|
61
|
+
llm-redact-pro) from ``begin``/``finalize``/``record`` when the row cannot
|
|
62
|
+
be committed. On the ``begin`` path the proxy answers a provider-shaped
|
|
63
|
+
503 WITHOUT contacting the upstream — no durably committed audit row, no
|
|
64
|
+
upstream contact; after a response has been committed the proxy can only
|
|
65
|
+
log the failure loudly. Never raised in the default fail-open mode, where
|
|
66
|
+
the concrete log swallows write faults by design.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class AuditLog(Protocol):
|
|
71
|
+
"""The audit-log surface the proxy depends on.
|
|
72
|
+
|
|
73
|
+
The concrete SQLite implementation (with the tamper-evident chain) is
|
|
74
|
+
``llm_redact_pro.audit.AuditLog``; the Free core holds only this structural
|
|
75
|
+
contract. ``verify()`` — used by the offline ``audit verify`` CLI, not the
|
|
76
|
+
request path — lives on the concrete class only. The write-ahead pair for
|
|
77
|
+
``[audit] required`` mode is the :class:`WriteAheadAudit` sub-Protocol —
|
|
78
|
+
kept out of this base so a pro package predating it still satisfies every
|
|
79
|
+
fail-open configuration, type contract included.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def record(self, entry: AuditRecord) -> None: ...
|
|
83
|
+
|
|
84
|
+
def recent(self, limit: int) -> list[dict[str, object]]: ...
|
|
85
|
+
|
|
86
|
+
def count(self) -> int: ...
|
|
87
|
+
|
|
88
|
+
def close(self) -> None: ...
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@runtime_checkable
|
|
92
|
+
class WriteAheadAudit(AuditLog, Protocol):
|
|
93
|
+
"""An audit log with the ``[audit] required`` write-ahead pair.
|
|
94
|
+
|
|
95
|
+
``begin`` durably commits a START row BEFORE any upstream contact
|
|
96
|
+
(raising :class:`AuditWriteError` on failure) and returns an opaque
|
|
97
|
+
non-None token; ``finalize`` commits the matching END row at completion.
|
|
98
|
+
``ProxyState`` resolves the capability once at startup (``isinstance``
|
|
99
|
+
checks method presence, the runtime-checkable contract) and refuses
|
|
100
|
+
required mode when the built log lacks the pair.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def begin(self, entry: AuditRecord) -> object: ...
|
|
104
|
+
|
|
105
|
+
def finalize(self, token: object, entry: AuditRecord) -> None: ...
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def default_audit_path() -> Path:
|
|
109
|
+
xdg = os.environ.get("XDG_DATA_HOME", str(Path.home() / ".local" / "share"))
|
|
110
|
+
return Path(xdg) / "llm-redact" / "audit.db"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def audit_hmac_key_from_env() -> bytes | None:
|
|
114
|
+
"""The tamper-evidence HMAC key, derived from LLM_REDACT_AUDIT_HMAC_KEY.
|
|
115
|
+
|
|
116
|
+
Env-only (never the config file, matching the vault key and S3
|
|
117
|
+
credentials). Any-length passphrase is hashed to 32 bytes. Returns None
|
|
118
|
+
when unset so the caller can fail closed with a clear message.
|
|
119
|
+
"""
|
|
120
|
+
raw = os.environ.get(AUDIT_HMAC_ENV, "")
|
|
121
|
+
if not raw:
|
|
122
|
+
return None
|
|
123
|
+
return hashlib.sha256(raw.encode("utf-8")).digest()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def build_audit(config: AuditConfig) -> AuditLog | None:
|
|
127
|
+
"""Fail-closed Free default for the audit log.
|
|
128
|
+
|
|
129
|
+
The audit log — and its tamper-evident chain — is a paid subsystem whose
|
|
130
|
+
implementation lives in llm-redact-pro. Disabled is None; enabled without
|
|
131
|
+
the pro package fails closed rather than silently dropping the audit trail.
|
|
132
|
+
"""
|
|
133
|
+
if not config.enabled:
|
|
134
|
+
return None
|
|
135
|
+
raise ConfigError(f"[audit] enabled = true requires an audit log; {_PRO_HINT}")
|
llm_redact/audit_s3.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Off-machine audit-sink contract (the Free side of the open-core split).
|
|
2
|
+
|
|
3
|
+
The concrete S3/GCS and Azure Blob sinks — the hand-rolled SigV4 and Azure
|
|
4
|
+
SharedKey signers, batch buffering, and client-side Fernet encryption — are a
|
|
5
|
+
paid (Pro-tier) subsystem in ``llm_redact_pro.audit_s3``. This module holds
|
|
6
|
+
only the seam:
|
|
7
|
+
|
|
8
|
+
- the ``S3AuditSink`` / ``AzureAuditSink`` Protocols the proxy runs and reads
|
|
9
|
+
counters from,
|
|
10
|
+
- the env-var names + key/credential helpers ``doctor`` uses for its posture
|
|
11
|
+
checks (generic env-reading glue, no paid secrecy value; credentials NEVER
|
|
12
|
+
come from the config file), and
|
|
13
|
+
- the fail-closed ``build_audit_sinks`` default.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import base64
|
|
19
|
+
import hashlib
|
|
20
|
+
import os
|
|
21
|
+
from typing import TYPE_CHECKING, Any, Protocol
|
|
22
|
+
|
|
23
|
+
from .config import ConfigError
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from .config import AuditConfig
|
|
27
|
+
|
|
28
|
+
_PRO_HINT = "install the llm-redact-pro package to enable it"
|
|
29
|
+
|
|
30
|
+
# Client-side batch encryption ([audit.s3]/[audit.azure] encryption = "fernet").
|
|
31
|
+
AUDIT_ENC_KEY_ENV = "LLM_REDACT_AUDIT_ENC_KEY"
|
|
32
|
+
AZURE_STORAGE_KEY_ENV = "AZURE_STORAGE_KEY"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def audit_enc_key_from_env() -> bytes | None:
|
|
36
|
+
"""SHA-256 of the env passphrase, urlsafe-base64 — a valid Fernet key
|
|
37
|
+
(the audit_hmac_key_from_env recipe, shaped for Fernet)."""
|
|
38
|
+
raw = os.environ.get(AUDIT_ENC_KEY_ENV, "")
|
|
39
|
+
if not raw:
|
|
40
|
+
return None
|
|
41
|
+
return base64.urlsafe_b64encode(hashlib.sha256(raw.encode("utf-8")).digest())
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Per-provider credential environment variables. GCS is reached through its
|
|
45
|
+
# S3-compatible XML API (interoperability) with HMAC interoperability keys read
|
|
46
|
+
# from GCS-specific vars. Credentials NEVER come from the config file.
|
|
47
|
+
_CREDENTIAL_ENV: dict[str, tuple[str, str, str | None]] = {
|
|
48
|
+
"aws": ("AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"),
|
|
49
|
+
"minio": ("AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"),
|
|
50
|
+
"ceph": ("AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"),
|
|
51
|
+
"gcs": ("GCS_HMAC_ACCESS_ID", "GCS_HMAC_SECRET", None),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def credential_env_names(provider: str) -> tuple[str, str, str | None]:
|
|
56
|
+
"""The (access, secret, session-token) env var names for a provider."""
|
|
57
|
+
return _CREDENTIAL_ENV.get(provider, _CREDENTIAL_ENV["aws"])
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class S3AuditSink(Protocol):
|
|
61
|
+
"""The S3/GCS audit-sink surface the proxy runs (concrete impl in pro)."""
|
|
62
|
+
|
|
63
|
+
batches_uploaded: int
|
|
64
|
+
rows_dropped: int
|
|
65
|
+
|
|
66
|
+
def add(self, row: dict[str, Any]) -> None: ...
|
|
67
|
+
|
|
68
|
+
async def run(self) -> None: ...
|
|
69
|
+
|
|
70
|
+
async def aclose(self) -> None: ...
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class AzureAuditSink(Protocol):
|
|
74
|
+
"""The Azure Blob audit-sink surface the proxy runs (concrete impl in pro)."""
|
|
75
|
+
|
|
76
|
+
batches_uploaded: int
|
|
77
|
+
rows_dropped: int
|
|
78
|
+
|
|
79
|
+
def add(self, row: dict[str, Any]) -> None: ...
|
|
80
|
+
|
|
81
|
+
async def run(self) -> None: ...
|
|
82
|
+
|
|
83
|
+
async def aclose(self) -> None: ...
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def build_audit_sinks(
|
|
87
|
+
config: AuditConfig,
|
|
88
|
+
) -> tuple[S3AuditSink | None, AzureAuditSink | None]:
|
|
89
|
+
"""Fail-closed Free default for the off-machine audit sinks.
|
|
90
|
+
|
|
91
|
+
Both sinks disabled is ``(None, None)``. Enabling either without the pro
|
|
92
|
+
package fails closed rather than silently dropping every batch — the sinks,
|
|
93
|
+
signers, and batch encryption are a paid subsystem.
|
|
94
|
+
"""
|
|
95
|
+
if not config.s3.enabled and not config.azure.enabled:
|
|
96
|
+
return None, None
|
|
97
|
+
raise ConfigError(f"audit backup sinks require an off-machine sink; {_PRO_HINT}")
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Detection quality benchmark: deterministic synthetic corpus + metrics.
|
|
2
|
+
|
|
3
|
+
Run with ``python -m llm_redact.bench``. The corpus is generated at runtime
|
|
4
|
+
(never committed — no realistic-looking secrets in the repository) from a
|
|
5
|
+
fixed seed, so results are reproducible and rule regressions are diffable.
|
|
6
|
+
"""
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from llm_redact.bench.corpus import generate
|
|
7
|
+
from llm_redact.bench.fp_scan import fp_failures, scan_fp_corpus, to_markdown_table
|
|
8
|
+
from llm_redact.bench.latency import (
|
|
9
|
+
ceiling_failures,
|
|
10
|
+
run_latency,
|
|
11
|
+
to_json_list,
|
|
12
|
+
)
|
|
13
|
+
from llm_redact.bench.latency import (
|
|
14
|
+
to_markdown as latency_to_markdown,
|
|
15
|
+
)
|
|
16
|
+
from llm_redact.bench.metrics import evaluate, to_json_dict, to_markdown
|
|
17
|
+
|
|
18
|
+
DEFAULT_FP_CORPUS = Path("bench/fp_corpus")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main(argv: list[str] | None = None) -> int:
|
|
22
|
+
parser = argparse.ArgumentParser(prog="python -m llm_redact.bench")
|
|
23
|
+
parser.add_argument("--seed", type=int, default=42)
|
|
24
|
+
parser.add_argument("--samples", type=int, default=25, help="positives per rule")
|
|
25
|
+
parser.add_argument("--out", type=Path, default=None, help="write report files here")
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"--fp-corpus",
|
|
28
|
+
type=Path,
|
|
29
|
+
default=None,
|
|
30
|
+
help=f"false-positive corpus root (default: {DEFAULT_FP_CORPUS} when present)",
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--latency",
|
|
34
|
+
action="store_true",
|
|
35
|
+
help="also run the in-process latency benchmark (adds ~1 minute)",
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--check",
|
|
39
|
+
action="store_true",
|
|
40
|
+
help="exit 1 if any rule misses its own generated positives (recall < 1.0),"
|
|
41
|
+
" the fp corpus deviates from its manifest, or (with --latency) a p50"
|
|
42
|
+
" smoke ceiling is crossed",
|
|
43
|
+
)
|
|
44
|
+
args = parser.parse_args(argv)
|
|
45
|
+
|
|
46
|
+
corpus = generate(seed=args.seed, samples_per_rule=args.samples)
|
|
47
|
+
result = evaluate(corpus)
|
|
48
|
+
markdown = to_markdown(result, seed=args.seed)
|
|
49
|
+
|
|
50
|
+
fp_root = args.fp_corpus
|
|
51
|
+
if fp_root is None and DEFAULT_FP_CORPUS.is_dir():
|
|
52
|
+
fp_root = DEFAULT_FP_CORPUS
|
|
53
|
+
fp_results = None
|
|
54
|
+
if fp_root is not None:
|
|
55
|
+
fp_results = scan_fp_corpus(fp_root)
|
|
56
|
+
markdown += (
|
|
57
|
+
"\n## False-positive corpus\n\n"
|
|
58
|
+
f"Vendored negatives at `{fp_root}`, gated on exact per-file counts.\n\n"
|
|
59
|
+
+ to_markdown_table(fp_results)
|
|
60
|
+
+ "\n"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
latency_stats = None
|
|
64
|
+
if args.latency:
|
|
65
|
+
latency_stats = run_latency(seed=args.seed)
|
|
66
|
+
markdown += "\n" + latency_to_markdown(latency_stats)
|
|
67
|
+
|
|
68
|
+
if args.out is not None:
|
|
69
|
+
args.out.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
(args.out / "report.md").write_text(markdown)
|
|
71
|
+
report = to_json_dict(result, seed=args.seed)
|
|
72
|
+
if latency_stats is not None:
|
|
73
|
+
report["latency"] = to_json_list(latency_stats)
|
|
74
|
+
(args.out / "report.json").write_text(json.dumps(report, indent=2))
|
|
75
|
+
print(f"report written to {args.out}/report.md and report.json")
|
|
76
|
+
else:
|
|
77
|
+
print(markdown)
|
|
78
|
+
|
|
79
|
+
if args.check:
|
|
80
|
+
failed = False
|
|
81
|
+
# Functional regression gate: the corpus is generated to match each
|
|
82
|
+
# rule, so a missed positive is a code regression, not corpus noise.
|
|
83
|
+
misses = {name: s for name, s in result.per_type.items() if s.recall < 1.0}
|
|
84
|
+
for name, score in misses.items():
|
|
85
|
+
print(f"CHECK FAILED: {name} recall {score.recall:.3f} ({score.fn} missed)")
|
|
86
|
+
failed = True
|
|
87
|
+
# Precision gate: the vendored negatives must match their manifest
|
|
88
|
+
# exactly, in both directions.
|
|
89
|
+
if fp_results is not None:
|
|
90
|
+
for line in fp_failures(fp_results):
|
|
91
|
+
print(f"CHECK FAILED (fp corpus): {line}")
|
|
92
|
+
failed = True
|
|
93
|
+
# Latency smoke ceilings: generous p50-only bounds against
|
|
94
|
+
# accidental quadratic behavior, not perf tuning.
|
|
95
|
+
if latency_stats is not None:
|
|
96
|
+
for line in ceiling_failures(latency_stats):
|
|
97
|
+
print(f"CHECK FAILED (latency): {line}")
|
|
98
|
+
failed = True
|
|
99
|
+
if failed:
|
|
100
|
+
return 1
|
|
101
|
+
fp_note = f", fp corpus clean ({len(fp_results)} files)" if fp_results is not None else ""
|
|
102
|
+
latency_note = ", latency ceilings ok" if latency_stats is not None else ""
|
|
103
|
+
print(
|
|
104
|
+
f"check passed: recall 1.0 on all {len(result.per_type)} types{fp_note}{latency_note}"
|
|
105
|
+
)
|
|
106
|
+
return 0
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
sys.exit(main())
|