@dzhechkov/p-replicator 1.5.17 → 1.6.0

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.
Files changed (85) hide show
  1. package/.dz-manifest.json +173 -65
  2. package/CHANGELOG.md +135 -0
  3. package/MULTIPLATFORM_ROADMAP.md +1 -1
  4. package/README/eng/01_quickstart.md +2 -2
  5. package/README/eng/02_user_guide.md +1 -1
  6. package/README/eng/03_admin_guide.md +2 -2
  7. package/README/eng/05_architecture.md +6 -2
  8. package/README/eng/README.md +1 -1
  9. package/README/ru/01_quickstart.md +2 -2
  10. package/README/ru/02_user_guide.md +1 -1
  11. package/README/ru/03_admin_guide.md +2 -2
  12. package/README/ru/05_architecture.md +1 -1
  13. package/README/ru/README.md +1 -1
  14. package/README/ru/html/index.html +7 -7
  15. package/README.md +41 -18
  16. package/bin/cli.js +0 -0
  17. package/package.json +11 -10
  18. package/sbom.json +347 -77
  19. package/src/utils.js +2 -0
  20. package/templates/.claude/agents/doc-validator.md +2 -1
  21. package/templates/.claude/agents/product-discoverer.md +1 -1
  22. package/templates/.claude/commands/next.md +16 -0
  23. package/templates/.claude/commands/replicate.md +126 -8
  24. package/templates/.claude/commands/start.md +19 -1
  25. package/templates/.claude/hooks/autocommit-insights.cjs +95 -10
  26. package/templates/.claude/hooks/autocommit-plans.cjs +95 -10
  27. package/templates/.claude/hooks/autocommit-roadmap.cjs +96 -13
  28. package/templates/.claude/hooks/check-ports.cjs +232 -0
  29. package/templates/.claude/hooks/session-insights.cjs +13 -1
  30. package/templates/.claude/hooks/state-update.cjs +13 -1
  31. package/templates/.claude/hooks/statusline.cjs +145 -18
  32. package/templates/.claude/rules/docker-ports.md +123 -0
  33. package/templates/.claude/rules/replicate-pipeline.md +5 -2
  34. package/templates/.claude/settings.json +5 -5
  35. package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/01-detect-parse.md +57 -14
  36. package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/02-analyze-map.md +9 -7
  37. package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/enhanced-recommendations.md +6 -4
  38. package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/extended-mapping.md +2 -2
  39. package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/ddd-agents.md +2 -2
  40. package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/ddd-skills.md +1 -1
  41. package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/enhanced-claude-md.md +2 -2
  42. package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/feature-suggestions.md +11 -2
  43. package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/start-command.md +1 -1
  44. package/templates/.claude/skills/goap-research-ed25519/SKILL.md +340 -47
  45. package/templates/.claude/skills/goap-research-ed25519/scripts/check_report_evidence.py +359 -3
  46. package/templates/.claude/skills/goap-research-ed25519/scripts/ed25519_verifier.py +386 -13
  47. package/templates/.claude/skills/goap-research-ed25519/scripts/fixture_legacy_v2_fact.json +23 -0
  48. package/templates/.claude/skills/goap-research-ed25519/scripts/fixtures_field_cases.json +133 -0
  49. package/templates/.claude/skills/goap-research-ed25519/scripts/goap_planner.py +314 -44
  50. package/templates/.claude/skills/goap-research-ed25519/scripts/learning_bridge.py +890 -303
  51. package/templates/.claude/skills/goap-research-ed25519/scripts/population_match.py +591 -0
  52. package/templates/.claude/skills/goap-research-ed25519/scripts/risk_statement.py +289 -0
  53. package/templates/.claude/skills/goap-research-ed25519/scripts/test_ed25519_verifier.py +57 -2
  54. package/templates/.claude/skills/goap-research-ed25519/scripts/test_evidence_provenance.py +969 -344
  55. package/templates/.claude/skills/goap-research-ed25519/scripts/test_goap_planner.py +420 -0
  56. package/templates/.claude/skills/goap-research-ed25519/scripts/test_population_match.py +544 -0
  57. package/templates/.claude/skills/goap-research-ed25519/scripts/test_risk_absolute.py +239 -0
  58. package/templates/.claude/skills/goap-research-ed25519/scripts/test_signature_v3.py +554 -0
  59. package/templates/.claude/skills/goap-research-ed25519/scripts/test_suite_completeness.py +90 -0
  60. package/templates/.claude/skills/requirements-validator/SKILL.md +7 -0
  61. package/templates/.claude/skills/requirements-validator/references/scoring-system.md +28 -0
  62. package/templates/.claude/skills/sparc-prd-mini/SKILL.md +143 -1
  63. package/tests/e2e/lifecycle.test.js +21 -10
  64. package/tests/snapshot/baseline.json +51 -34
  65. package/tests/snapshot/update-baseline.js +2 -1
  66. package/tests/unit/adr-decision-coverage.test.js +137 -0
  67. package/tests/unit/adr-scanner-contract.test.js +108 -0
  68. package/tests/unit/autocommit-deletion.test.js +242 -0
  69. package/tests/unit/check-ports.test.js +184 -0
  70. package/tests/unit/db-port-rule.test.js +216 -0
  71. package/tests/unit/detect-parse-anchor.test.js +109 -0
  72. package/tests/unit/external-dependency-check.test.js +209 -0
  73. package/tests/unit/growth-module-b2b-gate.test.js +104 -0
  74. package/tests/unit/hooks-project-anchored.test.js +223 -0
  75. package/tests/unit/hooks-report-failures.test.js +207 -0
  76. package/tests/unit/pipeline-file-ownership.test.js +95 -0
  77. package/tests/unit/roadmap-one-schema.test.js +179 -0
  78. package/tests/unit/sparc-reconciliation.test.js +117 -0
  79. package/tests/unit/spec-pseudocode-traceability.test.js +146 -0
  80. package/tests/unit/statusline-honest-labels.test.js +178 -0
  81. package/tests/unit/statusline-two-roots.test.js +237 -0
  82. package/tests/unit/sync-templates-guard.test.js +209 -0
  83. package/tests/unit/utils.test.js +2 -2
  84. package/tests/unit/validation-gate-teeth.test.js +158 -0
  85. package/LICENSE +0 -21
