@dzhechkov/p-replicator 1.5.16 → 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 +891 -280
- 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 +970 -306
- 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,554 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
ADR-003 / ADR-005 Confirmation tests — the v3 signed message (T-1…T-8, T-17, T-19).
|
|
4
|
+
|
|
5
|
+
cd .../goap-research-ed25519/scripts && python3 -m unittest discover -s . -p 'test_*.py' -v
|
|
6
|
+
|
|
7
|
+
The load-bearing test here is `test_trust_class_reclaim_breaks_v3_signature_discrimination`:
|
|
8
|
+
it models the LIVE attack — sign ISSUER_SIGNED while the pin is active, revoke the
|
|
9
|
+
pin at verify time, reclaim to SELF_ATTESTED, verify — because a pin that was never
|
|
10
|
+
active would fail for the wrong reason and prove nothing.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import copy
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
# Import NOTHING local before this line (stray __pycache__ reads as canonical drift).
|
|
20
|
+
sys.dont_write_bytecode = True
|
|
21
|
+
|
|
22
|
+
import unittest
|
|
23
|
+
|
|
24
|
+
import ed25519_verifier as ev
|
|
25
|
+
|
|
26
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _pop(description="men with obesity (BMI >= 30) enrolled in a weight-loss trial"):
|
|
30
|
+
return {
|
|
31
|
+
"description": description,
|
|
32
|
+
"criteria": {
|
|
33
|
+
"bmi_min": {"op": ">=", "value": 30, "kind": "baseline",
|
|
34
|
+
"verbatim": "men with obesity (BMI >= 30 kg/m2)",
|
|
35
|
+
"locator": "[Methods, Participants]"},
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _verifier():
|
|
41
|
+
v = ev.Ed25519Verifier()
|
|
42
|
+
v.generate_keypair()
|
|
43
|
+
return v
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _fixture(name):
|
|
47
|
+
with open(os.path.join(HERE, name), "r", encoding="utf-8") as handle:
|
|
48
|
+
return json.load(handle)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@unittest.skipIf(ev.CRYPTO_BACKEND is None, "No Ed25519 backend installed")
|
|
52
|
+
class MandatoryStudyPopulationTests(unittest.TestCase):
|
|
53
|
+
"""T-1 (AM-1 / FR-1) — five factories x four malformed inputs = 20 refusals.
|
|
54
|
+
|
|
55
|
+
ASSERTS THE `TypeError`, never "the suite is green" (AM-13): giving the argument
|
|
56
|
+
a default keeps the whole suite green and makes only this test fail — which is
|
|
57
|
+
exactly why it is written this way.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def setUp(self):
|
|
61
|
+
self.v = _verifier()
|
|
62
|
+
import evidence_fetch as ef
|
|
63
|
+
self.record = ef.FetchRecord(
|
|
64
|
+
url="https://pubmed.ncbi.nlm.nih.gov/123", final_url="https://pubmed.ncbi.nlm.nih.gov/123",
|
|
65
|
+
status=200, sha256_body="a" * 64, bytes_len=10, fetched_at="2026-08-03T10:00:00Z",
|
|
66
|
+
witness=ef._FETCH_WITNESS)
|
|
67
|
+
|
|
68
|
+
def _factories(self):
|
|
69
|
+
return {
|
|
70
|
+
"create_signed_fact": lambda **kw: self.v.create_signed_fact(
|
|
71
|
+
claim="c", source_url="https://x.test", source_content="b", issuer="researcher", **kw),
|
|
72
|
+
"create_issuer_signed_fact": lambda **kw: self.v.create_issuer_signed_fact(
|
|
73
|
+
claim="c", source_url="https://x.test", source_content="b", issuer="researcher", **kw),
|
|
74
|
+
"create_fetched_fact": lambda **kw: self.v.create_fetched_fact(
|
|
75
|
+
claim="c", fetch_record=self.record, issuer="researcher", **kw),
|
|
76
|
+
"create_listing_fact": lambda **kw: self.v.create_listing_fact(
|
|
77
|
+
claim="c", source_url="https://x.test", reason="offline run", **kw),
|
|
78
|
+
"create_asserted_fact": lambda **kw: self.v.create_asserted_fact(claim="c", **kw),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
def test_create_fact_without_study_population_raises(self):
|
|
82
|
+
cases = 0
|
|
83
|
+
for name, factory in self._factories().items():
|
|
84
|
+
with self.subTest(factory=name, case="omitted"):
|
|
85
|
+
with self.assertRaises(TypeError):
|
|
86
|
+
factory()
|
|
87
|
+
cases += 1
|
|
88
|
+
for case, value in (("None", None), ("empty string", ""),
|
|
89
|
+
("present but meaningless", {"description": " ", "criteria": {}})):
|
|
90
|
+
with self.subTest(factory=name, case=case):
|
|
91
|
+
with self.assertRaises(ValueError):
|
|
92
|
+
factory(study_population=value)
|
|
93
|
+
cases += 1
|
|
94
|
+
self.assertEqual(cases, 20, "5 factories x 4 malformed inputs")
|
|
95
|
+
|
|
96
|
+
def test_study_population_blank_or_empty_raises(self):
|
|
97
|
+
"""T-2 — the API-shape refusal, exercised through the fact-construction path."""
|
|
98
|
+
for bad in ({"description": "adults", "criteria": {}},
|
|
99
|
+
{"description": "", "criteria": {"bmi": 1}},
|
|
100
|
+
{"criteria": {"bmi": 1}},
|
|
101
|
+
"adults"):
|
|
102
|
+
with self.subTest(value=bad):
|
|
103
|
+
with self.assertRaises(ValueError):
|
|
104
|
+
self.v.create_asserted_fact(claim="c", study_population=bad)
|
|
105
|
+
|
|
106
|
+
def test_unstated_requires_reason(self):
|
|
107
|
+
with self.assertRaises(ValueError):
|
|
108
|
+
self.v.create_asserted_fact(claim="c", study_population={
|
|
109
|
+
"description": "population not stated", "criteria": {}, "unstated_reason": " "})
|
|
110
|
+
fact = self.v.create_asserted_fact(claim="c", study_population={
|
|
111
|
+
"description": "population not stated", "criteria": {},
|
|
112
|
+
"unstated_reason": "the abstract never describes who was enrolled"})
|
|
113
|
+
self.assertTrue(self.v.verify_fact(fact).verified)
|
|
114
|
+
|
|
115
|
+
def test_every_new_fact_is_v3(self):
|
|
116
|
+
for name, factory in self._factories().items():
|
|
117
|
+
with self.subTest(factory=name):
|
|
118
|
+
fact = factory(study_population=_pop())
|
|
119
|
+
self.assertEqual(ev.fact_schema_version(fact), 3)
|
|
120
|
+
self.assertIsNotNone(fact.study_population)
|
|
121
|
+
if fact.trust_class == ev.TRUST_CLASS_ISSUER_SIGNED:
|
|
122
|
+
# An issuer-signed fact verifies against the PINNED key, never the
|
|
123
|
+
# embedded one — that is the whole point of the class, so the pin
|
|
124
|
+
# must exist for this check to mean anything.
|
|
125
|
+
self.v.registry.add("researcher", self.v.get_public_key_b64())
|
|
126
|
+
self.assertTrue(self.v.verify_fact(fact).verified)
|
|
127
|
+
|
|
128
|
+
def test_from_dict_stays_permissive(self):
|
|
129
|
+
"""FR-1's other half: mandatory on CREATION, never on LOADING. A loader that
|
|
130
|
+
refuses a legacy record cannot report on it."""
|
|
131
|
+
legacy = _fixture("fixture_legacy_v1_fact.json")["fact"]
|
|
132
|
+
fact = ev.SignedFact.from_dict(legacy)
|
|
133
|
+
self.assertIsNone(fact.study_population)
|
|
134
|
+
self.assertEqual(ev.fact_schema_version(fact), 1)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@unittest.skipIf(ev.CRYPTO_BACKEND is None, "No Ed25519 backend installed")
|
|
138
|
+
class V3SignatureCoverageTests(unittest.TestCase):
|
|
139
|
+
"""T-3 / T-5 — the four new keys are UNDER the signature, both tamper directions."""
|
|
140
|
+
|
|
141
|
+
def setUp(self):
|
|
142
|
+
self.v = _verifier()
|
|
143
|
+
|
|
144
|
+
def _fact(self, **kw):
|
|
145
|
+
return self.v.create_listing_fact(claim="c", source_url="https://x.test",
|
|
146
|
+
reason="offline run", study_population=_pop(), **kw)
|
|
147
|
+
|
|
148
|
+
def test_tamper_study_population_breaks_v3_signature(self):
|
|
149
|
+
"""T-3 (AM-2 / FR-13) — strip / add / swap, the same three-test shape the
|
|
150
|
+
evidence axis already uses."""
|
|
151
|
+
stripped = self._fact()
|
|
152
|
+
self.assertTrue(self.v.verify_fact(stripped).verified)
|
|
153
|
+
stripped.study_population = None
|
|
154
|
+
self.assertFalse(self.v.verify_fact(stripped).verified, "stripping it must break the signature")
|
|
155
|
+
|
|
156
|
+
swapped = self._fact()
|
|
157
|
+
swapped.study_population = _pop("a completely different cohort")
|
|
158
|
+
self.assertFalse(self.v.verify_fact(swapped).verified, "swapping it must break the signature")
|
|
159
|
+
|
|
160
|
+
added = ev.SignedFact.from_dict(_fixture("fixture_legacy_v2_fact.json")["fact"])
|
|
161
|
+
self.assertTrue(self.v.verify_fact(added).verified)
|
|
162
|
+
added.study_population = _pop()
|
|
163
|
+
self.assertFalse(self.v.verify_fact(added).verified, "adding it to a v2 fact must break it")
|
|
164
|
+
|
|
165
|
+
def test_dispatch_tamper_table(self):
|
|
166
|
+
"""ADR-003 §2's dispatch rows — MEASURED, and one row came back different from
|
|
167
|
+
what the ADR predicted.
|
|
168
|
+
|
|
169
|
+
What refuses a tampered fact is the KEY-SET/VALUE difference between the two
|
|
170
|
+
reconstructed texts, never the `schema` marker or the `schema_version` field
|
|
171
|
+
(both are self-description). This module has already been taught that lesson
|
|
172
|
+
once: ADR-002's first draft credited the v2 marker with closing the downgrade
|
|
173
|
+
attack and its own discrimination run refuted it.
|
|
174
|
+
"""
|
|
175
|
+
rows = {}
|
|
176
|
+
edited = self._fact()
|
|
177
|
+
edited.trust_class = ev.TRUST_CLASS_ISSUER_SIGNED
|
|
178
|
+
rows["edit trust_class"] = edited
|
|
179
|
+
|
|
180
|
+
downgraded = self._fact()
|
|
181
|
+
downgraded.schema_version = 1
|
|
182
|
+
rows["schema_version = 1"] = downgraded
|
|
183
|
+
|
|
184
|
+
both_stripped = self._fact()
|
|
185
|
+
both_stripped.schema_version = None
|
|
186
|
+
both_stripped.study_population = None
|
|
187
|
+
rows["strip schema_version AND study_population (the real downgrade)"] = both_stripped
|
|
188
|
+
|
|
189
|
+
upgraded = ev.SignedFact.from_dict(_fixture("fixture_legacy_v2_fact.json")["fact"])
|
|
190
|
+
upgraded.schema_version = 3
|
|
191
|
+
rows["schema_version added to a v2 fact"] = upgraded
|
|
192
|
+
|
|
193
|
+
for name, fact in rows.items():
|
|
194
|
+
with self.subTest(row=name):
|
|
195
|
+
self.assertFalse(self.v.verify_fact(fact).verified, f"{name} must not verify")
|
|
196
|
+
|
|
197
|
+
def test_stripping_schema_version_alone_is_a_no_op_not_an_attack(self):
|
|
198
|
+
"""MEASURED CORRECTION to ADR-003 §2's predicted tamper table (reported, not
|
|
199
|
+
smoothed over — the ADR listed this row as "must fail").
|
|
200
|
+
|
|
201
|
+
It does NOT fail, and it should not: dispatch is by PRESENCE (D-5), so with
|
|
202
|
+
`study_population` still there the verifier recovers version 3, rebuilds the
|
|
203
|
+
identical text, and the signature matches. Nothing about the fact changed —
|
|
204
|
+
`schema_version` is not in the signed message, it is a convenience mirror of a
|
|
205
|
+
property already derivable from the fields. The attack it was supposed to model
|
|
206
|
+
is covered by the row above: stripping the FIELD as well really does downgrade
|
|
207
|
+
the reconstruction, and really does fail.
|
|
208
|
+
"""
|
|
209
|
+
fact = self._fact()
|
|
210
|
+
fact.schema_version = None
|
|
211
|
+
self.assertEqual(ev.fact_schema_version(fact), 3, "presence-dispatch recovers the version")
|
|
212
|
+
self.assertTrue(self.v.verify_fact(fact).verified,
|
|
213
|
+
"a no-op edit must not be reported as tampering")
|
|
214
|
+
|
|
215
|
+
def test_tamper_metadata_breaks_v3_signature(self):
|
|
216
|
+
"""T-5 / AC-13 — `evidence_note` is the MANDATORY reason for a LISTING_ONLY
|
|
217
|
+
degradation; rewriting it hollows out the audit trail the axis exists for."""
|
|
218
|
+
fact = self._fact()
|
|
219
|
+
self.assertIn("evidence_note", fact.metadata)
|
|
220
|
+
fact.metadata["evidence_note"] = "fetched and verified"
|
|
221
|
+
self.assertFalse(self.v.verify_fact(fact).verified)
|
|
222
|
+
|
|
223
|
+
stripped = self._fact()
|
|
224
|
+
stripped.metadata.pop("evidence_note")
|
|
225
|
+
self.assertFalse(self.v.verify_fact(stripped).verified)
|
|
226
|
+
|
|
227
|
+
def test_tamper_confidence_breaks_v3_signature(self):
|
|
228
|
+
"""A DIFFERENT protection than the min() clamp: it protects a consumer that
|
|
229
|
+
reads `fact.confidence` directly without re-running verify_fact()."""
|
|
230
|
+
fact = self._fact()
|
|
231
|
+
fact.confidence = 0.99
|
|
232
|
+
self.assertFalse(self.v.verify_fact(fact).verified)
|
|
233
|
+
|
|
234
|
+
def test_confidence_is_signed_as_a_fixed_width_string(self):
|
|
235
|
+
"""Float repr differs across runtimes; a signed text two runtimes serialize
|
|
236
|
+
differently is a signature that fails for the wrong reason."""
|
|
237
|
+
fact = self._fact()
|
|
238
|
+
message = json.loads(ev.canonical_fact_message_v3(fact))
|
|
239
|
+
self.assertEqual(message["confidence"], "0.5000")
|
|
240
|
+
self.assertIsInstance(message["confidence"], str)
|
|
241
|
+
|
|
242
|
+
def test_unserializable_payload_refuses_to_sign(self):
|
|
243
|
+
"""D-18 — a silently dropped key is an UNSIGNED key, so signing fails closed."""
|
|
244
|
+
with self.assertRaises(ValueError):
|
|
245
|
+
self.v.create_asserted_fact(claim="c", metadata={"bad": float("nan")},
|
|
246
|
+
study_population=_pop())
|
|
247
|
+
with self.assertRaises(ValueError):
|
|
248
|
+
self.v.create_asserted_fact(claim="c", study_population={
|
|
249
|
+
"description": "d", "criteria": {"bmi": float("inf")}})
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@unittest.skipIf(ev.CRYPTO_BACKEND is None, "No Ed25519 backend installed")
|
|
253
|
+
class TrustClassReclaimDiscriminationTests(unittest.TestCase):
|
|
254
|
+
"""T-4 (AM-4) — the MANDATORY discrimination proof, modelled as the live attack."""
|
|
255
|
+
|
|
256
|
+
def test_trust_class_reclaim_breaks_v3_signature_discrimination(self):
|
|
257
|
+
signer = ev.Ed25519Verifier(auto_generate_keypair=True)
|
|
258
|
+
verifier = ev.Ed25519Verifier(trusted_issuers={
|
|
259
|
+
"nature.com": {"pubkey_b64": signer.get_public_key_b64(), "status": "active"}})
|
|
260
|
+
|
|
261
|
+
fact = signer.create_issuer_signed_fact(
|
|
262
|
+
claim="testosterone therapy showed no excess cardiovascular harm",
|
|
263
|
+
source_url="https://nature.com/articles/example", source_content="body",
|
|
264
|
+
issuer="nature.com", study_population=_pop())
|
|
265
|
+
before = verifier.verify_fact(fact)
|
|
266
|
+
self.assertTrue(before.verified, "signed while the pin was ACTIVE")
|
|
267
|
+
self.assertEqual(before.trust_class, ev.TRUST_CLASS_ISSUER_SIGNED)
|
|
268
|
+
|
|
269
|
+
# …the key is revoked AFTER signing. This sequencing is the point: a pin that
|
|
270
|
+
# was never active would fail for the wrong reason and prove nothing.
|
|
271
|
+
verifier.registry.add("nature.com", signer.get_public_key_b64(), status="revoked")
|
|
272
|
+
self.assertFalse(verifier.verify_fact(fact).verified, "a revoked pin refuses the honest fact")
|
|
273
|
+
|
|
274
|
+
reclaimed = copy.deepcopy(fact)
|
|
275
|
+
reclaimed.trust_class = ev.TRUST_CLASS_SELF_ATTESTED # the attack, in a text editor
|
|
276
|
+
result = verifier.verify_fact(reclaimed)
|
|
277
|
+
self.assertFalse(result.verified,
|
|
278
|
+
f"the reclaim must not verify (got verified={result.verified}, "
|
|
279
|
+
f"confidence={result.confidence})")
|
|
280
|
+
self.assertEqual(result.confidence, 0.0,
|
|
281
|
+
"pre-fix this returned confidence=0.60 on a revoked-pin fact")
|
|
282
|
+
|
|
283
|
+
# …and the reverse direction, for completeness (FR-11).
|
|
284
|
+
promoted = signer.create_signed_fact(
|
|
285
|
+
claim="c", source_url="https://nature.com/articles/example", source_content="b",
|
|
286
|
+
issuer="nature.com", study_population=_pop())
|
|
287
|
+
self.assertTrue(ev.Ed25519Verifier().verify_fact(promoted).verified)
|
|
288
|
+
promoted.trust_class = ev.TRUST_CLASS_ISSUER_SIGNED
|
|
289
|
+
self.assertFalse(verifier.verify_fact(promoted).verified,
|
|
290
|
+
"SELF_ATTESTED -> ISSUER_SIGNED must break it too")
|
|
291
|
+
|
|
292
|
+
def test_legacy_reclaim_belt_is_bounded_and_honest(self):
|
|
293
|
+
"""M3.7 — the v1/v2 belt catches the REVOKED-BUT-PINNED case (issuer IS signed
|
|
294
|
+
in v1/v2, so the lookup cannot be redirected) and openly does NOT catch the
|
|
295
|
+
unpinned-issuer case, which stays launderable to SELF_ATTESTED @ 0.60."""
|
|
296
|
+
signer = ev.Ed25519Verifier(auto_generate_keypair=True)
|
|
297
|
+
legacy = ev.SignedFact(
|
|
298
|
+
claim="c", source_url="https://x.test", source_hash="0" * 64, issuer="nature.com",
|
|
299
|
+
issuer_pubkey=f"ed25519:{signer.get_public_key_b64()}", signature="",
|
|
300
|
+
timestamp="2026-08-03T12:16:18.191716Z", confidence=0.60,
|
|
301
|
+
trust_class=ev.TRUST_CLASS_SELF_ATTESTED)
|
|
302
|
+
legacy.signature, _ = signer.sign_content(ev.canonical_fact_message_v1(legacy))
|
|
303
|
+
self.assertEqual(ev.fact_schema_version(legacy), 1)
|
|
304
|
+
|
|
305
|
+
unpinned = ev.Ed25519Verifier()
|
|
306
|
+
residual = unpinned.verify_fact(legacy)
|
|
307
|
+
self.assertTrue(residual.verified, "the unclosable residual, named rather than hidden")
|
|
308
|
+
self.assertEqual(residual.confidence, 0.60)
|
|
309
|
+
|
|
310
|
+
pinned = ev.Ed25519Verifier(trusted_issuers={
|
|
311
|
+
"nature.com": {"pubkey_b64": signer.get_public_key_b64(), "status": "revoked"}})
|
|
312
|
+
caught = pinned.verify_fact(legacy)
|
|
313
|
+
self.assertFalse(caught.verified)
|
|
314
|
+
self.assertIn("revoked", caught.error)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@unittest.skipIf(ev.CRYPTO_BACKEND is None, "No Ed25519 backend installed")
|
|
318
|
+
class BackwardCompatibilityTests(unittest.TestCase):
|
|
319
|
+
"""T-6 / T-7 (AM-5) — COMMITTED fixtures, replayed. Comparing two CURRENT code
|
|
320
|
+
paths would mask exactly the regression these exist to catch."""
|
|
321
|
+
|
|
322
|
+
def test_v1_legacy_fact_still_verifies_after_v3(self):
|
|
323
|
+
fact = ev.SignedFact.from_dict(_fixture("fixture_legacy_v1_fact.json")["fact"])
|
|
324
|
+
self.assertEqual(ev.fact_schema_version(fact), 1)
|
|
325
|
+
self.assertTrue(ev.Ed25519Verifier().verify_fact(fact).verified)
|
|
326
|
+
|
|
327
|
+
def test_v2_legacy_fact_still_verifies_after_v3(self):
|
|
328
|
+
"""The v2 fixture was generated from PRE-CHANGE code and committed (M0.2):
|
|
329
|
+
generating it after the first edit would have made this test self-fulfilling."""
|
|
330
|
+
fact = ev.SignedFact.from_dict(_fixture("fixture_legacy_v2_fact.json")["fact"])
|
|
331
|
+
self.assertEqual(ev.fact_schema_version(fact), 2)
|
|
332
|
+
self.assertIsNotNone(fact.evidence_class)
|
|
333
|
+
self.assertIsNone(fact.study_population)
|
|
334
|
+
self.assertTrue(ev.Ed25519Verifier().verify_fact(fact).verified)
|
|
335
|
+
|
|
336
|
+
def test_v1_and_v2_messages_are_byte_frozen(self):
|
|
337
|
+
"""D-4 / D-4a — the two older texts must not gain a key when v3 lands."""
|
|
338
|
+
v1 = ev.SignedFact.from_dict(_fixture("fixture_legacy_v1_fact.json")["fact"])
|
|
339
|
+
v2 = ev.SignedFact.from_dict(_fixture("fixture_legacy_v2_fact.json")["fact"])
|
|
340
|
+
self.assertEqual(
|
|
341
|
+
sorted(json.loads(ev.canonical_fact_message_v1(v1))),
|
|
342
|
+
["claim", "issuer", "research_context", "source_hash", "source_url", "timestamp"])
|
|
343
|
+
self.assertEqual(
|
|
344
|
+
sorted(json.loads(ev.canonical_fact_message_v2(v2))),
|
|
345
|
+
["claim", "evidence_class", "fetch_date", "issuer", "research_context", "schema",
|
|
346
|
+
"source_date", "source_hash", "source_url", "timestamp"])
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@unittest.skipIf(ev.CRYPTO_BACKEND is None, "No Ed25519 backend installed")
|
|
350
|
+
class SignedFieldsTests(unittest.TestCase):
|
|
351
|
+
"""T-8 (D-6) — the pre-v3 hole becomes a VALUE a caller can branch on."""
|
|
352
|
+
|
|
353
|
+
def test_verification_result_names_unsigned_fields(self):
|
|
354
|
+
v1 = ev.SignedFact.from_dict(_fixture("fixture_legacy_v1_fact.json")["fact"])
|
|
355
|
+
v2 = ev.SignedFact.from_dict(_fixture("fixture_legacy_v2_fact.json")["fact"])
|
|
356
|
+
verifier = ev.Ed25519Verifier()
|
|
357
|
+
|
|
358
|
+
r1 = verifier.verify_fact(v1)
|
|
359
|
+
self.assertEqual(r1.schema_version, 1)
|
|
360
|
+
self.assertNotIn("trust_class", r1.signed_fields)
|
|
361
|
+
|
|
362
|
+
r2 = verifier.verify_fact(v2)
|
|
363
|
+
self.assertEqual(r2.schema_version, 2)
|
|
364
|
+
self.assertNotIn("trust_class", r2.signed_fields)
|
|
365
|
+
self.assertIn("evidence_class", r2.signed_fields)
|
|
366
|
+
|
|
367
|
+
signer = _verifier()
|
|
368
|
+
v3 = signer.create_asserted_fact(claim="c", study_population=_pop())
|
|
369
|
+
r3 = signer.verify_fact(v3)
|
|
370
|
+
self.assertEqual(r3.schema_version, 3)
|
|
371
|
+
for key in ("trust_class", "metadata", "confidence", "study_population"):
|
|
372
|
+
self.assertIn(key, r3.signed_fields)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
class TierCeilingTests(unittest.TestCase):
|
|
376
|
+
"""T-19 (AM-15 / ADR-005 / D-20) — the ceiling survives the schema bump."""
|
|
377
|
+
|
|
378
|
+
@unittest.skipIf(ev.CRYPTO_BACKEND is None, "No Ed25519 backend installed")
|
|
379
|
+
def test_am15_tier_ceiling_applies_to_schema_3_facts(self):
|
|
380
|
+
"""Three assertions, one property.
|
|
381
|
+
|
|
382
|
+
Minting schema 3 must NOT switch the third ceiling off. The predicate used to
|
|
383
|
+
read `fact_schema_version(fact) != 2` — "applies to exactly v2" — so every fact
|
|
384
|
+
this slice creates would have scored 0.60 instead of the promised 0.40 from an
|
|
385
|
+
unknown domain, silently. The tier-A row proves the tier term is actually being
|
|
386
|
+
MEASURED rather than being a constant; the v1 row proves the lower edge (no
|
|
387
|
+
retroactive re-scoring) still holds.
|
|
388
|
+
"""
|
|
389
|
+
v = _verifier()
|
|
390
|
+
unknown_domain = v.create_fetched_fact(
|
|
391
|
+
claim="c", issuer="researcher", study_population=_pop(),
|
|
392
|
+
fetch_record=_authentic_record("https://some-blog.example/p"))
|
|
393
|
+
self.assertEqual(ev.fact_schema_version(unknown_domain), 3)
|
|
394
|
+
self.assertEqual(v.verify_fact(unknown_domain).confidence, 0.40,
|
|
395
|
+
"tier D must cap a schema-3 fact, exactly as it caps a schema-2 one")
|
|
396
|
+
|
|
397
|
+
tier_a = v.create_fetched_fact(
|
|
398
|
+
claim="c", issuer="researcher", study_population=_pop(),
|
|
399
|
+
fetch_record=_authentic_record("https://www.cochrane.org/x"))
|
|
400
|
+
self.assertEqual(v.verify_fact(tier_a).confidence, 0.60,
|
|
401
|
+
"tier A leaves the trust ceiling binding — the tier term is measured, not constant")
|
|
402
|
+
|
|
403
|
+
legacy = ev.SignedFact(
|
|
404
|
+
claim="c", source_url="https://some-blog.example/p", source_hash="0" * 64,
|
|
405
|
+
issuer="researcher", issuer_pubkey=f"ed25519:{v.get_public_key_b64()}", signature="",
|
|
406
|
+
timestamp="2026-08-03T12:16:18.191716Z", confidence=0.60,
|
|
407
|
+
trust_class=ev.TRUST_CLASS_SELF_ATTESTED)
|
|
408
|
+
legacy.signature, _ = v.sign_content(ev.canonical_fact_message_v1(legacy))
|
|
409
|
+
self.assertEqual(ev.fact_schema_version(legacy), 1)
|
|
410
|
+
self.assertEqual(v.verify_fact(legacy).confidence, 0.60,
|
|
411
|
+
"v1 stays exempt — old records keep the semantics they were created under")
|
|
412
|
+
|
|
413
|
+
def test_the_scope_is_a_lower_bound_not_an_equality(self):
|
|
414
|
+
"""The shape of the fix, asserted directly: any FUTURE schema must inherit the
|
|
415
|
+
ceiling without another migration. An equality gate has to be re-edited by
|
|
416
|
+
every migration; a lower bound is edited once."""
|
|
417
|
+
self.assertEqual(ev.TIER_CEILING_MIN_SCHEMA, 2)
|
|
418
|
+
source = open(os.path.join(HERE, "ed25519_verifier.py"), encoding="utf-8").read()
|
|
419
|
+
self.assertNotIn("fact_schema_version(fact) != 2", source,
|
|
420
|
+
"the equality predicate must not return")
|
|
421
|
+
self.assertIn("< TIER_CEILING_MIN_SCHEMA", source,
|
|
422
|
+
"the scope must stay a `<` lower bound against the named constant")
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
@unittest.skipIf(ev.CRYPTO_BACKEND is None, "No Ed25519 backend installed")
|
|
426
|
+
class SchemaVersionRefusalTests(unittest.TestCase):
|
|
427
|
+
"""QE G6 — a schema the verifier cannot identify is REFUSED, never crashed on.
|
|
428
|
+
|
|
429
|
+
MEASURED before the fix: loading the committed v2 fixture with
|
|
430
|
+
`schema_version='not-a-number'` and calling `verify_fact` raised an UNCAUGHT
|
|
431
|
+
`ValueError` out of `int(fact.schema_version)`, taking down a gate run with a
|
|
432
|
+
traceback instead of returning `verified=False`. And within the old `>= 3` band the
|
|
433
|
+
value was not covered by the signed text, so `3` could be moved to `99` and the
|
|
434
|
+
fact still verified.
|
|
435
|
+
"""
|
|
436
|
+
|
|
437
|
+
def _v3_fact(self):
|
|
438
|
+
v = _verifier()
|
|
439
|
+
return v, v.create_listing_fact(claim="c", source_url="https://x.test",
|
|
440
|
+
reason="offline run", study_population=_pop())
|
|
441
|
+
|
|
442
|
+
def test_malformed_schema_version_refuses_instead_of_raising(self):
|
|
443
|
+
fact = ev.SignedFact.from_dict(_fixture("fixture_legacy_v2_fact.json")["fact"])
|
|
444
|
+
verifier = ev.Ed25519Verifier()
|
|
445
|
+
self.assertTrue(verifier.verify_fact(fact).verified, "the honest fixture still verifies")
|
|
446
|
+
|
|
447
|
+
fact.schema_version = "not-a-number"
|
|
448
|
+
result = verifier.verify_fact(fact) # must NOT raise
|
|
449
|
+
self.assertFalse(result.verified)
|
|
450
|
+
self.assertIn("not an integer", result.error)
|
|
451
|
+
self.assertEqual(result.schema_version, ev.SCHEMA_VERSION_UNIDENTIFIED,
|
|
452
|
+
"an unidentifiable record is not reported as legacy v1")
|
|
453
|
+
|
|
454
|
+
def test_every_malformed_spelling_is_refused(self):
|
|
455
|
+
for bad in ("not-a-number", "", "3.5", " ", [], {}, True, 2.9, object()):
|
|
456
|
+
with self.subTest(value=bad):
|
|
457
|
+
self.assertIsNone(ev.coerce_schema_version(bad))
|
|
458
|
+
for good, expected in ((3, 3), ("3", 3), (" 2 ", 2), (3.0, 3)):
|
|
459
|
+
with self.subTest(value=good):
|
|
460
|
+
self.assertEqual(ev.coerce_schema_version(good), expected)
|
|
461
|
+
|
|
462
|
+
def test_moving_the_version_inside_the_old_band_now_breaks_verification(self):
|
|
463
|
+
"""`3 → 99` used to verify: the band accepted anything at or above 3 as v3."""
|
|
464
|
+
v, fact = self._v3_fact()
|
|
465
|
+
self.assertTrue(v.verify_fact(fact).verified)
|
|
466
|
+
fact.schema_version = 99
|
|
467
|
+
result = v.verify_fact(fact)
|
|
468
|
+
self.assertFalse(result.verified, "an unknown schema must not be treated as v3")
|
|
469
|
+
self.assertIn("99", result.error)
|
|
470
|
+
self.assertEqual(ev.KNOWN_SCHEMA_VERSIONS, (1, 2, 3))
|
|
471
|
+
|
|
472
|
+
def test_an_unknown_schema_has_no_message_to_reconstruct(self):
|
|
473
|
+
v, fact = self._v3_fact()
|
|
474
|
+
fact.schema_version = 4
|
|
475
|
+
with self.assertRaises(ev.SchemaVersionError):
|
|
476
|
+
ev.canonical_fact_message(fact)
|
|
477
|
+
self.assertEqual(ev.signed_fields_for(fact), (),
|
|
478
|
+
"and no field may be claimed as signed for it")
|
|
479
|
+
|
|
480
|
+
def test_the_gate_reports_a_malformed_record_instead_of_dying(self):
|
|
481
|
+
"""The composition that motivated the finding: this ran inside a gate."""
|
|
482
|
+
import check_report_evidence as gate
|
|
483
|
+
fact = dict(_fixture("fixture_legacy_v2_fact.json")["fact"], schema_version="not-a-number")
|
|
484
|
+
findings = gate.verify_ledger_signatures([fact])
|
|
485
|
+
self.assertTrue(findings, "a record the verifier refuses must surface as a finding")
|
|
486
|
+
self.assertIn(findings[0].kind, ("TAMPERED_FACT", "UNVERIFIABLE_FACT"))
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _authentic_record(url):
|
|
490
|
+
import evidence_fetch as ef
|
|
491
|
+
return ef.FetchRecord(url=url, final_url=url, status=200, sha256_body="a" * 64,
|
|
492
|
+
bytes_len=10, fetched_at="2026-08-03T10:00:00Z", witness=ef._FETCH_WITNESS)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
class GrepGateTests(unittest.TestCase):
|
|
496
|
+
"""T-17 — no module builds a canonical fact message outside the three functions."""
|
|
497
|
+
|
|
498
|
+
def test_no_inlined_canonical_message_dict(self):
|
|
499
|
+
"""Step-0 recalled pattern #2: a new resolver does not deliver a migration
|
|
500
|
+
unless every call site is wired — so grep for old/duplicate literals."""
|
|
501
|
+
offenders = []
|
|
502
|
+
for name in ("population_match.py", "risk_statement.py", "check_report_evidence.py"):
|
|
503
|
+
text = open(os.path.join(HERE, name), encoding="utf-8").read()
|
|
504
|
+
for match in re.finditer(r'"claim"\s*:', text):
|
|
505
|
+
window = text[max(0, match.start() - 400):match.start() + 400]
|
|
506
|
+
if '"source_hash"' in window or '"issuer_pubkey"' in window:
|
|
507
|
+
offenders.append(f"{name}:{text[:match.start()].count(chr(10)) + 1}")
|
|
508
|
+
self.assertEqual(offenders, [], "a canonical message is built in ed25519_verifier.py only")
|
|
509
|
+
|
|
510
|
+
def test_verifier_never_imports_the_semantics_module(self):
|
|
511
|
+
"""05_architecture §1.1c — a crypto module whose correctness depends on
|
|
512
|
+
importing a semantics module repeats the fail-open shape source_tier_ceiling
|
|
513
|
+
was already bitten by."""
|
|
514
|
+
source = open(os.path.join(HERE, "ed25519_verifier.py"), encoding="utf-8").read()
|
|
515
|
+
self.assertNotIn("import population_match", source)
|
|
516
|
+
self.assertNotIn("from population_match", source)
|
|
517
|
+
|
|
518
|
+
def test_no_new_file_assumes_an_install_layout(self):
|
|
519
|
+
"""M7.2 — keeps the eventual cross-copy propagation cheap.
|
|
520
|
+
|
|
521
|
+
The needles are assembled at runtime so that this file, which must scan
|
|
522
|
+
ITSELF, does not fail on its own assertion text.
|
|
523
|
+
"""
|
|
524
|
+
needles = ("base" + "/skills", "templates" + "/.claude")
|
|
525
|
+
for name in ("population_match.py", "risk_statement.py", "test_signature_v3.py",
|
|
526
|
+
"test_population_match.py", "test_risk_absolute.py",
|
|
527
|
+
"fixtures_field_cases.json", "fixture_legacy_v2_fact.json"):
|
|
528
|
+
with open(os.path.join(HERE, name), encoding="utf-8") as handle:
|
|
529
|
+
text = handle.read()
|
|
530
|
+
for needle in needles:
|
|
531
|
+
with self.subTest(file=name, needle=needle):
|
|
532
|
+
self.assertNotIn(needle, text)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
class SkillDocumentationTests(unittest.TestCase):
|
|
536
|
+
"""T-16 (FR-15 / AC-15) — a WEAK test on purpose: it proves the sentence exists,
|
|
537
|
+
never that it is true. Documentation is layer 4 and this file says so."""
|
|
538
|
+
|
|
539
|
+
def test_skillmd_names_the_pre_v3_signature_hole(self):
|
|
540
|
+
path = os.path.join(os.path.dirname(HERE), "SKILL.md")
|
|
541
|
+
text = open(path, encoding="utf-8").read()
|
|
542
|
+
self.assertIn("can be rewritten without invalidating the signature", text)
|
|
543
|
+
self.assertIn("fact-v3", text)
|
|
544
|
+
self.assertIn("out of scope for this slice", text)
|
|
545
|
+
|
|
546
|
+
def test_skillmd_states_the_relative_risk_rule(self):
|
|
547
|
+
path = os.path.join(os.path.dirname(HERE), "SKILL.md")
|
|
548
|
+
text = open(path, encoding="utf-8").read()
|
|
549
|
+
self.assertIn("Relative risk never travels alone", text)
|
|
550
|
+
self.assertIn("BASELINE RISK NOT ESTABLISHED", text)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
if __name__ == "__main__":
|
|
554
|
+
unittest.main()
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
T-20 / D-21 (AM-14, ADR-006) — every `test_*.py` on disk is actually COLLECTED.
|
|
4
|
+
|
|
5
|
+
cd .../goap-research-ed25519/scripts && python3 -m unittest discover -s . -p 'test_*.py' -v
|
|
6
|
+
|
|
7
|
+
Why this file exists: before slice C, `test_ed25519_verifier.py` — the security suite
|
|
8
|
+
of the module the slice edits — was named by NO run command anywhere. A report of
|
|
9
|
+
"full suite green" would have been true of the command and false of the code. A test
|
|
10
|
+
module that nothing runs is a file, not a check; this makes forgetting one fail BY
|
|
11
|
+
NAME instead of by omission.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import glob
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
sys.dont_write_bytecode = True
|
|
19
|
+
|
|
20
|
+
import unittest
|
|
21
|
+
|
|
22
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
23
|
+
PATTERN = "test_*.py"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _on_disk():
|
|
27
|
+
return sorted(os.path.basename(p)[:-3] for p in glob.glob(os.path.join(HERE, PATTERN)))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _collected():
|
|
31
|
+
names = set()
|
|
32
|
+
|
|
33
|
+
def walk(suite):
|
|
34
|
+
for item in suite:
|
|
35
|
+
if isinstance(item, unittest.TestSuite):
|
|
36
|
+
walk(item)
|
|
37
|
+
elif isinstance(item, unittest.loader._FailedTest):
|
|
38
|
+
# A module discovery could not IMPORT is reported BY NAME, never
|
|
39
|
+
# silently skipped — a gate that cannot run has cleared nothing.
|
|
40
|
+
names.add(item.id().split(".")[-1])
|
|
41
|
+
else:
|
|
42
|
+
names.add(type(item).__module__)
|
|
43
|
+
|
|
44
|
+
walk(unittest.TestLoader().discover(start_dir=HERE, pattern=PATTERN, top_level_dir=HERE))
|
|
45
|
+
return sorted(names)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SuiteCompletenessTests(unittest.TestCase):
|
|
49
|
+
def test_discovery_collects_every_test_file_on_disk(self):
|
|
50
|
+
on_disk = _on_disk()
|
|
51
|
+
collected = _collected()
|
|
52
|
+
missing = [name for name in on_disk if name not in collected]
|
|
53
|
+
self.assertEqual(missing, [],
|
|
54
|
+
f"these modules exist but discovery does not collect them: {missing}")
|
|
55
|
+
self.assertGreaterEqual(len(on_disk), 6, "the directory must still hold its test modules")
|
|
56
|
+
|
|
57
|
+
def test_the_security_suite_of_the_edited_module_is_in_the_run(self):
|
|
58
|
+
"""The specific omission AM-14 caught, pinned by name so it cannot recur."""
|
|
59
|
+
collected = _collected()
|
|
60
|
+
for required in ("test_ed25519_verifier", "test_evidence_provenance", "test_goap_planner",
|
|
61
|
+
"test_signature_v3", "test_population_match", "test_risk_absolute"):
|
|
62
|
+
self.assertIn(required, collected)
|
|
63
|
+
|
|
64
|
+
def test_no_enumerated_run_command_is_reintroduced(self):
|
|
65
|
+
"""ADR-006 D5's Monitoring clause: a SUITE command must be the discovery one,
|
|
66
|
+
so a hand-kept module list can never drift from the directory.
|
|
67
|
+
|
|
68
|
+
Scope, stated precisely: TWO OR MORE module names after `-m unittest` is an
|
|
69
|
+
enumeration and fails. A single module (`python3 -m unittest test_x -v`) is a
|
|
70
|
+
run-just-this-file hint, not a claim about the suite, and is allowed. This
|
|
71
|
+
file is skipped — it is the checker, and its own prose describes the pattern.
|
|
72
|
+
"""
|
|
73
|
+
offenders = []
|
|
74
|
+
for name in sorted(os.listdir(HERE)):
|
|
75
|
+
if not name.endswith((".py", ".md")) or name == os.path.basename(__file__):
|
|
76
|
+
continue
|
|
77
|
+
with open(os.path.join(HERE, name), encoding="utf-8", errors="ignore") as handle:
|
|
78
|
+
text = handle.read()
|
|
79
|
+
for line in text.splitlines():
|
|
80
|
+
if "-m unittest" not in line or "discover" in line:
|
|
81
|
+
continue
|
|
82
|
+
modules = [token for token in line.split("-m unittest")[1].split()
|
|
83
|
+
if token.startswith("test_")]
|
|
84
|
+
if len(modules) > 1:
|
|
85
|
+
offenders.append(f"{name}: {line.strip()}")
|
|
86
|
+
self.assertEqual(offenders, [], f"enumerated run commands found: {offenders}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
unittest.main()
|