strom-research 1.14.0 → 1.15.0-beta.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/assets/lang/cs.json +39 -39
- package/dist/brief/sheet.js +4 -3
- package/dist/commands/connectors.js +72 -16
- package/dist/commands/media.js +3 -1
- package/dist/commands/read.js +6 -2
- package/dist/commands/setup.js +3 -1
- package/dist/commands/sources.js +29 -1
- package/dist/core/config.js +12 -0
- package/dist/core/samebook.js +42 -0
- package/dist/core/schema.js +1 -0
- package/dist/core/tree.js +1 -1
- package/package.json +4 -1
package/assets/lang/cs.json
CHANGED
|
@@ -184,7 +184,7 @@
|
|
|
184
184
|
"ui.tune.q.window": "Odpovědět na otázku ke čtení snímků – „{text}“: {label}?",
|
|
185
185
|
"ui.tune.q.kept": "✓ Odpověď uložena: {label}.",
|
|
186
186
|
"ui.tune.q.kept.tasks": "✓ Přidané úkoly: {tasks} – ve frontě s prioritou práce, ze které vzešly.",
|
|
187
|
-
"ui.tune.q.kept.notasks": "Teď není koho hledat: žádný úkol nepřibyl (každá osoba už jeden má, nebo není na kraji stromu ani v
|
|
187
|
+
"ui.tune.q.kept.notasks": "Teď není koho hledat: žádný úkol nepřibyl (každá osoba už jeden má, nebo není na kraji stromu ani v rozpracované linii).",
|
|
188
188
|
"ui.tune.q.kept.calibrate": "Kalibraci spouští člověk: strom media calibrate (cena se řekne předem).",
|
|
189
189
|
"ui.tune.q.kept.reset": "Vrátí se příkazem: {command}",
|
|
190
190
|
"ui.tune.q.kept.vision": "✓ Skeny se odteď čtou modelem {model} (model.vision).",
|
|
@@ -197,7 +197,7 @@
|
|
|
197
197
|
"ui.tune.q.c.never": "Nikdy",
|
|
198
198
|
"ui.tune.q.weak": "Kniha {book} ({title}): záporů na slabých skenech {n}, teď je tu ostřejší kopie. Hledat znovu?",
|
|
199
199
|
"ui.tune.q.weak.why": "{n} z {of} záporů vzniklo na pohledech zvětšených nad detail skenů; ostřejší kopie ({copies}) mají až {gain}× víc detailu.",
|
|
200
|
-
"ui.tune.q.c.edge": "Ano, u osob na kraji stromu nebo v
|
|
200
|
+
"ui.tune.q.c.edge": "Ano, u osob na kraji stromu nebo v rozpracované linii (úkolů asi {tasks})",
|
|
201
201
|
"ui.tune.q.c.brief": "Jen uvést v zadání agenta",
|
|
202
202
|
"ui.tune.q.c.no": "Ne",
|
|
203
203
|
"ui.tune.q.sharper": "Kniha {book} ({title}) se čte hůř než ostatní a její archiv dává ostřejší výřezy. Stáhnout ostřejší výřezy nejistých snímků?",
|
|
@@ -923,11 +923,11 @@
|
|
|
923
923
|
"ui.stats.cost.readers": ", z toho čtenáři skenů {cost}",
|
|
924
924
|
"ui.stats.stories": "Vyprávění: {n}",
|
|
925
925
|
"ui.status.data": "Osoby: {persons} · rodiny: {families}",
|
|
926
|
-
"ui.status.directions": " ·
|
|
926
|
+
"ui.status.directions": " · linie výzkumu: {n}",
|
|
927
927
|
"ui.status.check.ok": "Kontrola: v pořádku",
|
|
928
928
|
"ui.status.check.errors": "Kontrola: chyb {n} → strom check",
|
|
929
929
|
"ui.status.check.warnings": " · upozornění {n}",
|
|
930
|
-
"ui.status.dirs": "
|
|
930
|
+
"ui.status.dirs": "Linie výzkumu:",
|
|
931
931
|
"ui.status.recent": "Naposledy:",
|
|
932
932
|
"ui.help.cmd.menu": "Průvodní menu: vše, co člověk se stromem dělá, podle čísel",
|
|
933
933
|
"ui.help.cmd.status": "Rodokmen v kostce: data, kontrola, co čeká, co se naposledy změnilo, místo na disku",
|
|
@@ -945,10 +945,10 @@
|
|
|
945
945
|
"ui.help.cmd.chat": "Rozhovor s agentem o výzkumu",
|
|
946
946
|
"ui.help.cmd.run": "Nechat agenta pracovat samotného na dalších úkolech",
|
|
947
947
|
"ui.help.cmd.session.finish": "Požádat pracujícího agenta, ať zapíše, co našel, a skončí",
|
|
948
|
-
"ui.help.cmd.research.new": "
|
|
949
|
-
"ui.help.cmd.research.pause": "Pozastavit
|
|
950
|
-
"ui.help.cmd.research.resume": "Znovu se pustit do
|
|
951
|
-
"ui.help.cmd.research.done": "Ukončit
|
|
948
|
+
"ui.help.cmd.research.new": "Nová linie: předkové nebo potomci někoho, nebo jedna otázka",
|
|
949
|
+
"ui.help.cmd.research.pause": "Pozastavit linii: její úkoly počkají",
|
|
950
|
+
"ui.help.cmd.research.resume": "Znovu se pustit do pozastavené linie",
|
|
951
|
+
"ui.help.cmd.research.done": "Ukončit linii (nic se nesmaže)",
|
|
952
952
|
"ui.help.cmd.review": "Revize osoby: prověřit a doplnit, co o ní víme",
|
|
953
953
|
"ui.help.cmd.intake": "Přidat podklady od rodiny: dokumenty, fotky, rodokmen, co je známo",
|
|
954
954
|
"ui.help.cmd.sync": "Načíst úpravy z aplikace Strom nebo z jiného rodokmenu (nejdřív ukáže, zapíše s --apply)",
|
|
@@ -1213,7 +1213,7 @@
|
|
|
1213
1213
|
"ui.menu.app.tip.archive": "Tip: výzkum jde prohlédnout jako rodokmen v aplikaci Strom – volba {key}.",
|
|
1214
1214
|
"ui.dry.done": "Nanečisto (--dry-run): nic se nezapsalo – řádky výše říkají, co by se stalo.",
|
|
1215
1215
|
"ui.trees.persons": "osob: {n}",
|
|
1216
|
-
"ui.trees.researches": "
|
|
1216
|
+
"ui.trees.researches": "linií výzkumu: {n}",
|
|
1217
1217
|
"ui.trees.none": "zatím žádný rodokmen → strom init \"<název>\"",
|
|
1218
1218
|
"ui.trees.use": "teď se pracuje na „{name}“ ({root})",
|
|
1219
1219
|
"ui.error.live.not-started": "most se nespustil",
|
|
@@ -1342,11 +1342,11 @@
|
|
|
1342
1342
|
"ui.consent.hook.on": "Nechat hook „{name}“, aby se dozvídal všechno, co se uloží do výzkumů? Je to program na tomto počítači a udělá s tím, k čemu byl napsán.",
|
|
1343
1343
|
"ui.consent.hook.off": "Přestat hooku „{name}“ hlásit, co se ukládá?",
|
|
1344
1344
|
"ui.run.others": "⚠ V tomto rodokmenu už pracuje jiný běh (od {since}): tento si vezme jiné úkoly – náklady se sčítají.",
|
|
1345
|
-
"ui.menu.more": "Rozšířit výzkum: podklady,
|
|
1346
|
-
"ui.menu.more.app": "Rozšířit výzkum: podklady,
|
|
1345
|
+
"ui.menu.more": "Rozšířit výzkum: podklady, nová linie, revize osoby, jiný rodokmen",
|
|
1346
|
+
"ui.menu.more.app": "Rozšířit výzkum: podklady, nová linie, revize osoby, úpravy z aplikace Strom",
|
|
1347
1347
|
"ui.more.title": "Rozšířit výzkum",
|
|
1348
1348
|
"ui.more.intake": "Přidat podklady: dokumenty, fotky, rodokmen z jiného programu, co je známo",
|
|
1349
|
-
"ui.more.research": "
|
|
1349
|
+
"ui.more.research": "Nová linie výzkumu: předkové nebo potomci někoho dalšího, nebo jedna otázka",
|
|
1350
1350
|
"ui.more.sync": "Načíst úpravy z aplikace Strom nebo z jiného rodokmenu (.ged, .json)",
|
|
1351
1351
|
"ui.more.undo": "Vrátit poslední poslání z aplikace Strom ({when})",
|
|
1352
1352
|
"ui.more.undo.what": "Vrátit, co výzkum převzal z aplikace Strom {when}:",
|
|
@@ -1373,20 +1373,20 @@
|
|
|
1373
1373
|
"ui.link.what.chat.person": "rozhovor s agentem ({agent}) o osobě {person}.",
|
|
1374
1374
|
"ui.link.chat.say": "Podívejme se na osobu {person}: řekni mi krátce, co o ní výzkum ví, a zeptej se mě, co chci zjistit.",
|
|
1375
1375
|
"ui.link.what.review.person": "prověřit znovu osobu {person}.",
|
|
1376
|
-
"ui.link.what.direction.pause": "pozastavit
|
|
1377
|
-
"ui.link.what.direction.done": "ukončit
|
|
1378
|
-
"ui.link.what.direction.resume": "znovu spustit
|
|
1379
|
-
"ui.link.direction.none": "
|
|
1380
|
-
"ui.link.direction.cannot": "
|
|
1381
|
-
"ui.link.direction.why": "Proč? (pár slov, zůstane u
|
|
1382
|
-
"ui.link.what.chat.research": "rozhovor s agentem ({agent}) o
|
|
1383
|
-
"ui.link.chat.research.say": "Pojďme pracovat na
|
|
1376
|
+
"ui.link.what.direction.pause": "pozastavit linii „{name}“: její úkoly ({n}) počkají mimo frontu a nic nového se pro ni nenavrhne. Nic se nemaže.",
|
|
1377
|
+
"ui.link.what.direction.done": "ukončit linii „{name}“: její úkoly ({n}) odejdou z fronty. Nic se nemaže, jde ji znovu spustit.",
|
|
1378
|
+
"ui.link.what.direction.resume": "znovu spustit linii „{name}“: její úkoly ({n}) se vrátí do fronty a výzkum navrhne, co chybí.",
|
|
1379
|
+
"ui.link.direction.none": "Linie {id} v tomto výzkumu není. Nic se nestalo.",
|
|
1380
|
+
"ui.link.direction.cannot": "Linie „{name}“ už je {state}. Nic se nestalo.",
|
|
1381
|
+
"ui.link.direction.why": "Proč? (pár slov, zůstane u linie; Enter: bez důvodu)",
|
|
1382
|
+
"ui.link.what.chat.research": "rozhovor s agentem ({agent}) o linii „{name}“, jen její úkoly.",
|
|
1383
|
+
"ui.link.chat.research.say": "Pojďme pracovat na linii „{name}“ ({id}), na jejích úkolech.",
|
|
1384
1384
|
"ui.link.what.finish": "požádat agenta {agent}, aby dokončil sezení {session} ({task}): zapíše, co našel, a sezení zavře; při samostatné práci další nezačne.",
|
|
1385
1385
|
"ui.link.finish.none": "Sezení {session} už neběží (mezitím skončilo). Nic se nestalo.",
|
|
1386
1386
|
"ui.link.what.review.family": "prověřit znovu osobu {person} i s partnery a dětmi.",
|
|
1387
1387
|
"ui.link.what.review.line": "prověřit znovu osobu {person} i s předky.",
|
|
1388
|
-
"ui.link.what.research.ancestors": "hledat předky osoby {person} (
|
|
1389
|
-
"ui.link.what.research.descendants": "hledat potomky osoby {person} (
|
|
1388
|
+
"ui.link.what.research.ancestors": "hledat předky osoby {person} (nová linie výzkumu).",
|
|
1389
|
+
"ui.link.what.research.descendants": "hledat potomky osoby {person} (nová linie výzkumu).",
|
|
1390
1390
|
"ui.link.noperson": "Osoba {person} v tomto výzkumu není (nebo z něj byla vyřazena). Nic se nestalo.",
|
|
1391
1391
|
"ui.link.notask": "Tento úkol už nečeká (odpověď už má, nebo ho převzal agent). Nic se nestalo.",
|
|
1392
1392
|
"ui.link.task.none": "Úkol {task} v tomto výzkumu není. Nic se nestalo.",
|
|
@@ -1675,11 +1675,11 @@
|
|
|
1675
1675
|
"log.event.status": "Změněná jistota údaje: {who} – {fact}",
|
|
1676
1676
|
"log.event.restore": "Údaj znovu platí: {who} – {fact}",
|
|
1677
1677
|
"log.input.sync.undo": "Vrácený rodokmen: {name}",
|
|
1678
|
-
"log.research.add": "
|
|
1679
|
-
"log.research.edit": "
|
|
1680
|
-
"log.research.pause": "
|
|
1681
|
-
"log.research.resume": "
|
|
1682
|
-
"log.research.done": "
|
|
1678
|
+
"log.research.add": "Nová linie výzkumu: {name}",
|
|
1679
|
+
"log.research.edit": "Upravená linie výzkumu: {name}",
|
|
1680
|
+
"log.research.pause": "Linie pozastavena: {name}",
|
|
1681
|
+
"log.research.resume": "Linie znovu běží: {name}",
|
|
1682
|
+
"log.research.done": "Linie ukončena: {name}",
|
|
1683
1683
|
"log.mode.archive": "Výzkum je teď archiv",
|
|
1684
1684
|
"log.mode.research": "Výzkum je znovu zapnutý",
|
|
1685
1685
|
"log.reading.loose": "Pročteno snímků: {n}",
|
|
@@ -1717,21 +1717,21 @@
|
|
|
1717
1717
|
"ui.task.answer.window": "Zapsat k úkolu {task} odpověď: „{answer}“?",
|
|
1718
1718
|
"ui.intake.empty": "Není v tom nic, co by se dalo přidat.",
|
|
1719
1719
|
"ui.intake.ours": "{path} je složka samotného stromu (výzkumu), ne rodinné podklady.",
|
|
1720
|
-
"ui.research.exists": "
|
|
1721
|
-
"ui.more.directions": "
|
|
1722
|
-
"ui.dirs.pick": "
|
|
1720
|
+
"ui.research.exists": "Taková linie výzkumu už je: „{name}“ – agent v ní pokračuje.",
|
|
1721
|
+
"ui.more.directions": "Linie výzkumu: pozastavit, ukončit nebo znovu spustit",
|
|
1722
|
+
"ui.dirs.pick": "Která linie?",
|
|
1723
1723
|
"ui.dirs.item": "{name} – {state} · úkoly: {n}",
|
|
1724
1724
|
"ui.dirs.active": "běží",
|
|
1725
|
-
"ui.dirs.paused": "
|
|
1726
|
-
"ui.dirs.done": "
|
|
1725
|
+
"ui.dirs.paused": "pozastavená",
|
|
1726
|
+
"ui.dirs.done": "ukončená",
|
|
1727
1727
|
"ui.dirs.what": "„{name}“:",
|
|
1728
|
-
"ui.dirs.do.pause": "Pozastavit –
|
|
1729
|
-
"ui.dirs.do.done": "Ukončit –
|
|
1730
|
-
"ui.dirs.do.resume": "Znovu spustit –
|
|
1731
|
-
"ui.dirs.paused.now": "⏸ „{name}“ je pozastaven
|
|
1732
|
-
"ui.dirs.done.now": "■ „{name}“ je ukončen
|
|
1733
|
-
"ui.dirs.active.now": "▶ „{name}“ zase běží:
|
|
1734
|
-
"ui.research.resume": "
|
|
1728
|
+
"ui.dirs.do.pause": "Pozastavit – její úkoly počkají, agent vezme jiné",
|
|
1729
|
+
"ui.dirs.do.done": "Ukončit – její úkoly odejdou z fronty (nic se nemaže)",
|
|
1730
|
+
"ui.dirs.do.resume": "Znovu spustit – její úkoly se vrátí do fronty",
|
|
1731
|
+
"ui.dirs.paused.now": "⏸ „{name}“ je pozastavená: její úkoly ({n}) počkají, dokud se tady znovu nespustí.",
|
|
1732
|
+
"ui.dirs.done.now": "■ „{name}“ je ukončená: její úkoly ({n}) jsou mimo frontu – nic se nesmazalo, tady ji jde znovu spustit.",
|
|
1733
|
+
"ui.dirs.active.now": "▶ „{name}“ zase běží: její úkoly ({n}) jsou zpátky ve frontě.",
|
|
1734
|
+
"ui.research.resume": "Taková linie výzkumu už je, {state}: „{name}“. Spustit ji znovu?",
|
|
1735
1735
|
"ui.alone.gate.bad": "Čekám počet bodů od 0 do 100 (třeba 10).",
|
|
1736
1736
|
"ui.alone.addons": "Bez osobních doplňků agenta (skilly, pluginy, MCP servery): {state}",
|
|
1737
1737
|
"ui.alone.addons.about": "Agent při samostatné práci načte jen nástroje výzkumu. Spojení s aplikací Strom a prohlížeč fungují dál. Chat s agentem doplňky načítá vždy.",
|
package/dist/brief/sheet.js
CHANGED
|
@@ -28,11 +28,12 @@ const RARE = {
|
|
|
28
28
|
"search add": ["by"],
|
|
29
29
|
"task add": ["research", "anyway"],
|
|
30
30
|
"media view": ["scale", "max", "grey", "png", "image"],
|
|
31
|
-
read: ["context", "batch", "
|
|
32
|
-
|
|
31
|
+
read: ["context", "batch", "model", "minutes", "max"],
|
|
32
|
+
// a book the research has already: the refusal says --again where it applies
|
|
33
|
+
fetch: ["take", "result", "again"],
|
|
33
34
|
"source add": ["translation", "media"],
|
|
34
35
|
"source edit": ["translation", "media"],
|
|
35
|
-
"recordset add": ["images"],
|
|
36
|
+
"recordset add": ["images", "again"],
|
|
36
37
|
grep: ["regex", "files"],
|
|
37
38
|
// what a waiting task waits for is said when it is set waiting (task wait); edit fills it for an older one
|
|
38
39
|
"task edit": ["on", "kind", "to", "request", "url"],
|
|
@@ -18,7 +18,8 @@ import { EXIT, StromError, UsageError } from "../core/errors.js";
|
|
|
18
18
|
import { deadlineOf, REMIND_MS } from "../core/clock.js";
|
|
19
19
|
import { isAgent } from "../core/which.js";
|
|
20
20
|
import { RECORD_TYPES } from "../core/model.js";
|
|
21
|
-
import { requireRecord } from "../core/records.js";
|
|
21
|
+
import { requireRecord, update } from "../core/records.js";
|
|
22
|
+
import { sameBooks, sameBookText } from "../core/samebook.js";
|
|
22
23
|
import { guardIndexFirst } from "../core/indexfirst.js";
|
|
23
24
|
import { readable } from "../core/text.js";
|
|
24
25
|
import { findImage, isWhole, partHere, regionText, SHARPER } from "../core/media.js";
|
|
@@ -500,28 +501,49 @@ function netWords(ctx, c, levels) {
|
|
|
500
501
|
/** Up to this many books, each comes with the command to register it; more, one line each. */
|
|
501
502
|
const BOOKS_IN_FULL = 3;
|
|
502
503
|
const BOOKS_SHOWN = 30;
|
|
503
|
-
function bookLines(r, c, listOne) {
|
|
504
|
+
function bookLines(r, c, listOne, tree) {
|
|
504
505
|
// a connector that fetches nothing (the terms forbid it, or it only finds): the images come by hand
|
|
505
506
|
const fetches = !byHandOnly(c) && c.manifest.can.some((x) => x === "fetch" || x === "locate");
|
|
507
|
+
// a book the research has already (its link, or images fetched from it): read there, never registered again
|
|
508
|
+
const known = (b) => (tree ? sameBooks(tree, { ...(b.url ? { url: b.url } : {}), connector: c.name, ...(b.id !== undefined ? { book: String(b.id) } : {}) }) : []);
|
|
506
509
|
const line = (b) => `${b.title}${b.callNumber ? ` (${b.callNumber})` : ""}${b.years ? ` · ${b.years}` : ""}${b.kinds?.length ? ` · ${b.kinds.join(",")}` : ""}${b.images ? ` · ${b.images} images` : ""}`;
|
|
510
|
+
const here = (b) => {
|
|
511
|
+
const same = known(b);
|
|
512
|
+
return same.length ? ` · already in the research as ${same.map(sameBookText).join(", ")}` : "";
|
|
513
|
+
};
|
|
507
514
|
if (r.books.length > BOOKS_IN_FULL)
|
|
508
515
|
return [
|
|
509
|
-
...r.books.slice(0, BOOKS_SHOWN).map((b) => ` ${b.id ? `${b.id} ` : ""}${line(b)}`),
|
|
516
|
+
...r.books.slice(0, BOOKS_SHOWN).map((b) => ` ${b.id ? `${b.id} ` : ""}${line(b)}${here(b)}`),
|
|
510
517
|
r.books.length > BOOKS_SHOWN ? ` … ${r.books.length - BOOKS_SHOWN} more — narrow it with --years, or read them all with --json` : undefined,
|
|
511
518
|
` one of them, with the command to register it: ${listOne}`,
|
|
512
519
|
].filter((l) => l !== undefined);
|
|
513
|
-
return r.books.map((b) =>
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
:
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
520
|
+
return r.books.map((b) => {
|
|
521
|
+
const same = known(b);
|
|
522
|
+
if (same.length) {
|
|
523
|
+
const first = same[0];
|
|
524
|
+
return [
|
|
525
|
+
` ${line(b)}`,
|
|
526
|
+
b.url ? ` ${b.url}` : undefined,
|
|
527
|
+
` already in the research as ${same.map(sameBookText).join(", ")} — never registered again`,
|
|
528
|
+
` read it: strom read ${first.book.id} --images <from-to>`,
|
|
529
|
+
fetches && b.id ? ` its images not here yet: strom fetch ${c.name} ${shellArg(b.id)} --images <from-to> --recordset ${first.book.id}` : undefined,
|
|
530
|
+
]
|
|
531
|
+
.filter(Boolean)
|
|
532
|
+
.join("\n");
|
|
533
|
+
}
|
|
534
|
+
return [
|
|
535
|
+
` ${line(b)}`,
|
|
536
|
+
b.url ? ` ${b.url}` : undefined,
|
|
537
|
+
` strom recordset add ${shellArg(b.title)}${b.callNumber ? ` --call-number ${shellArg(b.callNumber)}` : ""}${b.kinds?.length ? ` --kinds ${b.kinds.join(",")}` : ""}${b.places?.length ? ` --places ${shellArg(b.places.join(","))}` : ""}${b.years ? ` --years ${b.years}` : ""}${b.url ? ` --url ${shellArg(b.url)}` : ""} --access online-free`,
|
|
538
|
+
!fetches
|
|
539
|
+
? ` then: the user saves the images by hand — strom task wait T… --images B…:<from-to> --on "<the book, its link, which images>"`
|
|
540
|
+
: b.id
|
|
541
|
+
? ` then: strom fetch ${c.name} ${shellArg(b.id)} --images <from-to> --recordset B…`
|
|
542
|
+
: undefined,
|
|
543
|
+
]
|
|
544
|
+
.filter(Boolean)
|
|
545
|
+
.join("\n");
|
|
546
|
+
});
|
|
525
547
|
}
|
|
526
548
|
function parseImages(v, max = 1000) {
|
|
527
549
|
if (v === undefined)
|
|
@@ -1226,6 +1248,7 @@ register({
|
|
|
1226
1248
|
{ name: "list", type: "boolean", description: "describe the book (title, images)" },
|
|
1227
1249
|
{ name: "take", type: "boolean", description: "through the browser: take over what it downloaded (checked, registered)" },
|
|
1228
1250
|
{ name: "result", type: "string", value: "<line>", description: "with --take: the line the browser script returned (strom-result …)" },
|
|
1251
|
+
{ name: "again", type: "string", value: "<reason>", description: "fetch although a book of the research is this book (the same link, or its images came from it): why — into --recordset, another book (kept on it); without it, into the inbox" },
|
|
1229
1252
|
{ name: "no-index", type: "boolean", description: "fetch the images although an index of the book's parish and years is known (read already, or of no use here) — a session of strom run needs it from as many images on as strom read --no-index says" },
|
|
1230
1253
|
],
|
|
1231
1254
|
examples: [
|
|
@@ -1261,6 +1284,8 @@ register({
|
|
|
1261
1284
|
? partRequest(tree, c, args[1], opts)
|
|
1262
1285
|
: { cmd: "fetch", book: need(args[1], "the book"), images: parseImages(opts.images, perHour) ?? needImages() };
|
|
1263
1286
|
const recordset = opts.recordset ? requireRecord(tree, String(opts.recordset), "recordset").id : undefined;
|
|
1287
|
+
if (request.cmd === "fetch")
|
|
1288
|
+
sameBookFirst(ctx, c, request, recordset, typeof opts.again === "string" ? opts.again.trim() || undefined : undefined);
|
|
1264
1289
|
return fetchWith(ctx, c, request, recordset, Boolean(opts["no-index"]));
|
|
1265
1290
|
},
|
|
1266
1291
|
}, {
|
|
@@ -1810,6 +1835,37 @@ function fromStore(ctx, c, request, recordset, h, via) {
|
|
|
1810
1835
|
},
|
|
1811
1836
|
};
|
|
1812
1837
|
}
|
|
1838
|
+
/**
|
|
1839
|
+
* A book the research has under another ID is never fetched again (B0389 = B0169: every image asked of the archive
|
|
1840
|
+
* twice, found by its hash only after): before any request, the book of the same link, or whose images came from this
|
|
1841
|
+
* book of the connector, is named — fetch into it, read it. --again with a reason fetches into --recordset all the
|
|
1842
|
+
* same and keeps why on it (the same book never asked again).
|
|
1843
|
+
*/
|
|
1844
|
+
function sameBookFirst(ctx, c, request, recordset, again) {
|
|
1845
|
+
const tree = ctx.tree();
|
|
1846
|
+
const target = recordset ? tree.get(recordset) : undefined;
|
|
1847
|
+
// the book registered first is fetched into as before: only one registered before the target makes it a copy
|
|
1848
|
+
const before = (b) => !target || b.created.localeCompare(target.created) < 0 || (b.created === target.created && b.id < target.id);
|
|
1849
|
+
const same = sameBooks(tree, { ...(target?.url ? { url: target.url } : {}), connector: c.name, book: String(request.book) }, recordset).filter((x) => before(x.book));
|
|
1850
|
+
if (!same.length)
|
|
1851
|
+
return;
|
|
1852
|
+
const first = same[0].book.id;
|
|
1853
|
+
const imgs = imagesArg(request.images);
|
|
1854
|
+
if (!recordset) {
|
|
1855
|
+
if (again)
|
|
1856
|
+
return;
|
|
1857
|
+
throw new UsageError(`book ${request.book} of ${c.name} is ${same.map(sameBookText).join(", ")} in the research — nothing fetched`, {
|
|
1858
|
+
hint: `its images: strom fetch ${c.name} ${shellArg(String(request.book))} --images ${imgs} --recordset ${first} (only those not here are asked for) · read it: strom read ${first} --images ${imgs} · into the inbox all the same: --again "<why>"`,
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
if (!again)
|
|
1862
|
+
throw new UsageError(`${recordset} is the same book as ${same.map(sameBookText).join(", ")} (${same[0].by === "url" ? "the same link" : `its images came from book ${request.book} of ${c.name}`}) — nothing fetched`, {
|
|
1863
|
+
hint: `fetch into it: strom fetch ${c.name} ${shellArg(String(request.book))} --images ${imgs} --recordset ${first} (only the images not here are asked for) · read it: strom read ${first} --images ${imgs} · ${recordset} is another book (another film): --again "<why>"`,
|
|
1864
|
+
});
|
|
1865
|
+
if (tree.dryRun)
|
|
1866
|
+
return;
|
|
1867
|
+
update(tree, recordset, "recordset", (cur) => ({ ...cur, duplicates: { of: [...new Set([...(cur.duplicates?.of ?? []), ...same.map((x) => x.book.id)])], reason: again } }), { op: "recordset.edit", summary: `${recordset} duplicates (beside ${same.map((x) => x.book.id).join(", ")})`, reason: again });
|
|
1868
|
+
}
|
|
1813
1869
|
/** strom fetch, once the request is known: checked against the archive's terms, then through strom or the user's browser. */
|
|
1814
1870
|
async function fetchWith(ctx, c, request, recordset, noIndex = false, again = false) {
|
|
1815
1871
|
const tree = ctx.tree();
|
|
@@ -1904,7 +1960,7 @@ async function fetchWith(ctx, c, request, recordset, noIndex = false, again = fa
|
|
|
1904
1960
|
fs.rmSync(workDir, { recursive: true, force: true });
|
|
1905
1961
|
const wait = later(r, "the same command");
|
|
1906
1962
|
return {
|
|
1907
|
-
text: lines(describeRun(r, `${request.cmd} (${took})`), r.books.length ? [`books (${r.books.length}):`, ...bookLines(r, c, `strom fetch ${c.name} <id> --list
|
|
1963
|
+
text: lines(describeRun(r, `${request.cmd} (${took})`), r.books.length ? [`books (${r.books.length}):`, ...bookLines(r, c, `strom fetch ${c.name} <id> --list`, tree)].join("\n") : advice ?? (r.stopped ? "no books — the run stopped before any came" : "no books found"), r.books.length ? advice : undefined, wait.line, near),
|
|
1908
1964
|
data: r,
|
|
1909
1965
|
...(wait.exitCode ? { exitCode: wait.exitCode } : r.stopped && !r.books.length ? { exitCode: 1 } : {}),
|
|
1910
1966
|
};
|
package/dist/commands/media.js
CHANGED
|
@@ -89,8 +89,10 @@ export function registerImages(tree, shared, items, recordset) {
|
|
|
89
89
|
}
|
|
90
90
|
});
|
|
91
91
|
const woken = [];
|
|
92
|
+
// a task that waits for images of a book is woken by that book alone, whatever other books it names
|
|
93
|
+
const wakes = (t, b) => (t.awaits ? t.awaits.recordset === b : t.where.includes(b) || (t.waitingOn ?? "").includes(b));
|
|
92
94
|
if (recordset && added.length)
|
|
93
|
-
for (const t of tree.list("task").filter((t) => t.state === "waiting" && (t
|
|
95
|
+
for (const t of tree.list("task").filter((t) => t.state === "waiting" && wakes(t, recordset))) {
|
|
94
96
|
// what it asked for and what is still missing, for the session that takes it up
|
|
95
97
|
const have = new Set(tree.list("media").filter((m) => m.recordset === recordset).map((m) => m.image)); // a part counts: it was asked for too
|
|
96
98
|
const missing = t.awaits?.recordset === recordset ? (parseImageList(t.awaits.images) ?? []).filter((n) => !have.has(n)) : [];
|
package/dist/commands/read.js
CHANGED
|
@@ -7,6 +7,7 @@ import path from "node:path";
|
|
|
7
7
|
import { register } from "../cli/registry.js";
|
|
8
8
|
import { lines, runs } from "../cli/format.js";
|
|
9
9
|
import { StromError, UsageError } from "../core/errors.js";
|
|
10
|
+
import { DEFAULT_READ_PARALLEL, READ_PARALLEL_MAX } from "../core/config.js";
|
|
10
11
|
import { normId, requireRecord } from "../core/records.js";
|
|
11
12
|
import { pageOf } from "../core/calibration.js";
|
|
12
13
|
import { coveringPart, findImage, IMAGE_RANGE_MAX, imageOfRef, onlyParts, regionText, wholeSizeOf } from "../core/media.js";
|
|
@@ -98,7 +99,7 @@ register({
|
|
|
98
99
|
{ name: "contrast", type: "boolean", description: "more contrast (faded ink)" },
|
|
99
100
|
{ name: "max", type: "string", value: "<px>", description: `size of the views (default: what the readers' model takes, ${IMAGE_MAX} or ${IMAGE_MAX_LARGE}); smaller = cheaper browsing` },
|
|
100
101
|
{ name: "batch", type: "string", value: "<n>", description: `views per reader — a double page in halves is two (default ${BATCH}, fewer where strom tuned the reading of this agent and model: strom config get reading.batch; at most ${BATCH_MAX})` },
|
|
101
|
-
{ name: "parallel", type: "string", value: "<n>", description:
|
|
102
|
+
{ name: "parallel", type: "string", value: "<n>", description: `readers at the same time, 1 to ${READ_PARALLEL_MAX} (default ${DEFAULT_READ_PARALLEL}, or the setting read.parallel: strom config set read.parallel <n>)` },
|
|
102
103
|
{ name: "model", type: "string", value: "<model>", description: "model of the readers (default: model.vision — never weaker for handwriting)" },
|
|
103
104
|
{ name: "minutes", type: "string", value: "<n>", description: READER_MINUTES_HELP, mistaken: ["timeout", "time", "time-limit", "max-minutes"] },
|
|
104
105
|
{ name: "no-index", type: "boolean", description: `browse the images although an index of the book's parish and years is known (read already, or of no use here) — from ${INDEX_FIRST_DEFAULT} images on (the person's answer: strom media calibrate --questions) strom names the index first, and a session of strom run needs this to go on` },
|
|
@@ -192,7 +193,10 @@ register({
|
|
|
192
193
|
const size = opts.batch === undefined ? readingOf(ctx.settings.config, sizes.key, { since, on: tuningOn(ctx.settings), root: tree.root }).readerBatch : Number(opts.batch);
|
|
193
194
|
if (!Number.isInteger(size) || size < 1)
|
|
194
195
|
throw new UsageError("--batch must be a positive number");
|
|
195
|
-
|
|
196
|
+
// readers at the same time: --parallel, else read.parallel (the tree's, else the user's), else DEFAULT_READ_PARALLEL
|
|
197
|
+
const parallel = opts.parallel === undefined ? ctx.settings.readParallel(tree.config) : Number(opts.parallel);
|
|
198
|
+
if (!Number.isInteger(parallel) || parallel < 1 || parallel > READ_PARALLEL_MAX)
|
|
199
|
+
throw new UsageError(`--parallel is a whole number from 1 to ${READ_PARALLEL_MAX}`, { hint: `readers at the same time (default ${DEFAULT_READ_PARALLEL}; strom config set read.parallel <n> keeps another)` });
|
|
196
200
|
// a reader's time limit: by hand, else by what it is given and what the readers before it needed (readerLimit)
|
|
197
201
|
const minutesSaid = opts.minutes === undefined ? undefined : Number(opts.minutes);
|
|
198
202
|
if (minutesSaid !== undefined && !(minutesSaid > 0))
|
package/dist/commands/setup.js
CHANGED
|
@@ -6,7 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
7
7
|
import { APP_URL_INVALID_SETTING, register } from "../cli/registry.js";
|
|
8
8
|
import { lines, secondsIn, table } from "../cli/format.js";
|
|
9
|
-
import { appWebPages, checkValue, configFile, DEFAULT_BUDGET, DEFAULT_RUN_MINUTES, OTHER_ENV, readStored, SETTINGS, settingDef, settingDescription, writeStored } from "../core/config.js";
|
|
9
|
+
import { appWebPages, checkValue, configFile, DEFAULT_BUDGET, DEFAULT_READ_PARALLEL, DEFAULT_RUN_MINUTES, OTHER_ENV, readStored, SETTINGS, settingDef, settingDescription, writeStored } from "../core/config.js";
|
|
10
10
|
import { syncAgentFiles } from "../agents/files.js";
|
|
11
11
|
import { gitVersion } from "../core/git.js";
|
|
12
12
|
import { linuxGitCommand } from "../core/deps.js";
|
|
@@ -780,6 +780,8 @@ function effective(ctx, def, recordset) {
|
|
|
780
780
|
return { value: DEFAULT_RUN_MINUTES, source: "default" };
|
|
781
781
|
if (def.key === "web.perHost")
|
|
782
782
|
return { value: WEB_PER_HOST, source: "default" };
|
|
783
|
+
if (def.key === "read.parallel")
|
|
784
|
+
return { value: DEFAULT_READ_PARALLEL, source: "default" };
|
|
783
785
|
if (def.key === "connectors.consent")
|
|
784
786
|
return { value: "off", source: "default" };
|
|
785
787
|
if (def.key === "agent.addons")
|
package/dist/commands/sources.js
CHANGED
|
@@ -15,6 +15,7 @@ import { partRegion } from "../core/views.js";
|
|
|
15
15
|
import { formatName } from "../core/people.js";
|
|
16
16
|
import { yearsOption, yearsOverlap } from "../core/years.js";
|
|
17
17
|
import { levelWords } from "../core/archiveasks.js";
|
|
18
|
+
import { bookUrlKey, sameBooks, sameBookText } from "../core/samebook.js";
|
|
18
19
|
function written(tree) {
|
|
19
20
|
return lines(...tree.written.map((o) => o.summary));
|
|
20
21
|
}
|
|
@@ -108,6 +109,25 @@ evidence, opts, merge = () => ({})) {
|
|
|
108
109
|
function editOptions(options, extra = []) {
|
|
109
110
|
return [...extra, ...(options ?? []).map((o) => ({ ...o, description: o.description.replace(/\s*\(default[^)]*\)/, "") }))];
|
|
110
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* A book registered twice is fetched twice (every image asked of the archive again): a link another book of the
|
|
114
|
+
* research has is refused, pointing to that book — unless --again says why this is another book (then kept on it).
|
|
115
|
+
*/
|
|
116
|
+
function sameLink(tree, url, again, said, self) {
|
|
117
|
+
const same = url ? sameBooks(tree, { url }, self).filter((x) => x.by === "url") : [];
|
|
118
|
+
if (!same.length) {
|
|
119
|
+
if (again)
|
|
120
|
+
said.push("--again: no other book of the research has this link — nothing to explain");
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
const first = same[0].book.id;
|
|
124
|
+
if (!again)
|
|
125
|
+
throw new UsageError(`this link is the book ${same.map(sameBookText).join(", ")} of the research already — the same book is never registered twice`, {
|
|
126
|
+
hint: `read it: strom read ${first} --images <from-to> · its images not here yet: strom fetch <connector> <book> --images <from-to> --recordset ${first} · another book at the same link (another film): --again "<why>"`,
|
|
127
|
+
});
|
|
128
|
+
said.push(`registered beside ${same.map((x) => x.book.id).join(", ")} (the same link): ${again}`);
|
|
129
|
+
return { of: same.map((x) => x.book.id), reason: again };
|
|
130
|
+
}
|
|
111
131
|
function readText(ctx) {
|
|
112
132
|
return (p) => fs.readFileSync(ctx.resolvePath(p), "utf8");
|
|
113
133
|
}
|
|
@@ -349,6 +369,7 @@ register({
|
|
|
349
369
|
{ name: "images", type: "string", value: "<n>", description: "number of images" },
|
|
350
370
|
{ name: "layout", type: "string", value: "<text>", description: "how pages are laid out (columns, two years per page, …)" },
|
|
351
371
|
{ name: "note", type: "string", value: "<text>", description: "short note" },
|
|
372
|
+
{ name: "again", type: "string", value: "<reason>", description: "register it although a book of the research has the same link: why it is another book (another film, another copy)" },
|
|
352
373
|
],
|
|
353
374
|
examples: ['strom recordset add "Týnec nad Labem 17, N 1861-1869" --repo R0001 --kinds baptism --places "Týnec nad Labem,Bělušice" --years 1861-1869 --access online-free --url https://…'],
|
|
354
375
|
run(ctx, { args, opts }) {
|
|
@@ -356,6 +377,7 @@ register({
|
|
|
356
377
|
if (opts.repo)
|
|
357
378
|
requireRecord(tree, opts.repo, "repository");
|
|
358
379
|
const said = [];
|
|
380
|
+
const duplicates = sameLink(tree, str(opts.url), str(opts.again), said);
|
|
359
381
|
const years = yearsOption(str(opts.years), said, "1784-1820");
|
|
360
382
|
const images = opts.images === undefined ? undefined : Number(opts.images);
|
|
361
383
|
if (images !== undefined && (!Number.isInteger(images) || images < 1))
|
|
@@ -373,6 +395,7 @@ register({
|
|
|
373
395
|
calibration: [],
|
|
374
396
|
layout: str(opts.layout),
|
|
375
397
|
note: str(opts.note),
|
|
398
|
+
duplicates,
|
|
376
399
|
}, (id) => `+${id} record set "${truncate(args[0], 60)}"`);
|
|
377
400
|
return { text: lines(written(tree), ...said), data: { recordset: b } };
|
|
378
401
|
},
|
|
@@ -731,6 +754,11 @@ register({
|
|
|
731
754
|
const images = opts.images === undefined ? undefined : Number(opts.images);
|
|
732
755
|
if (images !== undefined && (!Number.isInteger(images) || images < 1))
|
|
733
756
|
throw new UsageError("--images must be a positive number");
|
|
757
|
+
const cur = requireRecord(tree, args[0], "recordset");
|
|
758
|
+
// a link changed is checked; --again alone explains the link the book has
|
|
759
|
+
const url = str(opts.url);
|
|
760
|
+
const changed = url !== undefined && bookUrlKey(url) !== bookUrlKey(cur.url ?? "");
|
|
761
|
+
const duplicates = sameLink(tree, changed ? url : str(opts.again) ? cur.url : undefined, str(opts.again), said, cur.id);
|
|
734
762
|
const b = editFields(tree, args[0], "recordset", {
|
|
735
763
|
title: str(opts.title),
|
|
736
764
|
repository: opts.repo ? normId(opts.repo) : undefined,
|
|
@@ -742,7 +770,7 @@ register({
|
|
|
742
770
|
url: str(opts.url),
|
|
743
771
|
images,
|
|
744
772
|
layout: str(opts.layout),
|
|
745
|
-
}, {}, opts);
|
|
773
|
+
}, {}, opts, (cur) => (duplicates ? { duplicates: { of: [...new Set([...(cur.duplicates?.of ?? []), ...duplicates.of])], reason: duplicates.reason } } : {}));
|
|
746
774
|
return { text: lines(written(tree), ...said), data: { recordset: b } };
|
|
747
775
|
},
|
|
748
776
|
}, {
|
package/dist/core/config.js
CHANGED
|
@@ -27,6 +27,9 @@ const PERMISSION_ALIASES = { list: "auto", bypass: "full" };
|
|
|
27
27
|
*/
|
|
28
28
|
export const DEFAULT_BUDGET = 55_000;
|
|
29
29
|
export const DEFAULT_RUN_MINUTES = 60;
|
|
30
|
+
/** Readers of strom read at the same time: the default, and the most one research may set (each reader is an agent process). */
|
|
31
|
+
export const DEFAULT_READ_PARALLEL = 5;
|
|
32
|
+
export const READ_PARALLEL_MAX = 10;
|
|
30
33
|
export const SETTINGS = [
|
|
31
34
|
{ key: "home", env: "STROM_HOME", tree: false, kind: "path", description: "folder with the trees and shared data" },
|
|
32
35
|
{ key: "shared", env: "STROM_SHARED", tree: false, kind: "path", description: "shared data: scans, catalog, tools (default <home>/shared)" },
|
|
@@ -51,6 +54,7 @@ export const SETTINGS = [
|
|
|
51
54
|
{ key: "model.effort", env: "", tree: true, kind: "model", description: "reasoning effort of the agent's model in the sessions strom starts (Codex, Claude Code, Antigravity, Grok): high recommended — old handwriting read with the best setting; a plan's limit runs out sooner. Unset: the agent's own settings (Codex: its config.toml); only you raise it" },
|
|
52
55
|
{ key: "brief.budget", env: "STROM_BRIEF_BUDGET", tree: true, kind: "number", description: `size of the brief in tokens (default ${DEFAULT_BUDGET})` },
|
|
53
56
|
{ key: "run.minutes", env: "STROM_RUN_MINUTES", tree: true, kind: "number", description: `time limit of one \`strom run\` session (default ${DEFAULT_RUN_MINUTES})` },
|
|
57
|
+
{ key: "read.parallel", env: "", tree: true, kind: "number", description: `readers of \`strom read\` at the same time, 1 to ${READ_PARALLEL_MAX} (default ${DEFAULT_READ_PARALLEL}): --parallel of the command, else the tree's, else yours — the same readers and requests, only sooner; fewer when the agent's usage runs out too fast` },
|
|
54
58
|
// Read from the config files only (the user's or the tree's) — no variable: more requests to one server is the
|
|
55
59
|
// user's decision (as model.effort higher); lowering it is anyone's.
|
|
56
60
|
{ key: "web.perHost", env: "", tree: true, kind: "number", description: `web requests to one site (its mirrors with it) in one session through an agent's own web fetch (default ${WEB_PER_HOST}; past ${WEB_SOFT} they go slower and the agent builds a connector): past it a run refuses and a conversation asks you, and more pages of that site go through strom fetch with a connector — only you raise it` },
|
|
@@ -97,6 +101,7 @@ export const SETTINGS = [
|
|
|
97
101
|
const FIELDS = {
|
|
98
102
|
"brief.budget": "briefBudget",
|
|
99
103
|
"run.minutes": "runMinutes",
|
|
104
|
+
"read.parallel": "readParallel",
|
|
100
105
|
"web.perHost": "webPerHost",
|
|
101
106
|
"gedcom.for": "gedcomFor",
|
|
102
107
|
"strom.version": "stromVersion",
|
|
@@ -246,6 +251,8 @@ export function checkValue(def, raw, resolvePath, env = {}) {
|
|
|
246
251
|
const n = Number(v);
|
|
247
252
|
if (!Number.isFinite(n) || n <= 0)
|
|
248
253
|
throw new UsageError(`${def.key} must be a positive number`);
|
|
254
|
+
if (def.key === "read.parallel" && (!Number.isInteger(n) || n > READ_PARALLEL_MAX))
|
|
255
|
+
throw new UsageError(`${def.key} is a whole number from 1 to ${READ_PARALLEL_MAX}`, { hint: `readers at the same time — strom config set read.parallel ${DEFAULT_READ_PARALLEL}` });
|
|
249
256
|
return def.key === "brief.budget" || def.key === "web.perHost" ? Math.max(1, Math.round(n)) : n;
|
|
250
257
|
}
|
|
251
258
|
}
|
|
@@ -416,6 +423,11 @@ export class Settings {
|
|
|
416
423
|
const r = this.resolve(key, tree);
|
|
417
424
|
return r ? Number(r.value) : fallback;
|
|
418
425
|
}
|
|
426
|
+
/** Readers of strom read at the same time (read.parallel: tree > user, else DEFAULT_READ_PARALLEL); a stored value out of range is brought into it. */
|
|
427
|
+
readParallel(tree) {
|
|
428
|
+
const n = Math.round(this.number("read.parallel", tree, DEFAULT_READ_PARALLEL));
|
|
429
|
+
return Number.isFinite(n) ? Math.min(READ_PARALLEL_MAX, Math.max(1, n)) : DEFAULT_READ_PARALLEL;
|
|
430
|
+
}
|
|
419
431
|
/** Web requests to one server in one session through an agent's own web fetch before strom refuses or asks (web.perHost: tree > user). */
|
|
420
432
|
webPerHost(tree) {
|
|
421
433
|
const n = Math.round(this.number("web.perHost", tree, WEB_PER_HOST));
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// The same book under two IDs: one book of an archive registered twice is fetched twice — every image asked of the
|
|
2
|
+
// archive again, then found by its hash to be another book's. A book is known by its link, or by the book of a
|
|
3
|
+
// connector its images came from.
|
|
4
|
+
/** A link as it is compared: no fragment, no trailing slash, the scheme and host in lower case (http = https). */
|
|
5
|
+
export function bookUrlKey(url) {
|
|
6
|
+
const u = url.trim().replace(/#.*$/u, "").replace(/\/+$/u, "");
|
|
7
|
+
const m = /^([a-z][a-z0-9+.-]*):\/\/([^/?]*)(.*)$/iu.exec(u);
|
|
8
|
+
if (!m)
|
|
9
|
+
return u;
|
|
10
|
+
const scheme = m[1].toLowerCase().replace(/^https$/u, "http");
|
|
11
|
+
return `${scheme}://${m[2].toLowerCase()}${m[3]}`;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The books of the research that are this one: the same link, or images fetched through this connector from this
|
|
15
|
+
* book. `except` — the book asked about itself, and those it was knowingly registered beside (`duplicates`).
|
|
16
|
+
*/
|
|
17
|
+
export function sameBooks(tree, of, except) {
|
|
18
|
+
const key = of.url ? bookUrlKey(of.url) : undefined;
|
|
19
|
+
const media = tree.list("media");
|
|
20
|
+
const own = except ? tree.get(except) : undefined;
|
|
21
|
+
const skip = new Set([...(except ? [except] : []), ...(own?.duplicates?.of ?? [])]);
|
|
22
|
+
const out = [];
|
|
23
|
+
for (const b of tree.list("recordset")) {
|
|
24
|
+
if (skip.has(b.id) || b.duplicates?.of.includes(except ?? ""))
|
|
25
|
+
continue;
|
|
26
|
+
const mine = media.filter((m) => m.recordset === b.id);
|
|
27
|
+
const by = key && b.url && bookUrlKey(b.url) === key
|
|
28
|
+
? "url"
|
|
29
|
+
: of.connector && of.book !== undefined && mine.some((m) => m.fetched?.connector === of.connector && m.fetched?.book === String(of.book))
|
|
30
|
+
? "connector"
|
|
31
|
+
: undefined;
|
|
32
|
+
if (!by)
|
|
33
|
+
continue;
|
|
34
|
+
out.push({ book: b, images: new Set(mine.filter((m) => !m.part && m.image !== undefined).map((m) => m.image)).size, by });
|
|
35
|
+
}
|
|
36
|
+
// the book registered first is the book: the others are its copies
|
|
37
|
+
return out.sort((a, b) => a.book.created.localeCompare(b.book.created) || a.book.id.localeCompare(b.book.id));
|
|
38
|
+
}
|
|
39
|
+
/** "B0169 (148 images here)" */
|
|
40
|
+
export function sameBookText(s) {
|
|
41
|
+
return `${s.book.id}${s.images ? ` (${s.images} image${s.images === 1 ? "" : "s"} here)` : " (no images here yet)"}`;
|
|
42
|
+
}
|
package/dist/core/schema.js
CHANGED
|
@@ -49,6 +49,7 @@ export const SCHEMAS = {
|
|
|
49
49
|
images: { t: "number", int: true, min: 1 },
|
|
50
50
|
calibration: { t: "array", req: true, of: { image: { t: "number", req: true, int: true, min: 1 }, page: S({ req: true }) } },
|
|
51
51
|
layout: S({ max: 1000 }),
|
|
52
|
+
duplicates: { t: "object", of: { of: RS("recordset", 1), reason: S({ req: true, max: 1000 }) } },
|
|
52
53
|
},
|
|
53
54
|
place: {
|
|
54
55
|
names: { t: "array", req: true, min: 1, of: { name: S({ req: true }), lang: S(), from: { t: "number", int: true }, to: { t: "number", int: true } } },
|
package/dist/core/tree.js
CHANGED
|
@@ -27,7 +27,7 @@ import { lastBackup } from "./backup.js";
|
|
|
27
27
|
export const TREE_FILE = "strom.json";
|
|
28
28
|
// between releases the candidate being made (a bridge run from the sources says what it is: the Strom app goes by it);
|
|
29
29
|
// at a release the release's own (package.json)
|
|
30
|
-
export const VERSION = "1.
|
|
30
|
+
export const VERSION = "1.15.0-beta.1";
|
|
31
31
|
const GITIGNORE = `# Strom Research — local state and large files are not versioned
|
|
32
32
|
.strom/
|
|
33
33
|
/media/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "strom-research",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0-beta.1",
|
|
4
4
|
"description": "Genealogical research toolkit for AI coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -45,5 +45,8 @@
|
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/node": "^22.20.4",
|
|
47
47
|
"typescript": "^7.0.2"
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"tag": "beta"
|
|
48
51
|
}
|
|
49
52
|
}
|