@@ -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
- python3 -m unittest test_evidence_provenance -v
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 = v.create_signed_fact(claim="c", source_url="https://x.test", source_content="body", issuer="researcher")
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 v1.
118
- What breaks it is that the signed v2 text CONTAINS the three evidence keys
119
- at all — not the schema marker (a discrimination run refuted that)."""
120
- fact = self.v.create_fetched_fact(claim="c", fetch_record=self.record, issuer="researcher")
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), 1, "the fact now looks legacy…")
124
- self.assertFalse(self.v.verify_fact(fact).verified, "…but its v2 signature must not accept the v1 message")
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
- legacy = self.v.create_signed_fact(claim="c", source_url="https://x.test", source_content="b", issuer="researcher")
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.assertFalse(self.v.verify_fact(legacy).verified, "adding the field must break a v1 signature")
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", source_date="2024-01-01")
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
- asserted = v.create_asserted_fact(claim="c")
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.create_signed_fact(claim="c", source_url="https://some-blog.example/p",
389
- source_content="b", issuer="researcher")
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,375 +592,649 @@ class HardeningTests(unittest.TestCase):
484
592
  self.assertIn("future", reason)
485
593
 
486
594
 
487
- class LearningBridgeTests(unittest.TestCase):
488
- """Slice H the privacy invariant, rebuilt after a cross-model review broke the
489
- first design. The guard is an ALLOWLIST (a method lesson carries no digits and no
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 learning_bridge
495
- self.lb = learning_bridge
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 test_method_lessons_pass(self):
498
- for good in (
499
- "total testosterone is uninterpretable without SHBG — always order the companion test",
500
- "a relative risk without an absolute one misleads; convert before reporting",
501
- "check the study population before transferring an effect to a patient profile",
502
- "a prolonged fast lowers total testosterone substantially, with LH down and FSH normal",
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.assertTrue(self.lb.check_lesson(good).ok, good)
505
-
506
- def test_every_measured_bypass_from_the_review_is_closed(self):
507
- """These EXACT strings were measured passing by the reviewer. Each is a
508
- regression anchor: they must never be accepted again."""
509
- for bad in (
510
- "TSH was 8.04 mIU/L when fasting", # threshold-word laundering
511
- "testosterone 8.04 nmol/l was below baseline",
512
- "Alice's BMI 25 was below baseline",
513
- "Patient John Smith has HIV", # a name
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.assertFalse(self.lb.check_lesson(bad).ok, f"must refuse: {bad}")
524
-
525
- def test_a_number_needs_an_EXPLICIT_human_decision_never_an_inferred_one(self):
526
- """The first design inferred 'this number is a threshold' from a nearby word,
527
- and one stray 'when' laundered a lab value. Now a human passes the flag."""
528
- threshold = "transferrin saturation above 45% warrants attention in men"
529
- self.assertFalse(self.lb.check_lesson(threshold).ok, "digits refused by default")
530
- self.assertTrue(self.lb.check_lesson(threshold, allow_numbers=True).ok)
531
-
532
- def test_the_flag_cannot_wave_through_a_person_or_an_identifier(self):
533
- self.assertFalse(self.lb.check_lesson("у пациента 45%", allow_numbers=True).ok)
534
- self.assertFalse(self.lb.check_lesson("MRN 84729163 note", allow_numbers=True).ok)
535
- self.assertFalse(self.lb.check_lesson("Patient John Smith 45%", allow_numbers=True).ok)
536
-
537
- def test_the_accept_message_does_NOT_claim_safety(self):
538
- """A shape detector may not issue a safety certificate — saying 'safe to teach'
539
- transfers responsibility the guard does not have."""
540
- note = self.lb.check_lesson("a method lesson about companion tests").note
541
- self.assertNotIn("safe to teach", note)
542
- self.assertIn("not a safety certificate", note)
543
- self.assertIn("responsible", note)
544
-
545
- def test_empty_lesson_is_refused(self):
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 test_absent_dz_is_a_NOTE_not_a_failure(self):
549
- original = self.lb.shutil.which
550
- self.lb.shutil.which = lambda _name: None
551
- try:
552
- self.assertIn("self-learning is OFF", self.lb.status())
553
- self.assertIn("self-learning is OFF", self.lb.recall("anything"))
554
- code, message = self.lb.teach("a valid method lesson about companion tests")
555
- self.assertEqual(code, 0, "a missing optional dependency is not an error")
556
- self.assertIn("self-learning is OFF", message)
557
- finally:
558
- self.lb.shutil.which = original
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
559
772
 
560
- def test_privacy_guard_runs_BEFORE_any_dz_call(self):
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)
780
+
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 = lambda *a, **k: (calls.append(a) or (0, "", ""))
849
+ self.lb._run_dz = self._fake_store(calls=calls)
564
850
  try:
565
- code, message = self.lb.teach("тестостерон 8.04 нмоль/л у пациента")
566
- self.assertEqual(code, 1)
567
- self.assertIn("REFUSED", message)
568
- self.assertEqual(calls, [], "no dz call may happen for a refused lesson")
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
- def test_argument_injection_through_the_query_is_refused(self):
573
- """`recall("--all")` would become `dz recall --all …` and dump the whole store.
574
- This is ARGUMENT injection shell quoting does not address it."""
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 = lambda *a, **k: (calls.append(a) or (0, "", ""))
880
+ self.lb._run_dz = self._fake_store(calls=calls)
578
881
  try:
579
- self.assertIn("would be read as an option", self.lb.recall("--all"))
580
- code, msg = self.lb.teach("--reward 1.0 laundered")
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
- def test_older_cli_is_detected_by_CAPABILITY_not_by_exit_code(self):
588
- """The sharpest finding of the review: the previous dz did NOT reject --domain.
589
- Its parser accepted any `--key value` and cmdRecall ignored it, exiting 0 with
590
- unfiltered results — so an exit-code test could never fire, and the earlier
591
- version of THIS test fabricated a failure that no release ever produced. The
592
- observable difference is the boost note; its absence is what 'too old' means."""
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
- # Old CLI: exit 0, results, and NO boost note.
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
- out = self.lb.recall("transferrin")
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
- def test_current_cli_with_the_boost_note_reports_no_degradation(self):
605
- """The note text here is the REAL renderer output, not a paraphrase. The earlier
606
- version of this test invented a format the CLI never produced, so it blessed the
607
- substring check instead of proving the capability. The two tail phrases are
608
- pinned on the TypeScript side by a matching test."""
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
- # Copied VERBATIM from a live run:
611
- # node dist/bin.js recall "signature" --limit 2 --domain testing
612
- real_note = (' domain "health-research": among 6 candidate(s) — 1 exact match(es), '
613
- '3 changed position; foreign-domain lessons kept (a boost, not a filter)')
614
- self.lb._run_dz = lambda *a, **k: (
615
- 0, 'dz recall "x" — 1 hit(s)\n [0.90] (health-research) a lesson\n' + real_note, "")
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
- out = self.lb.recall("transferrin")
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
- def test_the_no_match_note_ALSO_counts_as_capability(self):
623
- """A --domain run that matched nothing still proves the CLI understands --domain.
624
- Reading only the 'a boost, not a filter' tail would have reported a perfectly
625
- current CLI as too old whenever the domain happened to match nothing."""
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
- real_note = (' domain "health-research": no lesson in this result carries it — '
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.assertNotIn("predates", self.lb.recall("x"))
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
- def test_capability_cannot_be_FORGED_by_recalled_lesson_content(self):
636
- """Round 2: the marker was the bare substring `domain "`, which a LESSON can
637
- contain. A probe the payload can forge is not a probe — an old CLI returning
638
- this ordinary lesson was certified as boost-capable."""
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
- self.assertIn("predates", self.lb.recall("x"))
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
- # ---------------------------------------------------------------- round 2
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)
987
+ # ------------------------------------------------------------------ round 11
701
988
 
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"))
709
-
710
- def test_round2_the_docstring_no_longer_promises_a_retry_that_does_not_exist(self):
711
- """The prose claimed recall retries without --domain; the code makes ONE call.
712
- Documentation of behaviour that does not exist is a defect with no stack trace,
713
- so it gets a test like any other."""
714
- calls = []
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 record(args, **kwargs):
718
- calls.append(list(args))
719
- return 1, "", "unknown option"
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 = record
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.assertNotIn("we retry WITHOUT the flag", self.lb.recall.__doc__ or "")
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
- # ---------------------------------------------------------------- round 3
731
- # The third review found the SAME class of defect a third time: two of the three
732
- # clauses were still enumerations. These anchor the token-allowlist rewrite.
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
- def test_round3_a_short_or_non_ascii_name_is_refused(self):
735
- """`Li` was too short for the old 3-letter pattern; `Élodie` starts with a
736
- Latin-1 capital it never matched; `method—John` hid behind an em dash that the
737
- whitespace tokenizer did not split, landing the name inside the exempt first
738
- token. Widening a name pattern buys one round; asking what an ACCEPTED token
739
- looks like ends the game."""
740
- for leak in ("patient Li has HIV", "patient Élodie has HIV", "method—John has HIV",
741
- "however Smith reported otherwise"):
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
- def test_round3_a_single_spelled_number_beside_a_person_is_a_reading(self):
746
- """`his tsh was twelve` passed: one number word was not treated as a number.
747
- A lone number word in ordinary prose still must not be refused."""
748
- self.assertFalse(self.lb.check_lesson("his tsh was twelve").ok)
749
- self.assertTrue(self.lb.check_lesson("one marker alone cannot close the question").ok)
750
-
751
- def test_round3_singular_person_is_a_record_PLURAL_is_a_population(self):
752
- """The first fix simply added `woman|man|adult|child` and immediately refused
753
- `…warrants attention in men`, a textbook population lesson. Grammar, not a word
754
- list, separates a cohort from a patient and grammar is checkable."""
755
- self.assertFalse(self.lb.check_lesson("the woman has tsh 8.04", allow_numbers=True).ok)
756
- self.assertTrue(
757
- self.lb.check_lesson("transferrin saturation above 45% warrants attention in men",
758
- allow_numbers=True).ok)
759
-
760
- def test_round3_a_token_mixing_letters_and_digits_is_an_identifier(self):
761
- """`passport ab1234567` walked through --allow-numbers, because the identifier
762
- list knew only email, phone and medical-record vocabulary. A threshold is a
763
- number with at most a short unit after it; letters BEFORE digits is a label."""
764
- for leak in ("passport ab1234567 identifies the holder",
765
- "trial NCT04368728 enrolled him at 8.04"):
766
- with self.subTest(leak=leak):
767
- self.assertFalse(self.lb.check_lesson(leak, allow_numbers=True).ok, leak)
768
- # …and a real threshold with a unit still passes.
769
- self.assertTrue(self.lb.check_lesson("a 56h fast lowers total testosterone by a third",
770
- allow_numbers=True).ok)
771
-
772
-
773
- # ---------------------------------------------------------------- round 4
774
-
775
- def test_round4_acronym_vs_name_is_decided_by_WHERE_the_capital_sits(self):
776
- """The previous predicate "two or more capitals" — was wrong in both
777
- directions at once: `Anne-Marie` and `McDonald` passed as acronyms while
778
- `apoB` was refused, contradicting this repo's own documentation."""
779
- for name in ("patient Anne-Marie has HIV", "patient McDonald has HIV"):
780
- with self.subTest(name=name):
781
- self.assertFalse(self.lb.check_lesson(name).ok, name)
782
- for acronym in ("measure apoB before interpreting risk",
783
- "total testosterone is uninterpretable without SHBG"):
784
- with self.subTest(acronym=acronym):
785
- self.assertTrue(self.lb.check_lesson(acronym).ok, acronym)
786
-
787
- def test_round4_punctuation_cannot_launder_an_identifier(self):
788
- """`ab1234567` was refused, so the same identifier written `ab—1234567` was
789
- split by the tokenizer into a lower-case word plus a permitted number. The raw
790
- text still shows the joiner, so that is where this one is checked."""
791
- for leak in ("passport ab—1234567 identifies the holder",
792
- "passport ab/1234567 identifies the holder",
793
- "trial NCT—04368728 enrolled a cohort"):
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'):
794
1112
  with self.subTest(leak=leak):
795
- self.assertFalse(self.lb.check_lesson(leak, allow_numbers=True).ok, leak)
796
- # A YEAR is not an identifier: a space is deliberately not a joiner.
797
- self.assertTrue(self.lb.check_lesson("the 2019 guideline moved the threshold",
798
- allow_numbers=True).ok)
799
- # …and a short lab acronym carrying a digit still passes.
800
- self.assertTrue(self.lb.check_lesson("hba1c drifts with red-cell turnover",
801
- allow_numbers=True).ok)
802
-
803
- def test_round4_the_probe_cannot_be_forged_by_an_exotic_line_separator(self):
804
- """`splitlines()` breaks on U+2028 and U+0085, which the renderer does not treat
805
- as line breaks — so a lesson containing one produced a 'line' the CLI never
806
- emitted, and that forged line satisfied the capability probe. Splitting exactly
807
- the way the producer joins keeps a forged tail on the hit's own line."""
808
- tail = 'domain "health-research": order unchanged, nothing was hidden'
809
- 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
- self.assertTrue(self.lb._boost_note_present(" [0.90] (general) x\n " + tail))
813
-
1113
+ self.assertFalse(self.lb.check_lesson(leak).ok, leak)
814
1114
 
815
- # ---------------------------------------------------------------- round 5
1115
+ # ------------------------------------------------------------------ round 13
816
1116
 
817
- def test_round5_ONE_definition_of_digit_everywhere(self):
818
- """The text scan used the regex `\\d` and the token scan used str.isdigit().
819
- `²` satisfies the second but not the first, so one half skipped it as a number
820
- and the other never refused it. Two definitions of the same word inside one
821
- guard is a gap by construction."""
822
- self.assertFalse(self.lb.check_lesson("threshold is ²").ok)
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
+ ):
1166
+ with self.subTest(leak=leak):
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
823
1191
 
824
- def test_round5_capitalisation_is_checked_even_when_a_token_carries_a_digit(self):
825
- """Digit-bearing tokens returned before the capitalisation check, so a Title
826
- Case name only had to carry a digit to become exempt."""
827
- self.assertFalse(self.lb.check_lesson("method Test2 applies", allow_numbers=True).ok)
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
828
1203
 
829
- def test_round5_joiners_and_alphabets_are_not_enumerated(self):
830
- """`ab:1234567` passed because a colon was not in the joiner list, and
831
- `αβ—1234567` because Greek was not in the letter class. Enumerating joiners and
832
- alphabets is the same losing game as enumerating names."""
833
- for leak in ("passport ab:1234567 identifies the holder",
834
- "passport αβ—1234567 identifies the holder"):
835
- with self.subTest(leak=leak):
836
- self.assertFalse(self.lb.check_lesson(leak, allow_numbers=True).ok, leak)
837
-
838
- def test_round5_the_word_person_is_a_person(self):
839
- """The singular list enumerated patient/subject/participant and omitted the
840
- plainest word of all."""
841
- self.assertFalse(self.lb.check_lesson("this person has tsh 8.04", allow_numbers=True).ok)
842
-
843
- def test_round5_the_acceptance_note_does_not_contradict_the_flag(self):
844
- """It said "no digits, no figures" on the --allow-numbers path, where digits
845
- were present and consciously allowed. A message that contradicts the flag the
846
- caller just passed teaches the reader to stop reading messages."""
847
- verdict = self.lb.check_lesson("a 56h fast lowers total testosterone by a third",
848
- allow_numbers=True)
849
- self.assertTrue(verdict.ok)
850
- self.assertNotIn("no digits", verdict.note)
851
- self.assertIn("ALLOWED by --allow-numbers", verdict.note)
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
852
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."""
1233
+ tail = 'domain "health-research": order unchanged, nothing was hidden'
1234
+ self.assertFalse(self.lb._boost_note_present(" [0.90] (general) x\u2028" + tail))
1235
+ self.assertTrue(self.lb._boost_note_present(" [0.90] (general) x\n " + tail))
853
1236
 
854
1237
  def test_a_real_failure_is_not_disguised_as_an_old_cli(self):
855
- """A crash, timeout or corrupt store must read as a failure, not as 'upgrade'."""
856
1238
  original = self.lb._run_dz
857
1239
  self.lb._run_dz = lambda *a, **k: (1, "", "store corrupt")
858
1240
  try:
@@ -862,7 +1244,250 @@ class LearningBridgeTests(unittest.TestCase):
862
1244
  finally:
863
1245
  self.lb._run_dz = original
864
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`.
865
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.
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)
866
1491
 
867
1492
 
868
1493
  if __name__ == "__main__":