@dzhechkov/p-replicator 1.5.17 → 1.5.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. package/.dz-manifest.json +51 -23
  2. package/README.md +5 -3
  3. package/package.json +1 -1
  4. package/sbom.json +106 -36
  5. package/templates/.claude/skills/goap-research-ed25519/SKILL.md +340 -47
  6. package/templates/.claude/skills/goap-research-ed25519/scripts/check_report_evidence.py +359 -3
  7. package/templates/.claude/skills/goap-research-ed25519/scripts/ed25519_verifier.py +386 -13
  8. package/templates/.claude/skills/goap-research-ed25519/scripts/fixture_legacy_v2_fact.json +23 -0
  9. package/templates/.claude/skills/goap-research-ed25519/scripts/fixtures_field_cases.json +133 -0
  10. package/templates/.claude/skills/goap-research-ed25519/scripts/goap_planner.py +314 -44
  11. package/templates/.claude/skills/goap-research-ed25519/scripts/learning_bridge.py +890 -303
  12. package/templates/.claude/skills/goap-research-ed25519/scripts/population_match.py +591 -0
  13. package/templates/.claude/skills/goap-research-ed25519/scripts/risk_statement.py +289 -0
  14. package/templates/.claude/skills/goap-research-ed25519/scripts/test_ed25519_verifier.py +57 -2
  15. package/templates/.claude/skills/goap-research-ed25519/scripts/test_evidence_provenance.py +969 -344
  16. package/templates/.claude/skills/goap-research-ed25519/scripts/test_goap_planner.py +420 -0
  17. package/templates/.claude/skills/goap-research-ed25519/scripts/test_population_match.py +544 -0
  18. package/templates/.claude/skills/goap-research-ed25519/scripts/test_risk_absolute.py +239 -0
  19. package/templates/.claude/skills/goap-research-ed25519/scripts/test_signature_v3.py +554 -0
  20. package/templates/.claude/skills/goap-research-ed25519/scripts/test_suite_completeness.py +90 -0
  21. package/tests/snapshot/baseline.json +24 -9
@@ -54,70 +54,171 @@ Provenance, not truth.
54
54
  ### The report gate
55
55
 
56
56
  ```bash
57
- python3 scripts/check_report_evidence.py --report report.md --facts facts.json
57
+ python3 scripts/check_report_evidence.py --report report.md --facts facts.json [--profile patient.json]
58
58
  ```
59
59
 
60
60
  `ASSERTED` claims must not appear in a report at all. `LISTING_ONLY` claims may appear only with a
61
61
  visible marker next to the claim. This is an exit code, not advice — exit `1` on violation, exit `2`
62
62
  when the inputs cannot be read (a gate that could not evaluate has cleared nothing).
63
63
 
64
- ## Self-learning (optional needs `dz` on PATH)
64
+ The gate merges four independent judgements over the same text and loses none of them: the evidence
65
+ axis, signature integrity, **population applicability** (`UNMARKED_POPULATION_MISMATCH`,
66
+ `POPULATION_UNKNOWN_UNMARKED`, `MISSING_STUDY_POPULATION`, `UNATTESTED_STUDY_POPULATION`,
67
+ `LEGACY_POPULATION_UNJUDGEABLE`) and the **relative-risk belt** (`RELATIVE_RISK_WITHOUT_ABSOLUTE`).
68
+ A population caveat only counts if it NAMES the diverging axis within 400 characters of **each**
69
+ occurrence — boilerplate that names nothing warns nobody.
65
70
 
66
- The most valuable signal in research is the moment a conclusion turned out to be
67
- WRONG. `scripts/learning_bridge.py` records those as METHOD lessons in the shared
68
- `dz` store and replays them at the start of the next investigation.
71
+ One exception, and it is the sanctioned path (QE G5): when the source states **no** population at
72
+ all, there is no axis to name, so the window must instead SAY the population is unknown — «не
73
+ указана», «not stated», `POPULATION_MATCH unknown` and their kin. Requiring the literal sentinel
74
+ `(study population)` punished `StudyPopulation.unstated(reason)`, the one honest way to declare it.
75
+
76
+ `--profile` is optional: without it the population *proximity* rules cannot run, and the gate PRINTS
77
+ `population applicability: NOT CHECKED — no --profile supplied` rather than passing silently. The
78
+ two attestation rules do NOT depend on it: an unjudgeable or unattested fact is unjudgeable for
79
+ every patient, so those fire with or without a profile (QE G4).
80
+
81
+ ### Running the test suite
82
+
83
+ ONE command, everywhere it is written — never a hand-kept list of module names, because a module no
84
+ command names is a file rather than a check:
85
+
86
+ ```bash
87
+ cd scripts && python3 -m unittest discover -s . -p 'test_*.py' -v
88
+ ```
89
+
90
+ ## Self-learning — you do the judging, not a regex (optional; needs `dz` on PATH)
91
+
92
+ The most valuable thing this skill produces is the moment a conclusion turned out to be
93
+ WRONG. Record it. But a lesson describes a METHOD, never a person — otherwise the shared
94
+ learned store quietly becomes a medical record, which nobody consented to.
69
95
 
