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 +9 -0
- package/README.md +1 -1
- package/dist/index.js +33 -13
- package/package.json +1 -1
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
|
-

|
|
2
2
|

|
|
3
3
|
[_2026-Forever_Healthy-573D7D.svg)](https://forever-healthy.org)
|
|
4
4
|
[](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,
|
|
2147
|
+
function daysSince(date, now2 = /* @__PURE__ */ new Date()) {
|
|
2147
2148
|
const t = new Date(date).getTime();
|
|
2148
|
-
return Number.isFinite(t) ? (
|
|
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:
|
|
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
|
|
2541
|
-
while (calls.length && calls[0] <=
|
|
2542
|
-
if (calls.length >= PER_WINDOW) await new Promise((r) => setTimeout(r, calls[0] + WINDOW_MS -
|
|
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,
|
|
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 = (
|
|
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:
|
|
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:
|
|
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.
|
|
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",
|