fh-library 0.2.19 → 0.2.21

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/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # FH Library — Change Log
2
2
 
3
+ ### v0.2.21 — 2026-11-09
4
+
5
+ * **Cooldowns count from the moment:** a failed page, a not-found ID and a site's wall are remembered with the time, not the date — a bare date counted from midnight UTC, so a page that failed at 23:53 UTC was fetched up the whole ladder again 13 minutes later.
6
+
7
+ ### v0.2.20 — 2026-11-09
8
+
9
+ * **`library_verify` without the reference list:** the decision model no longer gets the paper's References / Bibliography section — other papers' titles, which state nothing this paper found and could pass a claim on a cited title; 22.7% less text per full text (96 full texts measured). The stored text and `library_read` keep it.
10
+ * **Less noise to the decision model:** a paper's citation line (authors, journal, PMID, DOI) is sent as its year only — 9.8% of an abstract.
11
+
3
12
  ### v0.2.19 — 2026-11-09
4
13
 
5
14
  * **Faster full texts:** a browser rung (or Bright Data) that met the same site's challenge twice in a row is skipped for that site for `FH_LIBRARY_RETRY_AFTER_DAYS` — the other rungs still run, a success clears it; the open-access lookups (Europe PMC, Crossref, Unpaywall, OpenAlex) start together instead of one after the other, the downloads stay in tier order.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- ![Version 0.1.3](https://img.shields.io/badge/Version-0.2.19-green.svg)
