@dzhechkov/p-replicator 1.5.17 → 1.5.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dz-manifest.json +51 -23
- package/README.md +5 -3
- package/package.json +1 -1
- package/sbom.json +106 -36
- package/templates/.claude/skills/goap-research-ed25519/SKILL.md +340 -47
- package/templates/.claude/skills/goap-research-ed25519/scripts/check_report_evidence.py +359 -3
- package/templates/.claude/skills/goap-research-ed25519/scripts/ed25519_verifier.py +386 -13
- package/templates/.claude/skills/goap-research-ed25519/scripts/fixture_legacy_v2_fact.json +23 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/fixtures_field_cases.json +133 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/goap_planner.py +314 -44
- package/templates/.claude/skills/goap-research-ed25519/scripts/learning_bridge.py +890 -303
- package/templates/.claude/skills/goap-research-ed25519/scripts/population_match.py +591 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/risk_statement.py +289 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_ed25519_verifier.py +57 -2
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_evidence_provenance.py +969 -344
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_goap_planner.py +420 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_population_match.py +544 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_risk_absolute.py +239 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_signature_v3.py +554 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_suite_completeness.py +90 -0
- package/tests/snapshot/baseline.json +24 -9
|
@@ -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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
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
|
|
74
|
-
python3 scripts/learning_bridge.py
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
**
|
|
88
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
is
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
151
|
-
|
|
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
|