70
96
  ```bash
71
- python3 scripts/learning_bridge.py status # is the loop on at all?
97
+ python3 scripts/learning_bridge.py status # is the loop on at all?
72
98
  python3 scripts/learning_bridge.py recall "transferrin saturation" # traps already caught
73
- python3 scripts/learning_bridge.py teach "total testosterone is uninterpretable without SHBG"
74
- python3 scripts/learning_bridge.py check "<candidate>" # privacy guard alone
99
+ python3 scripts/learning_bridge.py check "<candidate>" # format check only
100
+ python3 scripts/learning_bridge.py teach "<rule>" --confirm-method
101
+ ```
102
+
103
+ **You are the check.** A script cannot tell a method from a case note: that is meaning,
104
+ not shape. `learning_bridge.py` refuses formatted identifiers (email, phone, record
105
+ numbers, letters against a long digit run) because those have a FORMAT and a regex is
106
+ reliable on them in any language. Everything below is yours, and the tool says so rather
107
+ than implying a guarantee it cannot keep.
108
+
109
+ ### The teach protocol — three steps, in order
110
+
111
+ **1. Write the RULE, not the case.** Do not edit the finding down; state what it taught.
112
+ The test is mechanical: *can you write the general rule WITHOUT the specific reading?*
113
+
114
+ | the case (do not record) | the rule (record this) |
115
+ |---|---|
116
+ | "total testosterone 8.04 in this patient was a fasting artifact" | "prolonged fasting lowers total testosterone — check the eating pattern before concluding" |
117
+ | "her ferritin 512 turned out to be inflammation, CRP was high" | "a single ferritin cannot separate overload from inflammation — pair it with CRP" |
118
+ | "the 56-year-old's TSH normalised on the repeat draw" | "a single out-of-range TSH warrants a repeat before any conclusion" |
119
+
120
+ If the rule cannot be written without the reading, there is no lesson yet — only a
121
+ finding. Do not record it.
122
+
123
+ **2. Read it back, hunting for the ONE person.** Redaction is not the goal; a lesson with
124
+ the numbers blanked out is still a case note. Ask instead: *could a reader who knows this
125
+ person recognise them here?*
126
+
127
+ The dangerous cases carry no name and no digits at all:
128
+
129
+ - a rare combination — "the patient with situs inversus who ran a marathon"
130
+ - a role that identifies — "my brother-in-law's cardiologist said"
131
+ - a timeline — "after the surgery last Tuesday"
132
+
133
+ None of these is detectable by any pattern, in any language. This step exists because you
134
+ can see what no scanner can.
135
+
136
+ **3. Record it, and own it.**
137
+
138
+ ```bash
139
+ python3 scripts/learning_bridge.py teach "<the rule>" --confirm-method
75
140
  ```
76
141
 
77
- **Recall at the START of an investigation** before interpreting anything, ask what
78
- this analyte has already fooled us with. **Teach at four moments**, not at random:
142
+ `--confirm-method` is your assertion that steps 1 and 2 were actually performed. The tool
143
+ does not verify it and does not pretend to it records that a judging agent made the
144
+ call. Without the flag nothing is written.
79
145
 
80
- | Moment | Why it is the signal |
146
+ ### When to teach four moments, not "when it feels useful"
147
+
148
+ | moment | why it is the signal |
81
149
  |---|---|
82
150
  | a conclusion was RETRACTED | the single most valuable lesson available |
83
- | a population check flipped a conclusion | the effect did not transfer, and now we know the shape |
151
+ | a population check flipped a conclusion | the effect did not transfer, and now you know the shape |
84
152
  | a preanalytical finding explained an alarming value | the value was an artifact; the rule generalises |
