tmr-gates 0.1.1__tar.gz

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.
@@ -0,0 +1,35 @@
1
+ name: publish
2
+ # Builds the package and publishes it to PyPI through trusted publishing
3
+ # (no API token in the repository). The PyPI project must list this workflow
4
+ # as a trusted publisher: owner vzolot, repository tmr-gates, workflow
5
+ # publish.yml, environment pypi.
6
+ on:
7
+ push:
8
+ tags: ["v*"]
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-python@v5
15
+ with:
16
+ python-version: "3.12"
17
+ - run: pip install -e .[test] build
18
+ - run: pytest -q
19
+ - run: python -m build
20
+ - uses: actions/upload-artifact@v4
21
+ with:
22
+ name: dist
23
+ path: dist/
24
+ publish:
25
+ needs: build
26
+ runs-on: ubuntu-latest
27
+ environment: pypi
28
+ permissions:
29
+ id-token: write
30
+ steps:
31
+ - uses: actions/download-artifact@v4
32
+ with:
33
+ name: dist
34
+ path: dist/
35
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,15 @@
1
+ name: tests
2
+ on: [push, pull_request]
3
+ jobs:
4
+ test:
5
+ runs-on: ubuntu-latest
6
+ strategy:
7
+ matrix:
8
+ python: ["3.9", "3.11", "3.13"]
9
+ steps:
10
+ - uses: actions/checkout@v4
11
+ - uses: actions/setup-python@v5
12
+ with:
13
+ python-version: ${{ matrix.python }}
14
+ - run: pip install -e .[test]
15
+ - run: pytest -q
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.pyc
3
+ dist/
4
+ build/
5
+ *.egg-info/
6
+ .pytest_cache/
7
+ .venv/
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ ## 0.1.1 (2026-10-24)
4
+
5
+ - All inline comments translated into English; no change in behaviour, the tests are unchanged.
6
+ - Publishing workflow for PyPI (trusted publishing on a version tag).
7
+
8
+ ## 0.1.0 (2026-10-23)
9
+
10
+ First release, extracted unchanged from the pipeline of The Machine Record.
11
+
12
+ - `evaluate()`, `precheck_sources()`, `precheck_verification()`: the publication gates.
13
+ - Source tiers for US, UK, EU, Japan, Korea, China and government domains.
14
+ - Court-record policy: a lone court document with full text can carry a case.
15
+ - `strip_private_names()`: removes names of private individuals from public text.
16
+ - Tests ported from the pipeline (66 checks).
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Volodymyr Zolotukhin, The Machine Record
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.5
2
+ Name: tmr-gates
3
+ Version: 0.1.1
4
+ Summary: Publication gates of The Machine Record: deterministic rules that decide whether a verified robot incident may be published.
5
+ Project-URL: Homepage, https://themachinerecord.com/methodology
6
+ Project-URL: Repository, https://github.com/vzolot/tmr-gates
7
+ Project-URL: Registry, https://themachinerecord.com
8
+ Author-email: Volodymyr Zolotukhin <hello@themachinerecord.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: fact-checking,incident,journalism,registry,robotics,verification
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Topic :: Text Processing :: Filters
18
+ Requires-Python: >=3.9
19
+ Provides-Extra: test
20
+ Requires-Dist: pytest>=7; extra == 'test'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # tmr-gates
24
+
25
+ The publication gates of [The Machine Record](https://themachinerecord.com), an open register of safety incidents involving commercial robots, as a standalone Python package with no dependencies.
26
+
27
+ The register publishes without human pre-moderation. A language model answers one question, "do these sources describe the same real-world event, and how well", and an adversarial pass looks for defamation, satire, unattributed accusations and stale news. Everything after that is rules. This package is those rules: they decide whether a verified event may be published, what evidence a grave allegation needs, which sources count, and which names must never appear in public text. Every rule is deterministic, documented here and covered by tests, so anyone can run the register's own cases through the same gates and get the same decisions.
28
+
29
+ The code is extracted unchanged from the production pipeline. The inline comments record why each rule exists, usually a case in the register that went wrong without it; this README documents the rules themselves.
30
+
31
+ ## Install
32
+
33
+ ```
34
+ pip install tmr-gates
35
+ ```
36
+
37
+ or straight from the repository:
38
+
39
+ ```
40
+ pip install git+https://github.com/vzolot/tmr-gates@v0.1.1
41
+ ```
42
+
43
+ Python 3.9 or newer. No runtime dependencies.
44
+
45
+ ## Use
46
+
47
+ ```python
48
+ from datetime import date
49
+ from tmr_gates import default_config, evaluate, precheck_sources, strip_private_names
50
+
51
+ cfg = default_config()
52
+
53
+ sources = [
54
+ {"url": "https://www.reuters.com/...", "domain": "reuters.com", "tier": 1,
55
+ "published_at": "2026-05-02", "article_text": "..."},
56
+ {"url": "https://www.localpaper.com/...", "domain": "localpaper.com", "tier": 2,
57
+ "published_at": "2026-05-03", "article_text": "..."},
58
+ ]
59
+
60
+ # Cheap checks before paying for a model call: enough independent domains, not all tier 3.
61
+ pre = precheck_sources(cfg=cfg, sources=sources)
62
+ assert pre.publish, pre.reasons
63
+
64
+ verification = { # the model's cross-verification of the sources
65
+ "same_event": True,
66
+ "verification_score": 0.91,
67
+ "incident_type": "injury",
68
+ "disagreements": [],
69
+ }
70
+ adversarial = {"risk_score": 0.1, "blocking": []} # the adversarial pass
71
+
72
+ decision = evaluate(
73
+ cfg=cfg, sources=sources, verification=verification, adversarial=adversarial,
74
+ company="Robot Co.", severity="minor_injury",
75
+ date_of_incident="2026-05-01", date_precision="day", today=date(2026, 5, 4),
76
+ )
77
+ print(decision.publish, decision.reasons, decision.checks)
78
+
79
+ # Public text never carries the names of private individuals.
80
+ clean = strip_private_names("Worker John Smith, 34, was pinned by the arm.")
81
+ ```
82
+
83
+ `Decision.publish` is the verdict, `Decision.reasons` lists every failed rule, and `Decision.checks` records what each rule saw, so a refusal can be audited without rerunning anything.
84
+
85
+ ## The rules
86
+
87
+ `evaluate()` applies them in this order. Each failure is recorded; the decision fails if any rule fails.
88
+
89
+ | # | Rule | Config key, default |
90
+ |---|------|---------------------|
91
+ | 0 | Kill switch. When auto-publishing is off nothing passes. The register's circuit breaker flips this when the share of disputed cases rises. | `auto_publish`, true |
92
+ | 1 | The sources describe the same event, according to the model's cross-verification. | |
93
+ | 2 | Enough independent domains. Syndication copies, subdomains and link shorteners of one outlet count once. A court record with full text is a primary document and suffices alone. | `min_independent_domains`, 2 |
94
+ | 3 | At least one tier 1 or tier 2 source among the independent domains. Tier 3 (social networks, blogs, tabloids, aggregators) is never enough on its own and is not counted as corroboration. | |
95
+ | 4 | A death or serious injury attributed to a named company needs three independent sources, one of them tier 1. A court record waives this: the allegation is the filing itself, on the record. | `grave_severities`, `grave_min_sources` 3, `grave_requires_tier1` true |
96
+ | 4a | The event is an incident at all, not a product launch, a demo or an opinion piece; a commercial robot or automated machine is the subject. | |
97
+ | 5 | No material disagreement between sources on company, model, date, place, severity or what happened. Silence is not a contradiction; a wording difference that is explained is not a contradiction. | |
98
+ | 6 | Verification score at or above the threshold, risk score at or below the ceiling. Court records get a lower score bar and a higher risk ceiling. | `min_verification_score` 0.80, `max_risk_score` 0.35, `court_min_verification_score` 0.75, `court_max_risk_score` 0.75 |
99
+ | 7 | No blocking flag from the adversarial pass: satire or fake, defamation pattern, single source behind many wrappers, named private individual, unattributed accusation, stale event presented as new. For court records the last three are waived and recorded, because a complaint is by nature an attributed accusation about named people on a past date. | |
100
+ | 8 | Date sanity. The date of the incident is known to the day or the month, is not in the future, and is not later than the earliest publication by more than two days. | |
101
+
102
+ `precheck_sources()` runs rules 2 and 3 before any model call, and `precheck_verification()` runs 1, 4a, 5, 6 and 8 after the first model pass, so a bundle that cannot pass is never sent to the expensive adversarial pass. Both are strict subsets of `evaluate()`: they can only reject what it would reject.
103
+
104
+ ### Source tiers
105
+
106
+ `tier_of(domain)` returns 1, 2 or 3.
107
+
108
+ - Tier 1: wire agencies, national outlets and public broadcasters in the US, UK, Germany, France, Italy, Spain, the Netherlands, Poland, Japan, Korea, Hong Kong and Taiwan; regulators and courts through government suffixes (`.gov`, `.gouv.fr`, `.bund.de`, `.go.jp`, `.go.kr`, `.europa.eu` and others); court dockets on CourtListener.
109
+ - Tier 2: every unknown domain, on purpose. Regional papers are where most incidents first appear, and demoting the unknown would switch corroboration off. The price of that assumption is paid by rule 4, where it matters most.
110
+ - Tier 3: social networks, blogs, video platforms, tabloids, content farms and syndication portals that republish other outlets' text under their own domain.
111
+
112
+ The lists live in `tmr_gates/tiers.py` and are the register's editorial judgement, published so it can be argued with.
113
+
114
+ ### Court records
115
+
116
+ `court_document(sources)` is true when a source is a docket on courtlistener.com with at least 1,500 characters of text. Such a record can carry a case alone (rule 2), waives the three-source rule for grave allegations (rule 4) and three adversarial flags (rule 7), and is judged against its own thresholds (rule 6). The reasoning: a filed complaint is a primary document, attributed by definition, and its allegations are reported as allegations.
117
+
118
+ ### Private individuals
119
+
120
+ `strip_private_names(text)` removes the names of private people from public text while keeping company names, places, public officials by role, and product names. It is a regular-expression stripper with explicit lists of protected forms (facilities, roads, months, corporate suffixes, known places). Names of officials, spokespeople and company representatives are kept when their role is stated. The register also runs a defamation rewriter before this; the stripper is the last line.
121
+
122
+ ## How The Machine Record uses it
123
+
124
+ The production pipeline imports this package and applies the gates to every candidate case after capture, corroboration and two model passes. Thresholds come from a database table so the circuit breaker can change them without a deploy. The methodology page at [themachinerecord.com/methodology](https://themachinerecord.com/methodology) describes the same rules in prose; this package is the executable version.
125
+
126
+ ## Tests
127
+
128
+ ```
129
+ pip install -e .[test]
130
+ pytest -q
131
+ ```
132
+
133
+ The tests are the acceptance criteria of the register: a tabloid alone never passes, a death attributed to a named company needs three sources and a tier 1, a future date fails, a court record passes with the flags a complaint always carries, and the stripper removes a named patient but keeps a hospital named after a person.
134
+
135
+ ## Licence
136
+
137
+ MIT. The case data of the register is published separately under CC BY 4.0 at [vzolot/machine-record-data](https://github.com/vzolot/machine-record-data).
@@ -0,0 +1,115 @@
1
+ # tmr-gates
2
+
3
+ The publication gates of [The Machine Record](https://themachinerecord.com), an open register of safety incidents involving commercial robots, as a standalone Python package with no dependencies.
4
+
5
+ The register publishes without human pre-moderation. A language model answers one question, "do these sources describe the same real-world event, and how well", and an adversarial pass looks for defamation, satire, unattributed accusations and stale news. Everything after that is rules. This package is those rules: they decide whether a verified event may be published, what evidence a grave allegation needs, which sources count, and which names must never appear in public text. Every rule is deterministic, documented here and covered by tests, so anyone can run the register's own cases through the same gates and get the same decisions.
6
+
7
+ The code is extracted unchanged from the production pipeline. The inline comments record why each rule exists, usually a case in the register that went wrong without it; this README documents the rules themselves.
8
+
9
+ ## Install
10
+
11
+ ```
12
+ pip install tmr-gates
13
+ ```
14
+
15
+ or straight from the repository:
16
+
17
+ ```
18
+ pip install git+https://github.com/vzolot/tmr-gates@v0.1.1
19
+ ```
20
+
21
+ Python 3.9 or newer. No runtime dependencies.
22
+
23
+ ## Use
24
+
25
+ ```python
26
+ from datetime import date
27
+ from tmr_gates import default_config, evaluate, precheck_sources, strip_private_names
28
+
29
+ cfg = default_config()
30
+
31
+ sources = [
32
+ {"url": "https://www.reuters.com/...", "domain": "reuters.com", "tier": 1,
33
+ "published_at": "2026-05-02", "article_text": "..."},
34
+ {"url": "https://www.localpaper.com/...", "domain": "localpaper.com", "tier": 2,
35
+ "published_at": "2026-05-03", "article_text": "..."},
36
+ ]
37
+
38
+ # Cheap checks before paying for a model call: enough independent domains, not all tier 3.
39
+ pre = precheck_sources(cfg=cfg, sources=sources)
40
+ assert pre.publish, pre.reasons
41
+
42
+ verification = { # the model's cross-verification of the sources
43
+ "same_event": True,
44
+ "verification_score": 0.91,
45
+ "incident_type": "injury",
46
+ "disagreements": [],
47
+ }
48
+ adversarial = {"risk_score": 0.1, "blocking": []} # the adversarial pass
49
+
50
+ decision = evaluate(
51
+ cfg=cfg, sources=sources, verification=verification, adversarial=adversarial,
52
+ company="Robot Co.", severity="minor_injury",
53
+ date_of_incident="2026-05-01", date_precision="day", today=date(2026, 5, 4),
54
+ )
55
+ print(decision.publish, decision.reasons, decision.checks)
56
+
57
+ # Public text never carries the names of private individuals.
58
+ clean = strip_private_names("Worker John Smith, 34, was pinned by the arm.")
59
+ ```
60
+
61
+ `Decision.publish` is the verdict, `Decision.reasons` lists every failed rule, and `Decision.checks` records what each rule saw, so a refusal can be audited without rerunning anything.
62
+
63
+ ## The rules
64
+
65
+ `evaluate()` applies them in this order. Each failure is recorded; the decision fails if any rule fails.
66
+
67
+ | # | Rule | Config key, default |
68
+ |---|------|---------------------|
69
+ | 0 | Kill switch. When auto-publishing is off nothing passes. The register's circuit breaker flips this when the share of disputed cases rises. | `auto_publish`, true |
70
+ | 1 | The sources describe the same event, according to the model's cross-verification. | |
71
+ | 2 | Enough independent domains. Syndication copies, subdomains and link shorteners of one outlet count once. A court record with full text is a primary document and suffices alone. | `min_independent_domains`, 2 |
72
+ | 3 | At least one tier 1 or tier 2 source among the independent domains. Tier 3 (social networks, blogs, tabloids, aggregators) is never enough on its own and is not counted as corroboration. | |
73
+ | 4 | A death or serious injury attributed to a named company needs three independent sources, one of them tier 1. A court record waives this: the allegation is the filing itself, on the record. | `grave_severities`, `grave_min_sources` 3, `grave_requires_tier1` true |
74
+ | 4a | The event is an incident at all, not a product launch, a demo or an opinion piece; a commercial robot or automated machine is the subject. | |
75
+ | 5 | No material disagreement between sources on company, model, date, place, severity or what happened. Silence is not a contradiction; a wording difference that is explained is not a contradiction. | |
76
+ | 6 | Verification score at or above the threshold, risk score at or below the ceiling. Court records get a lower score bar and a higher risk ceiling. | `min_verification_score` 0.80, `max_risk_score` 0.35, `court_min_verification_score` 0.75, `court_max_risk_score` 0.75 |
77
+ | 7 | No blocking flag from the adversarial pass: satire or fake, defamation pattern, single source behind many wrappers, named private individual, unattributed accusation, stale event presented as new. For court records the last three are waived and recorded, because a complaint is by nature an attributed accusation about named people on a past date. | |
78
+ | 8 | Date sanity. The date of the incident is known to the day or the month, is not in the future, and is not later than the earliest publication by more than two days. | |
79
+
80
+ `precheck_sources()` runs rules 2 and 3 before any model call, and `precheck_verification()` runs 1, 4a, 5, 6 and 8 after the first model pass, so a bundle that cannot pass is never sent to the expensive adversarial pass. Both are strict subsets of `evaluate()`: they can only reject what it would reject.
81
+
82
+ ### Source tiers
83
+
84
+ `tier_of(domain)` returns 1, 2 or 3.
85
+
86
+ - Tier 1: wire agencies, national outlets and public broadcasters in the US, UK, Germany, France, Italy, Spain, the Netherlands, Poland, Japan, Korea, Hong Kong and Taiwan; regulators and courts through government suffixes (`.gov`, `.gouv.fr`, `.bund.de`, `.go.jp`, `.go.kr`, `.europa.eu` and others); court dockets on CourtListener.
87
+ - Tier 2: every unknown domain, on purpose. Regional papers are where most incidents first appear, and demoting the unknown would switch corroboration off. The price of that assumption is paid by rule 4, where it matters most.
88
+ - Tier 3: social networks, blogs, video platforms, tabloids, content farms and syndication portals that republish other outlets' text under their own domain.
89
+
90
+ The lists live in `tmr_gates/tiers.py` and are the register's editorial judgement, published so it can be argued with.
91
+
92
+ ### Court records
93
+
94
+ `court_document(sources)` is true when a source is a docket on courtlistener.com with at least 1,500 characters of text. Such a record can carry a case alone (rule 2), waives the three-source rule for grave allegations (rule 4) and three adversarial flags (rule 7), and is judged against its own thresholds (rule 6). The reasoning: a filed complaint is a primary document, attributed by definition, and its allegations are reported as allegations.
95
+
96
+ ### Private individuals
97
+
98
+ `strip_private_names(text)` removes the names of private people from public text while keeping company names, places, public officials by role, and product names. It is a regular-expression stripper with explicit lists of protected forms (facilities, roads, months, corporate suffixes, known places). Names of officials, spokespeople and company representatives are kept when their role is stated. The register also runs a defamation rewriter before this; the stripper is the last line.
99
+
100
+ ## How The Machine Record uses it
101
+
102
+ The production pipeline imports this package and applies the gates to every candidate case after capture, corroboration and two model passes. Thresholds come from a database table so the circuit breaker can change them without a deploy. The methodology page at [themachinerecord.com/methodology](https://themachinerecord.com/methodology) describes the same rules in prose; this package is the executable version.
103
+
104
+ ## Tests
105
+
106
+ ```
107
+ pip install -e .[test]
108
+ pytest -q
109
+ ```
110
+
111
+ The tests are the acceptance criteria of the register: a tabloid alone never passes, a death attributed to a named company needs three sources and a tier 1, a future date fails, a court record passes with the flags a complaint always carries, and the stripper removes a named patient but keeps a hospital named after a person.
112
+
113
+ ## Licence
114
+
115
+ MIT. The case data of the register is published separately under CC BY 4.0 at [vzolot/machine-record-data](https://github.com/vzolot/machine-record-data).
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "tmr-gates"
7
+ version = "0.1.1"
8
+ description = "Publication gates of The Machine Record: deterministic rules that decide whether a verified robot incident may be published."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "Volodymyr Zolotukhin", email = "hello@themachinerecord.com" }]
13
+ keywords = ["verification", "journalism", "robotics", "incident", "registry", "fact-checking"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Science/Research",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Topic :: Text Processing :: Filters",
21
+ ]
22
+ dependencies = []
23
+
24
+ [project.urls]
25
+ Homepage = "https://themachinerecord.com/methodology"
26
+ Repository = "https://github.com/vzolot/tmr-gates"
27
+ Registry = "https://themachinerecord.com"
28
+
29
+ [project.optional-dependencies]
30
+ test = ["pytest>=7"]
31
+
32
+ [tool.hatch.build.targets.wheel]
33
+ packages = ["src/tmr_gates"]
34
+
35
+ [tool.pytest.ini_options]
36
+ pythonpath = ["src"]
37
+ testpaths = ["tests"]
@@ -0,0 +1,27 @@
1
+ """tmr-gates: the publication gates of The Machine Record as a standalone package."""
2
+ from .config import DEFAULTS, Config, default_config
3
+ from .gates import (
4
+ COURT_NON_BLOCKING,
5
+ Decision,
6
+ check_is_incident,
7
+ check_robot_subject,
8
+ court_document,
9
+ evaluate,
10
+ independent_domains,
11
+ material_disagreements,
12
+ min_score,
13
+ precheck_sources,
14
+ precheck_verification,
15
+ required_domains,
16
+ strip_private_names,
17
+ )
18
+ from .tiers import GOV_SUFFIXES, TIER1_DOMAINS, TIER3_DOMAINS, TIER3_HINTS, tier_of
19
+
20
+ __version__ = "0.1.0"
21
+ __all__ = [
22
+ "COURT_NON_BLOCKING", "DEFAULTS", "Config", "Decision", "GOV_SUFFIXES", "TIER1_DOMAINS",
23
+ "TIER3_DOMAINS", "TIER3_HINTS", "check_is_incident", "check_robot_subject", "court_document",
24
+ "default_config", "evaluate", "independent_domains", "material_disagreements", "min_score",
25
+ "precheck_sources", "precheck_verification", "required_domains", "strip_private_names",
26
+ "tier_of",
27
+ ]
@@ -0,0 +1,45 @@
1
+ """Thresholds the gates read. The Machine Record keeps the live values in a
2
+ database table so its circuit breaker can switch auto-publishing off without a
3
+ deploy; this package only needs a mapping with the same keys.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+ from typing import Any
9
+
10
+ DEFAULTS: dict[str, Any] = {
11
+ # Kill switch: when False, evaluate() never returns publish=True.
12
+ "auto_publish": True,
13
+ # Minimum cross-verification score from the model for press sources.
14
+ "min_verification_score": 0.80,
15
+ # A lone court document with full text is a primary record; lower bar.
16
+ "court_min_verification_score": 0.75,
17
+ # Maximum risk score from the adversarial pass for press sources.
18
+ "max_risk_score": 0.35,
19
+ # Court documents are allowed a higher risk score because the "accusation"
20
+ # is the filing itself, attributed and on the record.
21
+ "court_max_risk_score": 0.75,
22
+ # Independent domains required before anything is published.
23
+ "min_independent_domains": 2,
24
+ # Severities for which a named company needs three sources and a tier 1.
25
+ "grave_severities": ["serious_injury", "fatality"],
26
+ "grave_min_sources": 3,
27
+ "grave_requires_tier1": True,
28
+ }
29
+
30
+
31
+ @dataclass
32
+ class Config:
33
+ """A mapping of thresholds with DEFAULTS behind it."""
34
+
35
+ values: dict[str, Any]
36
+
37
+ def __getitem__(self, key: str) -> Any:
38
+ return self.values.get(key, DEFAULTS.get(key))
39
+
40
+ def get(self, key: str, default: Any = None) -> Any:
41
+ return self.values.get(key, DEFAULTS.get(key, default))
42
+
43
+
44
+ def default_config() -> Config:
45
+ return Config(dict(DEFAULTS))