1
+ ![Version 0.1.3](https://img.shields.io/badge/Version-0.2.21-green.svg)
2
2
  ![License MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
3
3
  [![Forever Healthy](https://img.shields.io/badge/(c)_2026-Forever_Healthy-573D7D.svg)](https://forever-healthy.org)
4
4
  [![npm](https://img.shields.io/badge/npm-fh--library-cb3837.svg)](https://www.npmjs.com/package/fh-library)
package/dist/index.js CHANGED
@@ -1815,6 +1815,7 @@ function emptyStatus(requested, type, id, libraryDir) {
1815
1815
  };
1816
1816
  }
1817
1817
  var today = () => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1818
+ var now = () => (/* @__PURE__ */ new Date()).toISOString().slice(0, 19) + "Z";
1818
1819
  function headLine(r) {
1819
1820
  return [
1820
1821
  r.authors.join(", ") || null,
@@ -2143,9 +2144,9 @@ function refreshRecord(p, m, r, xml) {
2143
2144
  writeMeta(p, next);
2144
2145
  return next;
2145
2146
  }
2146
- function daysSince(date, now = /* @__PURE__ */ new Date()) {
2147
+ function daysSince(date, now2 = /* @__PURE__ */ new Date()) {
2147
2148
  const t = new Date(date).getTime();
2148
- return Number.isFinite(t) ? (now.getTime() - t) / 864e5 : Infinity;
2149
+ return Number.isFinite(t) ? (now2.getTime() - t) / 864e5 : Infinity;
2149
2150
  }
2150
2151
  var FUNDING_RE = /^[*_\s]*(funding|financial support|sources? of (financial )?support|support(ed by)?|grant support|this (work|study|research) was (supported|funded))\b[^:]*:?[*_\s]*(.*)$/i;
2151
2152
  var COI_RE = /^[*_\s]*(conflicts? of interest|competing interests?|disclosures?|financial disclosures?|declarations? of (competing |conflicting )?interests?|conflict[- ]of[- ]interest statement)\b[^:]*:?[*_\s]*(.*)$/i;
@@ -2348,14 +2349,14 @@ function recordRung(url, rung2, ok, reason = "", dir = FH_LIBRARY_DIR) {
2348
2349
  if (ok) {
2349
2350
  if (!all[rung2]) return;
2350
2351
  delete all[rung2];
2351
- } else all[rung2] = { streak: (all[rung2]?.streak ?? 0) + 1, date: today(), reason: why };
2352
+ } else all[rung2] = { streak: (all[rung2]?.streak ?? 0) + 1, date: now(), reason: why };
2352
2353
  try {
2353
2354
  mkdirSync3(join5(dir, "walls"), { recursive: true });
2354
2355
  writeAtomic(file(dir, h), JSON.stringify(all) + "\n");
2355
2356
  } catch {
2356
2357
  }
2357
2358
  }
2358
- var wallNote = (r) => `${r.reason} ${r.streak}\xD7 on this host, last ${r.date}`;
2359
+ var wallNote = (r) => `${r.reason} ${r.streak}\xD7 on this host, last ${r.date.slice(0, 10)}`;
2359
2360
 
2360
2361
  // src/tiers/tier.ts
2361
2362
  var looked = (t, p, ctx) => ctx.lookups?.get(t.name) ?? t.lookup(p);
@@ -2537,9 +2538,9 @@ var MAX_WAIT_MS = 65e3;
2537
2538
  var TIMEOUT_MS3 = 1e4;
2538
2539
  var calls = [];
2539
2540
  async function pace() {
2540
- const now = Date.now();
2541
- while (calls.length && calls[0] <= now - WINDOW_MS) calls.shift();
2542
- if (calls.length >= PER_WINDOW) await new Promise((r) => setTimeout(r, calls[0] + WINDOW_MS - now + 200));
2541
+ const now2 = Date.now();
2542
+ while (calls.length && calls[0] <= now2 - WINDOW_MS) calls.shift();
2543
+ if (calls.length >= PER_WINDOW) await new Promise((r) => setTimeout(r, calls[0] + WINDOW_MS - now2 + 200));
2543
2544
  calls.push(Date.now());
2544
2545
  }
2545
2546
  async function coreFetch(url, key) {
@@ -3011,13 +3012,13 @@ function parseAttempt(a) {
3011
3012
  return m ? { tier: m[1], reason: m[2], date: m[3] ?? null } : null;
3012
3013
  }
3013
3014
  var attemptLabel = (m, date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) => `${m.tier} (${m.reason}) ${date}`;
3014
- function shouldSkip(attempts, tier, retryAfterDays, now = /* @__PURE__ */ new Date(), retryBlocked = false) {
3015
+ function shouldSkip(attempts, tier, retryAfterDays, now2 = /* @__PURE__ */ new Date(), retryBlocked = false) {
3015
3016
  for (const a of attempts) {
3016
3017
  const p = parseAttempt(a);
3017
3018
  if (!p || p.tier !== tier) continue;
3018
3019
  if (isPermanent(p.reason, retryBlocked)) return true;
3019
3020
  if (!p.date) return false;
3020
- const age = (now.getTime() - new Date(p.date).getTime()) / 864e5;
3021
+ const age = (now2.getTime() - new Date(p.date).getTime()) / 864e5;
3021
3022
  return age < retryAfterDays;
3022
3023
  }
3023
3024
  return false;
@@ -3113,7 +3114,26 @@ function isLocal(baseUrl) {
3113
3114
  }
3114
3115
  }
3115
3116
  function cleanMarkdown(md) {
3116
- return md.replace(/!\[[^\]]*\]\([^)]*\)/g, "").replace(/\]\([^)]*\)/g, "]").replace(/^\s*#+\s*(see more about|more for you|back to top)\b.*$/gim, "").replace(/\n{3,}/g, "\n\n");
3117
+ return paperYearOnly(dropReferences(md)).replace(/!\[[^\]]*\]\([^)]*\)/g, "").replace(/\]\([^)]*\)/g, "]").replace(/^\s*#+\s*(see more about|more for you|back to top)\b.*$/gim, "").replace(/\n{3,}/g, "\n\n");
3118
+ }
3119
+ var REF_HEAD = /^(#{1,6})\s*\**\s*(references|bibliography|literature cited|reference list|works cited|cited literature)\b[^\n]*$|^\**(references|bibliography|literature cited)\**:?[ \t]*$/gim;
3120
+ function dropReferences(md) {
3121
+ let out = md;
3122
+ for (; ; ) {
3123
+ REF_HEAD.lastIndex = 0;
3124
+ const m = REF_HEAD.exec(out);
3125
+ if (!m) return out;
3126
+ const level = m[1]?.length ?? 6;
3127
+ const rest = out.slice(m.index + m[0].length);
3128
+ const next = new RegExp(`^#{1,${level}}\\s`, "m").exec(rest);
3129
+ out = out.slice(0, m.index) + (next ? rest.slice(next.index) : "");
3130
+ }
3131
+ }
3132
+ function paperYearOnly(md) {
3133
+ return md.replace(/^(#[^\n]*\n\n)([^\n]*· (?:PMID \d+|DOI 10\.)[^\n]*)$/m, (_, title, line) => {
3134
+ const year = / · (\d{4})(?: · |$)/.exec(line)?.[1];
3135
+ return year ? `${title}${year}` : title.trimEnd();
3136
+ });
3117
3137
  }
3118
3138
  function windows(md, max) {
3119
3139
  const out = [];
@@ -3982,7 +4002,7 @@ async function libraryFetch(libraryDir, requested, args, converter, logs = {}, f
3982
4002
  if (!run) {
3983
4003
  run = (async () => {
3984
4004
  const r = await fetchImpl(url, logs.onTry, logs.onAlert);
3985
- const base = { url, fetched: today(), converted_by: converter };
4005
+ const base = { url, fetched: now(), converted_by: converter };
3986
4006
  if (!r.ok) {
3987
4007
  const error = r.first && /^HTTP \d{3}/.test(r.first) ? r.first : r.reason;
3988
4008
  writeUrlMeta(p, { ...base, final_url: r.url ?? url, title: null, format: "failed", content_type: "", level: "title", retrieved_by: r.via, error, files: [`${p.entry}-meta.md`] });
@@ -4475,14 +4495,14 @@ function freshMissing(p, retryDays) {
4475
4495
  return m && daysSince(m.checked) < retryDays ? m : null;
4476
4496
  }
4477
4497
  function rememberMissing(p, reason) {
4478
- const m = { checked: today(), reason };
4498
+ const m = { checked: now(), reason };
4479
4499
  mkdirSync10(p.entryDir, { recursive: true });
4480
4500
  writeAtomic(missingFile(p), `checked: ${m.checked}
4481
4501
  reason: ${m.reason}
4482
4502
  `);
4483
4503
  return m;
4484
4504
  }
4485
- var missingNote = (m, retryDays) => `${m.reason} (checked ${m.checked}; asked again after ${retryDays} day${retryDays === 1 ? "" : "s"})`;
4505
+ var missingNote = (m, retryDays) => `${m.reason} (checked ${m.checked.slice(0, 10)}; asked again after ${retryDays} day${retryDays === 1 ? "" : "s"})`;
4486
4506
 
4487
4507
  // src/pubmedbatch.ts
4488
4508
  var MAX_PMIDS = 200;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fh-library",
3
- "version": "0.2.19",
3
+ "version": "0.2.21",
4
4
  "mcpName": "io.github.forever-healthy/fh-library",
5
5
  "description": "MCP Server that builds a local Markdown library of the scientific papers cited by evidence reviews — fetches metadata, abstract and full text per PubMed ID through a configurable cascade of retrieval services",
6
6
  "type": "module",