85
153
  | an open question was closed | the answer, not the waiting |
86
154
 
87
- **PRIVACY IS AN INVARIANT, NOT A PREFERENCE.** A lesson describes a METHOD, never a
88
- person. `teach` REFUSES the wrong shape before any store is touched:
155
+ **Recall at the START of an investigation** before interpreting anything, ask what this
156
+ analyte has already fooled us with.
89
157
 
90
- ```
91
- good "a fast lowers total testosterone by roughly a third, LH down, FSH normal"
92
- bad "testosterone 8.04 nmol/l in this patient turned out to be a fasting artifact"
93
- bad "John Smith has HIV" — a capitalised word after the first is refused
158
+ ### Honest scope
159
+
160
+ Lessons are written to a SEPARATE store `<project>/.health-brain/.dz` and never to
161
+ the shared one. `recall` reads both, so engineering lessons transfer INTO this work while
162
+ medical ones never leave. That separation is the guarantee this skill makes, and it holds
163
+ for every command that reads the shared store, including ones not yet written: a store
164
+ that never receives the data cannot hand it out.
165
+
166
+ An earlier version kept one store and filtered each command that emits lesson text.
167
+ Review closed four such commands and immediately produced five more (`guard promote`,
168
+ `epoch-replay --emit`, `vector harmonize`, `consolidate --prune-quarantine`, the
169
+ `recall --forget` preview). Filtering per command is a discipline; a separate store is a
170
+ property. The export hold-out remains as a second line, not as the promise.
171
+
172
+ The format check is a helper, not a boundary: it catches identifier shapes and nothing more.
173
+
174
+ ### Run medical work in its own project directory
175
+
176
+ The separation above covers what THIS SKILL writes. It does not cover what the harness
177
+ records about the CONVERSATION, and that channel is real: the `UserPromptSubmit` recall
178
+ hook logs the first 200 characters of each prompt into the shared `.dz`, and
179
+ `dz consolidate` can harvest transcript messages into shared lessons tagged `general`.
180
+ So if you type a lab value into the chat, that text reaches the shared store no matter
181
+ what this skill does — the data enters upstream of it.
182
+
183
+ No check inside this skill can close that, and none is offered: classifying arbitrary
184
+ prompt text is the same undecidable problem that cost this feature seven review rounds.
185
+ What closes it is WHERE the work happens.
186
+
187
+ **Do medical work in a project directory of its own.** Then the "shared" store of that
188
+ project is itself medical, there is nothing to separate, and prompts, transcripts and
189
+ lessons all stay in one place you can inspect or delete as a unit:
190
+
191
+ ```bash
192
+ mkdir ~/health-research && cd ~/health-research # medical work lives here
193
+ # lessons: ~/health-research/.health-brain/.dz (this skill)
194
+ # prompts/transcripts: ~/health-research/.dz (the harness)
94
195
  ```
95
196
 
96
- The accepted form has three properties, all checkable: **no digits** (spelled-out
97
- values count `eight point zero four` is a value), **no identifiers**, and **lower
98
- case** after the first word. The last one replaced an attempt to detect names: a name
99
- is any word, no list of them exists, and the attempt let `John Smith has HIV` through.
100
- Lower case costs a keystroke and closes that hole. Acronyms are unaffected — a token
101
- passes if it is fully upper (`SHBG`, `TSH`) or opens lower (`apoB`, `mmHg`); only Title
102
- Case is refused, because Title Case is the shape of a name. A token mixing letters with
103
- a long digit run (`ab1234567`, `NCT04368728`) is an identifier and is refused however it
104
- is punctuated, while a short lab acronym carrying a digit (`hba1c`) is not.
105
-
106
- Pass `--allow-numbers` when the number IS the knowledge (a guideline threshold) and
107
- you take responsibility for it. It never waves through a person beside a number.
108
-
109
- The refusal is not bureaucracy — the method form is the more useful lesson: a reading
110
- helps once, a rule helps every time. Honest scope: this is a SHAPE detector, not a
111
- de-identifier. A lower-case sentence that names nobody and quotes no figure can still
112
- describe one person, and no regular expression can see that. The shape rule plus the
113
- four METHOD moments is what keeps the store clean; the guard alone is not a guarantee.
114
-
115
- Without `dz` installed the package behaves exactly as before and says so once. An
116
- older `dz` does **not** reject `--domain` — it ignores the flag and exits 0 — so recall
117
- makes ONE call and detects the older CLI by the ABSENCE of the boost note in the
118
- output, then states that results may mix other domains. A degraded loop that says so
119
- beats a silent one. A real failure (non-zero exit) is reported as itself: proceeding
120
- without prior lessons, never as "your CLI is old".
197
+ Do not run medical investigations inside a shared code repository. If you already have,
198
+ the prompt log is `.dz/recall-usage.jsonl` and the lessons are in `.health-brain/`.
199
+
200
+ Choosing the shared store anyway is not blocked it is ADVISED against. `dz teach
201
+ --domain health-research` into a shared store prints what follows from the choice and
202
+ the command that does it the other way; `dz recall --all --json --include-domain
203
+ health-research` hands the data over and says how much of it is medical. Both end with
204
+ "Nothing was blocked this is your call." The data is yours; our job is that the choice
205
+ is knowing rather than accidental.
206
+ Step 1 is what makes a lesson safe to keep: a rule about a method has nowhere to put a
207
+ person. But note what that is and is not — it is a discipline, performed by you and
208
+ asserted with `--confirm-method`, which nothing verifies. The property that holds
209
+ regardless is the SEPARATE STORE: these lessons are never written to the shared one.
210
+
211
+ This division is not a preference. An earlier version of this file asked a regex to
212
+ decide "method or person" from the text; seven rounds of independent review graded it F
213
+ and the finding count never converged, because that question is about meaning and every
214
+ pattern answering it fails in both directions at once admitting `patient McDonald has
215
+ HIV` while refusing `apoB`, and refusing a perfectly good Chinese lesson for containing
216
+ "a capitalised word".
217
+
218
+ Without `dz` installed the package behaves exactly as before and says so once. An older
219
+ `dz` does not reject `--domain` it ignores the flag and exits 0 so recall makes ONE
220
+ call and detects the older CLI by the ABSENCE of the boost note in the output. A real
221
+ failure (non-zero exit) is reported as itself: proceeding without prior lessons.
121
222
 
122
223
  ## Source Tiers
123
224
 
@@ -147,8 +248,135 @@ is flagged too: freshness that cannot be established is not freshness.
147
248
  Because the three evidence fields are part of the signed text, **both** tamper directions fail:
148
249
  stripping `evidence_class` makes the verifier build the v1 text, which no longer matches the signed
149
250
  v2 text; adding it to a legacy fact makes it build the v2 text, which does not match the signed v1
150
- text. (The marker is self-description and the growth point for a future v3 — it is not itself the
151
- protection; a discrimination run proved that.)
251
+ text. (The marker is self-description — it is not itself the protection; a discrimination run proved
252
+ that.)
253
+
254
+ **Schema v3** additionally covers four more fields:
255
+
256
+ | field | why it had to come inside the envelope |
257
+ |---|---|
258
+ | `study_population` | who the finding was measured in — see *Study population*, below |
259
+ | `trust_class` | it SELECTS the verification branch. A field that decides which branch verifies a fact must be inside the envelope that branch is verifying |
260
+ | `confidence` | protects a consumer that reads `fact.confidence` directly without re-running `verify_fact()`'s ceiling math. Signed as a fixed-width `"0.6000"` string, because float repr differs across runtimes |
261
+ | `metadata` | carries `evidence_note` — the mandatory reason for a `LISTING_ONLY` degradation. An audit trail that can be rewritten in a text editor is not an audit trail |
262
+
263
+ A v3 message carries `"schema": "fact-v3"`, and version is again chosen by **presence**
264
+ (`study_population` set ⇒ v3; `evidence_class` set ⇒ v2; otherwise v1). Facts also store an explicit
265
+ `schema_version`, which is a convenience mirror of that dispatch, **not** a protection: stripping it
266
+ alone is a MEASURED no-op — the verifier recovers version 3 from the fields and the signature still
267
+ matches. What refuses tampering is that the v3 text CONTAINS those four keys at all.
268
+
269
+ **Dispatch is EXACT, and a schema the verifier does not know is REFUSED** (QE G6). Two ways that
270
+ matters: `schema_version = "not-a-number"` used to raise an uncaught `ValueError` out of the middle
271
+ of a gate run — it now returns `verified=False` with `schema_version` reported as `0`
272
+ (unidentified); and the old `version >= 3` band accepted any number at or above 3 as "v3", so the
273
+ field could be edited `3 → 99` and the fact still verified. Both are refusals now, and adding a
274
+ future v4 is one new branch that the refusal forces you to write.
275
+
276
+ `VerificationResult` now reports `schema_version` and `signed_fields`, so a consumer asking *"may I
277
+ rely on this fact's `trust_class`?"* gets an answer from the object rather than from a paragraph.
278
+
279
+ ### What v3 does NOT fix — the pre-v3 hole, named
280
+
281
+ Facts signed under **v1 or v2 do not carry `trust_class`, `metadata` or `confidence` under their
282
+ signature.** For such a fact, `trust_class` **can be rewritten without invalidating the signature** —
283
+ for example from `ISSUER_SIGNED` to `SELF_ATTESTED`, which routes verification onto the embedded-key
284
+ branch that never consults the pin registry, so a fact whose issuer key was later **revoked** comes
285
+ back `verified=True` at confidence `0.60`. No amount of new code can retroactively cover bytes that
286
+ were signed without those fields.
287
+
288
+ Three things narrow it, and none of them closes it:
289
+
290
+ 1. A **bounded belt**: a pre-v3 fact claiming `SELF_ATTESTED` whose issuer holds a **non-active pin**
291
+ is now refused (`issuer` *is* signed in v1/v2, so the lookup cannot be redirected). An **unpinned**
292
+ issuer's fact remains launderable to `SELF_ATTESTED @ 0.60` — that residual is real and unclosed.
293
+ 2. The report gate makes the hole VISIBLE instead of laundering it. A used pre-v3 fact carrying no
294
+ population is a `LEGACY_POPULATION_UNJUDGEABLE` finding (exit 1), counted on its own
295
+ `legacy-population-unknown` line; a used pre-v3 fact that carries a `study_population` anyway is
296
+ an `UNATTESTED_STUDY_POPULATION` finding and is **not matched against the patient at all**.
297
+ Before this, a legitimately-signed v2 fact with an INJECTED population and `schema_version`
298
+ pinned to `2` verified, reported `POPULATION_MATCH full` with zero findings and exited 0
299
+ (MEASURED, QE G1) — the declared vulnerability, laundered into a clean match. Note the boundary:
300
+ this is a SCHEMA check. An injection that leaves `schema_version` absent or set to `3` is caught
301
+ by the SIGNATURE check instead (`TAMPERED_FACT`), because the rebuilt v3 text no longer matches.
302
+ 3. `VerificationResult.signed_fields` names, per fact, what the signature actually covered.
303
+
304
+ **Re-signing existing v1/v2 facts as v3 is out of scope for this slice.** The gap is named and dated
305
+ here rather than quietly carried.
306
+
307
+ ## Study population — was this number obtained in people like this patient?
308
+
309
+ Every newly created fact must state who the finding was measured in. `study_population` is a
310
+ **keyword-only argument with no default** on all five factories, so omitting it is a `TypeError`
311
+ from Python itself, not a convention a later author can soften:
312
+
313
+ ```python
314
+ fact = verifier.create_listing_fact(
315
+ claim="Omega-3 raises LDL by 44.5%",
316
+ source_url="https://pubmed.ncbi.nlm.nih.gov/…",
317
+ reason="card seen in the search listing; full text never opened",
318
+ study_population={
319
+ "description": "patients with severe hypertriglyceridemia",
320
+ "criteria": {
321
+ "triglycerides_mg_dl_min": {
322
+ "op": ">=", "value": 800, "kind": "baseline",
323
+ "verbatim": "baseline triglycerides >= 800 mg/dL",
324
+ "locator": "[Methods, Baseline characteristics]",
325
+ }
326
+ },
327
+ },
328
+ )
329
+ ```
330
+
331
+ If the source genuinely does not state it, say so — with a reason, stored verbatim:
332
+ `population_match.StudyPopulation.unstated("the abstract never describes who was enrolled")`.
333
+
334
+ `population_match.match_from_fact(fact.study_population, patient_values)` returns one of **four**
335
+ verdicts, and every non-`full` verdict enumerates NAMED discrepancies:
336
+
337
+ - **`full`** — every stated criterion is satisfied by a known patient value.
338
+ - **`partial`** — a **baseline** criterion is out of range: the patient could have enrolled, but the
339
+ effect was not measured from where he stands.
340
+ - **`none`** — an **eligibility** criterion excludes him: he would not have been in the study.
341
+ - **`unknown`** — the source does not state the axis, or the profile does not carry it. `unknown` is
342
+ never folded into `partial`: an unestablished criterion is not a milder kind of match.
343
+
344
+ ```
345
+ POPULATION_MATCH: partial
346
+ triglycerides — patient 236; study requires triglycerides >= 800 (baseline-out-of-range, below)
347
+ enrollable, but the effect was not measured from this starting value
348
+ "baseline triglycerides >= 800 mg/dL" [Methods, Baseline characteristics]
349
+ ```
350
+
351
+ **Honest scope:** nothing here verifies that `verbatim` was transcribed truthfully from the source,
352
+ or that the criterion the source used is the one that matters clinically. Both are printed next to
353
+ every discrepancy so a human can check and overrule the machine.
354
+
355
+ ## Relative risk never travels alone
356
+
357
+ "21× higher risk of heart attack" is **1 excess case per 1394 people**. "The risk doubles" is
358
+ **4 per 1000 over 25 years**. Both sentences in each pair are true; only one of each is interpretable,
359
+ and the uninterpretable one is the one that gets quoted.
360
+
361
+ `risk_statement.RiskStatement(relative, absolute)` takes both halves as **required positional**
362
+ arguments. `absolute` is either a real `AbsoluteEffect` or an explicit
363
+ `UnknownBaseline(reason=…)` — a caller with no baseline data cannot omit the absolute half; it must
364
+ SAY the baseline is unknown, and that sentence is printed in the slot where the number would have been:
365
+
366
+ ```
367
+ risk: HR 0.74 (relative)
368
+ absolute: BASELINE RISK NOT ESTABLISHED — the source reports no control-arm event rate
369
+ NNT: n/a — cannot be computed without a baseline
370
+ ```
371
+
372
+ NNT is **computed** from absolute figures (`1/|ARC−ART|`, or `M/N` from an "N excess per M" figure),
373
+ never supplied as prose; when the inputs cannot support a finite NNT the result is a **named** reason,
374
+ never `Infinity` and never `0`.
375
+
376
+ **Honest scope (this one matters).** The typed path above is the guarantee. The report gate's
377
+ relative-risk scan (`check_report_evidence.py`) matches **formats, not meaning** — it is a secondary
378
+ belt over free prose that the constructor never sees, a novel phrasing will slip past it, and it may
379
+ never be cited as evidence that the property holds. The gate's own output says so.
152
380
 
153
381
  Changing the issuer or moving the source URL after signing invalidates the signature. The code signs the raw canonical message bytes; Ed25519 performs its own internal hashing. Do not pre-hash with SHA-512 before signing.
154
382
 
@@ -182,8 +410,60 @@ Reordering, substituting, editing, relabeling, or moving a signed fact fails ver
182
410
  |---|---|
183
411
  | `development` | Allows unsigned claims with clear labels and lower confidence. |
184
412
  | `moderate` | Prefers signed and cross-checked sources but may continue with labeled uncertainty. |
185
- | `strict` | Rejects plans with unsigned, invalid, unknown, revoked, or mismatched claims. |
186
- | `paranoid` | Same as strict, with stronger source redundancy expectations. |
413
+ | `strict` | Rejects plans with unsigned, invalid, unknown, revoked, or mismatched claims. Verified goals additionally require the two capability facts below. |
414
+ | `paranoid` | Same as strict, with stronger source redundancy expectations. Same capability-fact requirement. |
415
+
416
+ ### What strict and paranoid actually require — the promise this package does NOT keep by default
417
+
418
+ In `strict`/`paranoid` mode the planner auto-upgrades goals to their verified variants, and
419
+ the verified actions (`configure_trusted_issuers`, `web_search_verified`,
420
+ `fetch_signed_source`) are gated on two **deployment capability facts** that no action can
421
+ manufacture:
422
+
423
+ - `issuer_keys_available` — real Ed25519 issuer key material genuinely exists for the
424
+ configured issuers (a list of issuer domain strings is NOT key possession — core rule 3).
425
+ - `source_class_verified` — this class of source is capable of Ed25519-signed delivery at all.
426
+
427
+ Both are constructor arguments on `GOAPResearchPlanner`, **default `False`**:
428
+
429
+ ```python
430
+ GOAPResearchPlanner(verification_mode="strict",
431
+ issuer_keys_available=True, # only with real key material
432
+ source_class_verified=True) # only for signed-delivery sources
433
+ ```
434
+
435
+ **Stated plainly: for this package's real health sources (PubMed, PMC, DOI, WHO) neither
436
+ fact holds — none of them delivers Ed25519-signed content and no real issuer keys exist —
437
+ so `strict`/`paranoid` high-stakes PLANNING is honestly unreachable out of the box.**
438
+ `GOAPResearchPlanner(verification_mode="strict").plan(goal_type="high_stakes", ...)`
439
+ returns a `PlanNotFound` with verdict `GOAL_UNREACHABLE` naming the missing facts. Earlier
440
+ versions instead fabricated a maximum-confidence "verified" plan backed by nothing; that
441
+ was a defect, not a capability. Until a genuine signed-source integration exists, only
442
+ `development` and `moderate` are fully real for the default sources; `strict`/`paranoid`
443
+ become real the day a deployment can truthfully pass both flags.
444
+
445
+ ### Planner return contract (no more `None`)
446
+
447
+ `find_research_plan()` and `GOAPResearchPlanner.plan()` return either a `ResearchPlan` or
448
+ a first-class `PlanNotFound` — **never `None`**. `PlanNotFound.verdict` distinguishes:
449
+
450
+ - `PlanVerdict.GOAL_UNREACHABLE` — proven: no action sequence can ever satisfy the goal
451
+ (decided promptly by a reachability closure, independent of any budget). Retrying with a
452
+ bigger budget cannot help.
453
+ - `PlanVerdict.SEARCH_EXHAUSTED` — the iteration budget (or the opt-in `max_seconds`
454
+ wall-clock ceiling) ran out first: an honest "don't know". A higher `max_iterations` MAY
455
+ help — it is not guaranteed to, because the closure ignores the verification gate, which
456
+ can permanently reject every path in `strict`/`paranoid` mode.
457
+
458
+ Branch on `isinstance(result, ResearchPlan)`. Legacy truthiness checks (`if plan:`) stay
459
+ safe because `PlanNotFound` is falsy — but **identity checks are NOT safe**:
460
+ `plan is None` is now always `False`, so `if plan is not None:` reads a no-plan result as
461
+ success. Migrate any such check.
462
+
463
+ `max_iterations` defaults to an adaptive, measurement-calibrated budget (floors: 5,000 for
464
+ `development`/`moderate`, 100,000 for `strict`/`paranoid`). On the FAILURE path the full
465
+ budget is burned (seconds, not milliseconds, at the strict floor); latency-sensitive
466
+ callers should pass `max_seconds` (e.g. `planner.plan(..., max_seconds=2.0)`) to bound it.
187
467
 
188
468
  ## Confidence Formula
189
469
 
@@ -198,6 +478,19 @@ confidence = min(
198
478
  The **weakest link decides**, never the average. An `ISSUER_SIGNED` fact that nobody read is capped
199
479
  at `0.0` by its evidence class — which is the whole point of the second axis.
200
480
 
481
+ **Scope of the tier ceiling (read this before the next schema migration).** It applies **from schema
482
+ v2 onward** — a lower bound, never an equality. v1 alone is exempt, deliberately: a record keeps the
483
+ semantics it was created under. The condition was once written as `!= 2`, which means *"applies to
484
+ exactly v2"*; the two readings agreed only while 2 was the newest schema, so minting v3 switched the
485
+ third ceiling off in silence (MEASURED: the same unknown-domain fact scored `0.40` at schema 2 and
486
+ `1.0` at schema 3). Any future migration that touches schema dispatch must re-check every other
487
+ function branching on an exact version equality.
488
+
489
+ **Applicability is reported BESIDE confidence, never inside it.** There is no fourth ceiling for
490
+ `POPULATION_MATCH`: collapsing "was this altered / did anyone read it / what kind of source is it"
491
+ and "was it measured in people like this patient" into one number would make the interesting states
492
+ inexpressible.
493
+
201
494
  Invalid signatures are rejected with confidence `0.0`; they are never a recoverable `0.5` penalty.
202
495
 
203
496
  ## Research Workflow