opencode-bioresearcher 1.10.0 → 1.11.1
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/connector-meta.json +1 -1
- package/package.json +1 -1
- package/skills/bioresearcher-deep-research/SKILL.md +99 -183
- package/skills/bioresearcher-deep-research/references/citations.md +4 -3
- package/skills/bioresearcher-deep-research/references/ensembl-pdb.md +2 -0
- package/skills/bioresearcher-deep-research/references/tool-selection.md +2 -0
- package/skills/bioresearcher-deep-research/references/worker-protocol.md +53 -15
- package/skills/bioresearcher-deep-research/scripts/evidence-ledger.py +237 -34
- package/skills/bioresearcher-deep-research/scripts/markdown-to-html.py +17 -8
- package/skills/bioresearcher-deep-research/scripts/vet-references.py +181 -18
|
@@ -16,6 +16,7 @@ Zero external dependencies (pure Python standard library).
|
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
18
|
import argparse
|
|
19
|
+
import difflib
|
|
19
20
|
import html
|
|
20
21
|
import json
|
|
21
22
|
import os
|
|
@@ -37,6 +38,26 @@ REF_LINE_RE = re.compile(
|
|
|
37
38
|
PMID_RE = re.compile(r'\bPMID[:\s]+\[?(\d{4,9})\]?', re.IGNORECASE)
|
|
38
39
|
DOI_RE = re.compile(r'(?:DOI[:\s]+|https?://(?:dx\.)?doi\.org/)?\b(10\.\d{4,9}/[^\s\]\)]+)', re.IGNORECASE)
|
|
39
40
|
|
|
41
|
+
TAIL_TOKEN_RE = re.compile(
|
|
42
|
+
r'(?:'
|
|
43
|
+
r'\[?\b(?:DOI|PMID|PMCID)\s*[:=\s]\s*[^\]\s]+\]?'
|
|
44
|
+
r'|https?://(?:dx\.)?doi\.org/\S+'
|
|
45
|
+
r'|https?://pubmed\.ncbi\.nlm\.nih\.gov/\d+/?'
|
|
46
|
+
r')\.?',
|
|
47
|
+
re.IGNORECASE,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
LOCATOR_AT_END_RE = re.compile(
|
|
51
|
+
r'(?<=\.\s)'
|
|
52
|
+
r'('
|
|
53
|
+
r'\b(?:19\d\d|20\d\d)\b'
|
|
54
|
+
r'(?:\s+[A-Za-z]{3,9}(?:\s+\d{1,2})?)?'
|
|
55
|
+
r'(?:;\s*[\w\s\(\)\:\.\-\[\]\/]+)?'
|
|
56
|
+
r')'
|
|
57
|
+
r'\.?\s*$',
|
|
58
|
+
re.IGNORECASE,
|
|
59
|
+
)
|
|
60
|
+
|
|
40
61
|
# Structural-audit patterns
|
|
41
62
|
CODE_BLOCK_RE = re.compile(r"(?ms)^(?:```|~~~)[^\n]*\n.*?^(?:```|~~~)[ \t]*$")
|
|
42
63
|
INTEXT_RE = re.compile(r"\[(\d{1,3}(?:\s*[,\u2013\-]\s*\d{1,3})*)\]")
|
|
@@ -186,6 +207,44 @@ def compute_token_overlap(t1: str, t2: str) -> float:
|
|
|
186
207
|
return len(toks1 & toks2) / min(len(toks1), len(toks2))
|
|
187
208
|
|
|
188
209
|
|
|
210
|
+
def split_citation_tail(text: str) -> tuple[str, str]:
|
|
211
|
+
"""Split citation into (pre_tail, tail) anchoring on trailing identifier tokens."""
|
|
212
|
+
matches = list(TAIL_TOKEN_RE.finditer(text))
|
|
213
|
+
if not matches:
|
|
214
|
+
return text.rstrip(), ""
|
|
215
|
+
tail_start = len(text)
|
|
216
|
+
for m in reversed(matches):
|
|
217
|
+
intervening = text[m.end():tail_start].strip(". \t\\[\\]\\(\\);,")
|
|
218
|
+
if intervening:
|
|
219
|
+
break
|
|
220
|
+
tail_start = m.start()
|
|
221
|
+
if tail_start >= len(text):
|
|
222
|
+
return text.rstrip(), ""
|
|
223
|
+
return text[:tail_start].rstrip(), text[tail_start:].strip()
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def validate_citation_invariants(original: str, enhanced: str) -> None:
|
|
227
|
+
"""Assert invariants to prevent locator/DOI corruption or deletion."""
|
|
228
|
+
m_orig_pmid = PMID_RE.search(original)
|
|
229
|
+
m_enh_pmid = PMID_RE.search(enhanced)
|
|
230
|
+
if m_orig_pmid:
|
|
231
|
+
assert m_enh_pmid and m_enh_pmid.group(1) == m_orig_pmid.group(1), (
|
|
232
|
+
f"PMID corrupted or deleted: {m_orig_pmid.group(1)} vs {m_enh_pmid.group(1) if m_enh_pmid else 'None'}"
|
|
233
|
+
)
|
|
234
|
+
m_orig_doi = DOI_RE.search(original)
|
|
235
|
+
m_enh_doi = DOI_RE.search(enhanced)
|
|
236
|
+
if m_orig_doi:
|
|
237
|
+
orig_doi = m_orig_doi.group(1).lower().rstrip('.')
|
|
238
|
+
assert m_enh_doi, f"Original DOI lost: {orig_doi}"
|
|
239
|
+
enh_doi = m_enh_doi.group(1).lower().rstrip('.')
|
|
240
|
+
assert orig_doi == enh_doi, f"Original DOI mutated: {orig_doi} -> {enh_doi}"
|
|
241
|
+
if m_enh_doi:
|
|
242
|
+
doi_val = m_enh_doi.group(1).rstrip(';.,')
|
|
243
|
+
assert not re.search(r'\(\d+\):', doi_val), f"DOI corrupted with issue/page locator: {doi_val}"
|
|
244
|
+
assert re.match(r'^10\.\d{4,9}/[^\s\]\)]+$', doi_val), f"DOI token structurally invalid: {doi_val}"
|
|
245
|
+
assert ".." not in enhanced.replace("...", ""), f"Double period introduced: {enhanced}"
|
|
246
|
+
|
|
247
|
+
|
|
189
248
|
def enhance_citation(original_text: str, doc: dict) -> tuple[str, list[str]]:
|
|
190
249
|
"""Compare and enhance citation string against NCBI document summary."""
|
|
191
250
|
changes: list[str] = []
|
|
@@ -197,8 +256,6 @@ def enhance_citation(original_text: str, doc: dict) -> tuple[str, list[str]]:
|
|
|
197
256
|
if overlap < 0.30 and len(ncbi_title) > 20:
|
|
198
257
|
return original_text, [f"WARNING: Title mismatch (overlap {overlap:.2f}). Expected '{ncbi_title[:40]}...'"]
|
|
199
258
|
|
|
200
|
-
updated = original_text.strip()
|
|
201
|
-
|
|
202
259
|
# Extract DOI from NCBI
|
|
203
260
|
ncbi_doi = ""
|
|
204
261
|
for aid in doc.get("articleids", []):
|
|
@@ -206,28 +263,37 @@ def enhance_citation(original_text: str, doc: dict) -> tuple[str, list[str]]:
|
|
|
206
263
|
ncbi_doi = str(aid.get("value", "")).strip().rstrip('.')
|
|
207
264
|
break
|
|
208
265
|
|
|
209
|
-
|
|
210
|
-
|
|
266
|
+
pre_tail, tail = split_citation_tail(original_text.strip())
|
|
267
|
+
|
|
268
|
+
# Update publication locator strictly in pre_tail (preceding DOI/PMID)
|
|
211
269
|
if pub_loc:
|
|
212
|
-
|
|
213
|
-
r'(\b(?:19\d\d|20\d\d)\b(?:\s*;\s*[\w\(\)\:\.\-\s]+?)?)\.?(\s+PMID:)',
|
|
214
|
-
re.IGNORECASE,
|
|
215
|
-
)
|
|
216
|
-
m = loc_pattern.search(updated)
|
|
270
|
+
m = LOCATOR_AT_END_RE.search(pre_tail)
|
|
217
271
|
if m:
|
|
218
|
-
current_loc = m.group(1).
|
|
219
|
-
pmid_lead = m.group(2)
|
|
272
|
+
current_loc = m.group(1).rstrip('. \t')
|
|
220
273
|
if current_loc != pub_loc:
|
|
221
|
-
|
|
274
|
+
pre_tail = pre_tail[:m.start(1)] + pub_loc + "."
|
|
222
275
|
changes.append(f"Updated publication info -> '{pub_loc}'")
|
|
276
|
+
else:
|
|
277
|
+
pre_tail = pre_tail.rstrip('.') + f". {pub_loc}."
|
|
278
|
+
changes.append(f"Added publication info -> '{pub_loc}'")
|
|
223
279
|
|
|
224
280
|
# Add missing DOI if available from NCBI and not present in citation
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
281
|
+
full_current = f"{pre_tail} {tail}".strip()
|
|
282
|
+
if ncbi_doi and ncbi_doi.lower() not in full_current.lower() and not DOI_RE.search(full_current):
|
|
283
|
+
doi_part = f"DOI: {ncbi_doi}."
|
|
284
|
+
if tail:
|
|
285
|
+
tail = f"{doi_part} {tail}"
|
|
286
|
+
else:
|
|
287
|
+
tail = doi_part
|
|
229
288
|
changes.append(f"Added DOI -> '{ncbi_doi}'")
|
|
230
289
|
|
|
290
|
+
updated = f"{pre_tail} {tail}".strip() if tail else pre_tail.strip()
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
validate_citation_invariants(original_text, updated)
|
|
294
|
+
except AssertionError as e:
|
|
295
|
+
return original_text, [f"WARNING: Invariant violation: {e}"]
|
|
296
|
+
|
|
231
297
|
return updated, changes
|
|
232
298
|
|
|
233
299
|
|
|
@@ -302,8 +368,24 @@ def vet_references(report_path: Path, apply_changes: bool = False) -> dict:
|
|
|
302
368
|
"suggested": enhanced_body,
|
|
303
369
|
})
|
|
304
370
|
|
|
371
|
+
new_text = text
|
|
305
372
|
if apply_changes and total_updated > 0:
|
|
306
373
|
new_text = text[:sec_match.start(1)] + new_section_text + text[sec_match.end(1):]
|
|
374
|
+
# Layer 2 invariant: assert structural integrity before disk write
|
|
375
|
+
post_audit = audit_structure(new_text)
|
|
376
|
+
if not post_audit["ok"]:
|
|
377
|
+
return {
|
|
378
|
+
"status": "error",
|
|
379
|
+
"message": f"Post-apply structural audit failed: {'; '.join(post_audit['errors'])}",
|
|
380
|
+
"total_citations": len(citations),
|
|
381
|
+
"pmid_citations": len(pmids_to_fetch),
|
|
382
|
+
"updated_count": 0,
|
|
383
|
+
"suggestions": [],
|
|
384
|
+
"warnings": list(reversed(warnings)),
|
|
385
|
+
"applied": False,
|
|
386
|
+
"original_text": text,
|
|
387
|
+
"new_text": text,
|
|
388
|
+
}
|
|
307
389
|
tmp_path = report_path.with_suffix(".tmp")
|
|
308
390
|
tmp_path.write_text(new_text, encoding="utf-8")
|
|
309
391
|
os.replace(tmp_path, report_path)
|
|
@@ -316,6 +398,8 @@ def vet_references(report_path: Path, apply_changes: bool = False) -> dict:
|
|
|
316
398
|
"suggestions": list(reversed(suggestions)),
|
|
317
399
|
"warnings": list(reversed(warnings)),
|
|
318
400
|
"applied": apply_changes and total_updated > 0,
|
|
401
|
+
"original_text": text,
|
|
402
|
+
"new_text": new_text,
|
|
319
403
|
}
|
|
320
404
|
|
|
321
405
|
|
|
@@ -355,7 +439,59 @@ def selftest() -> int:
|
|
|
355
439
|
print(f"FAIL {name}: expected ok={expect_ok}, got ok={got['ok']} errors={got['errors']}")
|
|
356
440
|
else:
|
|
357
441
|
print(f"PASS {name}")
|
|
358
|
-
|
|
442
|
+
|
|
443
|
+
# ---- Unit tests: enhance_citation & locator/tail isolation ----
|
|
444
|
+
mock_doc = {
|
|
445
|
+
"title": "Synthesis of conotoxin peptides and derivatives.",
|
|
446
|
+
"pubdate": "1979 Aug",
|
|
447
|
+
"volume": "27",
|
|
448
|
+
"issue": "8",
|
|
449
|
+
"pages": "1942-4",
|
|
450
|
+
"articleids": [{"idtype": "doi", "value": "10.1248/cpb.27.1942"}],
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
# Test 1: Repro defect - DOI ending in year-like digits (1942) must NEVER be spliced
|
|
454
|
+
repro_orig = "Takahashi M. Synthesis of conotoxin peptides. Chem Pharm Bull (Tokyo). 1979. DOI: 10.1248/cpb.27.1942. PMID: 540362."
|
|
455
|
+
repro_enh, repro_changes = enhance_citation(repro_orig, mock_doc)
|
|
456
|
+
if "10.1248/cpb.27.1942." not in repro_enh or "1979;27(8):1942-4." not in repro_enh:
|
|
457
|
+
failures += 1
|
|
458
|
+
print(f"FAIL repro-doi-tail-splicing: expected clean DOI preservation and locator update, got: {repro_enh}")
|
|
459
|
+
elif ";27(8):1942-4." in repro_enh.split("DOI:")[1]:
|
|
460
|
+
failures += 1
|
|
461
|
+
print(f"FAIL repro-doi-tail-splicing: locator was spliced into DOI! {repro_enh}")
|
|
462
|
+
else:
|
|
463
|
+
print("PASS repro-doi-tail-splicing: DOI preserved verbatim, locator updated before DOI")
|
|
464
|
+
|
|
465
|
+
# Test 2: Locator with internal whitespace (must not splice into 4-digit page numbers)
|
|
466
|
+
space_orig = "Takahashi M. Synthesis of conotoxin peptides. Chem Pharm Bull (Tokyo). 1979; 27(8): 1942-1944. DOI: 10.1248/cpb.27.1942. PMID: 540362."
|
|
467
|
+
space_enh, _ = enhance_citation(space_orig, mock_doc)
|
|
468
|
+
if "10.1248/cpb.27.1942." not in space_enh or "1942-1979" in space_enh:
|
|
469
|
+
failures += 1
|
|
470
|
+
print(f"FAIL locator-whitespace-handling: corrupted page/locator: {space_enh}")
|
|
471
|
+
else:
|
|
472
|
+
print("PASS locator-whitespace-handling: internal spaces handled cleanly")
|
|
473
|
+
|
|
474
|
+
# Test 3: Citation without DOI gets DOI added before PMID
|
|
475
|
+
no_doi_orig = "Takahashi M. Synthesis of conotoxin peptides. Chem Pharm Bull (Tokyo). 1979;27(8):1942-4. PMID: 540362."
|
|
476
|
+
no_doi_enh, no_doi_chg = enhance_citation(no_doi_orig, mock_doc)
|
|
477
|
+
if "DOI: 10.1248/cpb.27.1942. PMID: 540362." not in no_doi_enh:
|
|
478
|
+
failures += 1
|
|
479
|
+
print(f"FAIL add-missing-doi-before-pmid: got {no_doi_enh}")
|
|
480
|
+
else:
|
|
481
|
+
print("PASS add-missing-doi-before-pmid: DOI inserted before PMID")
|
|
482
|
+
|
|
483
|
+
# Test 4: Invariant enforcement rejects corrupted DOI modification
|
|
484
|
+
inv_orig = "Takahashi M. Title. Journal. 2020. DOI: 10.1000/182. PMID: 12345."
|
|
485
|
+
inv_bad = "Takahashi M. Title. Journal. 2020;1(2):3. DOI: 10.1000/182;1(2):3. PMID: 12345."
|
|
486
|
+
try:
|
|
487
|
+
validate_citation_invariants(inv_orig, inv_bad)
|
|
488
|
+
failures += 1
|
|
489
|
+
print("FAIL invariant-validation: failed to catch corrupted DOI with semicolon")
|
|
490
|
+
except AssertionError:
|
|
491
|
+
print("PASS invariant-validation: correctly caught corrupted DOI")
|
|
492
|
+
|
|
493
|
+
total_groups = len(cases) + 4
|
|
494
|
+
print(f"[vet-references] selftest: {total_groups - failures}/{total_groups} group(s) passed"
|
|
359
495
|
+ (" — FAILURES PRESENT" if failures else ""))
|
|
360
496
|
return 1 if failures else 0
|
|
361
497
|
|
|
@@ -368,6 +504,7 @@ def main():
|
|
|
368
504
|
parser.add_argument("report", help="Path to markdown research report (e.g. final_report.md)")
|
|
369
505
|
parser.add_argument("--apply", action="store_true", help="Apply verified citation updates in-place")
|
|
370
506
|
parser.add_argument("--json", action="store_true", help="Output results in structured JSON")
|
|
507
|
+
parser.add_argument("--diff", action="store_true", help="Print unified diff of applied changes to stdout")
|
|
371
508
|
parser.add_argument("--timeout", type=float, default=15.0, help="HTTP timeout in seconds (default 15)")
|
|
372
509
|
args = parser.parse_args()
|
|
373
510
|
|
|
@@ -402,7 +539,20 @@ def main():
|
|
|
402
539
|
sys.stderr.write(f"[vet-references] Unexpected failure: {e}. Preserving original citations.\n")
|
|
403
540
|
sys.exit(0)
|
|
404
541
|
|
|
542
|
+
if res.get("status") == "error":
|
|
543
|
+
sys.stderr.write(f"[vet-references] error: {res.get('message', 'Unknown vetting error')}\n")
|
|
544
|
+
if args.json:
|
|
545
|
+
print(json.dumps({"audit": audit, **res}, indent=2))
|
|
546
|
+
sys.exit(1)
|
|
547
|
+
|
|
405
548
|
if args.json:
|
|
549
|
+
if args.diff and res.get("applied"):
|
|
550
|
+
res["diff"] = "".join(difflib.unified_diff(
|
|
551
|
+
res["original_text"].splitlines(keepends=True),
|
|
552
|
+
res["new_text"].splitlines(keepends=True),
|
|
553
|
+
fromfile=f"{report_path} (original)",
|
|
554
|
+
tofile=f"{report_path} (vetted)",
|
|
555
|
+
))
|
|
406
556
|
print(json.dumps({"audit": audit, **res}, indent=2))
|
|
407
557
|
return
|
|
408
558
|
|
|
@@ -420,8 +570,21 @@ def main():
|
|
|
420
570
|
for s in res.get("suggestions", []):
|
|
421
571
|
chg = ", ".join(s["changes"])
|
|
422
572
|
print(f" - [{s['index']}] PMID {s['pmid']}: {chg}")
|
|
423
|
-
if
|
|
573
|
+
if args.apply:
|
|
574
|
+
print(f" - OLD: {s['original']}")
|
|
575
|
+
print(f" + NEW: {s['suggested']}")
|
|
576
|
+
else:
|
|
424
577
|
print(f" Suggested: {s['suggested']}")
|
|
578
|
+
if args.diff and res.get("applied"):
|
|
579
|
+
diff_lines = list(difflib.unified_diff(
|
|
580
|
+
res["original_text"].splitlines(keepends=True),
|
|
581
|
+
res["new_text"].splitlines(keepends=True),
|
|
582
|
+
fromfile=f"{report_path} (original)",
|
|
583
|
+
tofile=f"{report_path} (vetted)",
|
|
584
|
+
))
|
|
585
|
+
if diff_lines:
|
|
586
|
+
print("[vet-references] Unified diff:")
|
|
587
|
+
sys.stdout.writelines(diff_lines)
|
|
425
588
|
elif res.get("warnings"):
|
|
426
589
|
print("[vet-references] Citations processed with warnings; check mismatched records above.")
|
|
427
590
|
else:
|