@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
|
@@ -6,7 +6,9 @@ Every test here names the ADR property it proves. The three tamper tests are the
|
|
|
6
6
|
core: an evidence class that can be edited with a text editor is not evidence of
|
|
7
7
|
anything, and this feature would be decoration without them.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
SUITE RUN — the ONE canonical command (ADR-006 / D-21), never an enumerated file list:
|
|
10
|
+
|
|
11
|
+
cd .../goap-research-ed25519/scripts && python3 -m unittest discover -s . -p 'test_*.py' -v
|
|
10
12
|
"""
|
|
11
13
|
|
|
12
14
|
import sys
|
|
@@ -19,6 +21,7 @@ import sys
|
|
|
19
21
|
sys.dont_write_bytecode = True
|
|
20
22
|
|
|
21
23
|
import json
|
|
24
|
+
import os
|
|
22
25
|
import threading
|
|
23
26
|
import unittest
|
|
24
27
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
@@ -35,6 +38,66 @@ def _verifier():
|
|
|
35
38
|
return v
|
|
36
39
|
|
|
37
40
|
|
|
41
|
+
def _pop():
|
|
42
|
+
"""A minimal VALID study population for tests whose subject is the EVIDENCE axis.
|
|
43
|
+
|
|
44
|
+
Slice C made `study_population` a keyword-only argument with no default on all
|
|
45
|
+
five factories (FR-1 / D-1), which by construction breaks every existing caller —
|
|
46
|
+
that is the invariant working, not the invariant failing. These call sites are
|
|
47
|
+
threaded explicitly rather than given a default, because a default would keep
|
|
48
|
+
every recorded Confirmation green while deleting the guarantee.
|
|
49
|
+
|
|
50
|
+
Deliberately a STATED population, not `unstated(...)`: a blanket `unstated`
|
|
51
|
+
substitute would make every fact in this file read as "the paper never said",
|
|
52
|
+
which is a different claim from "this test does not care".
|
|
53
|
+
"""
|
|
54
|
+
return {
|
|
55
|
+
"description": "adults enrolled in the cited cohort",
|
|
56
|
+
"criteria": {
|
|
57
|
+
"age": {"op": "range", "value": [18, 80], "kind": "eligibility",
|
|
58
|
+
"verbatim": "adults aged 18-80", "locator": "[Methods]"},
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _load_v1_fixture():
|
|
64
|
+
"""A GENUINE pre-axis record, replayed from the committed artifact.
|
|
65
|
+
|
|
66
|
+
Since slice C, every `create_*` factory mints a v3 fact — so a test whose SUBJECT
|
|
67
|
+
is a legacy record can no longer obtain one as a side effect of calling a
|
|
68
|
+
constructor. It must load one (or build one deliberately, `_legacy_v1_fact`
|
|
69
|
+
below). Retyping the expected schema number instead would keep the test green and
|
|
70
|
+
quietly delete its subject.
|
|
71
|
+
"""
|
|
72
|
+
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fixture_legacy_v1_fact.json")
|
|
73
|
+
with open(path, "r", encoding="utf-8") as handle:
|
|
74
|
+
return ev.SignedFact.from_dict(json.load(handle)["fact"])
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _legacy_v1_fact(verifier, source_url, claim="c"):
|
|
78
|
+
"""Build a genuine v1 fact for a caller that needs a specific source_url.
|
|
79
|
+
|
|
80
|
+
It carries no `evidence_class`, no `study_population` and no `schema_version`, and
|
|
81
|
+
it is signed over `canonical_fact_message_v1` — the byte-frozen text (D-4). This
|
|
82
|
+
is what the pre-slice `create_signed_fact` produced; the committed fixture is the
|
|
83
|
+
same shape with a fixed URL.
|
|
84
|
+
"""
|
|
85
|
+
fact = ev.SignedFact(
|
|
86
|
+
claim=claim,
|
|
87
|
+
source_url=source_url,
|
|
88
|
+
source_hash=ev.hashlib.sha256(b"body").hexdigest(),
|
|
89
|
+
issuer="researcher",
|
|
90
|
+
issuer_pubkey=f"ed25519:{verifier.get_public_key_b64()}",
|
|
91
|
+
signature="",
|
|
92
|
+
timestamp="2026-08-03T12:16:18.191716Z",
|
|
93
|
+
confidence=0.60,
|
|
94
|
+
trust_class=ev.TRUST_CLASS_SELF_ATTESTED,
|
|
95
|
+
)
|
|
96
|
+
assert ev.fact_schema_version(fact) == 1, "the helper must produce a v1 fact, not a v3 one"
|
|
97
|
+
fact.signature, _ = verifier.sign_content(ev.canonical_fact_message_v1(fact))
|
|
98
|
+
return fact
|
|
99
|
+
|
|
100
|
+
|
|
38
101
|
def _authentic_record(**overrides):
|
|
39
102
|
"""A FetchRecord bearing this process's fetch witness.
|
|
40
103
|
|
|
@@ -65,7 +128,8 @@ class EvidenceAxisTests(unittest.TestCase):
|
|
|
65
128
|
verifiable as signed, and still refused by the report gate. Collapsing the
|
|
66
129
|
two axes into one field would make this state impossible to say."""
|
|
67
130
|
v = _verifier()
|
|
68
|
-
fact = v.create_asserted_fact(claim="Testosterone recovers without refeeding", issuer="researcher"
|
|
131
|
+
fact = v.create_asserted_fact(claim="Testosterone recovers without refeeding", issuer="researcher",
|
|
132
|
+
study_population=_pop())
|
|
69
133
|
# Promote the trust axis while leaving the evidence axis untouched, then
|
|
70
134
|
# re-sign: this is a legitimate actor asserting provenance of a record it
|
|
71
135
|
# never read — the dangerous quadrant, and it must be expressible.
|
|
@@ -85,15 +149,23 @@ class EvidenceAxisTests(unittest.TestCase):
|
|
|
85
149
|
|
|
86
150
|
def test_asserted_confidence_is_zero_not_low(self):
|
|
87
151
|
v = _verifier()
|
|
88
|
-
fact = v.create_asserted_fact(claim="X causes Y")
|
|
152
|
+
fact = v.create_asserted_fact(claim="X causes Y", study_population=_pop())
|
|
89
153
|
self.assertEqual(fact.confidence, 0.0)
|
|
90
154
|
self.assertEqual(ev.evidence_ceiling(ev.EVIDENCE_ASSERTED), 0.0)
|
|
91
155
|
|
|
92
156
|
def test_legacy_fact_evidence_is_unknown_not_guessed(self):
|
|
93
|
-
"""A pre-axis fact must read as UNKNOWN — neither ASSERTED nor VERIFIED.
|
|
157
|
+
"""A pre-axis fact must read as UNKNOWN — neither ASSERTED nor VERIFIED.
|
|
158
|
+
|
|
159
|
+
SUBJECT RE-DERIVED IN SLICE C (AM-13, 04_domain_model.md §3.1), not nudged:
|
|
160
|
+
this test used to CREATE its "legacy" fact with `create_signed_fact(...)`.
|
|
161
|
+
Since FR-1 that call mints a **v3** fact, so a freshly created fact can no
|
|
162
|
+
longer be a legacy one. Changing the expected `1` to `3` would have kept the
|
|
163
|
+
test green and deleted its subject — a legacy RECORD. It is loaded instead.
|
|
164
|
+
"""
|
|
94
165
|
v = _verifier()
|
|
95
|
-
legacy =
|
|
166
|
+
legacy = _load_v1_fixture()
|
|
96
167
|
self.assertIsNone(legacy.evidence_class)
|
|
168
|
+
self.assertIsNone(legacy.study_population, "a pre-axis fact states no population either")
|
|
97
169
|
self.assertEqual(ev.fact_schema_version(legacy), 1)
|
|
98
170
|
self.assertEqual(ev.evidence_ceiling(None), 1.0, "unknown contributes no ceiling of its own")
|
|
99
171
|
self.assertTrue(v.verify_fact(legacy).verified)
|
|
@@ -108,30 +180,49 @@ class SignatureCoverageTests(unittest.TestCase):
|
|
|
108
180
|
|
|
109
181
|
def test_evidence_class_tamper_swap_fails(self):
|
|
110
182
|
"""Relabel LISTING_ONLY → FETCH_VERIFIED on a signed fact."""
|
|
111
|
-
fact = self.v.create_listing_fact(claim="c", source_url="https://x.test", reason="offline run"
|
|
183
|
+
fact = self.v.create_listing_fact(claim="c", source_url="https://x.test", reason="offline run",
|
|
184
|
+
study_population=_pop())
|
|
112
185
|
self.assertTrue(self.v.verify_fact(fact).verified)
|
|
113
186
|
fact.evidence_class = ev.EVIDENCE_FETCH_VERIFIED
|
|
114
187
|
self.assertFalse(self.v.verify_fact(fact).verified, "swapping the class must break the signature")
|
|
115
188
|
|
|
116
189
|
def test_evidence_class_tamper_strip_fails(self):
|
|
117
|
-
"""DOWNGRADE attack: remove the field so verification falls back to
|
|
118
|
-
What breaks it is that the signed
|
|
119
|
-
|
|
120
|
-
|
|
190
|
+
"""DOWNGRADE attack: remove the field so verification falls back to an older
|
|
191
|
+
schema. What breaks it is that the signed text CONTAINS the evidence keys at
|
|
192
|
+
all — not the schema marker (a discrimination run refuted that).
|
|
193
|
+
|
|
194
|
+
THE MECHANISM CHANGED IN SLICE C, and the expected value with it (AM-13,
|
|
195
|
+
04_domain_model.md §3.1). This fact is now **v3**: it carries an explicit
|
|
196
|
+
`schema_version` and a `study_population`, so stripping `evidence_class` no
|
|
197
|
+
longer makes it *look* legacy. It stays v3, with `evidence_class: null` inside
|
|
198
|
+
the signed key set — and the signature still breaks, for a DIFFERENT reason
|
|
199
|
+
than before (a changed value in the v3 text, not a fallback to the v1 text).
|
|
200
|
+
The `3` below is that re-derivation, not a nudge to keep the test green.
|
|
201
|
+
"""
|
|
202
|
+
fact = self.v.create_fetched_fact(claim="c", fetch_record=self.record, issuer="researcher",
|
|
203
|
+
study_population=_pop())
|
|
121
204
|
self.assertTrue(self.v.verify_fact(fact).verified)
|
|
122
205
|
fact.evidence_class = None
|
|
123
|
-
self.assertEqual(ev.fact_schema_version(fact),
|
|
124
|
-
|
|
206
|
+
self.assertEqual(ev.fact_schema_version(fact), 3,
|
|
207
|
+
"a v3 fact does not fall back to v1 when evidence_class is stripped")
|
|
208
|
+
self.assertFalse(self.v.verify_fact(fact).verified,
|
|
209
|
+
"…and the mutated v3 text must not match the signed one")
|
|
125
210
|
|
|
126
211
|
def test_evidence_class_tamper_add_fails(self):
|
|
127
|
-
"""UPGRADE attack: bolt the field onto a legacy v1 fact.
|
|
128
|
-
|
|
212
|
+
"""UPGRADE attack: bolt the field onto a legacy v1 fact.
|
|
213
|
+
|
|
214
|
+
The subject is a LEGACY record, so it is loaded rather than created — since
|
|
215
|
+
FR-1, `create_signed_fact` mints v3 (AM-13).
|
|
216
|
+
"""
|
|
217
|
+
legacy = _load_v1_fixture()
|
|
129
218
|
self.assertTrue(self.v.verify_fact(legacy).verified)
|
|
130
219
|
legacy.evidence_class = ev.EVIDENCE_FETCH_VERIFIED
|
|
131
|
-
self.
|
|
220
|
+
self.assertEqual(ev.fact_schema_version(legacy), 2, "it now presents itself as v2…")
|
|
221
|
+
self.assertFalse(self.v.verify_fact(legacy).verified, "…but adding the field must break a v1 signature")
|
|
132
222
|
|
|
133
223
|
def test_fetch_date_and_source_date_are_also_covered(self):
|
|
134
|
-
fact = self.v.create_fetched_fact(claim="c", fetch_record=self.record, issuer="researcher",
|
|
224
|
+
fact = self.v.create_fetched_fact(claim="c", fetch_record=self.record, issuer="researcher",
|
|
225
|
+
source_date="2024-01-01", study_population=_pop())
|
|
135
226
|
self.assertTrue(self.v.verify_fact(fact).verified)
|
|
136
227
|
fact.fetch_date = "2099-01-01T00:00:00Z"
|
|
137
228
|
self.assertFalse(self.v.verify_fact(fact).verified, "fetch_date is signed, not decorative")
|
|
@@ -139,7 +230,8 @@ class SignatureCoverageTests(unittest.TestCase):
|
|
|
139
230
|
def test_v1_and_v2_messages_differ_for_identical_shared_fields(self):
|
|
140
231
|
"""The two schemas are distinguishable because v2 carries extra keys; the
|
|
141
232
|
marker is self-description and the v3 growth point, not the protection."""
|
|
142
|
-
fact = self.v.create_listing_fact(claim="c", source_url="https://x.test", reason="r"
|
|
233
|
+
fact = self.v.create_listing_fact(claim="c", source_url="https://x.test", reason="r",
|
|
234
|
+
study_population=_pop())
|
|
143
235
|
self.assertNotEqual(ev.canonical_fact_message_v1(fact), ev.canonical_fact_message_v2(fact))
|
|
144
236
|
self.assertIn(ev.FACT_SCHEMA_V2, ev.canonical_fact_message_v2(fact))
|
|
145
237
|
|
|
@@ -150,8 +242,9 @@ class EarnedClassTests(unittest.TestCase):
|
|
|
150
242
|
def test_manual_path_cannot_mint_fetch_verified(self):
|
|
151
243
|
"""The load-bearing property: no manual route yields FETCH_VERIFIED."""
|
|
152
244
|
v = _verifier()
|
|
153
|
-
listing = v.create_listing_fact(claim="c", source_url="https://x.test", reason="hand-supplied body"
|
|
154
|
-
|
|
245
|
+
listing = v.create_listing_fact(claim="c", source_url="https://x.test", reason="hand-supplied body",
|
|
246
|
+
study_population=_pop())
|
|
247
|
+
asserted = v.create_asserted_fact(claim="c", study_population=_pop())
|
|
155
248
|
self.assertEqual(listing.evidence_class, ev.EVIDENCE_LISTING_ONLY)
|
|
156
249
|
self.assertEqual(asserted.evidence_class, ev.EVIDENCE_ASSERTED)
|
|
157
250
|
|
|
@@ -161,7 +254,8 @@ class EarnedClassTests(unittest.TestCase):
|
|
|
161
254
|
final_url = "https://x.test"
|
|
162
255
|
# no sha256_body, no fetched_at
|
|
163
256
|
with self.assertRaises(ValueError):
|
|
164
|
-
v.create_fetched_fact(claim="c", fetch_record=NotAFetch(), issuer="researcher"
|
|
257
|
+
v.create_fetched_fact(claim="c", fetch_record=NotAFetch(), issuer="researcher",
|
|
258
|
+
study_population=_pop())
|
|
165
259
|
|
|
166
260
|
# Forging it in the JSON is caught by the signature (ADR-002).
|
|
167
261
|
forged = json.loads(listing.to_json())
|
|
@@ -172,12 +266,14 @@ class EarnedClassTests(unittest.TestCase):
|
|
|
172
266
|
v = _verifier()
|
|
173
267
|
error_page = _authentic_record(url="https://x.test", final_url="https://x.test", status=404)
|
|
174
268
|
with self.assertRaises(ValueError):
|
|
175
|
-
v.create_fetched_fact(claim="c", fetch_record=error_page, issuer="researcher"
|
|
269
|
+
v.create_fetched_fact(claim="c", fetch_record=error_page, issuer="researcher",
|
|
270
|
+
study_population=_pop())
|
|
176
271
|
|
|
177
272
|
def test_listing_fact_demands_a_stated_reason(self):
|
|
178
273
|
v = _verifier()
|
|
179
274
|
with self.assertRaises(ValueError):
|
|
180
|
-
v.create_listing_fact(claim="c", source_url="https://x.test", reason=" "
|
|
275
|
+
v.create_listing_fact(claim="c", source_url="https://x.test", reason=" ",
|
|
276
|
+
study_population=_pop())
|
|
181
277
|
|
|
182
278
|
|
|
183
279
|
class FetchTests(unittest.TestCase):
|
|
@@ -259,7 +355,8 @@ class FetchTests(unittest.TestCase):
|
|
|
259
355
|
v = _verifier()
|
|
260
356
|
failure = ef.fetch_source("http://127.0.0.1:1/unreachable", timeout=1, _allow_private=True)
|
|
261
357
|
self.assertIsInstance(failure, ef.FetchFailure)
|
|
262
|
-
fact = v.create_listing_fact(claim="c", source_url=failure.url, reason=failure.reason
|
|
358
|
+
fact = v.create_listing_fact(claim="c", source_url=failure.url, reason=failure.reason,
|
|
359
|
+
study_population=_pop())
|
|
263
360
|
self.assertEqual(fact.evidence_class, ev.EVIDENCE_LISTING_ONLY)
|
|
264
361
|
self.assertIn("evidence_note", fact.metadata)
|
|
265
362
|
|
|
@@ -365,28 +462,38 @@ class HardeningTests(unittest.TestCase):
|
|
|
365
462
|
) # constructed by hand → no witness
|
|
366
463
|
self.assertFalse(forged.is_authentic())
|
|
367
464
|
with self.assertRaises(ValueError) as ctx:
|
|
368
|
-
v.create_fetched_fact(claim="c", fetch_record=forged, issuer="researcher"
|
|
465
|
+
v.create_fetched_fact(claim="c", fetch_record=forged, issuer="researcher",
|
|
466
|
+
study_population=_pop())
|
|
369
467
|
self.assertIn("fetch_source", str(ctx.exception))
|
|
370
468
|
|
|
371
469
|
import types
|
|
372
470
|
ns = types.SimpleNamespace(status=200, final_url="u", sha256_body="d" * 64, fetched_at="t")
|
|
373
471
|
with self.assertRaises(ValueError):
|
|
374
|
-
v.create_fetched_fact(claim="c", fetch_record=ns, issuer="researcher")
|
|
472
|
+
v.create_fetched_fact(claim="c", fetch_record=ns, issuer="researcher", study_population=_pop())
|
|
375
473
|
|
|
376
474
|
def test_qe6_tier_ceiling_is_actually_applied_to_v2_facts(self):
|
|
377
475
|
"""The documented min(trust, evidence, tier) was not implemented — tier was
|
|
378
476
|
never consulted, so an unknown-domain fact kept 0.60 instead of 0.40."""
|
|
379
477
|
v = _verifier()
|
|
380
|
-
unknown = v.create_listing_fact(claim="c", source_url="https://some-blog.example/p", reason="listing"
|
|
478
|
+
unknown = v.create_listing_fact(claim="c", source_url="https://some-blog.example/p", reason="listing",
|
|
479
|
+
study_population=_pop())
|
|
381
480
|
self.assertEqual(v.verify_fact(unknown).confidence, 0.40, "tier D must cap below the 0.50 evidence ceiling")
|
|
382
|
-
known = v.create_listing_fact(claim="c", source_url="https://www.cochrane.org/x", reason="listing"
|
|
481
|
+
known = v.create_listing_fact(claim="c", source_url="https://www.cochrane.org/x", reason="listing",
|
|
482
|
+
study_population=_pop())
|
|
383
483
|
self.assertEqual(v.verify_fact(known).confidence, 0.50, "tier A leaves the evidence ceiling as the binding one")
|
|
384
484
|
|
|
385
485
|
def test_qe6b_legacy_facts_are_not_retroactively_rescored(self):
|
|
386
|
-
"""Wiring a NEW ceiling must not change how OLD records score.
|
|
486
|
+
"""Wiring a NEW ceiling must not change how OLD records score.
|
|
487
|
+
|
|
488
|
+
This is the LOWER edge of D-20's bound (ADR-005): the tier ceiling applies
|
|
489
|
+
from schema v2 **onward**, and v1 alone stays exempt. Its sibling
|
|
490
|
+
`test_signature_v3.py::test_am15_tier_ceiling_applies_to_schema_3_facts` holds
|
|
491
|
+
the upper edge. The fact is built as a genuine v1 record because every
|
|
492
|
+
factory now mints v3 (AM-13).
|
|
493
|
+
"""
|
|
387
494
|
v = _verifier()
|
|
388
|
-
legacy = v
|
|
389
|
-
|
|
495
|
+
legacy = _legacy_v1_fact(v, "https://some-blog.example/p")
|
|
496
|
+
self.assertEqual(ev.fact_schema_version(legacy), 1)
|
|
390
497
|
self.assertEqual(v.verify_fact(legacy).confidence, 0.60, "a v1 fact keeps its original semantics")
|
|
391
498
|
|
|
392
499
|
def test_qe16_frozen_v1_fixture_still_verifies(self):
|
|
@@ -416,7 +523,8 @@ class HardeningTests(unittest.TestCase):
|
|
|
416
523
|
"""Flipping a signed ASSERTED record to FETCH_VERIFIED in the JSON fooled the
|
|
417
524
|
gate: it read evidence_class as an unsigned string and never verified."""
|
|
418
525
|
v = _verifier()
|
|
419
|
-
asserted = v.create_asserted_fact(claim="the IMEI ban was introduced in Turkey"
|
|
526
|
+
asserted = v.create_asserted_fact(claim="the IMEI ban was introduced in Turkey",
|
|
527
|
+
study_population=_pop())
|
|
420
528
|
forged = json.loads(asserted.to_json())
|
|
421
529
|
forged["evidence_class"] = ev.EVIDENCE_FETCH_VERIFIED
|
|
422
530
|
findings = gate.verify_ledger_signatures([forged])
|
|
@@ -484,336 +592,649 @@ class HardeningTests(unittest.TestCase):
|
|
|
484
592
|
self.assertIn("future", reason)
|
|
485
593
|
|
|
486
594
|
|
|
487
|
-
class
|
|
488
|
-
"""
|
|
489
|
-
|
|
490
|
-
identifiers), because enumerating what personal data looks like is an arms race
|
|
491
|
-
whose every gap is a leaked medical fact."""
|
|
595
|
+
class _BridgeHarness:
|
|
596
|
+
"""The fixture the bridge tests share: one temporary CWD per test, and a fake `dz`
|
|
597
|
+
that keeps each project's store as a real file on disk."""
|
|
492
598
|
|
|
493
599
|
def setUp(self):
|
|
494
|
-
import
|
|
495
|
-
|
|
600
|
+
import os
|
|
601
|
+
import tempfile
|
|
602
|
+
import learning_bridge as lb
|
|
603
|
+
self.lb = lb
|
|
604
|
+
# teach() resolves BOTH stores from the CWD, so these tests must not run in the
|
|
605
|
+
# skill directory: _protect_brain would create a real `.health-brain/` inside a
|
|
606
|
+
# package that is vendored into 11 byte-identical copies, which is the same class
|
|
607
|
+
# of accident as the __pycache__ this file's header describes.
|
|
608
|
+
self._cwd = os.getcwd()
|
|
609
|
+
self._tmp = tempfile.TemporaryDirectory()
|
|
610
|
+
os.chdir(self._tmp.name)
|
|
611
|
+
|
|
612
|
+
def tearDown(self):
|
|
613
|
+
import os
|
|
614
|
+
os.chdir(self._cwd)
|
|
615
|
+
self._tmp.cleanup()
|
|
496
616
|
|
|
497
|
-
def
|
|
498
|
-
for
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
617
|
+
def _store_rows(self, project):
|
|
618
|
+
"""The rows a fake store holds for one project — read from disk, as the mock
|
|
619
|
+
wrote them."""
|
|
620
|
+
import json as _json
|
|
621
|
+
import os
|
|
622
|
+
path = os.path.join(project, ".dz", "memory", "patterns.jsonl")
|
|
623
|
+
try:
|
|
624
|
+
with open(path, encoding="utf-8") as handle:
|
|
625
|
+
return [_json.loads(line) for line in handle if line.strip()]
|
|
626
|
+
except OSError:
|
|
627
|
+
return []
|
|
628
|
+
|
|
629
|
+
def _fake_store(self, honours_project=True, calls=None):
|
|
630
|
+
"""A mock dz that keeps each store as a REAL FILE at
|
|
631
|
+
`<project>/.dz/memory/patterns.jsonl`.
|
|
632
|
+
|
|
633
|
+
ROUND 17 CHANGED THIS ON PURPOSE. The previous mock held one in-memory list and
|
|
634
|
+
ignored `--project` on reads, so every store was the same store — which is
|
|
635
|
+
precisely the condition finding 7 is about, and a mock that is permanently in
|
|
636
|
+
that condition cannot tell it apart from a healthy one. Writing files means
|
|
637
|
+
aliasing is REAL: if two `--project` paths resolve to the same directory (a
|
|
638
|
+
symlink at any depth, a bind mount), they are one store here exactly as they
|
|
639
|
+
would be for the real CLI, with no modelling assumption in between.
|
|
640
|
+
|
|
641
|
+
`honours_project=False` simulates the older CLI that accepts `--project` and
|
|
642
|
+
writes from the current directory anyway: the brain never grows.
|
|
643
|
+
"""
|
|
644
|
+
import json as _json
|
|
645
|
+
import os
|
|
646
|
+
root = os.path.realpath(os.getcwd())
|
|
647
|
+
counter = {"n": 0}
|
|
648
|
+
|
|
649
|
+
def path_for(project):
|
|
650
|
+
return os.path.join(project, ".dz", "memory", "patterns.jsonl")
|
|
651
|
+
|
|
652
|
+
def read(project):
|
|
653
|
+
return self._store_rows(project)
|
|
654
|
+
|
|
655
|
+
def write(project, rows):
|
|
656
|
+
path = path_for(project)
|
|
657
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
658
|
+
with open(path, "w", encoding="utf-8") as handle:
|
|
659
|
+
for row in rows:
|
|
660
|
+
handle.write(_json.dumps(row) + "\n")
|
|
661
|
+
|
|
662
|
+
def run(args, **kwargs):
|
|
663
|
+
args = list(args)
|
|
664
|
+
if calls is not None:
|
|
665
|
+
calls.append(list(args))
|
|
666
|
+
project = args[args.index("--project") + 1] if "--project" in args else root
|
|
667
|
+
if args and args[0] == "teach":
|
|
668
|
+
target = project if honours_project else root
|
|
669
|
+
counter["n"] += 1
|
|
670
|
+
write(target, read(target) + [{"dzId": f"id:{counter['n']}",
|
|
671
|
+
"pattern": args[1],
|
|
672
|
+
"domain": "health-research"}])
|
|
673
|
+
return 0, "taught", ""
|
|
674
|
+
if args and args[0] == "recall" and "--forget" in args:
|
|
675
|
+
dz_id = args[args.index("--forget") + 1]
|
|
676
|
+
write(project, [r for r in read(project) if r.get("dzId") != dz_id])
|
|
677
|
+
return 0, "forgotten", ""
|
|
678
|
+
# Since round 15 the canary is looked up BY ITS OWN TEXT, so the mock has to
|
|
679
|
+
# answer that question rather than only report a count.
|
|
680
|
+
if args and "--usage" in args:
|
|
681
|
+
return 0, _json.dumps({"top": read(project)}), ""
|
|
682
|
+
if args and args[0] == "recall":
|
|
683
|
+
return 0, f"dz recall --all --stats — {len(read(project))} learned pattern(s)", ""
|
|
684
|
+
return 0, "", ""
|
|
685
|
+
|
|
686
|
+
return run
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
class LearningBridgeTests(_BridgeHarness, unittest.TestCase):
|
|
690
|
+
"""Slice H — the loop, after the design changed.
|
|
691
|
+
|
|
692
|
+
THE HISTORY MATTERS, because these tests are much smaller than the ones they
|
|
693
|
+
replaced and that is the point. The earlier version of learning_bridge.py tried to
|
|
694
|
+
decide FROM THE TEXT whether a lesson described a method or a person. Seven rounds
|
|
695
|
+
of independent cross-model review graded it F and the finding count never converged
|
|
696
|
+
(11, 10, 5, 3, 6, 6, 8): the question is about meaning, so every pattern answering
|
|
697
|
+
it failed in both directions — admitting `patient McDonald has HIV` while refusing
|
|
698
|
+
`apoB`, and refusing a legitimate Chinese lesson for "a capitalised word".
|
|
699
|
+
|
|
700
|
+
The work is now split by nature. FORMAT is checked here (identifiers have a shape).
|
|
701
|
+
MEANING is judged by the agent, per the protocol in SKILL.md. The GUARANTEE is the
|
|
702
|
+
export hold-out, tested on the TypeScript side where it lives.
|
|
703
|
+
"""
|
|
704
|
+
|
|
705
|
+
# ------------------------------------------------------------ format: blocking
|
|
706
|
+
|
|
707
|
+
def test_formatted_identifiers_are_refused_in_any_script(self):
|
|
708
|
+
"""These have a FORMAT, which is the one thing a regex is reliable about."""
|
|
709
|
+
for leak in (
|
|
710
|
+
"contact me at a.b@clinic.org",
|
|
711
|
+
"MRN 84729163 note",
|
|
712
|
+
"passport ab1234567 identifies the holder",
|
|
713
|
+
"passport ab::1234567 identifies the holder", # a RUN of joiners
|
|
714
|
+
"passport αβ—1234567 identifies the holder", # any script
|
|
715
|
+
"trial NCT04368728 enrolled a cohort",
|
|
503
716
|
):
|
|
504
|
-
self.
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
"
|
|
512
|
-
"
|
|
513
|
-
"
|
|
514
|
-
"MRN 84729163: creatinine elevated", # an identifier
|
|
515
|
-
"reach me at doctor@clinic.example", # an email
|
|
516
|
-
"call +7 916 123 45 67", # a phone
|
|
517
|
-
"05/08/2026 repeat the panel", # a non-ISO date
|
|
518
|
-
"8 мая 2026 пересдать", # a Russian date
|
|
519
|
-
"sodium 128 mEq/L", # a unit outside the old list
|
|
520
|
-
"creatinine 130 µmol/L", # a micro sign
|
|
521
|
-
"8.04 nanomoles per litre", # a spelled-out unit
|
|
717
|
+
with self.subTest(leak=leak):
|
|
718
|
+
self.assertFalse(self.lb.check_lesson(leak).ok, leak)
|
|
719
|
+
|
|
720
|
+
def test_a_threshold_is_not_an_identifier(self):
|
|
721
|
+
"""Measured false positives from the review, kept as regressions: a comparison
|
|
722
|
+
operator marks a threshold, and four digits is a year."""
|
|
723
|
+
for good in (
|
|
724
|
+
"hcg>10000 warrants confirmation",
|
|
725
|
+
"prefer pre-2020 baselines for longitudinal comparisons",
|
|
726
|
+
"a cohort of 1200000 people shifted the estimate",
|
|
522
727
|
):
|
|
523
|
-
self.
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
self.
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
"
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
728
|
+
with self.subTest(good=good):
|
|
729
|
+
self.assertTrue(self.lb.check_lesson(good).ok, good)
|
|
730
|
+
|
|
731
|
+
def test_the_check_no_longer_judges_MEANING_and_says_so(self):
|
|
732
|
+
"""The load-bearing change. `patient McDonald has HIV` is NOT refused here —
|
|
733
|
+
not because it is acceptable, but because this layer cannot tell. Pretending it
|
|
734
|
+
could was the defect. SKILL.md addresses that question to the agent, which can
|
|
735
|
+
read meaning and is already running."""
|
|
736
|
+
verdict = self.lb.check_lesson("patient McDonald has HIV")
|
|
737
|
+
self.assertTrue(verdict.ok, "the format check passes it — meaning is not its job")
|
|
738
|
+
self.assertIn("does not know whether the sentence describes a method or a person",
|
|
739
|
+
verdict.note)
|
|
740
|
+
|
|
741
|
+
def test_lessons_in_any_language_pass_the_format_check(self):
|
|
742
|
+
"""A round refused this exact Chinese lesson for containing "a capitalised
|
|
743
|
+
word", which is meaningless for Han script."""
|
|
744
|
+
for good in ("检查 研究人群 再外推结论", "общий тестостерон нечитаем без ГСПГ",
|
|
745
|
+
"measure apoB before interpreting risk"):
|
|
746
|
+
with self.subTest(good=good):
|
|
747
|
+
self.assertTrue(self.lb.check_lesson(good).ok, good)
|
|
748
|
+
|
|
749
|
+
def test_a_number_is_a_NOTICE_not_a_refusal(self):
|
|
750
|
+
"""A number may be knowledge (a guideline threshold) or a reading from one
|
|
751
|
+
person. Only the second is a problem and only a reader can tell, so the check
|
|
752
|
+
observes it out loud instead of guessing."""
|
|
753
|
+
verdict = self.lb.check_lesson("transferrin saturation above 45% warrants attention")
|
|
754
|
+
self.assertTrue(verdict.ok)
|
|
755
|
+
self.assertTrue(any("is it knowledge" in n for n in verdict.notices))
|
|
756
|
+
|
|
757
|
+
def test_ONE_digit_predicate(self):
|
|
758
|
+
"""`isdigit` alone misses `²` and Roman numerals; a round found the two halves
|
|
759
|
+
of this file disagreeing about the word "digit"."""
|
|
760
|
+
self.assertTrue(self.lb._has_digit("threshold is ²"))
|
|
761
|
+
self.assertTrue(self.lb._has_digit("Ⅻ"))
|
|
762
|
+
self.assertFalse(self.lb._has_digit("no numbers here"))
|
|
763
|
+
|
|
764
|
+
def test_an_empty_lesson_is_refused(self):
|
|
765
|
+
self.assertFalse(self.lb.check_lesson("").ok)
|
|
546
766
|
self.assertFalse(self.lb.check_lesson(" ").ok)
|
|
547
767
|
|
|
548
|
-
def
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
self.
|
|
768
|
+
def test_round8_identifier_formats_that_got_through(self):
|
|
769
|
+
"""Every one of these was probed by review and returned ok=True."""
|
|
770
|
+
for leak in (
|
|
771
|
+
'contact "john..doe"@example.com for escalation', # quoted local part
|
|
772
|
+
|
|
773
|
+
"call +44/20/7946/0958 before the draw", # slash separators
|
|
774
|
+
"medical-record number 84729163 belongs to the subject", # hyphenated vocabulary
|
|
775
|
+
"accession 1234567AB identifies the specimen", # digits BEFORE letters
|
|
776
|
+
"accession AB_1234567 identifies the specimen", # underscore is a word char
|
|
777
|
+
):
|
|
778
|
+
with self.subTest(leak=leak):
|
|
779
|
+
self.assertFalse(self.lb.check_lesson(leak).ok, leak)
|
|
559
780
|
|
|
560
|
-
def
|
|
781
|
+
def test_a_two_group_number_is_NOT_classified(self):
|
|
782
|
+
"""`555-1234` and `500-1000` are the SAME SHAPE. An earlier pass refused both as
|
|
783
|
+
phones, which broke `sample 500-1000 records`; refusing neither is the honest
|
|
784
|
+
resolution, and it is stated rather than hidden. Three or more groups (a card
|
|
785
|
+
number, a full phone) is a different shape and is still caught."""
|
|
786
|
+
self.assertTrue(self.lb.check_lesson("call 555-1234 before the draw").ok)
|
|
787
|
+
self.assertTrue(self.lb.check_lesson("sample 500-1000 records for manual validation").ok)
|
|
788
|
+
self.assertFalse(self.lb.check_lesson("card 4111 1111 1111 1111 belongs to the patient").ok)
|
|
789
|
+
|
|
790
|
+
def test_unicode_dashes_and_fullwidth_cannot_launder_a_format(self):
|
|
791
|
+
"""U+2011 folds to U+2010 under NFKC, not to ASCII `-`, so a vocabulary pattern
|
|
792
|
+
written in ASCII missed `medical‑record number 84729163`."""
|
|
793
|
+
self.assertFalse(self.lb.check_lesson("medical\u2011record number 84729163 belongs to the subject").ok)
|
|
794
|
+
# Dashes fold by Unicode CATEGORY, so the ones I never listed fold too.
|
|
795
|
+
self.assertFalse(self.lb.check_lesson("medical\u058arecord number 84729163 belongs to the subject").ok)
|
|
796
|
+
self.assertFalse(self.lb.check_lesson("medical\u2e3arecord number 84729163 belongs to the subject").ok)
|
|
797
|
+
# A word beside a long number is a QUANTITY, whatever its case. `NCT 04368728`
|
|
798
|
+
# and `PCR 100000 reads per sample` are the SAME SHAPE — an upper-case token, a
|
|
799
|
+
# space, a long number — so neither is classified. Refusing both broke a real
|
|
800
|
+
# method lesson; refusing neither is the honest position, stated in the code.
|
|
801
|
+
self.assertTrue(self.lb.check_lesson("in a cohort of 123456789 people, stratify first").ok)
|
|
802
|
+
self.assertTrue(self.lb.check_lesson("PCR 100000 reads per sample should trigger review").ok)
|
|
803
|
+
self.assertTrue(self.lb.check_lesson("trial NCT 04368728 enrolled a cohort").ok)
|
|
804
|
+
# A dilution series has a two-digit group and is not a phone.
|
|
805
|
+
self.assertTrue(self.lb.check_lesson("use a 1000-100-10 dilution series to test linearity").ok)
|
|
806
|
+
|
|
807
|
+
def test_round8_legitimate_lessons_that_were_wrongly_refused(self):
|
|
808
|
+
"""The other half of the same finding: the phone pattern matched any run of nine
|
|
809
|
+
digits, and the medical-record vocabulary fired on the PHRASE with no value."""
|
|
810
|
+
for good in (
|
|
811
|
+
"in a cohort of 123456789 people, stratify before extrapolating",
|
|
812
|
+
"audit medical record number quality before dataset linkage",
|
|
813
|
+
):
|
|
814
|
+
with self.subTest(good=good):
|
|
815
|
+
self.assertTrue(self.lb.check_lesson(good).ok, good)
|
|
816
|
+
|
|
817
|
+
def test_round8_the_docstring_no_longer_describes_the_deleted_guard(self):
|
|
818
|
+
"""Review found the module docstring still promising that check_lesson refuses
|
|
819
|
+
patient-shaped text, enforces no digits and lower case. Stale prose about a
|
|
820
|
+
safety property is a defect with no stack trace, so it gets a test."""
|
|
821
|
+
doc = self.lb.__doc__ or ""
|
|
822
|
+
self.assertIn("does NOT decide whether a lesson describes a method or a person", doc)
|
|
823
|
+
self.assertNotIn("REFUSES the second shape", doc)
|
|
824
|
+
# …and the docstring's own former bad example is indeed accepted now.
|
|
825
|
+
self.assertTrue(self.lb.check_lesson(
|
|
826
|
+
"the patient has fasted 3 days weekly for 7 years, testosterone 8.04").ok)
|
|
827
|
+
|
|
828
|
+
# ------------------------------------------------------- meaning: the human gate
|
|
829
|
+
|
|
830
|
+
def test_teach_REFUSES_without_an_explicit_confirmation(self):
|
|
831
|
+
"""The meaning check cannot be performed by this file, so it is not faked: the
|
|
832
|
+
caller asserts it followed the protocol, and without that assertion nothing is
|
|
833
|
+
written. The refusal restates the protocol rather than just saying no."""
|
|
834
|
+
code, message = self.lb.teach("total testosterone is uninterpretable without SHBG")
|
|
835
|
+
self.assertEqual(code, 1)
|
|
836
|
+
self.assertIn("NOT RECORDED", message)
|
|
837
|
+
self.assertIn("Write the RULE it taught", message)
|
|
838
|
+
|
|
839
|
+
def test_confirmation_cannot_override_a_formatted_identifier(self):
|
|
840
|
+
"""--confirm-method asserts a judgement about MEANING. It has no authority over
|
|
841
|
+
format: an email address is an identifier whatever anyone confirms."""
|
|
842
|
+
code, message = self.lb.teach("write to a.b@clinic.org", confirmed=True)
|
|
843
|
+
self.assertEqual(code, 1)
|
|
844
|
+
self.assertIn("REFUSED", message)
|
|
845
|
+
|
|
846
|
+
def test_a_confirmed_lesson_is_written_and_the_notices_travel_with_it(self):
|
|
561
847
|
calls = []
|
|
562
848
|
original = self.lb._run_dz
|
|
563
|
-
self.lb._run_dz =
|
|
849
|
+
self.lb._run_dz = self._fake_store(calls=calls)
|
|
564
850
|
try:
|
|
565
|
-
code, message = self.lb.teach("
|
|
566
|
-
self.assertEqual(code,
|
|
567
|
-
self.assertIn("
|
|
568
|
-
self.
|
|
851
|
+
code, message = self.lb.teach("saturation above 45% warrants a repeat draw", confirmed=True)
|
|
852
|
+
self.assertEqual(code, 0)
|
|
853
|
+
self.assertIn("confirmed by the caller", message)
|
|
854
|
+
self.assertIn("NOTICED", message, "the number observation is still reported")
|
|
855
|
+
real = [c for c in calls if c and c[0] == "teach"
|
|
856
|
+
and not c[1].startswith(self.lb._CANARY_TEXT)]
|
|
857
|
+
self.assertEqual(len(real), 1)
|
|
858
|
+
self.assertIn("--domain", real[0])
|
|
859
|
+
self.assertIn(self.lb.LEARNING_DOMAIN, real[0])
|
|
569
860
|
finally:
|
|
570
861
|
self.lb._run_dz = original
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
862
|
+
def test_the_note_never_claims_to_have_checked_meaning(self):
|
|
863
|
+
"""The sentence that replaced a promise the code could not keep."""
|
|
864
|
+
note = self.lb.check_lesson("a lesson about companion tests").note
|
|
865
|
+
self.assertIn("That is all this check can tell you", note)
|
|
866
|
+
self.assertNotIn("safe", note.lower())
|
|
867
|
+
|
|
868
|
+
# ------------------------------------------------- ADR-004: the separate store
|
|
869
|
+
|
|
870
|
+
def test_teach_writes_ONLY_to_the_health_brain(self):
|
|
871
|
+
"""A store that never receives the data cannot hand it out from any command.
|
|
872
|
+
|
|
873
|
+
The assertion is about WRITES. Round 17 added a READ of the shared store — the
|
|
874
|
+
canary must be absent from it — so "every call is scoped to the brain" is no
|
|
875
|
+
longer the right shape of the promise, and asserting it would have made the
|
|
876
|
+
finding-7 fix look like a regression. Reading the shared store is what recall
|
|
877
|
+
has always done; what must never happen is a `teach` aimed anywhere else."""
|
|
575
878
|
calls = []
|
|
576
879
|
original = self.lb._run_dz
|
|
577
|
-
self.lb._run_dz =
|
|
880
|
+
self.lb._run_dz = self._fake_store(calls=calls)
|
|
578
881
|
try:
|
|
579
|
-
self.
|
|
580
|
-
|
|
581
|
-
self.assertEqual(code, 1)
|
|
582
|
-
self.assertIn("read it as an option", msg)
|
|
583
|
-
self.assertEqual(calls, [], "neither call may reach dz")
|
|
882
|
+
code, _ = self.lb.teach("a method lesson about fasting windows", confirmed=True)
|
|
883
|
+
self.assertEqual(code, 0)
|
|
584
884
|
finally:
|
|
585
885
|
self.lb._run_dz = original
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
886
|
+
writes = [c for c in calls if c and c[0] == "teach"]
|
|
887
|
+
self.assertTrue(writes, "something was written")
|
|
888
|
+
for call in writes:
|
|
889
|
+
self.assertIn("--project", call, call)
|
|
890
|
+
self.assertIn(self.lb.HEALTH_BRAIN_DIRNAME, call[call.index("--project") + 1])
|
|
891
|
+
# …and nothing at all was written to the shared store.
|
|
892
|
+
import os
|
|
893
|
+
self.assertEqual(self._store_rows(os.path.realpath(os.getcwd())), [])
|
|
894
|
+
def test_teach_FAILS_LOUDLY_when_the_write_did_not_land_in_the_brain(self):
|
|
895
|
+
"""An older CLI accepts --project on a write and ignores it, returning 0."""
|
|
593
896
|
original = self.lb._run_dz
|
|
594
|
-
|
|
595
|
-
self.lb._run_dz = lambda *a, **k: (0, "dz recall \"x\" — 1 hit(s)\n [0.90] (general) a lesson", "")
|
|
897
|
+
self.lb._run_dz = self._fake_store(honours_project=False)
|
|
596
898
|
try:
|
|
597
|
-
|
|
598
|
-
self.assertIn("a lesson", out, "the results are still returned")
|
|
599
|
-
self.assertIn("WITHOUT the domain boost", out)
|
|
600
|
-
self.assertIn("predates", out)
|
|
899
|
+
code, message = self.lb.teach("a method lesson", confirmed=True)
|
|
601
900
|
finally:
|
|
602
901
|
self.lb._run_dz = original
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
902
|
+
self.assertEqual(code, 1)
|
|
903
|
+
self.assertIn("does not honour --project", message)
|
|
904
|
+
|
|
905
|
+
def test_round14_the_canary_cleanup_uses_an_ID_not_the_text(self):
|
|
906
|
+
"""`--forget` takes dzIds. The first version passed the canary TEXT, so the probe
|
|
907
|
+
stayed in the brain forever — and the suite blessed it, because the mock returned
|
|
908
|
+
success for `forget`. Caught by RUNNING the flow, which is why this test asserts
|
|
909
|
+
the SHAPE of the cleanup call rather than that it returned 0."""
|
|
910
|
+
calls = []
|
|
609
911
|
original = self.lb._run_dz
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
912
|
+
canary = f"{self.lb._CANARY_TEXT} [deadbeef1234]"
|
|
913
|
+
|
|
914
|
+
def run(args, **kwargs):
|
|
915
|
+
calls.append(list(args))
|
|
916
|
+
if args and args[0] == "teach":
|
|
917
|
+
return 0, "taught", ""
|
|
918
|
+
if args and "--usage" in args:
|
|
919
|
+
import json as _json
|
|
920
|
+
return 0, _json.dumps({"top": [{"dzId": "teach:abc123", "pattern": canary,
|
|
921
|
+
"domain": "health-research"}]}), ""
|
|
922
|
+
if args and args[0] == "recall" and "--forget" in args:
|
|
923
|
+
return 0, "forgotten", ""
|
|
924
|
+
return 0, "dz recall --all --stats — 1 learned pattern(s)", ""
|
|
925
|
+
|
|
926
|
+
self.lb._run_dz = run
|
|
616
927
|
try:
|
|
617
|
-
|
|
618
|
-
self.assertNotIn("predates", out)
|
|
928
|
+
self.lb._forget_canary("/tmp/whatever/.health-brain", canary)
|
|
619
929
|
finally:
|
|
620
930
|
self.lb._run_dz = original
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
""
|
|
624
|
-
|
|
625
|
-
|
|
931
|
+
forgets = [c for c in calls if "--forget" in c]
|
|
932
|
+
self.assertEqual(len(forgets), 1)
|
|
933
|
+
target = forgets[0][forgets[0].index("--forget") + 1]
|
|
934
|
+
self.assertEqual(target, "teach:abc123", "forget by dzId, never by text")
|
|
935
|
+
self.assertNotIn(canary, forgets[0])
|
|
936
|
+
|
|
937
|
+
def test_round14_the_CANARY_is_written_before_the_real_lesson(self):
|
|
938
|
+
"""The ORDERING is the fix. Counting around the real write detected the leak only
|
|
939
|
+
after permitting it — "fails closed" described the return status, not the
|
|
940
|
+
mutation. Now a harmless probe pays that cost instead of a patient's finding."""
|
|
941
|
+
calls = []
|
|
626
942
|
original = self.lb._run_dz
|
|
627
|
-
|
|
628
|
-
'order unchanged, nothing was hidden')
|
|
629
|
-
self.lb._run_dz = lambda *a, **k: (0, 'dz recall "x" — 1 hit(s)\n [0.90] (general) x\n' + real_note, "")
|
|
943
|
+
self.lb._run_dz = self._fake_store(honours_project=False, calls=calls)
|
|
630
944
|
try:
|
|
631
|
-
self.
|
|
945
|
+
self.lb.teach("a method lesson about fasting windows", confirmed=True)
|
|
632
946
|
finally:
|
|
633
947
|
self.lb._run_dz = original
|
|
948
|
+
teaches = [c for c in calls if c and c[0] == "teach"]
|
|
949
|
+
self.assertTrue(teaches, "something was written")
|
|
950
|
+
self.assertTrue(teaches[0][1].startswith(self.lb._CANARY_TEXT),
|
|
951
|
+
"the FIRST write is the canary")
|
|
952
|
+
self.assertTrue(all(c[1].startswith(self.lb._CANARY_TEXT) for c in teaches),
|
|
953
|
+
"the real lesson is NEVER written when routing is broken")
|
|
954
|
+
def test_recall_reads_BOTH_stores_and_only_one_of_them_is_written(self):
|
|
955
|
+
"""One-way transfer: engineering lessons flow INTO this work, medical ones never
|
|
956
|
+
leave. Two calls out, one of them scoped to the health brain."""
|
|
957
|
+
calls = []
|
|
958
|
+
original = self.lb._run_dz
|
|
959
|
+
|
|
960
|
+
def record(args, **kw):
|
|
961
|
+
calls.append(list(args))
|
|
962
|
+
return 0, 'dz recall "x" — 1 hit(s)\n [0.90] (d) a lesson', ""
|
|
634
963
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
964
|
+
self.lb._run_dz = record
|
|
965
|
+
try:
|
|
966
|
+
out = self.lb.recall("fasting")
|
|
967
|
+
finally:
|
|
968
|
+
self.lb._run_dz = original
|
|
969
|
+
self.assertEqual(len(calls), 2, "the health brain and the shared brain")
|
|
970
|
+
scoped = [c for c in calls if "--project" in c]
|
|
971
|
+
self.assertEqual(len(scoped), 1, "exactly one call is scoped to the health brain")
|
|
972
|
+
self.assertIn(self.lb.HEALTH_BRAIN_DIRNAME, scoped[0][scoped[0].index("--project") + 1])
|
|
973
|
+
self.assertIn("health brain", out)
|
|
974
|
+
self.assertIn("never leave the health brain", out)
|
|
975
|
+
|
|
976
|
+
def test_status_names_both_stores_and_the_direction(self):
|
|
639
977
|
original = self.lb._run_dz
|
|
640
|
-
self.lb._run_dz = lambda *a, **k: (
|
|
641
|
-
0,
|
|
642
|
-
'dz recall "x" — 1 hit(s)\n'
|
|
643
|
-
' [0.90] (general) remember domain "ownership" before reporting',
|
|
644
|
-
"",
|
|
645
|
-
)
|
|
978
|
+
self.lb._run_dz = lambda *a, **k: (0, "dz recall --all --stats — 7 learned pattern(s)", "")
|
|
646
979
|
try:
|
|
647
|
-
|
|
980
|
+
out = self.lb.status()
|
|
648
981
|
finally:
|
|
649
982
|
self.lb._run_dz = original
|
|
983
|
+
self.assertIn("health brain", out)
|
|
984
|
+
self.assertIn("shared brain", out)
|
|
985
|
+
self.assertIn("WRITTEN ONLY to the health brain", out)
|
|
650
986
|
|
|
651
|
-
#
|
|
652
|
-
# Every case below is a REPRODUCER the cross-model reviewer ran against the
|
|
653
|
-
# previous guard and got the wrong answer for. They stay here permanently: a
|
|
654
|
-
# privacy guard is only as good as the bypasses it can no longer be talked into.
|
|
655
|
-
|
|
656
|
-
def test_round2_a_name_beside_a_diagnosis_is_REFUSED(self):
|
|
657
|
-
"""The worst miss of the round. The old rule hunted a name — two capitalised
|
|
658
|
-
words in a row, exempting the sentence start — so BOTH of these passed."""
|
|
659
|
-
for leak in ("John Smith has HIV", "Patient John has HIV"):
|
|
660
|
-
with self.subTest(leak=leak):
|
|
661
|
-
self.assertFalse(self.lb.check_lesson(leak).ok)
|
|
662
|
-
|
|
663
|
-
def test_round2_a_capitalised_word_after_the_first_is_refused_whatever_it_is(self):
|
|
664
|
-
"""The replacement rule is a property of the ACCEPTED form (a method lesson is
|
|
665
|
-
written in lower case), not a hunt for names — because names cannot be listed.
|
|
666
|
-
It refuses legitimate title-cased method vocabulary too, deliberately: the fix
|
|
667
|
-
is one keystroke and the false negative it closes is a named person."""
|
|
668
|
-
verdict = self.lb.check_lesson("compare Testosterone Replacement Therapy against placebo")
|
|
669
|
-
self.assertFalse(verdict.ok)
|
|
670
|
-
self.assertIn("lower case", verdict.note)
|
|
671
|
-
# …and the lower-cased form is accepted, so the advice actually works.
|
|
672
|
-
self.assertTrue(self.lb.check_lesson("compare testosterone replacement therapy against placebo").ok)
|
|
673
|
-
|
|
674
|
-
def test_round2_ALL_CAPS_and_mixed_case_acronyms_still_pass(self):
|
|
675
|
-
"""The capitalisation rule must not cost the vocabulary lessons are written in."""
|
|
676
|
-
for ok_text in (
|
|
677
|
-
"total testosterone is uninterpretable without SHBG",
|
|
678
|
-
"a single ferritin cannot separate overload from inflammation, pair it with CRP",
|
|
679
|
-
# NOT first in the sentence — round 4 found this test placed `apoB` at the
|
|
680
|
-
# start, where the sentence-start exemption made it pass no matter what the
|
|
681
|
-
# acronym rule did. A test whose subject sits in the one exempt position
|
|
682
|
-
# proves nothing about the rule it claims to cover.
|
|
683
|
-
"measure apoB before interpreting LDL alone",
|
|
684
|
-
):
|
|
685
|
-
with self.subTest(text=ok_text):
|
|
686
|
-
self.assertTrue(self.lb.check_lesson(ok_text).ok, ok_text)
|
|
687
|
-
|
|
688
|
-
def test_round2_a_value_SPELLED_OUT_is_still_a_value(self):
|
|
689
|
-
"""`John's TSH was eight point zero four` carried a reading past a digit-only rule."""
|
|
690
|
-
self.assertFalse(self.lb.check_lesson("his tsh was eight point zero four").ok)
|
|
691
|
-
# A lone number-word is ordinary method prose and must NOT be refused.
|
|
692
|
-
self.assertTrue(self.lb.check_lesson("one marker alone cannot close the question").ok)
|
|
693
|
-
|
|
694
|
-
def test_round2_allow_numbers_never_waves_through_a_person(self):
|
|
695
|
-
"""--allow-numbers is for a guideline threshold, never for a person plus a
|
|
696
|
-
reading. The old person list held only `his`/`her`, so these walked through."""
|
|
697
|
-
for leak in ("she has TSH 8.04 mIU/L", "subject has TSH 8.04 mIU/L",
|
|
698
|
-
"the participant had ferritin 512"):
|
|
699
|
-
with self.subTest(leak=leak):
|
|
700
|
-
self.assertFalse(self.lb.check_lesson(leak, allow_numbers=True).ok)
|
|
701
|
-
|
|
702
|
-
def test_round2_only_a_double_dash_is_refused_as_an_option(self):
|
|
703
|
-
"""`dz` treats `-contrast` as ordinary positional text (measured). Refusing it
|
|
704
|
-
blocked legitimate lessons to defend against nothing, and a guard that refuses
|
|
705
|
-
safe input teaches people to route around the guard."""
|
|
706
|
-
self.assertTrue(self.lb.check_lesson("-negative findings still require confirmation").ok)
|
|
707
|
-
self.assertIn("--", self.lb.teach("--all")[1])
|
|
708
|
-
self.assertIn("--", self.lb.recall("--all"))
|
|
987
|
+
# ------------------------------------------------------------------ round 11
|
|
709
988
|
|
|
710
|
-
def
|
|
711
|
-
"""
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
989
|
+
def test_round11_a_NESTED_dz_symlink_is_refused(self):
|
|
990
|
+
"""Resolving only `.health-brain` missed `ln -s ../.dz .health-brain/.dz`: the
|
|
991
|
+
parent resolved distinctly while the directory dz actually writes to WAS the
|
|
992
|
+
shared store. The path that matters is the one that receives the data."""
|
|
993
|
+
import os
|
|
994
|
+
import tempfile
|
|
995
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
996
|
+
os.makedirs(os.path.join(tmp, ".dz"))
|
|
997
|
+
os.makedirs(os.path.join(tmp, self.lb.HEALTH_BRAIN_DIRNAME))
|
|
998
|
+
os.symlink(os.path.join(tmp, ".dz"),
|
|
999
|
+
os.path.join(tmp, self.lb.HEALTH_BRAIN_DIRNAME, ".dz"))
|
|
1000
|
+
distinct, message = self.lb._health_brain_is_distinct(tmp)
|
|
1001
|
+
self.assertFalse(distinct)
|
|
1002
|
+
self.assertIn("shared store", message)
|
|
1003
|
+
|
|
1004
|
+
def test_round11_a_failing_health_store_is_NOT_diagnosed_as_an_old_cli(self):
|
|
1005
|
+
"""`--domain` goes to the health call alone, so judging the capability on the
|
|
1006
|
+
MERGED output blamed an old CLI whenever the health store failed — a confident
|
|
1007
|
+
diagnosis of the wrong thing, which is worse than silence."""
|
|
715
1008
|
original = self.lb._run_dz
|
|
716
1009
|
|
|
717
|
-
def
|
|
718
|
-
|
|
719
|
-
|
|
1010
|
+
def half(args, **kw):
|
|
1011
|
+
if "--project" in args:
|
|
1012
|
+
return 1, "", "health corrupt"
|
|
1013
|
+
return 0, 'dz recall "x" — 1 hit(s)\n [0.90] (qe) a shared lesson', ""
|
|
720
1014
|
|
|
721
|
-
self.lb._run_dz =
|
|
1015
|
+
self.lb._run_dz = half
|
|
722
1016
|
try:
|
|
723
|
-
self.lb.recall("x")
|
|
724
|
-
self.assertEqual(len(calls), 1, "exactly one call, no fallback")
|
|
1017
|
+
out = self.lb.recall("x")
|
|
725
1018
|
finally:
|
|
726
1019
|
self.lb._run_dz = original
|
|
727
|
-
self.
|
|
1020
|
+
self.assertIn("health brain UNAVAILABLE", out)
|
|
1021
|
+
self.assertNotIn("predates", out)
|
|
728
1022
|
|
|
1023
|
+
def test_round11_a_failing_SHARED_store_is_named_too(self):
|
|
1024
|
+
"""Reporting only when BOTH fail swallowed half the loop silently."""
|
|
1025
|
+
original = self.lb._run_dz
|
|
729
1026
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
1027
|
+
def half(args, **kw):
|
|
1028
|
+
if "--project" in args:
|
|
1029
|
+
return 0, 'dz recall "x" — 1 hit(s)\n [0.90] (health-research) h', ""
|
|
1030
|
+
return 1, "", "shared corrupt"
|
|
733
1031
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
1032
|
+
self.lb._run_dz = half
|
|
1033
|
+
try:
|
|
1034
|
+
out = self.lb.recall("x")
|
|
1035
|
+
finally:
|
|
1036
|
+
self.lb._run_dz = original
|
|
1037
|
+
self.assertIn("shared brain unavailable", out)
|
|
1038
|
+
|
|
1039
|
+
def test_round11_identifier_and_quantity_boundary(self):
|
|
1040
|
+
"""Both halves of one finding: formats that got through, and magnitudes that
|
|
1041
|
+
were wrongly refused."""
|
|
1042
|
+
for leak in ('"john@doe"@example.com is the contact', "MRN № 84729163 note",
|
|
1043
|
+
"MRN = 84729163 note", "medical record number is 84729163",
|
|
1044
|
+
"call +44/20/7946/0958 before the draw"):
|
|
742
1045
|
with self.subTest(leak=leak):
|
|
743
1046
|
self.assertFalse(self.lb.check_lesson(leak).ok, leak)
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
1047
|
+
for good in ("use a 100000-fold dilution to test linearity",
|
|
1048
|
+
"amplify DNA×100000 before sequencing",
|
|
1049
|
+
"test concentration steps +1 / 2 / 3 / 4 before fitting"):
|
|
1050
|
+
with self.subTest(good=good):
|
|
1051
|
+
self.assertTrue(self.lb.check_lesson(good).ok, good)
|
|
1052
|
+
|
|
1053
|
+
# ------------------------------------------------------------------ round 12
|
|
1054
|
+
|
|
1055
|
+
def test_round12_the_brain_is_SELF_IGNORING_for_git(self):
|
|
1056
|
+
"""The monorepo .gitignore rule shipped with nothing: a consumer got a plaintext
|
|
1057
|
+
medical store one `git add -A` from a push, while the ADR promised protection
|
|
1058
|
+
from exactly that. A `.gitignore` holding `*` inside the directory needs no
|
|
1059
|
+
cooperation from the project and travels with the directory."""
|
|
1060
|
+
import os
|
|
1061
|
+
import tempfile
|
|
1062
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1063
|
+
brain = os.path.join(tmp, self.lb.HEALTH_BRAIN_DIRNAME)
|
|
1064
|
+
self.lb._protect_brain(brain)
|
|
1065
|
+
marker = os.path.join(brain, ".gitignore")
|
|
1066
|
+
self.assertTrue(os.path.exists(marker))
|
|
1067
|
+
self.assertIn("*", open(marker, encoding="utf-8").read())
|
|
1068
|
+
# A deliberate edit is preserved — as long as it STILL IGNORES. Round 14
|
|
1069
|
+
# showed the earlier rule accepted any regular file, so an empty or unrelated
|
|
1070
|
+
# .gitignore satisfied the check while ignoring nothing.
|
|
1071
|
+
with open(marker, "w", encoding="utf-8") as handle:
|
|
1072
|
+
handle.write("# my own note\n*\n")
|
|
1073
|
+
self.lb._protect_brain(brain)
|
|
1074
|
+
self.assertEqual(open(marker, encoding="utf-8").read(), "# my own note\n*\n")
|
|
1075
|
+
|
|
1076
|
+
def test_round14_an_ignore_file_that_ignores_NOTHING_is_refused(self):
|
|
1077
|
+
"""Presence is not validity, one level up from where round 13 fixed it: the
|
|
1078
|
+
directory case was closed and the case that actually happens — a pre-existing or
|
|
1079
|
+
hand-edited file with no rule in it — was left open."""
|
|
1080
|
+
import os
|
|
1081
|
+
import tempfile
|
|
1082
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1083
|
+
brain = os.path.join(tmp, self.lb.HEALTH_BRAIN_DIRNAME)
|
|
1084
|
+
os.makedirs(brain)
|
|
1085
|
+
for content in ("", "# just a comment\n", "*.log\n"):
|
|
1086
|
+
with open(os.path.join(brain, ".gitignore"), "w", encoding="utf-8") as handle:
|
|
1087
|
+
handle.write(content)
|
|
1088
|
+
with self.subTest(content=content), self.assertRaises(RuntimeError):
|
|
1089
|
+
self.lb._protect_brain(brain)
|
|
1090
|
+
|
|
1091
|
+
def test_round12_the_write_check_opts_INTO_the_domain_it_counts(self):
|
|
1092
|
+
"""The verification defeated itself: `recall --all` applies the export hold-out,
|
|
1093
|
+
which withholds this very domain, so a health-only brain read 0 before AND 0
|
|
1094
|
+
after a successful write. A check built on another safety measure must account
|
|
1095
|
+
for that measure."""
|
|
1096
|
+
calls = []
|
|
1097
|
+
original = self.lb._run_dz
|
|
1098
|
+
self.lb._run_dz = self._fake_store(calls=calls)
|
|
1099
|
+
try:
|
|
1100
|
+
code, _ = self.lb.teach("a method lesson", confirmed=True)
|
|
1101
|
+
self.assertEqual(code, 0)
|
|
1102
|
+
finally:
|
|
1103
|
+
self.lb._run_dz = original
|
|
1104
|
+
counts = [c for c in calls if "--stats" in c]
|
|
1105
|
+
self.assertTrue(counts, "the write is verified by counting")
|
|
1106
|
+
for call in counts:
|
|
1107
|
+
self.assertIn("--include-domain", call)
|
|
1108
|
+
self.assertIn(self.lb.LEARNING_DOMAIN, call)
|
|
1109
|
+
def test_round12_identifier_formats(self):
|
|
1110
|
+
for leak in ("MRN, 84729163 belongs to the subject",
|
|
1111
|
+
'contact "john"@[192.0.2.1] before the draw'):
|
|
766
1112
|
with self.subTest(leak=leak):
|
|
767
|
-
self.assertFalse(self.lb.check_lesson(leak
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1113
|
+
self.assertFalse(self.lb.check_lesson(leak).ok, leak)
|
|
1114
|
+
|
|
1115
|
+
# ------------------------------------------------------------------ round 13
|
|
1116
|
+
|
|
1117
|
+
def test_round13_a_store_symlinked_OUT_of_the_brain_is_refused(self):
|
|
1118
|
+
"""Comparing against OUR project let `ln -s ../../B/.dz A/.health-brain/.dz`
|
|
1119
|
+
through: B's shared store is not A's, so the check passed while the lesson landed
|
|
1120
|
+
in another project's store — and the count then grew THERE and confirmed success.
|
|
1121
|
+
Containment is one question with one answer, not a list of forbidden places."""
|
|
1122
|
+
import os
|
|
1123
|
+
import tempfile
|
|
1124
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1125
|
+
a = os.path.join(tmp, "A")
|
|
1126
|
+
b = os.path.join(tmp, "B")
|
|
1127
|
+
os.makedirs(os.path.join(a, self.lb.HEALTH_BRAIN_DIRNAME))
|
|
1128
|
+
os.makedirs(os.path.join(b, ".dz"))
|
|
1129
|
+
os.symlink(os.path.join(b, ".dz"),
|
|
1130
|
+
os.path.join(a, self.lb.HEALTH_BRAIN_DIRNAME, ".dz"))
|
|
1131
|
+
distinct, message = self.lb._health_brain_is_distinct(a)
|
|
1132
|
+
self.assertFalse(distinct)
|
|
1133
|
+
self.assertIn("OUTSIDE the brain", message)
|
|
1134
|
+
|
|
1135
|
+
def test_round13_verification_FAILS_CLOSED_when_it_cannot_count(self):
|
|
1136
|
+
"""An unreadable brain is not evidence the write landed."""
|
|
1137
|
+
original = self.lb._run_dz
|
|
1138
|
+
self.lb._run_dz = lambda args, **kw: (
|
|
1139
|
+
(1, "", "unreadable") if args and args[0] == "recall" else (0, "taught", "")
|
|
1140
|
+
)
|
|
1141
|
+
try:
|
|
1142
|
+
code, message = self.lb.teach("a method lesson", confirmed=True)
|
|
1143
|
+
finally:
|
|
1144
|
+
self.lb._run_dz = original
|
|
1145
|
+
self.assertEqual(code, 1)
|
|
1146
|
+
self.assertIn("REFUSED", message)
|
|
1147
|
+
def test_round13_a_brain_without_its_ignore_rule_is_REFUSED(self):
|
|
1148
|
+
"""ADR-004 promises protection from a routine `git add -A`. Swallowing the
|
|
1149
|
+
failure to write the ignore rule left a plaintext medical store staged by the
|
|
1150
|
+
next one while the write reported success — a promise kept in prose only."""
|
|
1151
|
+
import os
|
|
1152
|
+
import tempfile
|
|
1153
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1154
|
+
brain = os.path.join(tmp, self.lb.HEALTH_BRAIN_DIRNAME)
|
|
1155
|
+
os.makedirs(brain)
|
|
1156
|
+
# A directory where the ignore FILE should be: opening it for write raises.
|
|
1157
|
+
os.makedirs(os.path.join(brain, ".gitignore"))
|
|
1158
|
+
with self.assertRaises(RuntimeError):
|
|
1159
|
+
self.lb._protect_brain(brain)
|
|
1160
|
+
|
|
1161
|
+
def test_round13_identifier_separators_are_not_enumerated(self):
|
|
1162
|
+
for leak in ("MRN. 84729163 belongs to the subject",
|
|
1163
|
+
"MRN; 84729163 belongs to the subject",
|
|
1164
|
+
"medical record number / 84729163 belongs to the subject",
|
|
1165
|
+
):
|
|
794
1166
|
with self.subTest(leak=leak):
|
|
795
|
-
self.assertFalse(self.lb.check_lesson(leak
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
#
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
1167
|
+
self.assertFalse(self.lb.check_lesson(leak).ok, leak)
|
|
1168
|
+
self.assertTrue(self.lb.check_lesson(
|
|
1169
|
+
"audit medical record number quality before dataset linkage").ok)
|
|
1170
|
+
# ROUND 15: a run of two-digit groups is NO LONGER classified. `01 42 68 53 00`
|
|
1171
|
+
# (a French phone) and `10 20 30 40 50 weeks` (a time course) are the same shape;
|
|
1172
|
+
# a unit LIST missed `weeks` and inverting it to "any following word" matched
|
|
1173
|
+
# `before`. Neither is classified, consistently with 555-1234 and NCT/PCR.
|
|
1174
|
+
self.assertTrue(self.lb.check_lesson("call 01 42 68 53 00 before the draw").ok)
|
|
1175
|
+
self.assertTrue(self.lb.check_lesson("measure at 10 20 30 40 50 weeks").ok)
|
|
1176
|
+
# A grouped card number is still caught, by the three-groups-of-three branch.
|
|
1177
|
+
self.assertFalse(self.lb.check_lesson("card 4111 1111 1111 1111 belongs").ok)
|
|
1178
|
+
|
|
1179
|
+
# --------------------------------------------------------------- dz integration
|
|
1180
|
+
|
|
1181
|
+
def test_absent_dz_is_a_NOTE_never_a_failure(self):
|
|
1182
|
+
original = self.lb.dz_path
|
|
1183
|
+
self.lb.dz_path = lambda: None
|
|
1184
|
+
try:
|
|
1185
|
+
self.assertIn("self-learning is OFF", self.lb.status())
|
|
1186
|
+
code, message = self.lb.teach("a method lesson", confirmed=True)
|
|
1187
|
+
self.assertEqual(code, 0, "an optional dependency must never break the package")
|
|
1188
|
+
self.assertIn("harness-cli", message)
|
|
1189
|
+
finally:
|
|
1190
|
+
self.lb.dz_path = original
|
|
1191
|
+
|
|
1192
|
+
def test_older_cli_is_detected_by_CAPABILITY_not_by_exit_code(self):
|
|
1193
|
+
"""The previous dz did NOT reject --domain: it ignored the flag and exited 0, so
|
|
1194
|
+
an exit-code test could never fire. The observable difference is the boost note."""
|
|
1195
|
+
original = self.lb._run_dz
|
|
1196
|
+
self.lb._run_dz = lambda *a, **k: (0, 'dz recall "x" — 1 hit(s)\n [0.90] (general) a lesson', "")
|
|
1197
|
+
try:
|
|
1198
|
+
out = self.lb.recall("transferrin")
|
|
1199
|
+
self.assertIn("a lesson", out, "the results are still returned")
|
|
1200
|
+
self.assertIn("predates", out)
|
|
1201
|
+
finally:
|
|
1202
|
+
self.lb._run_dz = original
|
|
1203
|
+
|
|
1204
|
+
def test_capability_cannot_be_FORGED_by_recalled_content(self):
|
|
1205
|
+
"""A probe the payload can forge is not a probe: the marker was once the bare
|
|
1206
|
+
substring `domain "`, which a lesson can contain."""
|
|
1207
|
+
original = self.lb._run_dz
|
|
1208
|
+
self.lb._run_dz = lambda *a, **k: (
|
|
1209
|
+
0, 'dz recall "x" — 1 hit(s)\n [0.90] (general) remember domain "ownership" first', "")
|
|
1210
|
+
try:
|
|
1211
|
+
self.assertIn("predates", self.lb.recall("x"))
|
|
1212
|
+
finally:
|
|
1213
|
+
self.lb._run_dz = original
|
|
1214
|
+
|
|
1215
|
+
def test_the_real_note_is_recognised_on_BOTH_renderings(self):
|
|
1216
|
+
"""Copied verbatim from live runs. A --domain run that matched nothing still
|
|
1217
|
+
proves the CLI understands --domain."""
|
|
1218
|
+
original = self.lb._run_dz
|
|
1219
|
+
for note in (
|
|
1220
|
+
' domain "health-research": among 6 candidate(s) — 1 exact match(es), 3 changed '
|
|
1221
|
+
'position; foreign-domain lessons kept (a boost, not a filter)',
|
|
1222
|
+
' domain "health-research": no lesson in this result carries it — order unchanged, '
|
|
1223
|
+
'nothing was hidden',
|
|
1224
|
+
):
|
|
1225
|
+
self.lb._run_dz = lambda *a, _n=note, **k: (0, 'dz recall "x" — 1 hit(s)\n [0.90] (x) y\n' + _n, "")
|
|
1226
|
+
with self.subTest(note=note[:40]):
|
|
1227
|
+
self.assertNotIn("predates", self.lb.recall("x"))
|
|
1228
|
+
self.lb._run_dz = original
|
|
1229
|
+
|
|
1230
|
+
def test_an_exotic_line_separator_cannot_forge_the_probe(self):
|
|
1231
|
+
"""splitlines() breaks on U+2028 where the renderer does not, so a lesson
|
|
1232
|
+
containing one produced a "line" the CLI never emitted."""
|
|
808
1233
|
tail = 'domain "health-research": order unchanged, nothing was hidden'
|
|
809
1234
|
self.assertFalse(self.lb._boost_note_present(" [0.90] (general) x\u2028" + tail))
|
|
810
|
-
self.assertFalse(self.lb._boost_note_present(" [0.90] (general) x " + tail))
|
|
811
|
-
# A REAL note — its own line — is still detected.
|
|
812
1235
|
self.assertTrue(self.lb._boost_note_present(" [0.90] (general) x\n " + tail))
|
|
813
1236
|
|
|
814
|
-
|
|
815
1237
|
def test_a_real_failure_is_not_disguised_as_an_old_cli(self):
|
|
816
|
-
"""A crash, timeout or corrupt store must read as a failure, not as 'upgrade'."""
|
|
817
1238
|
original = self.lb._run_dz
|
|
818
1239
|
self.lb._run_dz = lambda *a, **k: (1, "", "store corrupt")
|
|
819
1240
|
try:
|
|
@@ -823,7 +1244,250 @@ class LearningBridgeTests(unittest.TestCase):
|
|
|
823
1244
|
finally:
|
|
824
1245
|
self.lb._run_dz = original
|
|
825
1246
|
|
|
1247
|
+
def test_only_a_double_dash_is_refused_as_an_option(self):
|
|
1248
|
+
"""`dz` treats `-contrast` as ordinary text; refusing it defended against
|
|
1249
|
+
nothing and blocked legitimate lessons."""
|
|
1250
|
+
self.assertTrue(self.lb.check_lesson("-negative findings still require confirmation").ok)
|
|
1251
|
+
self.assertIn("--", self.lb.teach("--all", confirmed=True)[1])
|
|
1252
|
+
self.assertIn("--", self.lb.recall("--all"))
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
|
|
1256
|
+
class RoundSixteenRegressions(unittest.TestCase):
|
|
1257
|
+
"""Each name records what round-16 review found, so a rewrite cannot quietly undo it."""
|
|
1258
|
+
|
|
1259
|
+
# The suite imports the bridge inside each test, as the other bridge tests do.
|
|
1260
|
+
def setUp(self):
|
|
1261
|
+
import os, tempfile, learning_bridge
|
|
1262
|
+
self.os, self.tempfile, self.lb = os, tempfile, learning_bridge
|
|
1263
|
+
|
|
1264
|
+
def test_brain_symlinked_to_a_nested_project_is_refused(self):
|
|
1265
|
+
# `/A/.health-brain -> /A/subproject` passed BOTH containment checks (inside A,
|
|
1266
|
+
# store inside it) and the canary, found through the same alias, certified the
|
|
1267
|
+
# wrong store. Containment cannot say what a directory IS.
|
|
1268
|
+
with self.tempfile.TemporaryDirectory() as tmp:
|
|
1269
|
+
root = self.os.path.realpath(tmp)
|
|
1270
|
+
sub = self.os.path.join(root, "subproject")
|
|
1271
|
+
self.os.makedirs(self.os.path.join(sub, ".dz", "memory"))
|
|
1272
|
+
self.os.symlink(sub, self.os.path.join(root, self.lb.HEALTH_BRAIN_DIRNAME))
|
|
1273
|
+
ok, why = self.lb._health_brain_is_distinct(root)
|
|
1274
|
+
self.assertFalse(ok)
|
|
1275
|
+
self.assertIn("holds a dz store of its own", why)
|
|
1276
|
+
|
|
1277
|
+
def test_symlink_to_a_directory_that_is_not_a_project_is_still_allowed(self):
|
|
1278
|
+
# The refusal is aimed at another PROJECT, not at symlinks as such — a link to
|
|
1279
|
+
# ordinary storage stays a legitimate way to place the brain.
|
|
1280
|
+
with self.tempfile.TemporaryDirectory() as tmp:
|
|
1281
|
+
root = self.os.path.realpath(tmp)
|
|
1282
|
+
target = self.os.path.join(root, "elsewhere")
|
|
1283
|
+
self.os.makedirs(target)
|
|
1284
|
+
self.os.symlink(target, self.os.path.join(root, self.lb.HEALTH_BRAIN_DIRNAME))
|
|
1285
|
+
ok, _ = self.lb._health_brain_is_distinct(root)
|
|
1286
|
+
self.assertTrue(ok)
|
|
1287
|
+
|
|
1288
|
+
def test_gitignore_with_a_later_negation_is_refused(self):
|
|
1289
|
+
# git applies the LAST matching rule, so `*` then `!.dz/` leaves the store
|
|
1290
|
+
# exposed. Asserting that a `*` line EXISTS approved a file that does not ignore.
|
|
1291
|
+
with self.tempfile.TemporaryDirectory() as tmp:
|
|
1292
|
+
brain = self.os.path.join(tmp, self.lb.HEALTH_BRAIN_DIRNAME)
|
|
1293
|
+
self.os.makedirs(brain)
|
|
1294
|
+
with open(self.os.path.join(brain, ".gitignore"), "w", encoding="utf-8") as fh:
|
|
1295
|
+
fh.write("*\n!.dz/\n!.dz/**\n")
|
|
1296
|
+
with self.assertRaises(RuntimeError) as ctx:
|
|
1297
|
+
self.lb._protect_brain(brain)
|
|
1298
|
+
self.assertIn("IN FORCE", str(ctx.exception))
|
|
1299
|
+
|
|
1300
|
+
def test_gitignore_that_really_ignores_is_accepted(self):
|
|
1301
|
+
for body in ("*\n", "# note\n*\n", "!keep\n*\n"):
|
|
1302
|
+
with self.tempfile.TemporaryDirectory() as tmp:
|
|
1303
|
+
brain = self.os.path.join(tmp, self.lb.HEALTH_BRAIN_DIRNAME)
|
|
1304
|
+
self.os.makedirs(brain)
|
|
1305
|
+
with open(self.os.path.join(brain, ".gitignore"), "w", encoding="utf-8") as fh:
|
|
1306
|
+
fh.write(body)
|
|
1307
|
+
self.lb._protect_brain(brain) # must not raise
|
|
1308
|
+
|
|
1309
|
+
def test_identifiers_round_sixteen_missed(self):
|
|
1310
|
+
for text in (
|
|
1311
|
+
"contact customer!@localhost before enrollment", # RFC local part, not the four picked chars
|
|
1312
|
+
"phone: 5551234567 belongs to the patient", # the LABEL decides
|
|
1313
|
+
"call + (44) 20 7946 0958 before the draw", # separator after the `+`
|
|
1314
|
+
):
|
|
1315
|
+
with self.subTest(text=text):
|
|
1316
|
+
self.assertFalse(self.lb.check_lesson(text).ok)
|
|
1317
|
+
|
|
1318
|
+
def test_method_prose_round_sixteen_wrongly_refused(self):
|
|
1319
|
+
for text in (
|
|
1320
|
+
"use a 1000-1000-1000 dilution series to test linearity", # 3 groups is undecidable
|
|
1321
|
+
"compare medical record number in 3 hospitals", # a lone digit is not a record number
|
|
1322
|
+
):
|
|
1323
|
+
with self.subTest(text=text):
|
|
1324
|
+
self.assertTrue(self.lb.check_lesson(text).ok)
|
|
1325
|
+
|
|
1326
|
+
|
|
1327
|
+
class RoundSeventeenRegressions(_BridgeHarness, unittest.TestCase):
|
|
1328
|
+
"""One test per round-17 finding, named so the finding is identifiable from the
|
|
1329
|
+
failure line alone."""
|
|
1330
|
+
|
|
1331
|
+
def test_finding7_descendant_symlink_is_caught_by_the_ABSENCE_of_the_canary(self):
|
|
1332
|
+
"""FINDING 7 (HIGH) — `.health-brain/.dz/memory -> ../../.dz/memory`.
|
|
1333
|
+
|
|
1334
|
+
`.health-brain` and `.health-brain/.dz` are REAL directories, so every path check
|
|
1335
|
+
in _health_brain_is_distinct passes (asserted below — that is what makes this a
|
|
1336
|
+
finding rather than a duplicate of round 11/13/16). The alias sits one level
|
|
1337
|
+
deeper, where dz actually keeps the data, so the canary, its lookup, both counts
|
|
1338
|
+
and the real teach all travelled it and the bridge certified the SHARED store as
|
|
1339
|
+
the health brain.
|
|
826
1340
|
|
|
1341
|
+
The fix is an OUTCOME check, not another path shape: the canary must be present
|
|
1342
|
+
in the brain AND absent from the shared store. This test is written against the
|
|
1343
|
+
mechanism, not the shape — the fake store keeps real files, so the symlink makes
|
|
1344
|
+
the two paths one store the same way it would for the real CLI.
|
|
1345
|
+
|
|
1346
|
+
MEASURED DISCRIMINATION: with the absence check removed, this test FAILS at
|
|
1347
|
+
`assertEqual(code, 1)` — teach returns 0 and reports "recorded in the health
|
|
1348
|
+
brain" while the lesson sits in the shared store.
|
|
1349
|
+
"""
|
|
1350
|
+
import os
|
|
1351
|
+
root = os.path.realpath(os.getcwd())
|
|
1352
|
+
os.makedirs(os.path.join(root, ".dz", "memory"))
|
|
1353
|
+
os.makedirs(os.path.join(root, self.lb.HEALTH_BRAIN_DIRNAME, ".dz"))
|
|
1354
|
+
os.symlink(os.path.join("..", "..", ".dz", "memory"),
|
|
1355
|
+
os.path.join(root, self.lb.HEALTH_BRAIN_DIRNAME, ".dz", "memory"))
|
|
1356
|
+
|
|
1357
|
+
# Every PATH check passes. The finding is exactly that this is not enough.
|
|
1358
|
+
distinct, _ = self.lb._health_brain_is_distinct(root)
|
|
1359
|
+
self.assertTrue(distinct, "the path checks see nothing wrong — that is the finding")
|
|
1360
|
+
|
|
1361
|
+
original = self.lb._run_dz
|
|
1362
|
+
self.lb._run_dz = self._fake_store()
|
|
1363
|
+
try:
|
|
1364
|
+
code, message = self.lb.teach("fasting windows need a repeat draw", confirmed=True)
|
|
1365
|
+
finally:
|
|
1366
|
+
self.lb._run_dz = original
|
|
1367
|
+
|
|
1368
|
+
self.assertEqual(code, 1, "an aliased store must be REFUSED, not certified")
|
|
1369
|
+
self.assertIn("SAME store", message)
|
|
1370
|
+
self.assertIn("nonce", message, "the user must be able to find any stray probe")
|
|
1371
|
+
rows = self._store_rows(root)
|
|
1372
|
+
self.assertFalse(any("fasting windows" in r["pattern"] for r in rows),
|
|
1373
|
+
"the real lesson must never reach the shared store")
|
|
1374
|
+
self.assertEqual(rows, [], "and the probe must not be left behind either")
|
|
1375
|
+
|
|
1376
|
+
def test_finding7_a_healthy_brain_still_teaches(self):
|
|
1377
|
+
"""The absence check must not refuse the ordinary case: two real, separate
|
|
1378
|
+
stores. A guard that refuses everything is not a guard."""
|
|
1379
|
+
import os
|
|
1380
|
+
original = self.lb._run_dz
|
|
1381
|
+
self.lb._run_dz = self._fake_store()
|
|
1382
|
+
try:
|
|
1383
|
+
code, message = self.lb.teach("repeat a lone abnormal result", confirmed=True)
|
|
1384
|
+
finally:
|
|
1385
|
+
self.lb._run_dz = original
|
|
1386
|
+
self.assertEqual(code, 0, message)
|
|
1387
|
+
brain = os.path.join(os.path.realpath(os.getcwd()), self.lb.HEALTH_BRAIN_DIRNAME)
|
|
1388
|
+
self.assertTrue(any("repeat a lone" in r["pattern"] for r in self._store_rows(brain)))
|
|
1389
|
+
self.assertEqual(self._store_rows(os.path.realpath(os.getcwd())), [],
|
|
1390
|
+
"nothing reached the shared store")
|
|
1391
|
+
|
|
1392
|
+
def test_finding8_a_negation_that_does_not_match_the_store_is_SAFE(self):
|
|
1393
|
+
"""FINDING 8 (MEDIUM) — round 16 read "last match wins" as "any later `!` cancels
|
|
1394
|
+
the `*`", which refused this file although `!README.md` never matches `.dz`."""
|
|
1395
|
+
import os
|
|
1396
|
+
for body in ("*\n!README.md\n", "*\n!README.md\n!LICENSE\n", "*\n!*.md\n"):
|
|
1397
|
+
with self.subTest(body=body):
|
|
1398
|
+
brain = os.path.join(os.path.realpath(os.getcwd()),
|
|
1399
|
+
"safe-" + str(abs(hash(body))))
|
|
1400
|
+
os.makedirs(brain)
|
|
1401
|
+
with open(os.path.join(brain, ".gitignore"), "w", encoding="utf-8") as fh:
|
|
1402
|
+
fh.write(body)
|
|
1403
|
+
self.lb._protect_brain(brain) # must not raise
|
|
1404
|
+
|
|
1405
|
+
def test_finding8_the_round16_refusals_still_refuse(self):
|
|
1406
|
+
"""…and the fix must not reopen what round 16 closed: these negations DO match
|
|
1407
|
+
the store, so the last matching rule really does re-expose it."""
|
|
1408
|
+
import os
|
|
1409
|
+
for body in ("*\n!.dz/\n!.dz/**\n", "*\n!.dz/\n", "*\n!.dz/**\n", "*\n!*\n"):
|
|
1410
|
+
with self.subTest(body=body):
|
|
1411
|
+
brain = os.path.join(os.path.realpath(os.getcwd()),
|
|
1412
|
+
"unsafe-" + str(abs(hash(body))))
|
|
1413
|
+
os.makedirs(brain)
|
|
1414
|
+
with open(os.path.join(brain, ".gitignore"), "w", encoding="utf-8") as fh:
|
|
1415
|
+
fh.write(body)
|
|
1416
|
+
with self.assertRaises(RuntimeError) as ctx:
|
|
1417
|
+
self.lb._protect_brain(brain)
|
|
1418
|
+
self.assertIn("IN FORCE", str(ctx.exception))
|
|
1419
|
+
|
|
1420
|
+
def test_finding9_formatted_identifiers_that_still_passed(self):
|
|
1421
|
+
"""FINDING 9 (MEDIUM) — two CLOSED, SPECIFIED formats (a UUID, the 3-2-4
|
|
1422
|
+
national-identifier grouping), plus the two consequences of round 16's rule that
|
|
1423
|
+
a record-number value must be 3+ digits or letters-and-digits mixed."""
|
|
1424
|
+
for text in (
|
|
1425
|
+
"MRN: 7",
|
|
1426
|
+
"SSN 123-45-6789",
|
|
1427
|
+
"Patient key 550e8400-e29b-41d4-a716-446655440000",
|
|
1428
|
+
"Patient code A12345678",
|
|
1429
|
+
"MRN № 7",
|
|
1430
|
+
"MRN = 7",
|
|
1431
|
+
"PATIENT KEY 550E8400-E29B-41D4-A716-446655440000 IN THE LEDGER",
|
|
1432
|
+
):
|
|
1433
|
+
with self.subTest(text=text):
|
|
1434
|
+
self.assertFalse(self.lb.check_lesson(text).ok, text)
|
|
1435
|
+
|
|
1436
|
+
def test_finding9_prose_reached_across_words_is_still_allowed(self):
|
|
1437
|
+
"""The other half: the separator/prose distinction must not re-refuse the method
|
|
1438
|
+
prose rounds 15 and 16 fixed."""
|
|
1439
|
+
for text in (
|
|
1440
|
+
"compare medical record number in 3 hospitals",
|
|
1441
|
+
"Compare medical record number use in 300 hospitals.",
|
|
1442
|
+
"audit medical record number quality before dataset linkage",
|
|
1443
|
+
"medical record number completeness across 3 sites is the first check",
|
|
1444
|
+
):
|
|
1445
|
+
with self.subTest(text=text):
|
|
1446
|
+
self.assertTrue(self.lb.check_lesson(text).ok, text)
|
|
1447
|
+
|
|
1448
|
+
def test_finding10_medical_method_prose_is_no_longer_refused(self):
|
|
1449
|
+
"""FINDING 10 (MEDIUM) — four ordinary method sentences read as identifiers."""
|
|
1450
|
+
for text in (
|
|
1451
|
+
"Use apoB@baseline and apoB@week12 as study endpoints.",
|
|
1452
|
+
"Allocate 1000-1000-1000-1000 participants across four trial arms.",
|
|
1453
|
+
"Compare medical record number use in 300 hospitals.",
|
|
1454
|
+
"Use ISO15189-compliant laboratory workflows.",
|
|
1455
|
+
):
|
|
1456
|
+
with self.subTest(text=text):
|
|
1457
|
+
self.assertTrue(self.lb.check_lesson(text).ok, text)
|
|
1458
|
+
|
|
1459
|
+
def test_finding10_the_identifiers_those_branches_exist_for_are_still_caught(self):
|
|
1460
|
+
"""Each loosening is paired with the case it must NOT let through: the bare-host
|
|
1461
|
+
email branch still catches `localhost`, and the card branch still catches a
|
|
1462
|
+
number whose Luhn check digit holds. MEASURED: 4111111111111111 satisfies Luhn,
|
|
1463
|
+
1000100010001000 does not — the shapes are identical, the outcome is not."""
|
|
1464
|
+
for text in (
|
|
1465
|
+
"contact customer!@localhost before enrollment",
|
|
1466
|
+
"contact me at a.b@clinic.org",
|
|
1467
|
+
"card 4111 1111 1111 1111 belongs to the patient",
|
|
1468
|
+
"card 5500 0000 0000 0004 belongs to the patient",
|
|
1469
|
+
"trial NCT04368728 enrolled a cohort",
|
|
1470
|
+
"passport ab1234567 identifies the holder",
|
|
1471
|
+
):
|
|
1472
|
+
with self.subTest(text=text):
|
|
1473
|
+
self.assertFalse(self.lb.check_lesson(text).ok, text)
|
|
1474
|
+
self.assertTrue(self.lb._luhn_ok("4111111111111111"))
|
|
1475
|
+
self.assertFalse(self.lb._luhn_ok("1000100010001000"))
|
|
1476
|
+
|
|
1477
|
+
def test_finding11_the_module_header_describes_the_TWO_store_topology(self):
|
|
1478
|
+
"""FINDING 11 (LOW) — the header still taught the superseded one-store rule
|
|
1479
|
+
("a loop only compounds when recall and teach hit ONE"), which a maintainer could
|
|
1480
|
+
follow straight through the isolation the rest of the file enforces. ADR-003 is
|
|
1481
|
+
AMENDED rather than rewritten, and so is this: the old reasoning stays visible,
|
|
1482
|
+
marked as superseded."""
|
|
1483
|
+
doc = self.lb.__doc__ or ""
|
|
1484
|
+
self.assertIn("SUPERSEDED", doc)
|
|
1485
|
+
self.assertIn("RECALL READS BOTH", doc)
|
|
1486
|
+
self.assertIn(self.lb.HEALTH_BRAIN_DIRNAME, doc)
|
|
1487
|
+
self.assertLess(doc.index("SUPERSEDED"), doc.index("hit ONE"),
|
|
1488
|
+
"the one-store claim must be marked superseded BEFORE it is quoted")
|
|
1489
|
+
# …and the sentence round 8 pinned is still there.
|
|
1490
|
+
self.assertIn("does NOT decide whether a lesson describes a method or a person", doc)
|
|
827
1491
|
|
|
828
1492
|
|
|
829
1493
|
if __name__ == "__main__":
|