@dzhechkov/p-replicator 1.5.17 → 1.5.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dz-manifest.json +51 -23
- package/README.md +5 -3
- package/package.json +1 -1
- package/sbom.json +106 -36
- package/templates/.claude/skills/goap-research-ed25519/SKILL.md +340 -47
- package/templates/.claude/skills/goap-research-ed25519/scripts/check_report_evidence.py +359 -3
- package/templates/.claude/skills/goap-research-ed25519/scripts/ed25519_verifier.py +386 -13
- package/templates/.claude/skills/goap-research-ed25519/scripts/fixture_legacy_v2_fact.json +23 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/fixtures_field_cases.json +133 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/goap_planner.py +314 -44
- package/templates/.claude/skills/goap-research-ed25519/scripts/learning_bridge.py +890 -303
- package/templates/.claude/skills/goap-research-ed25519/scripts/population_match.py +591 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/risk_statement.py +289 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_ed25519_verifier.py +57 -2
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_evidence_provenance.py +969 -344
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_goap_planner.py +420 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_population_match.py +544 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_risk_absolute.py +239 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_signature_v3.py +554 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_suite_completeness.py +90 -0
- package/tests/snapshot/baseline.json +24 -9
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
ADR-002 Confirmation tests — relative risk never travels alone (T-12…T-14).
|
|
4
|
+
|
|
5
|
+
cd .../goap-research-ed25519/scripts && python3 -m unittest discover -s . -p 'test_*.py' -v
|
|
6
|
+
|
|
7
|
+
T-12 is the load-bearing one and it is written by REFLECTION over every public
|
|
8
|
+
callable the module exports, not against a hand-listed set — so a renderer added
|
|
9
|
+
next month is covered without editing this file.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import inspect
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
sys.dont_write_bytecode = True
|
|
16
|
+
|
|
17
|
+
import unittest
|
|
18
|
+
|
|
19
|
+
import risk_statement as rs
|
|
20
|
+
import check_report_evidence as gate
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _public_callables(module):
|
|
24
|
+
return [(name, obj) for name, obj in vars(module).items()
|
|
25
|
+
if not name.startswith("_") and callable(obj)
|
|
26
|
+
and getattr(obj, "__module__", None) == module.__name__]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class StructuralBanTests(unittest.TestCase):
|
|
30
|
+
"""T-12 — no path emits a relative figure without its absolute counterpart."""
|
|
31
|
+
|
|
32
|
+
def test_relative_risk_cannot_be_emitted_by_any_path(self):
|
|
33
|
+
"""Reflection over EVERY public callable × a bare relative effect.
|
|
34
|
+
|
|
35
|
+
A callable that accepts a lone RelativeEffect and hands back prose carrying
|
|
36
|
+
its value would be exactly the third path DC-2 forbids. Each one must either
|
|
37
|
+
refuse (TypeError/ValueError) or return something that is not user-facing
|
|
38
|
+
prose about the number.
|
|
39
|
+
"""
|
|
40
|
+
relative = rs.RelativeEffect(kind="RR", value=21.0)
|
|
41
|
+
checked = 0
|
|
42
|
+
for name, obj in _public_callables(rs):
|
|
43
|
+
with self.subTest(callable=name):
|
|
44
|
+
checked += 1
|
|
45
|
+
try:
|
|
46
|
+
result = obj(relative)
|
|
47
|
+
except (TypeError, ValueError):
|
|
48
|
+
continue # refused — the required shape
|
|
49
|
+
if isinstance(result, str):
|
|
50
|
+
self.fail(f"{name}() emitted prose from a lone relative effect: {result!r}")
|
|
51
|
+
self.assertNotIsInstance(result, rs.RiskStatement,
|
|
52
|
+
f"{name}() built a statement with no absolute half")
|
|
53
|
+
self.assertGreaterEqual(checked, 8, "reflection must actually have covered the module")
|
|
54
|
+
|
|
55
|
+
def test_relative_effect_has_no_prose_accessor(self):
|
|
56
|
+
"""ADR-002 §2 — no __str__/__format__ override, no accessor returning prose."""
|
|
57
|
+
relative = rs.RelativeEffect(kind="HR", value=0.74)
|
|
58
|
+
self.assertIs(type(relative).__str__, object.__str__,
|
|
59
|
+
"RelativeEffect must not override __str__")
|
|
60
|
+
self.assertIs(type(relative).__format__, object.__format__,
|
|
61
|
+
"RelativeEffect must not override __format__")
|
|
62
|
+
for name in dir(relative):
|
|
63
|
+
if name.startswith("_"):
|
|
64
|
+
continue
|
|
65
|
+
attribute = getattr(relative, name)
|
|
66
|
+
if callable(attribute) and not inspect.signature(attribute).parameters:
|
|
67
|
+
self.assertNotIsInstance(attribute(), str,
|
|
68
|
+
f"RelativeEffect.{name}() returns user-facing prose")
|
|
69
|
+
|
|
70
|
+
def test_thirty_refusal_cases(self):
|
|
71
|
+
"""The constructor refuses each omission SEPARATELY, over all 6 relative kinds:
|
|
72
|
+
18 type/omission refusals + 12 blank-reason refusals = 30 (ADR-002 family 1)."""
|
|
73
|
+
refusals = 0
|
|
74
|
+
for kind in rs.RELATIVE_KINDS:
|
|
75
|
+
relative = rs.RelativeEffect(kind=kind, value=2.0)
|
|
76
|
+
with self.subTest(kind=kind, case="omitted"):
|
|
77
|
+
with self.assertRaises(TypeError):
|
|
78
|
+
rs.RiskStatement(relative)
|
|
79
|
+
refusals += 1
|
|
80
|
+
with self.subTest(kind=kind, case="None"):
|
|
81
|
+
with self.assertRaises(TypeError):
|
|
82
|
+
rs.RiskStatement(relative, None)
|
|
83
|
+
refusals += 1
|
|
84
|
+
with self.subTest(kind=kind, case="wrong type"):
|
|
85
|
+
with self.assertRaises(TypeError):
|
|
86
|
+
rs.RiskStatement(relative, "1 excess case per 1394 people")
|
|
87
|
+
refusals += 1
|
|
88
|
+
for blank in ("", " "):
|
|
89
|
+
with self.subTest(kind=kind, case=f"blank reason {blank!r}"):
|
|
90
|
+
with self.assertRaises(rs.RiskError):
|
|
91
|
+
rs.RiskStatement(relative, rs.UnknownBaseline(reason=blank))
|
|
92
|
+
refusals += 1
|
|
93
|
+
self.assertEqual(refusals, 30, "6 kinds x (3 absolute refusals + 2 blank reasons)")
|
|
94
|
+
|
|
95
|
+
def test_unknown_baseline_is_printed_in_the_number_s_slot(self):
|
|
96
|
+
"""«если базовый риск неизвестен — так и писать, а не опускать»."""
|
|
97
|
+
statement = rs.RiskStatement(
|
|
98
|
+
rs.RelativeEffect(kind="HR", value=0.74),
|
|
99
|
+
rs.UnknownBaseline(reason="the source reports no control-arm event rate"),
|
|
100
|
+
)
|
|
101
|
+
rendered = rs.render_risk(statement)
|
|
102
|
+
self.assertIn(rs.BASELINE_UNKNOWN_TEXT, rendered)
|
|
103
|
+
self.assertIn("no control-arm event rate", rendered)
|
|
104
|
+
|
|
105
|
+
def test_intervention_requires_a_named_nnt(self):
|
|
106
|
+
relative = rs.RelativeEffect(kind="RR", value=0.8)
|
|
107
|
+
absolute = rs.AbsoluteEffect(control_events=10, control_denominator=1000)
|
|
108
|
+
with self.assertRaises(rs.RiskError):
|
|
109
|
+
rs.RiskStatement(relative, absolute, intervention=True)
|
|
110
|
+
statement = rs.RiskStatement(relative, absolute, intervention=True,
|
|
111
|
+
nnt=rs.NNT(not_applicable_reason="no treated arm reported"))
|
|
112
|
+
self.assertIn("n/a — no treated arm reported", rs.render_risk(statement))
|
|
113
|
+
with self.assertRaises(rs.RiskError):
|
|
114
|
+
rs.NNT(value=250, not_applicable_reason="both set")
|
|
115
|
+
with self.assertRaises(rs.RiskError):
|
|
116
|
+
rs.NNT()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class NNTFixtureTests(unittest.TestCase):
|
|
120
|
+
"""T-14 — both real numeric cases, COMPUTED (FR-7), never hand-assembled strings."""
|
|
121
|
+
|
|
122
|
+
def test_nnt_from_field_fixtures(self):
|
|
123
|
+
"""Case 1: «в 21 раз выше» is 1 excess case per 1394 → NNT 1394.
|
|
124
|
+
|
|
125
|
+
Case 2: «риск удваивается» is 4 per 1000 over 25 years → NNT 250 over that
|
|
126
|
+
window. INTERPRETATION, stated because the brief does not state a baseline:
|
|
127
|
+
the "4 per 1000 over 25 y" figure is surfaced AS GIVEN and read as the excess;
|
|
128
|
+
no control/treated split is inferred, because inventing a denominator is the
|
|
129
|
+
exact harm this module exists to prevent (02_research.md's explicit note).
|
|
130
|
+
"""
|
|
131
|
+
first = rs.nnt_from_excess(1, 1394)
|
|
132
|
+
self.assertEqual(first.value, 1394)
|
|
133
|
+
self.assertIsNone(first.not_applicable_reason)
|
|
134
|
+
|
|
135
|
+
second = rs.nnt_from_excess(4, 1000, horizon="25 years")
|
|
136
|
+
self.assertEqual(second.value, 250)
|
|
137
|
+
self.assertEqual(second.horizon, "25 years")
|
|
138
|
+
self.assertIn("250 over 25 years", second.render())
|
|
139
|
+
|
|
140
|
+
def test_nnt_from_two_arms_is_one_over_the_absolute_difference(self):
|
|
141
|
+
absolute = rs.AbsoluteEffect(control_events=20, control_denominator=1000,
|
|
142
|
+
treated_events=10, treated_denominator=1000,
|
|
143
|
+
horizon="5 years")
|
|
144
|
+
self.assertEqual(rs.nnt_from_absolute(absolute).value, 100)
|
|
145
|
+
|
|
146
|
+
def test_undefined_nnt_is_named_never_infinity_or_zero(self):
|
|
147
|
+
"""D-15 — the failure mode is a NAMED reason, not a number that lies."""
|
|
148
|
+
for nnt in (rs.nnt_from_excess(0, 1000),
|
|
149
|
+
rs.nnt_from_excess(1, 0),
|
|
150
|
+
rs.nnt_from_excess("n/a", 1000),
|
|
151
|
+
rs.nnt_from_absolute(rs.UnknownBaseline(reason="no control arm reported")),
|
|
152
|
+
rs.nnt_from_absolute(rs.AbsoluteEffect(control_events=5, control_denominator=100)),
|
|
153
|
+
rs.nnt_from_absolute(rs.AbsoluteEffect(control_events=5, control_denominator=100,
|
|
154
|
+
treated_events=5, treated_denominator=100))):
|
|
155
|
+
self.assertIsNone(nnt.value)
|
|
156
|
+
self.assertTrue(str(nnt.not_applicable_reason).strip())
|
|
157
|
+
self.assertIn("n/a", nnt.render())
|
|
158
|
+
with self.assertRaises(rs.RiskError):
|
|
159
|
+
rs.NNT(value=float("inf"))
|
|
160
|
+
|
|
161
|
+
def test_full_rendering_of_the_field_case(self):
|
|
162
|
+
statement = rs.RiskStatement(
|
|
163
|
+
rs.RelativeEffect(kind="RR", value=21.0),
|
|
164
|
+
rs.AbsoluteEffect(control_events=1, control_denominator=1394),
|
|
165
|
+
nnt=rs.nnt_from_absolute(rs.AbsoluteEffect(control_events=1, control_denominator=1394)),
|
|
166
|
+
)
|
|
167
|
+
rendered = rs.render_risk(statement)
|
|
168
|
+
self.assertIn("RR 21 (relative)", rendered)
|
|
169
|
+
self.assertIn("1 per 1394", rendered)
|
|
170
|
+
self.assertIn("observational association", rendered)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class ReportBeltTests(unittest.TestCase):
|
|
174
|
+
"""T-13 (AM-3, SAFEGUARD) — the belt must actually FIRE on a real input."""
|
|
175
|
+
|
|
176
|
+
def _exit_code(self, text):
|
|
177
|
+
return 1 if gate.scan_relative_risk(text) else 0
|
|
178
|
+
|
|
179
|
+
def test_relative_risk_without_absolute_fires_report_gate(self):
|
|
180
|
+
bare = "Вывод: у него риск инфаркта выше в 21 раз, поэтому нужно действовать немедленно."
|
|
181
|
+
findings = gate.scan_relative_risk(bare)
|
|
182
|
+
self.assertTrue(findings, "a bare 21x must not pass")
|
|
183
|
+
self.assertEqual(findings[0].kind, "RELATIVE_RISK_WITHOUT_ABSOLUTE")
|
|
184
|
+
self.assertEqual(self._exit_code(bare), 1)
|
|
185
|
+
|
|
186
|
+
paired = ("Вывод: у него риск инфаркта выше в 21 раз — это 1 избыточный случай "
|
|
187
|
+
"на 1394 человека.")
|
|
188
|
+
self.assertEqual(self._exit_code(paired), 0, "the absolute companion clears it")
|
|
189
|
+
|
|
190
|
+
def test_doubles_without_absolute_fires_too(self):
|
|
191
|
+
bare = "При таком уровне риск удваивается."
|
|
192
|
+
self.assertEqual(self._exit_code(bare), 1)
|
|
193
|
+
paired = "При таком уровне риск удваивается — это 4 на 1000 человек за 25 лет."
|
|
194
|
+
self.assertEqual(self._exit_code(paired), 0)
|
|
195
|
+
|
|
196
|
+
def test_english_and_hazard_ratio_forms(self):
|
|
197
|
+
self.assertEqual(self._exit_code("The risk doubles in this cohort."), 1)
|
|
198
|
+
self.assertEqual(self._exit_code("Mortality fell (HR 0.74)."), 1)
|
|
199
|
+
self.assertEqual(self._exit_code("Mortality fell (HR 0.74); absolute risk 2 per 1000."), 0)
|
|
200
|
+
|
|
201
|
+
def test_explicit_unknown_baseline_sentence_clears_the_belt(self):
|
|
202
|
+
text = ("Mortality fell (HR 0.74). " + rs.BASELINE_UNKNOWN_TEXT +
|
|
203
|
+
" — the source reports no control-arm event rate.")
|
|
204
|
+
self.assertEqual(self._exit_code(text), 0)
|
|
205
|
+
|
|
206
|
+
def test_the_russian_adverb_does_not_clear_the_belt(self):
|
|
207
|
+
"""QE G7 — `абсолютн` also matched the FILLER ADVERB «абсолютно».
|
|
208
|
+
|
|
209
|
+
MEASURED before the fix: `scan_relative_risk('Риск удваивается, это абсолютно
|
|
210
|
+
доказано.')` returned NO findings, while the same sentence without the adverb
|
|
211
|
+
returned `['RELATIVE_RISK_WITHOUT_ABSOLUTE']` — a rhetorical word disarmed the
|
|
212
|
+
belt, which is the worst possible clearing condition for a medical report.
|
|
213
|
+
"""
|
|
214
|
+
self.assertEqual(self._exit_code("Риск удваивается, это абсолютно доказано."), 1)
|
|
215
|
+
self.assertEqual(self._exit_code("Риск удваивается, это доказано."), 1,
|
|
216
|
+
"the control: the adverb was the ONLY difference")
|
|
217
|
+
|
|
218
|
+
def test_genuine_adjectival_forms_still_clear_it(self):
|
|
219
|
+
"""DISCRIMINATION in the other direction: the fix must not blind the belt to a
|
|
220
|
+
real absolute figure. Every declension of the ADJECTIVE still clears — the fix
|
|
221
|
+
is a lookahead, not a hand-kept list of endings."""
|
|
222
|
+
for phrase in ("абсолютный риск 4 на 1000",
|
|
223
|
+
"абсолютное снижение составило 2 случая",
|
|
224
|
+
"в абсолютных числах это 3 человека",
|
|
225
|
+
"абсолютная разница — 1 случай",
|
|
226
|
+
"с абсолютным риском 5 на 1000"):
|
|
227
|
+
with self.subTest(phrase=phrase):
|
|
228
|
+
self.assertEqual(self._exit_code("Риск удваивается: " + phrase + "."), 0)
|
|
229
|
+
|
|
230
|
+
def test_the_belt_declares_itself_a_belt(self):
|
|
231
|
+
"""D-19 — the gate's OWN output must say the scan matches formats, not meaning,
|
|
232
|
+
so nobody quotes it as evidence that the property holds."""
|
|
233
|
+
rendered = gate.render_population_and_risk({}, True)
|
|
234
|
+
self.assertIn("FORMATS, not meaning", rendered)
|
|
235
|
+
self.assertIn("test_risk_absolute.py", rendered)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
if __name__ == "__main__":
|
|
239
|
+
unittest.main()
|