strom-research 1.18.0-beta.3 → 1.18.0-beta.4
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.cs.md +3 -2
- package/CHANGELOG.de.md +3 -2
- package/CHANGELOG.md +3 -2
- package/assets/lang/cs.json +12 -0
- package/assets/lang/de.json +12 -0
- package/dist/brief/brief.js +6 -1
- package/dist/cli/ui.js +3 -0
- package/dist/commands/document.js +14 -13
- package/dist/commands/intake.js +6 -4
- package/dist/commands/output.js +4 -2
- package/dist/commands/readings.js +18 -6
- package/dist/commands/sync.js +12 -7
- package/dist/core/live.js +9 -3
- package/dist/core/phrases.js +9 -0
- package/dist/core/readsync.js +269 -0
- package/dist/core/review.js +11 -1
- package/dist/core/stromapp.js +21 -0
- package/dist/core/sync.js +181 -13
- package/dist/core/tree.js +1 -1
- package/dist/gedcom/export.js +6 -0
- package/package.json +1 -1
package/CHANGELOG.cs.md
CHANGED
|
@@ -7,7 +7,7 @@ Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou n
|
|
|
7
7
|
|
|
8
8
|
## 1.18.0
|
|
9
9
|
|
|
10
|
-
*Připravuje se: bety do 1.18.0-beta.
|
|
10
|
+
*Připravuje se: bety do 1.18.0-beta.4. Oddíl bude úplný s vydáním.*
|
|
11
11
|
|
|
12
12
|
- **Fotky z telefonu nastojato.** Fotka otočená podle údaje EXIF se čte tak, jak se má dívat, nastojato; místa na ní
|
|
13
13
|
vyznačená (výřezy, poloviny dvoustrany, uložená část) jsou místa obrázku, jak se ukazuje. Fotka sama zůstává, jak
|
|
@@ -20,7 +20,8 @@ Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou n
|
|
|
20
20
|
z matriky, dopis, listinu, náhrobek, rub fotky: přepis slovo od slova, překlad, význam starých slov a zkratek
|
|
21
21
|
a osoby, které jmenuje, i s tím, kdo ze stromu to může být. Nic se nedomýšlí: jméno, které v dokumentu není,
|
|
22
22
|
vypadne — i příjmení, nikdy se nebere od otce ani z jiné rodiny —, co nejde přečíst, je označené; druhý čtenář
|
|
23
|
-
může číst naslepo. Do stromu se z toho nic nezapíše samo.
|
|
23
|
+
může číst naslepo. Do stromu se z toho nic nezapíše samo. Dokument už přečtený to o sobě řekne, takže se omylem
|
|
24
|
+
nečte podruhé.
|
|
24
25
|
- **Fotky z telefonu.** `strom phone on`, které zapíná jen člověk, pustí k počítači telefon v domácí síti (Wi-Fi
|
|
25
26
|
nebo kabel — nikdy mobilní síť ani VPN). Telefon se jednou spáruje přes QR kód; jeho stránka pak fotí nebo vybírá
|
|
26
27
|
z galerie a posílá fotky celé do stromu výzkumu, s poznámkou a osobou, kterou ukazují — stejně jako „Přidat
|
package/CHANGELOG.de.md
CHANGED
|
@@ -7,7 +7,7 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
|
|
|
7
7
|
|
|
8
8
|
## 1.18.0
|
|
9
9
|
|
|
10
|
-
*In Vorbereitung: die Betas bis 1.18.0-beta.
|
|
10
|
+
*In Vorbereitung: die Betas bis 1.18.0-beta.4. Der Abschnitt ist mit dem Release vollständig.*
|
|
11
11
|
|
|
12
12
|
- **Handyfotos aufrecht.** Ein Foto, das nach seiner EXIF-Ausrichtung gedreht ist, wird gelesen, wie es gesehen
|
|
13
13
|
werden soll, aufrecht; die darauf markierten Stellen (Ausschnitte, die Hälften einer Doppelseite, ein gespeicherter
|
|
@@ -21,7 +21,8 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
|
|
|
21
21
|
Abschrift Wort für Wort, ihre Übersetzung, was alte Wörter und Abkürzungen bedeuten, und die genannten Personen,
|
|
22
22
|
samt wem im Stammbaum sie entsprechen können. Nichts wird ergänzt: ein Name, der nicht im Dokument steht, fällt
|
|
23
23
|
weg — auch ein Nachname, nie vom Vater oder aus einer anderen Familie genommen —, Unleserliches wird markiert; ein
|
|
24
|
-
zweiter Leser kann blind lesen. In den Stammbaum gelangt nichts von selbst.
|
|
24
|
+
zweiter Leser kann blind lesen. In den Stammbaum gelangt nichts von selbst. Ein schon gelesenes Dokument sagt es,
|
|
25
|
+
so wird es nicht versehentlich ein zweites Mal gelesen.
|
|
25
26
|
- **Fotos vom Handy.** `strom phone on`, das nur ein Mensch einschaltet, lässt ein Handy im Heimnetz (WLAN oder
|
|
26
27
|
Kabel — nie ein Mobilfunknetz oder ein VPN) zu diesem Computer. Das Handy wird einmal per QR-Code gekoppelt; seine
|
|
27
28
|
Seite fotografiert dann oder wählt aus der Galerie und schickt die Fotos ganz in einen Stammbaum der Forschung, mit
|
package/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,7 @@ The changes of each version in short. The full notes and the install lines are o
|
|
|
7
7
|
|
|
8
8
|
## 1.18.0
|
|
9
9
|
|
|
10
|
-
*In preparation: the betas up to 1.18.0-beta.
|
|
10
|
+
*In preparation: the betas up to 1.18.0-beta.4. The section is complete with the release.*
|
|
11
11
|
|
|
12
12
|
- **Phone photos upright.** A photo turned by its EXIF orientation is read as it is meant to be seen, upright; the
|
|
13
13
|
places marked on it (excerpts, the halves of a spread, a cut-out part) are of the picture as it is shown. The
|
|
@@ -20,7 +20,8 @@ The changes of each version in short. The full notes and the install lines are o
|
|
|
20
20
|
entry, a letter, a deed, a gravestone, the back of a photo: the transcript word for word, its translation, what its
|
|
21
21
|
old words and abbreviations mean, and the people it names, with who of the tree they may be. Nothing is filled in:
|
|
22
22
|
a name the document does not have is left out — a surname too, never taken from a father or another family —,
|
|
23
|
-
what cannot be read is marked; a second reader can read it blind. Nothing of it enters the tree by itself.
|
|
23
|
+
what cannot be read is marked; a second reader can read it blind. Nothing of it enters the tree by itself. A
|
|
24
|
+
document read already says so, so it is not read a second time by mistake.
|
|
24
25
|
- **Photos from a phone.** `strom phone on`, turned on by the person alone, lets a phone on the home network (Wi-Fi
|
|
25
26
|
or a cable — never a mobile network or a VPN) reach this computer. The phone is paired once by its QR code; its
|
|
26
27
|
page then takes photos or picks them from the gallery and sends them whole into a tree of the research, with a
|
package/assets/lang/cs.json
CHANGED
|
@@ -674,6 +674,15 @@
|
|
|
674
674
|
"sync.reading.note": "Uživatel přečetl tento záznam v aplikaci Strom jinak ({source}); čtení výzkumu platí, dokud se záznam nepřečte znovu:",
|
|
675
675
|
"sync.reading.page": "strana",
|
|
676
676
|
"sync.reading.text": "přepis",
|
|
677
|
+
"read.note.source.edited": "Přepis byl upraven v aplikaci Strom; čtení {read} má: {text}",
|
|
678
|
+
"read.note.edited": "Přepis byl upraven v aplikaci Strom: vodítko, dokud se záznam nepřečte ({read})",
|
|
679
|
+
"read.note.one": "Přečetl jeden čtenář ({read}): možné do druhého, slepého čtení a vlastního pohledu výzkumu na záznam",
|
|
680
|
+
"read.note.differ": "Čtenáři {read} přečetli jinak: {list} — vodítko, dokud se záznam nepřečte",
|
|
681
|
+
"read.note.absent": "Čtení {read} to tak neuvádí: vodítko, dokud se záznam nepřečte",
|
|
682
|
+
"read.note.uncertain": "Čtenář {read} si tím nebyl jistý: vodítko, dokud se záznam nepřečte",
|
|
683
|
+
"read.verify.what": "Ověřit čtení dokumentu u osoby {name}: {list}",
|
|
684
|
+
"read.verify.why": "údaje stojí na čtení čtenářů stromu ({read}, potvrzeném v aplikaci Strom, {source}), které ještě není jisté: jeden čtenář, rozdílná čtení, nebo ručně upravený přepis; druhé, slepé čtení a vlastní pohled výzkumu na záznam je doloží, nebo ukáže chybu",
|
|
685
|
+
"read.verify.done": "po jednom čtenáři druhý, slepý (strom document again {read} --readers 2) a zápis prohlédnutý výzkumem samotným (strom source show {source}, jeho snímek): čtení se shodují → pravděpodobné, prokázané až po tomto pohledu a jen u primární informace; liší se → rozpor s oběma čteními",
|
|
677
686
|
"archive.held": "odloženo: výzkum je Archiv Stromu (data přicházejí z aplikace Strom); do fronty se vrátí, až se zapne práce s agentem",
|
|
678
687
|
"archive.removed": "odebráno v aplikaci Strom",
|
|
679
688
|
"archive.wait": "Rozhodnutí o stahování z archivu {title}",
|
|
@@ -1172,6 +1181,8 @@
|
|
|
1172
1181
|
"ui.sync.do.family.cite.read": "bude ho citovat rodina (její sňatek), na čtení záznamu z aplikace",
|
|
1173
1182
|
"ui.sync.do.source.new": "pramen výzkumu; přepis z aplikace je vodítko, dokud záznam nepřečte výzkum",
|
|
1174
1183
|
"ui.sync.do.source.new.read": "pramen výzkumu; přepis z aplikace platí jako čtení záznamu",
|
|
1184
|
+
"ui.sync.do.source.new.reading": "pramen výzkumu z jeho vlastního čtení dokumentu (snímek a výřez zápisu): údaje tak jisté, jak je čtenáři přečetli",
|
|
1185
|
+
"ui.sync.do.source.new.reading.edited": "pramen výzkumu z jeho čtení dokumentu, přepis upravený v aplikaci: údaje jsou vodítko, dokud záznam nepřečte výzkum",
|
|
1175
1186
|
"ui.sync.do.source.changed": "převezme se: je to čtení z aplikace",
|
|
1176
1187
|
"ui.sync.do.source.changed.read": "převezme se: je to čtení z aplikace a platí",
|
|
1177
1188
|
"ui.sync.do.source.reading": "uloží se vedle čtení výzkumu; výzkum záznam přečte znovu",
|
|
@@ -1180,6 +1191,7 @@
|
|
|
1180
1191
|
"ui.sync.do.fact.cite": "údaj výzkumu ho bude citovat (vodítko, dokud záznam nepřečte výzkum)",
|
|
1181
1192
|
"ui.sync.do.fact.cite.read": "údaj výzkumu ho bude citovat a stojí na čtení záznamu z aplikace",
|
|
1182
1193
|
"ui.sync.do.reading": "přidá se na čtení záznamu z aplikace",
|
|
1194
|
+
"ui.sync.do.reading.document": "přidá se tak jistý, jak ho čtenáři dokumentu přečetli (jeden čtenář: možné, dva ve shodě: pravděpodobné, rozdílně: vodítko)",
|
|
1183
1195
|
"ui.sync.family.new": "{n}. nová rodina: {who}{kids}",
|
|
1184
1196
|
"ui.sync.child.new": "{n}. {who}: dítě — {child}",
|
|
1185
1197
|
"ui.sync.child.gone": "{n}. {who}: už to není jejich dítě — {child}",
|
package/assets/lang/de.json
CHANGED
|
@@ -674,6 +674,15 @@
|
|
|
674
674
|
"sync.reading.note": "Der Benutzer hat diesen Eintrag in der Strom-App anders gelesen ({source}); die Lesung der Forschung gilt, bis er erneut gelesen ist:",
|
|
675
675
|
"sync.reading.page": "Seite",
|
|
676
676
|
"sync.reading.text": "Abschrift",
|
|
677
|
+
"read.note.source.edited": "Die Abschrift wurde in der Strom-App geändert; die Lesung {read} hat: {text}",
|
|
678
|
+
"read.note.edited": "Die Abschrift wurde in der Strom-App geändert: ein Hinweis, bis der Eintrag gelesen ist ({read})",
|
|
679
|
+
"read.note.one": "Von einem Leser gelesen ({read}): möglich bis zu einer zweiten, blinden Lesung und dem eigenen Blick der Forschung auf den Eintrag",
|
|
680
|
+
"read.note.differ": "Die Leser von {read} lasen es verschieden: {list} — ein Hinweis, bis der Eintrag gelesen ist",
|
|
681
|
+
"read.note.absent": "Die Lesung {read} sagt es nicht so: ein Hinweis, bis der Eintrag gelesen ist",
|
|
682
|
+
"read.note.uncertain": "Der Leser von {read} war sich dessen nicht sicher: ein Hinweis, bis der Eintrag gelesen ist",
|
|
683
|
+
"read.verify.what": "Die Lesung eines Dokuments für {name} prüfen: {list}",
|
|
684
|
+
"read.verify.why": "die Angaben stehen auf einer Lesung der Leser von strom ({read}, in der Strom-App bestätigt, {source}), die noch nicht sicher ist: ein Leser, verschiedene Lesungen oder die Abschrift von Hand geändert; eine zweite, blinde Lesung und der eigene Blick der Forschung auf den Eintrag belegen sie oder zeigen einen Fehler",
|
|
685
|
+
"read.verify.done": "nach einem Leser ein zweiter, blinder (strom document again {read} --readers 2), und der Eintrag von der Forschung selbst angesehen (strom source show {source}, sein Bild): die Lesungen stimmen überein → wahrscheinlich, belegt erst nach diesem Blick und nur bei primärer Information; sie weichen ab → ein Widerspruch mit beiden Lesungen",
|
|
677
686
|
"archive.held": "zurückgestellt: die Forschung ist ein Strom-Archiv (die Daten kommen aus der Strom-App); zurück in der Warteschlange, wenn die Arbeit mit einem Agenten eingeschaltet wird",
|
|
678
687
|
"archive.removed": "in der Strom-App entfernt",
|
|
679
688
|
"archive.wait": "Entscheidung über das Herunterladen aus dem Archiv {title}",
|
|
@@ -1172,6 +1181,8 @@
|
|
|
1172
1181
|
"ui.sync.do.family.cite.read": "die Familie (ihre Heirat) zitiert sie, auf die eigene Lesung des Eintrags",
|
|
1173
1182
|
"ui.sync.do.source.new": "eine Quelle der Forschung; die eigene Abschrift ist ein Hinweis, bis die Forschung den Eintrag liest",
|
|
1174
1183
|
"ui.sync.do.source.new.read": "eine Quelle der Forschung; die eigene Abschrift gilt als Lesung des Eintrags",
|
|
1184
|
+
"ui.sync.do.source.new.reading": "eine Quelle der Forschung aus ihrer eigenen Lesung des Dokuments (Bild und Ausschnitt des Eintrags): die Angaben so sicher, wie die Leser sie lasen",
|
|
1185
|
+
"ui.sync.do.source.new.reading.edited": "eine Quelle der Forschung aus ihrer Lesung des Dokuments, die Abschrift in der App geändert: die Angaben ein Hinweis, bis die Forschung den Eintrag liest",
|
|
1175
1186
|
"ui.sync.do.source.changed": "übernommen: es ist die eigene Lesung",
|
|
1176
1187
|
"ui.sync.do.source.changed.read": "übernommen: es ist die eigene Lesung, und sie gilt",
|
|
1177
1188
|
"ui.sync.do.source.reading": "neben der Lesung der Forschung festgehalten; die Forschung liest den Eintrag erneut",
|
|
@@ -1180,6 +1191,7 @@
|
|
|
1180
1191
|
"ui.sync.do.fact.cite": "die Angabe der Forschung zitiert sie (ein Hinweis, bis die Forschung den Eintrag liest)",
|
|
1181
1192
|
"ui.sync.do.fact.cite.read": "die Angabe der Forschung zitiert sie und steht auf der eigenen Lesung des Eintrags",
|
|
1182
1193
|
"ui.sync.do.reading": "hinzugefügt auf der eigenen Lesung des Eintrags",
|
|
1194
|
+
"ui.sync.do.reading.document": "wird so sicher hinzugefügt, wie die Leser des Dokuments es lasen (ein Leser: möglich, zwei übereinstimmend: wahrscheinlich, verschieden: ein Hinweis)",
|
|
1183
1195
|
"ui.sync.family.new": "{n}. eine neue Familie: {who}{kids}",
|
|
1184
1196
|
"ui.sync.child.new": "{n}. {who}: ein Kind — {child}",
|
|
1185
1197
|
"ui.sync.child.gone": "{n}. {who}: nicht mehr ihr Kind — {child}",
|
package/dist/brief/brief.js
CHANGED
|
@@ -22,6 +22,7 @@ import { clock, hostAllowed, hostState, REFUSED_FOREVER } from "../core/net.js";
|
|
|
22
22
|
import { archiveLevel } from "../core/terms.js";
|
|
23
23
|
import { onHost, permissionQuestions, setHosts } from "../core/archivewait.js";
|
|
24
24
|
import { runs, runsArg, shellArg } from "../cli/format.js";
|
|
25
|
+
import { readingsLine, readingsOf } from "../core/readsync.js";
|
|
25
26
|
import { commandSheet } from "./sheet.js";
|
|
26
27
|
import { foldText } from "../core/text.js";
|
|
27
28
|
import { subjectPeople } from "../core/records.js";
|
|
@@ -933,9 +934,13 @@ export function buildBrief(tree, opts) {
|
|
|
933
934
|
const file = inputPath(tree, i);
|
|
934
935
|
// all that strom input show says of it, so it needs no extra command (K6)
|
|
935
936
|
const small = !i.text && i.kind === "text" && file && (i.size ?? 0) < 20_000 && /\.(txt|md|csv)$/i.test(file) ? readSmall(file) : undefined;
|
|
937
|
+
// read already (strom document read): its result, the file not read again
|
|
938
|
+
const read = readingsLine(tree, i);
|
|
939
|
+
const readDone = readingsOf(tree, i).some((r) => r.state === "done");
|
|
936
940
|
return [
|
|
937
941
|
`${i.id} ${i.name} [${i.kind} · ${i.state}]${i.from ? ` from ${i.from}` : ""}${i.mime ? ` · ${i.mime}${i.size !== undefined ? ` ${Math.round(i.size / 1024)} kB` : ""}` : ""}`,
|
|
938
|
-
file ? `file: ${file}${i.kind === "document" || i.kind === "photo" ? " — open it and read it yourself" : ""}` : "",
|
|
942
|
+
file ? `file: ${file}${(i.kind === "document" || i.kind === "photo") && !readDone ? " — open it and read it yourself" : ""}` : "",
|
|
943
|
+
read ? `read: ${read}` : "",
|
|
939
944
|
i.batch ? `batch: ${i.batch}${i.path ? ` · ${i.path}` : ""}` : "",
|
|
940
945
|
i.persons?.length ? `of: ${i.persons.join(", ")}` : "",
|
|
941
946
|
i.sorted ? `sorted: ${i.sorted.as}${i.sorted.source ? ` → ${i.sorted.source}` : ""}${i.sorted.reason ? ` (${i.sorted.reason})` : ""}` : "",
|
package/dist/cli/ui.js
CHANGED
|
@@ -1073,6 +1073,8 @@ export const UI = {
|
|
|
1073
1073
|
"ui.sync.do.family.cite.read": "the family (its marriage) cites it, on the file's reading of the record",
|
|
1074
1074
|
"ui.sync.do.source.new": "a source of the research; the file's transcript is a lead until the research reads the record",
|
|
1075
1075
|
"ui.sync.do.source.new.read": "a source of the research; the file's transcript counts as a reading of the record",
|
|
1076
|
+
"ui.sync.do.source.new.reading": "a source of the research from its own reading of the document (its picture and the entry's excerpt): the facts as sure as its readers read them",
|
|
1077
|
+
"ui.sync.do.source.new.reading.edited": "a source of the research from its reading of the document, the transcript changed in the app: the facts a lead until the research reads the record",
|
|
1076
1078
|
"ui.sync.do.source.changed": "taken: it is the file's reading",
|
|
1077
1079
|
"ui.sync.do.source.changed.read": "taken: it is the file's reading, and it counts",
|
|
1078
1080
|
"ui.sync.do.source.reading": "kept beside the research's reading; the research reads the record again",
|
|
@@ -1081,6 +1083,7 @@ export const UI = {
|
|
|
1081
1083
|
"ui.sync.do.fact.cite": "the research's fact cites it (a lead on it until the research reads the record)",
|
|
1082
1084
|
"ui.sync.do.fact.cite.read": "the research's fact cites it and stands on the file's reading of the record",
|
|
1083
1085
|
"ui.sync.do.reading": "added on the file's reading of the record",
|
|
1086
|
+
"ui.sync.do.reading.document": "added as sure as the readers of the document read it (one reader: possible; two alike: probable; apart: a lead)",
|
|
1084
1087
|
"ui.sync.family.new": "{n}. a new family: {who}{kids}",
|
|
1085
1088
|
"ui.sync.child.new": "{n}. {who}: a child — {child}",
|
|
1086
1089
|
"ui.sync.child.gone": "{n}. {who}: no longer their child — {child}",
|
|
@@ -562,20 +562,21 @@ register({
|
|
|
562
562
|
tree: true,
|
|
563
563
|
args: [{ name: "reading", description: "the reading: D…", required: true }],
|
|
564
564
|
examples: ["strom document show D20260101120000-ab12 --json"],
|
|
565
|
-
run: (ctx, { args }) =>
|
|
566
|
-
const tree = ctx.tree();
|
|
567
|
-
const id = readId(args[0]);
|
|
568
|
-
const s = loadState(tree.root, id);
|
|
569
|
-
let result;
|
|
570
|
-
if (s)
|
|
571
|
-
result = buildResult(tree, settled(tree, s));
|
|
572
|
-
else if (fs.existsSync(resultPath(tree.root, id)))
|
|
573
|
-
result = JSON.parse(fs.readFileSync(resultPath(tree.root, id), "utf8"));
|
|
574
|
-
else
|
|
575
|
-
throw new UsageError(`no reading ${id}`, { hint: "strom document list" });
|
|
576
|
-
return { text: said(ctx, result), data: result };
|
|
577
|
-
},
|
|
565
|
+
run: (ctx, { args }) => showReading(ctx, readId(args[0])),
|
|
578
566
|
});
|
|
567
|
+
/** A reading of a document as strom document show (and strom readings D…) says it: its result, or what its readers wrote so far. */
|
|
568
|
+
export function showReading(ctx, id) {
|
|
569
|
+
const tree = ctx.tree();
|
|
570
|
+
const s = loadState(tree.root, id);
|
|
571
|
+
let result;
|
|
572
|
+
if (s)
|
|
573
|
+
result = buildResult(tree, settled(tree, s));
|
|
574
|
+
else if (fs.existsSync(resultPath(tree.root, id)))
|
|
575
|
+
result = JSON.parse(fs.readFileSync(resultPath(tree.root, id), "utf8"));
|
|
576
|
+
else
|
|
577
|
+
throw new UsageError(`no reading ${id}`, { hint: "strom document list" });
|
|
578
|
+
return { text: said(ctx, result), data: result };
|
|
579
|
+
}
|
|
579
580
|
register({
|
|
580
581
|
path: ["document", "list"],
|
|
581
582
|
summary: "The readings of documents of this research, newest first",
|
package/dist/commands/intake.js
CHANGED
|
@@ -8,6 +8,7 @@ import fs from "node:fs";
|
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { register } from "../cli/registry.js";
|
|
10
10
|
import { counted, lines, moreLine, paginate, table, truncate } from "../cli/format.js";
|
|
11
|
+
import { readingsLine, readingsOf } from "../core/readsync.js";
|
|
11
12
|
import { notFound, UsageError } from "../core/errors.js";
|
|
12
13
|
import { collectFiles, fileSha256, inputPath, MAX_IN_TREE, mimeOf, storeShared } from "../core/media.js";
|
|
13
14
|
import { create, normId, requireRecord, update } from "../core/records.js";
|
|
@@ -248,8 +249,9 @@ register({
|
|
|
248
249
|
const all = ctx.tree().list("input").filter((i) => !opts.state || i.state === opts.state);
|
|
249
250
|
const page = paginate(all, ctx.limit, ctx.page);
|
|
250
251
|
return {
|
|
251
|
-
|
|
252
|
-
|
|
252
|
+
// a file read already (strom document read) says so: its reading, not read again
|
|
253
|
+
text: all.length ? lines(table(page.items.map((i) => [i.id, i.kind, i.state, truncate(i.name, 60), i.read?.length ? `read ${i.read[i.read.length - 1]} (strom readings ${i.read[i.read.length - 1]})` : ""])), moreLine(page, "strom input list")) : 'no inputs → strom intake <files or folder>',
|
|
254
|
+
data: { total: all.length, inputs: opts.full ? page.items : page.items.map((i) => ({ id: i.id, kind: i.kind, state: i.state, name: i.name, ...(i.read?.length ? { read: i.read } : {}) })) },
|
|
253
255
|
};
|
|
254
256
|
},
|
|
255
257
|
}, {
|
|
@@ -263,10 +265,10 @@ register({
|
|
|
263
265
|
const i = requireRecord(tree, args[0], "input");
|
|
264
266
|
const abs = inputPath(tree, i);
|
|
265
267
|
const tasks = tree.list("task").filter((t) => t.where.includes(i.id));
|
|
266
|
-
const text = lines(`${i.id} ${i.name} [${i.kind} · ${i.state}]`, abs ? `file ${abs}` : undefined, i.from ? `from ${i.from}` : undefined, i.batch ? `batch ${i.batch}${i.path ? ` · ${i.path}` : ""}` : undefined, i.persons?.length ? `of ${i.persons.join(", ")}` : undefined, i.sorted ? `sorted ${i.sorted.as}${i.sorted.source ? ` → ${i.sorted.source}` : ""}${i.sorted.reason ? ` (${i.sorted.reason})` : ""}` : undefined, i.mime ? `type ${i.mime}${i.size !== undefined ? ` · ${Math.round(i.size / 1024)} kB` : ""}` : undefined, i.imported ? `import ${i.imported.persons} persons, ${i.imported.families} families as leads, cited as ${i.source}` : undefined, i.text ? `\n${i.text}` : undefined, !i.text && i.kind === "text" && abs && fs.existsSync(abs) && (i.size ?? 0) < 20_000 && /\.(txt|md|csv)$/i.test(abs) ? `\n${fs.readFileSync(abs, "utf8").trim()}` : undefined, tasks.length ? `\ntasks\n${table(tasks.map((t) => [` ${t.id}`, t.state, truncate(t.what, 60)]))}` : undefined, ...i.notes.map((n) => `note ${n.text}`), i.kind === "document" && abs
|
|
268
|
+
const text = lines(`${i.id} ${i.name} [${i.kind} · ${i.state}]`, abs ? `file ${abs}` : undefined, i.from ? `from ${i.from}` : undefined, i.batch ? `batch ${i.batch}${i.path ? ` · ${i.path}` : ""}` : undefined, i.persons?.length ? `of ${i.persons.join(", ")}` : undefined, i.sorted ? `sorted ${i.sorted.as}${i.sorted.source ? ` → ${i.sorted.source}` : ""}${i.sorted.reason ? ` (${i.sorted.reason})` : ""}` : undefined, i.mime ? `type ${i.mime}${i.size !== undefined ? ` · ${Math.round(i.size / 1024)} kB` : ""}` : undefined, i.imported ? `import ${i.imported.persons} persons, ${i.imported.families} families as leads, cited as ${i.source}` : undefined, i.read?.length ? `read ${readingsLine(tree, i)}` : undefined, i.text ? `\n${i.text}` : undefined, !i.text && i.kind === "text" && abs && fs.existsSync(abs) && (i.size ?? 0) < 20_000 && /\.(txt|md|csv)$/i.test(abs) ? `\n${fs.readFileSync(abs, "utf8").trim()}` : undefined, tasks.length ? `\ntasks\n${table(tasks.map((t) => [` ${t.id}`, t.state, truncate(t.what, 60)]))}` : undefined, ...i.notes.map((n) => `note ${n.text}`), i.kind === "document" && abs && !readingsOf(tree, i).some((r) => r.state === "done")
|
|
267
269
|
? `\nread the file (a big image: strom media view ${i.id} --grid, then --crop), then record what it says: strom source add … --input ${i.id}`
|
|
268
270
|
: undefined);
|
|
269
|
-
return { text, data: { input: i, path: abs, tasks: tasks.map((t) => t.id) } };
|
|
271
|
+
return { text, data: { input: i, path: abs, tasks: tasks.map((t) => t.id), ...(i.read?.length ? { readings: readingsOf(tree, i) } : {}) } };
|
|
270
272
|
},
|
|
271
273
|
}, {
|
|
272
274
|
path: ["input", "done"],
|
package/dist/commands/output.js
CHANGED
|
@@ -13,7 +13,7 @@ import { UsageError } from "../core/errors.js";
|
|
|
13
13
|
import { excerptSettings, planExcerpts } from "../core/excerpt.js";
|
|
14
14
|
import { runGit, tracked } from "../core/git.js";
|
|
15
15
|
import { Settings } from "../core/config.js";
|
|
16
|
-
import { appKnowsArchive, appKnowsNoCouple, appShowsHypothesisLinks, appDecidesConflicts, appOpensLinks, appReadsTitles, appShowsCoupleEvents, appShowsEdges, appShowsSourceReads, appShowsFactStatus, appShowsStoryDrafts, appTurnsExcerpts } from "../core/stromapp.js";
|
|
16
|
+
import { appKnowsArchive, appKnowsNoCouple, appShowsHypothesisLinks, appShowsReadings, appDecidesConflicts, appOpensLinks, appReadsTitles, appShowsCoupleEvents, appShowsEdges, appShowsSourceReads, appShowsFactStatus, appShowsStoryDrafts, appTurnsExcerpts } from "../core/stromapp.js";
|
|
17
17
|
import { EXCERPT_QUALITIES, EXCERPT_SCOPES } from "../core/model.js";
|
|
18
18
|
import { ui } from "../cli/ui.js";
|
|
19
19
|
import { isAgent } from "../core/which.js";
|
|
@@ -91,7 +91,9 @@ export function writeTreeGed(tree, opts) {
|
|
|
91
91
|
const hypothesisLinks = opts.for === "strom" && appShowsHypothesisLinks(new Settings(tree.env, {}), opts.stromVersion);
|
|
92
92
|
// …and the sides of a conflict of the user's edit, for an app that decides it
|
|
93
93
|
const conflictSides = opts.for === "strom" && appDecidesConflicts(new Settings(tree.env, {}), opts.stromVersion);
|
|
94
|
-
|
|
94
|
+
// …and the reading of a document each source is an entry of, for an app that opens it
|
|
95
|
+
const readings = opts.for === "strom" && appShowsReadings(new Settings(tree.env, {}), opts.stromVersion);
|
|
96
|
+
const result = exportGedcom(tree, { for: opts.for, stromVersion: opts.stromVersion, ...(persons ? { persons } : {}), ...(images ? { excerpts: images.of } : {}), ...(clips ? { clips } : {}), ...(research ? { research } : {}), ...(edges ? { edges } : {}), ...(storyDrafts ? { storyDrafts } : {}), ...(coupleResi ? { coupleResi } : {}), ...(sourceReads ? { sourceReads } : {}), ...(factStatus ? { factStatus } : {}), ...(archive ? { archive } : {}), ...(turnsExcerpts ? { turnsExcerpts } : {}), ...(noCouple ? { noCouple } : {}), ...(titles ? { titles } : {}), ...(hypothesisLinks ? { hypothesisLinks } : {}), ...(conflictSides ? { conflictSides } : {}), ...(readings ? { readings } : {}) });
|
|
95
97
|
const findings = validateGedcom(result.text, opts.for === "standard" ? { strict: true } : {});
|
|
96
98
|
const file = opts.out ?? gedFile(tree, opts.for, opts.research);
|
|
97
99
|
if (!hasGedErrors(findings) && !tree.dryRun) {
|
|
@@ -9,6 +9,8 @@ import { counted, lines, runs, shellArg, truncate } from "../cli/format.js";
|
|
|
9
9
|
import { UsageError } from "../core/errors.js";
|
|
10
10
|
import { requireRecord } from "../core/records.js";
|
|
11
11
|
import { imageRange } from "../core/media.js";
|
|
12
|
+
import { listReadings, READ_ID } from "../core/document.js";
|
|
13
|
+
import { showReading } from "./document.js";
|
|
12
14
|
import { parseReport } from "../core/reader.js";
|
|
13
15
|
import { foldText } from "../core/text.js";
|
|
14
16
|
import { REDUCED_BELOW, viewRegion } from "../core/views.js";
|
|
@@ -396,20 +398,29 @@ register({
|
|
|
396
398
|
"blocks with nothing found only what the reader could not read (a possible match is often there), the gaps in\n" +
|
|
397
399
|
"the page numbering and the parts of the book they saw. One image's block word for word: --image N.\n" +
|
|
398
400
|
"Default: the newest reading; with --images, --image or --match all of them. Nothing is written.",
|
|
399
|
-
args: [{ name: "which", description: "a record set (B…),
|
|
401
|
+
args: [{ name: "which", description: "a record set (B…), a reading: its report's name or the start of it (a day), or a reading of a document (D…)" }],
|
|
400
402
|
options: [
|
|
401
403
|
{ name: "images", type: "string", value: "<from-to>", description: "only these image numbers, e.g. 40-69 or 5-113,130-228" },
|
|
402
404
|
{ name: "image", type: "string", value: "<n>", description: "that image's whole block, word for word (a few: 35-38)" },
|
|
403
405
|
{ name: "match", type: "string", multiple: true, value: "<words>", description: "only blocks that mention it — any spelling of accents (repeatable: variants)" },
|
|
404
406
|
{ name: "list", type: "boolean", description: "the readings there are, one line each" },
|
|
405
407
|
],
|
|
406
|
-
examples: ["strom readings", "strom readings B0001 --images 40-69 --match Novák --match Nowak", "strom readings B0001 --image 57", "strom readings --list"],
|
|
408
|
+
examples: ["strom readings", "strom readings B0001 --images 40-69 --match Novák --match Nowak", "strom readings B0001 --image 57", "strom readings D20260101120000-ab12", "strom readings --list"],
|
|
407
409
|
run(ctx, { args, opts }) {
|
|
408
410
|
const tree = ctx.tree();
|
|
411
|
+
const which = args[0]?.trim();
|
|
412
|
+
// a reading of a document (strom document read): its result, as strom document show says it
|
|
413
|
+
if (which && READ_ID.test(which))
|
|
414
|
+
return showReading(ctx, which);
|
|
409
415
|
let all = loadReadings(tree);
|
|
410
|
-
|
|
416
|
+
// the readings of documents, one line each, after those of the books (--list)
|
|
417
|
+
const documents = () => listReadings(tree.root).map((r) => ({ line: `${r.id} · document · ${r.state} · ${r.files.join(", ")} · ${counted(r.pages, "page")} — strom readings ${r.id}`, data: { id: r.id, state: r.state, files: r.files, pages: r.pages } }));
|
|
418
|
+
if (all.length === 0) {
|
|
419
|
+
const docs = opts.list ? documents() : [];
|
|
420
|
+
if (docs.length)
|
|
421
|
+
return { text: lines(...docs.map((d) => d.line)), data: { readings: [], documents: docs.map((d) => d.data) } };
|
|
411
422
|
return { text: "no readings yet (strom read writes them into notes/readings)", data: { readings: [] } };
|
|
412
|
-
|
|
423
|
+
}
|
|
413
424
|
let book;
|
|
414
425
|
if (which && /^[Bb]\d+$/.test(which))
|
|
415
426
|
book = requireRecord(tree, which, "recordset").id;
|
|
@@ -439,8 +450,9 @@ register({
|
|
|
439
450
|
if (shown.length === 0)
|
|
440
451
|
return { text: `no reader reported ${image !== undefined ? `image ${runs([...image])}` : "these images"}${book ? ` of ${book}` : ""} · strom readings --list`, data: { readings: [] } };
|
|
441
452
|
if (opts.list) {
|
|
442
|
-
const
|
|
443
|
-
|
|
453
|
+
const docs = which || ranges || image !== undefined || match.length ? [] : documents();
|
|
454
|
+
const text = lines(...shown.flatMap(({ r, blocks }) => readingHead(r, blocks)[0].concat(r.question ? ` · "${truncate(r.question, 60)}"` : "")), ...docs.map((d) => d.line));
|
|
455
|
+
return { text, data: { readings: shown.map(({ r, blocks }) => ({ stem: r.stem, question: r.question, reports: r.reports, images: blocks.map((b) => b.num) })), ...(docs.length ? { documents: docs.map((d) => d.data) } : {}) } };
|
|
444
456
|
}
|
|
445
457
|
if (image !== undefined) {
|
|
446
458
|
const text = lines(...shown.flatMap(({ r, blocks }) => blocks.flatMap((b) => [`${b.report}${b.recordset ? ` · ${b.recordset}` : ""}${r.question ? ` · question: ${truncate(r.question, 120)}` : ""}`, `## ${b.text}`, ""])));
|
package/dist/commands/sync.js
CHANGED
|
@@ -77,7 +77,7 @@ function newReads(c, incoming) {
|
|
|
77
77
|
return !!s && s.quay !== 0 && (incoming.transcripts === "evidence" || !!s.verified);
|
|
78
78
|
}
|
|
79
79
|
/** One change as the user reads it, with what strom does with it. */
|
|
80
|
-
function changeLine(tree, c, incoming, lang) {
|
|
80
|
+
function changeLine(tree, c, incoming, lang, readings) {
|
|
81
81
|
const name = (key) => {
|
|
82
82
|
if (!key)
|
|
83
83
|
return "?";
|
|
@@ -150,11 +150,16 @@ function changeLine(tree, c, incoming, lang) {
|
|
|
150
150
|
? "ui.sync.do.detail"
|
|
151
151
|
: c.kind === "fact.part" && (c.action === "add" || c.action === "user")
|
|
152
152
|
? `ui.sync.do.part${c.action === "user" ? ".user" : ""}`
|
|
153
|
-
: c.kind
|
|
154
|
-
? `ui.sync.do
|
|
155
|
-
: c.kind === "fact.
|
|
156
|
-
?
|
|
157
|
-
:
|
|
153
|
+
: c.kind === "source.new" && c.reading?.ok
|
|
154
|
+
? `ui.sync.do.source.new.reading${c.reading.edited ? ".edited" : ""}`
|
|
155
|
+
: c.kind.startsWith("source.") || c.kind === "fact.cite" || c.kind === "person.cite" || c.kind === "family.cite"
|
|
156
|
+
? `ui.sync.do.${c.kind}${c.reads || (c.kind === "source.new" && newReads(c, incoming)) ? ".read" : ""}`
|
|
157
|
+
: c.kind === "fact.new" && c.reads
|
|
158
|
+
? "ui.sync.do.reading"
|
|
159
|
+
// a fact on an entry of the research's reading of a document: as sure as its readers read it
|
|
160
|
+
: c.kind === "fact.new" && c.fact?.cites?.some((x) => readings?.get(x.source)?.ok)
|
|
161
|
+
? "ui.sync.do.reading.document"
|
|
162
|
+
: `ui.sync.do.${c.action}`;
|
|
158
163
|
// an archive: nobody reads a record there, nor is an agent told — said without it
|
|
159
164
|
const said = actsAsArchive(tree) && `${does}.archive` in UI ? `${does}.archive` : does;
|
|
160
165
|
return `${ui(lang, key, v)} → ${ui(lang, said)}`;
|
|
@@ -163,7 +168,7 @@ function planText(tree, plan, incoming, file, lang, edits) {
|
|
|
163
168
|
const shown = file.includes(" ") ? `"${file}"` : file;
|
|
164
169
|
if (!plan.changes.length)
|
|
165
170
|
return ui(lang, "ui.sync.nothing", { file: path.basename(file) });
|
|
166
|
-
return lines(ui(lang, "ui.sync.title", { file: path.basename(file), n: plan.changes.length }), plan.base ? ui(lang, "ui.sync.base", { head: plan.head?.slice(0, 8) ?? incoming.since ?? "" }) : ui(lang, plan.head ? "ui.sync.headmissing" : "ui.sync.nobase", { head: plan.head?.slice(0, 8) ?? "" }), plan.partial ? ui(lang, "ui.sync.partial") : undefined, plan.identity.strangers.length ? ui(lang, "ui.sync.strangers", { ids: plan.identity.strangers.join(", ") }) : undefined, "", ...plan.changes.map((c) => ` ${changeLine(tree, c, incoming, lang)}`), "",
|
|
171
|
+
return lines(ui(lang, "ui.sync.title", { file: path.basename(file), n: plan.changes.length }), plan.base ? ui(lang, "ui.sync.base", { head: plan.head?.slice(0, 8) ?? incoming.since ?? "" }) : ui(lang, plan.head ? "ui.sync.headmissing" : "ui.sync.nobase", { head: plan.head?.slice(0, 8) ?? "" }), plan.partial ? ui(lang, "ui.sync.partial") : undefined, plan.identity.strangers.length ? ui(lang, "ui.sync.strangers", { ids: plan.identity.strangers.join(", ") }) : undefined, "", ...plan.changes.map((c) => ` ${changeLine(tree, c, incoming, lang, plan.readings)}`), "",
|
|
167
172
|
// an archive mirrors the app whatever the setting says (planSync)
|
|
168
173
|
actsAsArchive(tree) ? ui(lang, "ui.sync.edits.archive") : ui(lang, "ui.sync.edits", { mode: ui(lang, edits === "user" ? "ui.sync.mode.user" : "ui.sync.mode.conflict") }), ui(lang, "ui.sync.next", { file: shown }));
|
|
169
174
|
}
|
package/dist/core/live.js
CHANGED
|
@@ -112,7 +112,7 @@ import { knownNewerVersion, updateChannel } from "./update.js";
|
|
|
112
112
|
import { whatsNew } from "./whatsnew.js";
|
|
113
113
|
import { gitProgram, runGit } from "./git.js";
|
|
114
114
|
import { RECENT_HOURS, recentChanges, recentNow } from "./recent.js";
|
|
115
|
-
import { appKnowsArchive, appKnowsNoCouple, appShowsHypothesisLinks, appDecidesConflicts, appOpensLinks, appReadsTitles, appShowsCoupleEvents, appShowsEdges, appShowsFactStatus, appShowsSourceReads, appShowsStoryDrafts, appTurnsExcerpts, appUrlSetting, isAppVersion, isStromAppOrigin } from "./stromapp.js";
|
|
115
|
+
import { appKnowsArchive, appKnowsNoCouple, appShowsHypothesisLinks, appShowsReadings, appDecidesConflicts, appOpensLinks, appReadsTitles, appShowsCoupleEvents, appShowsEdges, appShowsFactStatus, appShowsSourceReads, appShowsStoryDrafts, appTurnsExcerpts, appUrlSetting, isAppVersion, isStromAppOrigin } from "./stromapp.js";
|
|
116
116
|
import { Settings } from "./config.js";
|
|
117
117
|
import { LINK_SCHEME, linkActions, linkHandlerState, linkHandlerStateLater, linkScheme } from "./links.js";
|
|
118
118
|
import { autoTidy } from "./tidy.js";
|
|
@@ -702,7 +702,7 @@ function idsOf(applied, known) {
|
|
|
702
702
|
return Object.keys(ids.persons).length || Object.keys(ids.sources).length ? { ids } : {};
|
|
703
703
|
}
|
|
704
704
|
/** What the bridge does that an app may ask about (each added once, never taken away). */
|
|
705
|
-
export const BRIDGE_FEATURES = ["sync.again", "sync.undoneSince", "sync.takenBack", "sync.conflictEdit", "sync.since", "sync.ids", "family.noCouple", "family.alone", "adopt.transfer", "adopt.empty", "material.list", "person.titles", "media.codes", "hypothesis.links", "status.recent", "spend.readers", "hypothesis.intree", "archive.permission", "task.check", "waiting.groups", "waiting.sent", "waiting.note", "batch.kept", "live.end", "whats.new", "terminal.choice", "person.marriedSurname", "phone", "read.now"];
|
|
705
|
+
export const BRIDGE_FEATURES = ["sync.again", "sync.undoneSince", "sync.takenBack", "sync.conflictEdit", "sync.since", "sync.ids", "family.noCouple", "family.alone", "adopt.transfer", "adopt.empty", "material.list", "person.titles", "media.codes", "hypothesis.links", "status.recent", "spend.readers", "hypothesis.intree", "archive.permission", "task.check", "waiting.groups", "waiting.sent", "waiting.note", "batch.kept", "live.end", "whats.new", "terminal.choice", "person.marriedSurname", "phone", "read.now", "read.sync"];
|
|
706
706
|
/** /status recent, when it is ready. */
|
|
707
707
|
function recentOf(root, env, head) {
|
|
708
708
|
const recent = head ? recentNow(root, env, head) : undefined;
|
|
@@ -1364,12 +1364,13 @@ export function serveLive(root, env) {
|
|
|
1364
1364
|
const titles = appReadsTitles(new Settings(env, {}), version);
|
|
1365
1365
|
const hypothesisLinks = appShowsHypothesisLinks(new Settings(env, {}), version);
|
|
1366
1366
|
const conflictSides = appDecidesConflicts(new Settings(env, {}), version);
|
|
1367
|
+
const readings = appShowsReadings(new Settings(env, {}), version);
|
|
1367
1368
|
const offered = opens ? links(env) : [];
|
|
1368
1369
|
ged = {
|
|
1369
1370
|
head: h,
|
|
1370
1371
|
links: offered.join(" "),
|
|
1371
1372
|
version: version ?? "",
|
|
1372
|
-
text: exportGedcom(tree, { for: "strom", ...(h ? { head: h } : {}), ...(images ? { excerpts: images.of } : {}), ...(opens ? { clips: true, research: true } : {}), ...(edges ? { edges } : {}), ...(storyDrafts ? { storyDrafts } : {}), ...(coupleResi ? { coupleResi } : {}), ...(sourceReads ? { sourceReads } : {}), ...(factStatus ? { factStatus } : {}), ...(archive ? { archive } : {}), ...(turnsExcerpts ? { turnsExcerpts } : {}), ...(noCouple ? { noCouple } : {}), ...(titles ? { titles } : {}), ...(hypothesisLinks ? { hypothesisLinks } : {}), ...(conflictSides ? { conflictSides } : {}), ...(offered.length ? { links: offered, ...(linkScheme(env) !== LINK_SCHEME ? { linkScheme: linkScheme(env) } : {}) } : {}) }).text,
|
|
1373
|
+
text: exportGedcom(tree, { for: "strom", ...(h ? { head: h } : {}), ...(images ? { excerpts: images.of } : {}), ...(opens ? { clips: true, research: true } : {}), ...(edges ? { edges } : {}), ...(storyDrafts ? { storyDrafts } : {}), ...(coupleResi ? { coupleResi } : {}), ...(sourceReads ? { sourceReads } : {}), ...(factStatus ? { factStatus } : {}), ...(archive ? { archive } : {}), ...(turnsExcerpts ? { turnsExcerpts } : {}), ...(noCouple ? { noCouple } : {}), ...(titles ? { titles } : {}), ...(hypothesisLinks ? { hypothesisLinks } : {}), ...(conflictSides ? { conflictSides } : {}), ...(readings ? { readings } : {}), ...(offered.length ? { links: offered, ...(linkScheme(env) !== LINK_SCHEME ? { linkScheme: linkScheme(env) } : {}) } : {}) }).text,
|
|
1373
1374
|
};
|
|
1374
1375
|
}
|
|
1375
1376
|
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store", "X-Strom-Head": h }).end(ged.text);
|
|
@@ -1701,6 +1702,11 @@ export function serveLive(root, env) {
|
|
|
1701
1702
|
: {}),
|
|
1702
1703
|
// the changes the research could not write (the rest is written): what and why
|
|
1703
1704
|
...(data.skipped?.length ? { skipped: data.skipped } : {}),
|
|
1705
|
+
// the sources that said they are an entry of a reading of a document (_STROM_READING): taken as the
|
|
1706
|
+
// research's reading, or not (why: a stable code) and so the app's own source as any
|
|
1707
|
+
...(changes.some((c) => c.reading)
|
|
1708
|
+
? { readings: changes.filter((c) => c.reading).map((c) => ({ source: c.source.startsWith("x:") ? `@${c.source.slice(2)}@` : c.source, read: c.reading.id, ok: c.reading.ok, ...(c.reading.entry ? { entry: c.reading.entry } : {}), ...(c.reading.code ? { code: c.reading.code } : {}), ...(c.reading.edited ? { edited: true } : {}) })) }
|
|
1709
|
+
: {}),
|
|
1704
1710
|
head: head(root),
|
|
1705
1711
|
...(data.input ? { input: data.input } : {}),
|
|
1706
1712
|
conflicts,
|
package/dist/core/phrases.js
CHANGED
|
@@ -60,6 +60,15 @@ export const PHRASES = {
|
|
|
60
60
|
"sync.reading.note": "The user read this record otherwise in the Strom app ({source}); the research's reading stays until it is read again:",
|
|
61
61
|
"sync.reading.page": "page",
|
|
62
62
|
"sync.reading.text": "transcript",
|
|
63
|
+
"read.note.source.edited": "The transcript was changed in the Strom app; the reading {read} has: {text}",
|
|
64
|
+
"read.note.edited": "The transcript was changed in the Strom app: a lead until the record is read ({read})",
|
|
65
|
+
"read.note.one": "Read by one reader ({read}): possible until a second, blind reading and the research's own look at the record",
|
|
66
|
+
"read.note.differ": "The readers of {read} read it apart: {list} — a lead until the record is read",
|
|
67
|
+
"read.note.absent": "The reading {read} does not say it so: a lead until the record is read",
|
|
68
|
+
"read.note.uncertain": "The reader of {read} was unsure of it: a lead until the record is read",
|
|
69
|
+
"read.verify.what": "Check the reading of a document for {name}: {list}",
|
|
70
|
+
"read.verify.why": "the facts stand on a reading by strom's readers ({read}, confirmed in the Strom app, {source}) not sure yet: one reader, readers apart, or the transcript changed by hand; a second, blind reading and the research's own look at the record prove them or show a mistake",
|
|
71
|
+
"read.verify.done": "after one reader a second, blind one (strom document again {read} --readers 2), and the entry looked at by the research itself (strom source show {source}, its image): the readings agree → probable, proven only after that look and only for primary information; they differ → a conflict with both readings",
|
|
63
72
|
"archive.held": "kept for later: the research is a Strom archive (the data come from the Strom app); back in the queue when work with an agent is switched on",
|
|
64
73
|
"archive.removed": "removed in the Strom app",
|
|
65
74
|
"archive.wait": "Decision on downloading from the archive {title}",
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// A reading of a document confirmed in the Strom app (spec 013 U4d): the app writes what the person took of a reading
|
|
2
|
+
// into its tree, one source per entry carrying the reading's mark (_STROM_READING D…, the original's _STROM_SHA), and
|
|
3
|
+
// sends it as any edit. Here the mark is checked against the reading the research keeps, the source is made the
|
|
4
|
+
// research's own (its input, images, the entry's excerpt) and each fact citing it gets its status by what the readers
|
|
5
|
+
// read — never by what the app says.
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { buildResult, loadState, READ_ID, resultPath } from "./document.js";
|
|
9
|
+
import { normalizeDate } from "./gdate.js";
|
|
10
|
+
import { Settings } from "./config.js";
|
|
11
|
+
import { exifOrientation, imageSizeOfFile } from "../image/index.js";
|
|
12
|
+
import { shownToFile } from "../image/image.js";
|
|
13
|
+
import { inputPath, mimeOf, storeShared } from "./media.js";
|
|
14
|
+
import { create, update } from "./records.js";
|
|
15
|
+
import { foldText } from "./text.js";
|
|
16
|
+
/** The reading's result as the research keeps it: its finished result (in git), else its state here when done. */
|
|
17
|
+
export function readingResult(tree, id) {
|
|
18
|
+
if (!READ_ID.test(id))
|
|
19
|
+
return undefined;
|
|
20
|
+
try {
|
|
21
|
+
const file = resultPath(tree.root, id);
|
|
22
|
+
if (fs.existsSync(file))
|
|
23
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// an unreadable result is none
|
|
27
|
+
}
|
|
28
|
+
const s = loadState(tree.root, id);
|
|
29
|
+
return s?.state === "done" ? buildResult(tree, s) : undefined;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The readings of an input (Input.read) as an agent is told of them: each one's ID and state here (its state on this
|
|
33
|
+
* computer, else its result kept in git) — one read already is not read again, its result is strom readings D….
|
|
34
|
+
*/
|
|
35
|
+
export function readingsOf(tree, input) {
|
|
36
|
+
return (input.read ?? []).map((id) => ({ id, state: loadState(tree.root, id)?.state ?? readingResult(tree, id)?.state ?? "gone" }));
|
|
37
|
+
}
|
|
38
|
+
/** One line of an input's readings for an agent: "D… done — strom readings D…, not read again", or nothing. */
|
|
39
|
+
export function readingsLine(tree, input) {
|
|
40
|
+
const all = readingsOf(tree, input);
|
|
41
|
+
if (!all.length)
|
|
42
|
+
return undefined;
|
|
43
|
+
const done = all.filter((r) => r.state === "done");
|
|
44
|
+
return `${all.map((r) => `${r.id} ${r.state}`).join(", ")} — ${done.length ? `read already: strom readings ${done[done.length - 1].id}; not read again` : `strom readings ${all[all.length - 1].id}`}`;
|
|
45
|
+
}
|
|
46
|
+
/** The words of a transcript as they are compared: NFC, one space, the line breaks (and the reading's " | ") as spaces. */
|
|
47
|
+
// the reading writes a line break of the record as " | ", the app may send it as a new line: the same
|
|
48
|
+
const plain = (t) => (t ?? "").normalize("NFC").replace(/\s*\|\s*/gu, " ").replace(/\s+/gu, " ").trim();
|
|
49
|
+
const tokens = (t) => new Set(foldText(t).split(/[^\p{L}\p{M}\p{N}]+/u).filter(Boolean));
|
|
50
|
+
/** How much of two transcripts is the same words (0–1). */
|
|
51
|
+
function likeness(a, b) {
|
|
52
|
+
const x = tokens(a);
|
|
53
|
+
const y = tokens(b);
|
|
54
|
+
if (!x.size || !y.size)
|
|
55
|
+
return 0;
|
|
56
|
+
let both = 0;
|
|
57
|
+
for (const w of x)
|
|
58
|
+
if (y.has(w))
|
|
59
|
+
both++;
|
|
60
|
+
return (2 * both) / (x.size + y.size);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Whether the mark is the research's reading: it exists and is read, one of its pages is the original the source
|
|
64
|
+
* names (by its content), and the source is one entry of it — by the entry the app names, else its words. The
|
|
65
|
+
* person's words changed since are theirs (edited); the source is still that entry.
|
|
66
|
+
*/
|
|
67
|
+
export function checkReading(tree, mark, text) {
|
|
68
|
+
const id = mark.id;
|
|
69
|
+
const no = (code) => ({ id, ok: false, code });
|
|
70
|
+
if (!READ_ID.test(id))
|
|
71
|
+
return no("reading.none");
|
|
72
|
+
const result = readingResult(tree, id);
|
|
73
|
+
if (!result)
|
|
74
|
+
return loadState(tree.root, id) ? no("reading.state") : no("reading.none");
|
|
75
|
+
if (result.state !== "done")
|
|
76
|
+
return no("reading.state");
|
|
77
|
+
const shaOf = (p) => p.sha ?? tree.get(p.file)?.sha;
|
|
78
|
+
const pages = result.pages.filter((p) => mark.shas.includes(shaOf(p) ?? ""));
|
|
79
|
+
if (!pages.length)
|
|
80
|
+
return no("reading.sha");
|
|
81
|
+
const on = result.entries.filter((e) => pages.some((p) => p.n === e.page));
|
|
82
|
+
let entry;
|
|
83
|
+
if (mark.entry)
|
|
84
|
+
entry = on.find((e) => e.id === mark.entry);
|
|
85
|
+
else {
|
|
86
|
+
const words = plain(text);
|
|
87
|
+
entry = on.find((e) => plain(e.transcript) === words) ?? (on.length === 1 ? on[0] : undefined);
|
|
88
|
+
if (!entry && words) {
|
|
89
|
+
const best = on.map((e) => ({ e, k: likeness(e.transcript, words) })).sort((a, b) => b.k - a.k)[0];
|
|
90
|
+
if (best && best.k >= 0.6)
|
|
91
|
+
entry = best.e;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (!entry)
|
|
95
|
+
return no("reading.entry");
|
|
96
|
+
const page = result.pages.find((p) => p.n === entry.page);
|
|
97
|
+
const edited = !!plain(text) && plain(text) !== plain(entry.transcript);
|
|
98
|
+
return { id, ok: true, entry, page, result, ...(edited ? { edited: true } : {}) };
|
|
99
|
+
}
|
|
100
|
+
/** The kind of record a kind of document is. */
|
|
101
|
+
const KIND_OF_DOC = {
|
|
102
|
+
"register.baptism": "baptism",
|
|
103
|
+
"register.birth": "birth",
|
|
104
|
+
"register.marriage": "marriage",
|
|
105
|
+
"register.death": "death",
|
|
106
|
+
"register.burial": "burial",
|
|
107
|
+
"register.other": "church",
|
|
108
|
+
letter: "letter",
|
|
109
|
+
deed: "notarial",
|
|
110
|
+
certificate: "certificate",
|
|
111
|
+
list: "census",
|
|
112
|
+
"photo.back": "photo",
|
|
113
|
+
};
|
|
114
|
+
/** The source's kind and information by the kind of document the readers said it is. */
|
|
115
|
+
export function readingKind(result) {
|
|
116
|
+
const k = result.doc.kind ?? "";
|
|
117
|
+
const kind = KIND_OF_DOC[k];
|
|
118
|
+
// a register, a certificate, a deed: written at the time by who was there, for most of what it says
|
|
119
|
+
const primary = k.startsWith("register.") || k === "certificate" || k === "deed";
|
|
120
|
+
return { ...(kind ? { kind } : {}), information: primary ? "primary" : "unknown" };
|
|
121
|
+
}
|
|
122
|
+
const kindOf = (k) => (k === "CHR" ? "BAPM" : k.toUpperCase());
|
|
123
|
+
const firstPart = (s) => foldText((s ?? "").split(",")[0].replace(/\[\?\]/gu, ""));
|
|
124
|
+
/** Whether a place of the fact is the place the reader read (as written, or a place of the tree it is). */
|
|
125
|
+
function samePlace(tree, fact, read) {
|
|
126
|
+
if (!read)
|
|
127
|
+
return false;
|
|
128
|
+
const f = firstPart(fact);
|
|
129
|
+
if (!f)
|
|
130
|
+
return false;
|
|
131
|
+
if (firstPart(read.text) === f)
|
|
132
|
+
return true;
|
|
133
|
+
const known = read.known ? tree.get(read.known) : undefined;
|
|
134
|
+
return !!known && known.names.some((n) => firstPart(n.name) === f);
|
|
135
|
+
}
|
|
136
|
+
function sameDate(fact, read) {
|
|
137
|
+
if (!read)
|
|
138
|
+
return false;
|
|
139
|
+
const a = normalizeDate(fact) ?? plain(fact).toUpperCase();
|
|
140
|
+
const b = normalizeDate(read.gedcom ?? read.text) ?? plain(read.gedcom ?? read.text).toUpperCase();
|
|
141
|
+
return a === b;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* How sure a fact citing the reading is (the decision of 19 Oct 2026): the person changed the transcript, the
|
|
145
|
+
* reading does not say it, or its reader was unsure — a lead; read by one reader — possible (a check waits); read
|
|
146
|
+
* alike by two readers, blind — probable. Proven only after the research's own look at the record, never here.
|
|
147
|
+
*/
|
|
148
|
+
export function readingGrade(tree, check, fact) {
|
|
149
|
+
const e = check.entry;
|
|
150
|
+
if (check.edited)
|
|
151
|
+
return { status: "lead", why: "edited" };
|
|
152
|
+
const kind = kindOf(fact.kind);
|
|
153
|
+
const two = e.agree !== undefined;
|
|
154
|
+
const varied = (fields) => (e.variants ?? []).filter((v) => fields.includes(v.field));
|
|
155
|
+
// what a person of it is: an occupation, a residence
|
|
156
|
+
if (kind === "OCCU" || kind === "RESI") {
|
|
157
|
+
const said = foldText(fact.value ?? fact.place ?? "");
|
|
158
|
+
const field = kind === "OCCU" ? "occupation" : "residence";
|
|
159
|
+
const p = e.persons.find((x) => {
|
|
160
|
+
const v = foldText(x[field] ?? "");
|
|
161
|
+
return !!v && !!said && (v === said || v.includes(said) || said.includes(v));
|
|
162
|
+
});
|
|
163
|
+
if (!p)
|
|
164
|
+
return { status: "lead", why: "absent" };
|
|
165
|
+
if (p.uncertain?.includes(field))
|
|
166
|
+
return { status: "lead", why: "uncertain" };
|
|
167
|
+
if (!two)
|
|
168
|
+
return { status: "possible", why: "one" };
|
|
169
|
+
const words = tokens(p[field] ?? "");
|
|
170
|
+
const other = (e.variants ?? []).filter((v) => v.field === "entry" || (v.field === "transcript" && [...tokens(v.at)].some((w) => words.has(w))));
|
|
171
|
+
return other.length ? { status: "lead", why: "differ", variants: other.map(({ at, other: o }) => ({ at, other: o })) } : { status: "probable", why: "two" };
|
|
172
|
+
}
|
|
173
|
+
const events = e.events.filter((x) => kindOf(x.type) === kind);
|
|
174
|
+
const ev = events.find((x) => (!fact.date || sameDate(fact.date, x.date)) && (!fact.place || samePlace(tree, fact.place, x.place)));
|
|
175
|
+
if (!ev)
|
|
176
|
+
return { status: "lead", why: "absent" };
|
|
177
|
+
const asked = [fact.date ? "date" : "", fact.place ? "place" : ""].filter(Boolean);
|
|
178
|
+
if (asked.some((f) => ev.uncertain?.includes(f)))
|
|
179
|
+
return { status: "lead", why: "uncertain" };
|
|
180
|
+
if (!two)
|
|
181
|
+
return { status: "possible", why: "one" };
|
|
182
|
+
const other = varied(["entry", ev.type, ...asked.map((f) => `${ev.type}.${f}`)]);
|
|
183
|
+
return other.length ? { status: "lead", why: "differ", variants: other.map(({ at, other: o }) => ({ at, other: o })) } : { status: "probable", why: "two" };
|
|
184
|
+
}
|
|
185
|
+
const RANK = { lead: 0, possible: 1, probable: 2 };
|
|
186
|
+
/** The best of the grades of a fact's readings (cited twice: the surer one counts). */
|
|
187
|
+
export function bestGrade(grades) {
|
|
188
|
+
return grades.reduce((a, g) => (!a || RANK[g.status] > RANK[a.status] ? g : a), undefined);
|
|
189
|
+
}
|
|
190
|
+
/** Whether a status is surer than another (proven and the rest above all a reading gives). */
|
|
191
|
+
export function surer(a, than) {
|
|
192
|
+
const r = (s) => (s && s in RANK ? RANK[s] : s === "proven" ? 3 : s === "disproven" ? -1 : 0);
|
|
193
|
+
return r(a) > r(than);
|
|
194
|
+
}
|
|
195
|
+
export function readingSource(check, text) {
|
|
196
|
+
const e = check.entry;
|
|
197
|
+
const r = check.result;
|
|
198
|
+
const { kind, information } = readingKind(r);
|
|
199
|
+
const input = check.page?.file.startsWith("I") ? check.page.file : undefined;
|
|
200
|
+
return {
|
|
201
|
+
...(kind ? { kind } : {}),
|
|
202
|
+
information,
|
|
203
|
+
form: "original",
|
|
204
|
+
// the person's words where they changed them: theirs stand, the reading's beside them (a note)
|
|
205
|
+
transcript: check.edited && text ? text.trim() : e.transcript,
|
|
206
|
+
...(e.translation ? { translation: e.translation } : {}),
|
|
207
|
+
...(r.doc.language?.[0] ? { language: r.doc.language[0] } : {}),
|
|
208
|
+
...(input ? { input } : {}),
|
|
209
|
+
reading: check.id,
|
|
210
|
+
readingEntry: e.id,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const CLIPPED = new Set(["image/jpeg", "image/png"]);
|
|
214
|
+
/**
|
|
215
|
+
* The reading's page the entry is on as the source's image (a picture: registered by its content, the one there
|
|
216
|
+
* already when it is), the entry's region of it as the source's excerpt, and the input sorted as the source.
|
|
217
|
+
*/
|
|
218
|
+
export function takeReadingImages(tree, source, check, reason) {
|
|
219
|
+
const out = {};
|
|
220
|
+
const page = check.page;
|
|
221
|
+
const entry = check.entry;
|
|
222
|
+
const rec = tree.get(page.file);
|
|
223
|
+
let media = rec?.type === "media" ? rec : undefined;
|
|
224
|
+
const input = rec?.type === "input" ? rec : undefined;
|
|
225
|
+
if (input) {
|
|
226
|
+
const sha = input.sha;
|
|
227
|
+
media = sha ? tree.list("media").find((m) => !m.retracted && m.sha === sha) : undefined;
|
|
228
|
+
const file = inputPath(tree, input);
|
|
229
|
+
const mime = input.mime ?? mimeOf(input.name);
|
|
230
|
+
const shared = new Settings(tree.env, {}).shared()?.value;
|
|
231
|
+
// a picture of one page (a PDF's pages are no image of their own): registered as the record's image
|
|
232
|
+
if (!media && sha && shared && file && fs.existsSync(file) && mime.startsWith("image/") && page.page <= 1) {
|
|
233
|
+
const rel = input.file.startsWith("media:") ? input.file.slice(6) : path.relative(shared, tree.dryRun ? file : storeShared(shared, file, sha)).split(path.sep).join("/");
|
|
234
|
+
const size = imageSizeOfFile(file);
|
|
235
|
+
media = create(tree, "media", {
|
|
236
|
+
sha,
|
|
237
|
+
file: rel,
|
|
238
|
+
mime,
|
|
239
|
+
size: input.size ?? fs.statSync(file).size,
|
|
240
|
+
...(size ? { width: size.width, height: size.height } : {}),
|
|
241
|
+
from: input.name,
|
|
242
|
+
accessed: new Date().toISOString().slice(0, 10),
|
|
243
|
+
}, (id) => `+${id} image "${input.name.slice(0, 50)}" of ${source.id} (read ${check.id})`, [source.id, input.id]);
|
|
244
|
+
out.added = media.id;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (media) {
|
|
248
|
+
out.media = media.id;
|
|
249
|
+
const shared = new Settings(tree.env, {}).shared()?.value;
|
|
250
|
+
const file = shared ? path.join(shared, media.file) : undefined;
|
|
251
|
+
// the entry where it lies in the file: the reading's region is of the page upright
|
|
252
|
+
const r = entry.region;
|
|
253
|
+
const clip = r && CLIPPED.has(media.mime) && !source.clips?.length ? { media: media.id, region: shownToFile({ x: r[0], y: r[1], w: r[2], h: r[3] }, file && fs.existsSync(file) ? exifOrientation(file) : undefined) } : undefined;
|
|
254
|
+
if (clip)
|
|
255
|
+
out.clip = clip;
|
|
256
|
+
update(tree, source.id, "source", (x) => ({ ...x, media: [...new Set([...(x.media ?? []), media.id])], ...(clip ? { clips: [clip] } : {}) }), {
|
|
257
|
+
op: "source.media",
|
|
258
|
+
summary: `${source.id} image ${media.id}${clip ? " with the part the entry is on" : ""} (${reason})`,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
if (input && input.state !== "processed") {
|
|
262
|
+
out.input = { id: input.id, before: { state: input.state, ...(input.sorted ? { sorted: input.sorted } : {}) } };
|
|
263
|
+
update(tree, input.id, "input", (x) => ({ ...x, state: "processed", sorted: { as: "source", source: source.id, reason: `read ${check.id}, confirmed in the Strom app`, at: new Date().toISOString(), by: tree.actor } }), {
|
|
264
|
+
op: "input.sort",
|
|
265
|
+
summary: `${input.id} the source ${source.id} (read ${check.id}, ${reason})`,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return out;
|
|
269
|
+
}
|
package/dist/core/review.js
CHANGED
|
@@ -13,6 +13,7 @@ import { birthEvent, displayName, familiesAsPartner, lifespan } from "./people.j
|
|
|
13
13
|
import { phrase } from "./phrases.js";
|
|
14
14
|
import { foldText } from "./text.js";
|
|
15
15
|
import { weighedSources } from "./conflicts.js";
|
|
16
|
+
import { loadState } from "./document.js";
|
|
16
17
|
import { parseYears } from "./years.js";
|
|
17
18
|
export const REVIEW_KINDS = ["mentions", "entries", "verify", "reread", "conflicts", "marriage", "death"];
|
|
18
19
|
/** At most so many records, entries or facts in one task: a session's worth. */
|
|
@@ -122,6 +123,15 @@ export function readersOf(tree) {
|
|
|
122
123
|
for (const [id, app] of fromApp)
|
|
123
124
|
if (app.read)
|
|
124
125
|
out.set(id, new Set(["user"]));
|
|
126
|
+
// an entry of a reading of a document the app sent back: its readers read it (their model), not whoever ran the sync
|
|
127
|
+
const readings = new Set();
|
|
128
|
+
for (const s of tree.list("source")) {
|
|
129
|
+
if (!s.reading)
|
|
130
|
+
continue;
|
|
131
|
+
readings.add(s.id);
|
|
132
|
+
if (!out.has(s.id))
|
|
133
|
+
out.set(s.id, new Set([loadState(tree.root, s.reading)?.model ?? "unknown"]));
|
|
134
|
+
}
|
|
125
135
|
for (const op of tree.readOps()) {
|
|
126
136
|
if (op.op !== "source.add" && op.op !== "source.edit")
|
|
127
137
|
continue;
|
|
@@ -129,7 +139,7 @@ export function readersOf(tree) {
|
|
|
129
139
|
for (const id of op.targets) {
|
|
130
140
|
if (!id.startsWith("S"))
|
|
131
141
|
continue;
|
|
132
|
-
if (fromApp.has(id) && (op.op === "source.add" || !model.has(op.by)))
|
|
142
|
+
if ((fromApp.has(id) || readings.has(id)) && (op.op === "source.add" || !model.has(op.by)))
|
|
133
143
|
continue;
|
|
134
144
|
(out.get(id) ?? out.set(id, new Set()).get(id)).add(who);
|
|
135
145
|
}
|
package/dist/core/stromapp.js
CHANGED
|
@@ -373,6 +373,27 @@ export function appShowsFactStatus(settings, version) {
|
|
|
373
373
|
return (a[i] ?? 0) > (b[i] ?? 0);
|
|
374
374
|
return true;
|
|
375
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* The first Strom app that opens a reading of a document from its source (spec 013 U6): each source the research made
|
|
378
|
+
* of a reading confirmed in the app carries 1 _STROM_READING D… (2 _STROM_ENTRY e…). An older app lists the tag as one
|
|
379
|
+
* it does not know and loses nothing, so an unknown version gets it once the app is out. Not released yet: set with
|
|
380
|
+
* the app's U6 — never to a beta version; beta/dev copies of the app get it at once.
|
|
381
|
+
*/
|
|
382
|
+
export const APP_SHOWS_READINGS = undefined;
|
|
383
|
+
export function appShowsReadings(settings, version) {
|
|
384
|
+
if (anotherCopy(settings))
|
|
385
|
+
return true;
|
|
386
|
+
if (!APP_SHOWS_READINGS)
|
|
387
|
+
return false;
|
|
388
|
+
if (!version)
|
|
389
|
+
return true;
|
|
390
|
+
const n = (v) => v.split(".").map((x) => Number.parseInt(x, 10) || 0);
|
|
391
|
+
const [a, b] = [n(version), n(APP_SHOWS_READINGS)];
|
|
392
|
+
for (let i = 0; i < 3; i++)
|
|
393
|
+
if ((a[i] ?? 0) !== (b[i] ?? 0))
|
|
394
|
+
return (a[i] ?? 0) > (b[i] ?? 0);
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
376
397
|
/**
|
|
377
398
|
* The Strom app shows what each variant of an open hypothesis would connect (the app's spec
|
|
378
399
|
* docs/ZADANI_VYZKUM_nejista-spojeni.md, phase 1): 2 _VAR under each 1 _STROM_HYPO with its claim (3 TITL), its links
|
package/dist/core/sync.js
CHANGED
|
@@ -42,6 +42,7 @@ import { now, Tree, typeOfId } from "./tree.js";
|
|
|
42
42
|
import { actsAsArchive } from "./state.js";
|
|
43
43
|
import { appSides } from "./conflicts.js";
|
|
44
44
|
import { APP_DATA_SEX_UNKNOWN, isAppVersion } from "./stromapp.js";
|
|
45
|
+
import { bestGrade, checkReading, readingGrade, readingResult, readingSource, surer, takeReadingImages } from "./readsync.js";
|
|
45
46
|
import { cleanTitle, familiesAsPartner, formatName, foundTitles, gedcomTitledName, parseName, primaryName, sameName as sameWords, titledName } from "./people.js";
|
|
46
47
|
/** The words of a _STAT (the Strom app's own, also Legacy's and FTM's) as a union, or nothing it names. */
|
|
47
48
|
function unionOfStat(value) {
|
|
@@ -454,6 +455,19 @@ export function stampAppVersion(text, version) {
|
|
|
454
455
|
lines.splice(sour + 1, 0, `2 VERS ${version}`);
|
|
455
456
|
return lines.join(eol);
|
|
456
457
|
}
|
|
458
|
+
/**
|
|
459
|
+
* The reading a source of the app is an entry of: 1 _STROM_READING D… (2 _STROM_ENTRY e…), and the originals its
|
|
460
|
+
* excerpts are cut from (2 _STROM_SHA under its OBJE, or 1 _STROM_SHA of its own).
|
|
461
|
+
*/
|
|
462
|
+
function readingMarkOf(r) {
|
|
463
|
+
const n = r.children.find((c) => c.tag === "_STROM_READING");
|
|
464
|
+
const id = n?.value.trim();
|
|
465
|
+
if (!n || !id)
|
|
466
|
+
return {};
|
|
467
|
+
const entry = val(n, "_STROM_ENTRY")?.trim();
|
|
468
|
+
const shas = [...children(r, "_STROM_SHA"), ...children(r, "OBJE").flatMap((o) => children(o, "_STROM_SHA"))].map((x) => x.value.trim().toLowerCase()).filter(Boolean);
|
|
469
|
+
return { reading: { id, ...(entry ? { entry } : {}), shas: [...new Set(shas)] } };
|
|
470
|
+
}
|
|
457
471
|
/** A GEDCOM file (or one of ours, in memory) as a snapshot; an unreadable one throws. */
|
|
458
472
|
export function readGedcom(text) {
|
|
459
473
|
const { records, problems } = parseGedcomText(text);
|
|
@@ -485,6 +499,7 @@ export function readGedcom(text) {
|
|
|
485
499
|
...(url ? { url } : {}),
|
|
486
500
|
...(note ? { note } : {}),
|
|
487
501
|
...(/^y(es)?$/i.test(val(r, "_STROM_VERIFIED")?.trim() ?? "") ? { verified: true } : {}),
|
|
502
|
+
...readingMarkOf(r),
|
|
488
503
|
});
|
|
489
504
|
}
|
|
490
505
|
const cite = (n) => {
|
|
@@ -710,6 +725,15 @@ export function readStromJson(data) {
|
|
|
710
725
|
...(str(a.note) ? { note: str(a.note) } : {}),
|
|
711
726
|
...(q !== undefined ? { quay: q } : {}),
|
|
712
727
|
...(a.transcriptVerified === true ? { verified: true } : {}),
|
|
728
|
+
...(str(a.reading)
|
|
729
|
+
? {
|
|
730
|
+
reading: {
|
|
731
|
+
id: str(a.reading),
|
|
732
|
+
...(str(a.readingEntry) ? { entry: str(a.readingEntry) } : {}),
|
|
733
|
+
shas: [...new Set([a.readingSha, ...(Array.isArray(a.excerpts) ? a.excerpts.map((x) => x?.originalSha) : [])].map((x) => str(x)?.toLowerCase()).filter((x) => !!x))],
|
|
734
|
+
},
|
|
735
|
+
}
|
|
736
|
+
: {}),
|
|
713
737
|
});
|
|
714
738
|
}
|
|
715
739
|
const citesOf = (ids) => {
|
|
@@ -1857,8 +1881,9 @@ export function planSync(tree, incoming, edits, opts = {}) {
|
|
|
1857
1881
|
const carried = new Set([...incoming.persons.values()].flatMap((p) => p.facts.map((f) => kindOf(f.kind))));
|
|
1858
1882
|
const carriedFam = new Set(incoming.families.flatMap((f) => f.facts.map((x) => kindOf(x.kind))));
|
|
1859
1883
|
// the sources first: what the file's citations are the research has, or what comes with them
|
|
1860
|
-
const
|
|
1861
|
-
const
|
|
1884
|
+
const readings = new Map();
|
|
1885
|
+
const sources = planSources(tree, incoming, ours, base, push, readings);
|
|
1886
|
+
const reads = readsFor(tree, incoming, sources, changes, readings);
|
|
1862
1887
|
const idOf = (key) => (key.startsWith("x:") ? key : (resolve(tree, key)?.id ?? key));
|
|
1863
1888
|
// who the records name at a fact: the same person by their key or a name of theirs; the role "other" is any role
|
|
1864
1889
|
// (the Strom app has no name for an informant or a midwife)
|
|
@@ -2501,7 +2526,7 @@ export function planSync(tree, incoming, edits, opts = {}) {
|
|
|
2501
2526
|
if (left.length)
|
|
2502
2527
|
c.uncited = [...new Set(left)];
|
|
2503
2528
|
}
|
|
2504
|
-
return { changes, sources, base: !!base, ...(incoming.head ? { head: incoming.head } : {}), partial, identity };
|
|
2529
|
+
return { changes, sources, readings, base: !!base, ...(incoming.head ? { head: incoming.head } : {}), partial, identity };
|
|
2505
2530
|
}
|
|
2506
2531
|
/**
|
|
2507
2532
|
* The send a copy of the app says it is a copy of (_STROM_SINCE R…, the app's beta.34): that send as it came, kept, its
|
|
@@ -2767,7 +2792,7 @@ export function readByResearch(tree) {
|
|
|
2767
2792
|
* before), or new. Of the research's, what the user changed in the app since it was given — the app's own source
|
|
2768
2793
|
* corrected, a record the research read a second reading beside it — and "transcript verified" ticked.
|
|
2769
2794
|
*/
|
|
2770
|
-
function planSources(tree, incoming, ours, base, push) {
|
|
2795
|
+
function planSources(tree, incoming, ours, base, push, readings) {
|
|
2771
2796
|
const map = new Map();
|
|
2772
2797
|
let readSet;
|
|
2773
2798
|
if (!incoming.sources?.size)
|
|
@@ -2780,11 +2805,19 @@ function planSources(tree, incoming, ours, base, push) {
|
|
|
2780
2805
|
mine = tree.get(mine.mergedInto);
|
|
2781
2806
|
if (mine && (mine.type !== "source" || mine.retracted))
|
|
2782
2807
|
mine = undefined;
|
|
2783
|
-
//
|
|
2808
|
+
// an entry of a reading of a document the person confirmed in the app: the research's reading, when it is one
|
|
2809
|
+
// (it exists and is read, the original is its page, the words its entry's) — else the app's own source as any
|
|
2810
|
+
const check = !mine && inc.reading ? checkReading(tree, inc.reading, inc.text) : undefined;
|
|
2811
|
+
// one the research has: the source it made of that entry before (a send the app has not had back yet), the same entry
|
|
2812
|
+
if (check?.ok)
|
|
2813
|
+
mine ??= live.find((s) => s.reading === check.id && s.readingEntry === check.entry.id);
|
|
2784
2814
|
mine ??= live.find((s) => sameEntry(tree, s, pageOf(s), inc));
|
|
2785
2815
|
if (!mine) {
|
|
2816
|
+
if (check?.ok)
|
|
2817
|
+
readings.set(key, check);
|
|
2818
|
+
const reading = check ? { reading: { id: check.id, ok: check.ok, ...(check.entry ? { entry: check.entry.id } : {}), ...(check.code ? { code: check.code } : {}), ...(check.edited ? { edited: true } : {}) } } : {};
|
|
2786
2819
|
if (inc.title || inc.text)
|
|
2787
|
-
push({ kind: "source.new", action: "add", source: key, text: inc.title || inc.text.slice(0, 60) });
|
|
2820
|
+
push({ kind: "source.new", action: "add", source: key, text: inc.title || inc.text.slice(0, 60), ...reading });
|
|
2788
2821
|
continue;
|
|
2789
2822
|
}
|
|
2790
2823
|
map.set(key, mine.id);
|
|
@@ -2813,11 +2846,14 @@ function planSources(tree, incoming, ours, base, push) {
|
|
|
2813
2846
|
* not authored) whose transcript counts — the app's own source when the send says evidence, or verified (now or
|
|
2814
2847
|
* before); the research's own one cited by the user when the send says evidence.
|
|
2815
2848
|
*/
|
|
2816
|
-
function readsFor(tree, incoming, sources, changes) {
|
|
2849
|
+
function readsFor(tree, incoming, sources, changes, readings) {
|
|
2817
2850
|
const evidence = incoming.transcripts === "evidence";
|
|
2818
2851
|
return (key) => {
|
|
2819
2852
|
const inc = incoming.sources?.get(key);
|
|
2820
2853
|
const id = sources.get(key);
|
|
2854
|
+
// a reading of the research: its readers read it, not the user (the facts' status is by what they read)
|
|
2855
|
+
if (!id && readings.has(key))
|
|
2856
|
+
return false;
|
|
2821
2857
|
if (!id) {
|
|
2822
2858
|
// new: the app's quality 0 is someone's word, no record
|
|
2823
2859
|
return !!inc && inc.quay !== 0 && (evidence || !!inc.verified);
|
|
@@ -3134,6 +3170,8 @@ export function applySync(tree, plan, incoming, source, only) {
|
|
|
3134
3170
|
// the app's sources first: the facts cite them
|
|
3135
3171
|
const made = new Map(); // a new source's key → its ID
|
|
3136
3172
|
const srcId = (key) => plan.sources.get(key) ?? made.get(key);
|
|
3173
|
+
// the sources that are an entry of a reading of the research: its ID → the reading as checked
|
|
3174
|
+
const checks = new Map();
|
|
3137
3175
|
for (const c of plan.changes.filter((x) => x.kind === "source.new" && taken(x))) {
|
|
3138
3176
|
const inc = incoming.sources?.get(c.source);
|
|
3139
3177
|
if (!inc)
|
|
@@ -3141,6 +3179,33 @@ export function applySync(tree, plan, incoming, source, only) {
|
|
|
3141
3179
|
const repository = inc.repo ? repositoryFor(tree, inc.repo, applied) : undefined;
|
|
3142
3180
|
const [form, information] = QUAY_EVIDENCE[inc.quay ?? -1] ?? ["original", "unknown"];
|
|
3143
3181
|
const title = inc.title || inc.text.slice(0, 60);
|
|
3182
|
+
const check = plan.readings?.get(c.source);
|
|
3183
|
+
if (check) {
|
|
3184
|
+
// the research's reading of the record: its transcript, translation, language, input and images; the person's
|
|
3185
|
+
// words where they changed them (the reading's kept beside them, theirs not yet a reading that counts)
|
|
3186
|
+
const read = readingSource(check, inc.text);
|
|
3187
|
+
const notes = [inc.note, check.edited ? phrase(tree.lang, "read.note.source.edited", { read: check.id, text: check.entry.transcript }) : undefined].filter(Boolean).join("\n\n");
|
|
3188
|
+
const made1 = create(tree, "source", {
|
|
3189
|
+
...read,
|
|
3190
|
+
kind: read.kind ?? sourceKind(incoming, c.source),
|
|
3191
|
+
title,
|
|
3192
|
+
...(repository ? { repository } : {}),
|
|
3193
|
+
...(inc.page ? { locator: inc.page } : {}),
|
|
3194
|
+
...(inc.date ? { date: inc.date } : {}),
|
|
3195
|
+
...(inc.url ? { url: inc.url } : {}),
|
|
3196
|
+
...(check.edited ? { app: { read: false } } : {}),
|
|
3197
|
+
...(notes ? { note: notes } : {}),
|
|
3198
|
+
}, (id) => `+${id} source "${truncateText(title, 50)}" (read ${check.id} ${check.entry.id}, confirmed in the Strom app, ${source.id})`, read.input ? [read.input] : []);
|
|
3199
|
+
made.set(c.source, made1.id);
|
|
3200
|
+
applied.push({ do: "source.add", id: made1.id, before: c.source });
|
|
3201
|
+
const took = takeReadingImages(tree, made1, check, `the Strom app, ${source.id}`);
|
|
3202
|
+
if (took.added)
|
|
3203
|
+
applied.push({ do: "media.add", id: took.added });
|
|
3204
|
+
if (took.input)
|
|
3205
|
+
applied.push({ do: "input.sort", id: took.input.id, before: JSON.stringify(took.input.before) });
|
|
3206
|
+
checks.set(made1.id, check);
|
|
3207
|
+
continue;
|
|
3208
|
+
}
|
|
3144
3209
|
const made1 = create(tree, "source", {
|
|
3145
3210
|
kind: sourceKind(incoming, c.source),
|
|
3146
3211
|
title,
|
|
@@ -3161,7 +3226,64 @@ export function applySync(tree, plan, incoming, source, only) {
|
|
|
3161
3226
|
/** Whether a citation of this source stands on a reading of a record: the user's, when it counts. */
|
|
3162
3227
|
const reading = (id) => {
|
|
3163
3228
|
const s = tree.get(id);
|
|
3164
|
-
|
|
3229
|
+
// a reading of the research's readers: its facts by what they read (graded), unless the person's words changed
|
|
3230
|
+
// there count now (verified)
|
|
3231
|
+
return !!s && !isWeak(s) && (s.app ? s.app.read : !s.reading && evidence);
|
|
3232
|
+
};
|
|
3233
|
+
/** A source that is an entry of a reading of the research (made now, or by a sync before): the reading as checked. */
|
|
3234
|
+
const checkOf = (id) => {
|
|
3235
|
+
if (checks.has(id))
|
|
3236
|
+
return checks.get(id);
|
|
3237
|
+
const s = tree.get(id);
|
|
3238
|
+
const result = s?.reading && !s.retracted ? readingResult(tree, s.reading) : undefined;
|
|
3239
|
+
const entry = result?.entries.find((e) => e.id === s.readingEntry);
|
|
3240
|
+
const k = result && entry ? { id: s.reading, ok: true, entry, page: result.pages.find((p) => p.n === entry.page), result, ...(s.app ? { edited: true } : {}) } : undefined;
|
|
3241
|
+
if (k)
|
|
3242
|
+
checks.set(id, k);
|
|
3243
|
+
return k;
|
|
3244
|
+
};
|
|
3245
|
+
/** How sure a fact is by the readings it cites (decision of 19 Oct 2026; none cited: undefined), and which they are. */
|
|
3246
|
+
const graded = (cits, f) => {
|
|
3247
|
+
if (!f)
|
|
3248
|
+
return undefined;
|
|
3249
|
+
const each = cits.flatMap((c) => {
|
|
3250
|
+
const k = checkOf(c.source);
|
|
3251
|
+
return k && !tree.get(c.source)?.app?.read ? [{ g: readingGrade(tree, k, f), read: k.id, source: c.source }] : [];
|
|
3252
|
+
});
|
|
3253
|
+
const grade = bestGrade(each.map((x) => x.g));
|
|
3254
|
+
return grade ? { grade, reads: [...new Set(each.map((x) => x.read))], sources: [...new Set(each.map((x) => x.source))] } : undefined;
|
|
3255
|
+
};
|
|
3256
|
+
/** Why a fact of a reading is not probable, beside it (both readings where they differ). */
|
|
3257
|
+
const gradeNote = (gr) => {
|
|
3258
|
+
const read = gr.reads.join(", ");
|
|
3259
|
+
switch (gr.grade.why) {
|
|
3260
|
+
case "two":
|
|
3261
|
+
return undefined;
|
|
3262
|
+
case "differ":
|
|
3263
|
+
return phrase(tree.lang, "read.note.differ", { read, list: (gr.grade.variants ?? []).map((v) => `"${v.at}" / "${v.other}"`).join("; ") });
|
|
3264
|
+
default:
|
|
3265
|
+
return phrase(tree.lang, `read.note.${gr.grade.why}`, { read });
|
|
3266
|
+
}
|
|
3267
|
+
};
|
|
3268
|
+
// the people whose facts stand on a reading not yet sure (one reader, readers apart, the words changed): one task each
|
|
3269
|
+
const readVerify = new Map();
|
|
3270
|
+
const toCheck = (owner, gr) => {
|
|
3271
|
+
if (!gr || gr.grade.status === "probable")
|
|
3272
|
+
return;
|
|
3273
|
+
const people = owner.startsWith("F") ? (tree.get(owner)?.partners ?? []) : [owner];
|
|
3274
|
+
for (const p of people)
|
|
3275
|
+
for (const s1 of gr.sources)
|
|
3276
|
+
(readVerify.get(p) ?? readVerify.set(p, new Set()).get(p)).add(s1);
|
|
3277
|
+
};
|
|
3278
|
+
/** A fact of the research a reading now makes surer (never less sure): its status raised. */
|
|
3279
|
+
const lift = (owner, e, gr) => {
|
|
3280
|
+
if (!gr || !surer(gr.grade.status, e.status))
|
|
3281
|
+
return;
|
|
3282
|
+
update(tree, owner, owner.startsWith("F") ? "family" : "person", (o) => ({ ...o, events: o.events.map((x) => (x.id === e.id ? { ...x, status: gr.grade.status } : x)) }), {
|
|
3283
|
+
op: "event.status",
|
|
3284
|
+
summary: `${e.id} ${gr.grade.status}: read ${gr.reads.join(", ")} (${reason})`,
|
|
3285
|
+
});
|
|
3286
|
+
applied.push({ do: "event.status", id: e.id, before: e.status });
|
|
3165
3287
|
};
|
|
3166
3288
|
/** The citations of a fact from the file: its sources (where in them), else the sync. */
|
|
3167
3289
|
const citesOf = (f) => {
|
|
@@ -3181,7 +3303,7 @@ export function applySync(tree, plan, incoming, source, only) {
|
|
|
3181
3303
|
const unread = cits.filter((c) => {
|
|
3182
3304
|
const s = tree.get(c.source);
|
|
3183
3305
|
// a reading of the user's that counts is the first reading: read again only by the usual rules
|
|
3184
|
-
return !!s?.app && !s.app.read && !isWeak(s) && !researchRead(tree, s, (read ??= readByResearch(tree)));
|
|
3306
|
+
return !!s?.app && !s.app.read && !s.reading && !isWeak(s) && !researchRead(tree, s, (read ??= readByResearch(tree)));
|
|
3185
3307
|
});
|
|
3186
3308
|
if (!unread.length)
|
|
3187
3309
|
return;
|
|
@@ -3233,9 +3355,12 @@ export function applySync(tree, plan, incoming, source, only) {
|
|
|
3233
3355
|
const theirs = citesOf(c.fact);
|
|
3234
3356
|
if (c.kind === "fact.new" || !mine || (c.action === "add" && !mine)) {
|
|
3235
3357
|
const citations = theirs.length ? theirs : [cite(c.fact.kind)];
|
|
3236
|
-
const
|
|
3358
|
+
const gr = graded(citations, c.fact);
|
|
3359
|
+
const note = gr ? gradeNote(gr) : undefined;
|
|
3360
|
+
const { event } = addEvent(tree, owner, { ...fields(c.fact), status: gr ? gr.grade.status : citations.some((x) => reading(x.source)) ? "probable" : "lead", ...(note ? { note } : {}), citations });
|
|
3237
3361
|
applied.push({ do: "event.add", id: event.id });
|
|
3238
3362
|
toVerify(owner, citations);
|
|
3363
|
+
toCheck(owner, gr);
|
|
3239
3364
|
}
|
|
3240
3365
|
else if (c.action === "refine") {
|
|
3241
3366
|
// the record's fact stays as it is proven; what the user added to it cites their tree (or the sources they gave)
|
|
@@ -3274,6 +3399,9 @@ export function applySync(tree, plan, incoming, source, only) {
|
|
|
3274
3399
|
if (stale.length)
|
|
3275
3400
|
applied.push({ do: "event.uncite", id: mine.id, before: JSON.stringify(stale) });
|
|
3276
3401
|
toVerify(owner, theirs);
|
|
3402
|
+
const gr = graded(theirs, c.fact);
|
|
3403
|
+
lift(owner, eventById(tree, mine.id) ?? mine, gr);
|
|
3404
|
+
toCheck(owner, gr);
|
|
3277
3405
|
applied.push({ do: "event.edit", id: mine.id, before: { kind: mine.kind, ...(mine.date ? { date: mine.date } : {}), ...(mine.place ? { place: mine.place } : {}), ...(mine.value ? { value: mine.value } : {}) } });
|
|
3278
3406
|
}
|
|
3279
3407
|
else if (c.action === "user") {
|
|
@@ -3283,9 +3411,12 @@ export function applySync(tree, plan, incoming, source, only) {
|
|
|
3283
3411
|
break;
|
|
3284
3412
|
// the user's word: no record makes it probable — unless it is their reading of one, which counts
|
|
3285
3413
|
const citations = theirs.length ? theirs : [cite(c.fact.kind)];
|
|
3286
|
-
const
|
|
3414
|
+
const gr = graded(citations, c.fact);
|
|
3415
|
+
const note = gr ? gradeNote(gr) : undefined;
|
|
3416
|
+
const { event } = addEvent(tree, owner, { ...fields(c.fact), status: gr ? gr.grade.status : citations.some((x) => reading(x.source)) ? "probable" : "possible", ...(note ? { note } : {}), citations });
|
|
3287
3417
|
applied.push({ do: "event.add", id: event.id });
|
|
3288
3418
|
toVerify(owner, citations);
|
|
3419
|
+
toCheck(owner, gr);
|
|
3289
3420
|
}
|
|
3290
3421
|
else {
|
|
3291
3422
|
const title = editTitle(c.name ?? owner, eventName(c.fact.kind, tree.lang, c.fact.label), factWords(tree, incoming, mine), factWords(tree, incoming, c.fact));
|
|
@@ -3331,12 +3462,16 @@ export function applySync(tree, plan, incoming, source, only) {
|
|
|
3331
3462
|
if (!add.length)
|
|
3332
3463
|
break;
|
|
3333
3464
|
const up = (mine.status === "lead" || mine.status === "possible") && add.some((x) => reading(x.source));
|
|
3334
|
-
|
|
3465
|
+
// a reading the person gave it: the research's fact as the readers read it (surer only, never less)
|
|
3466
|
+
const gr = graded(add, mine);
|
|
3467
|
+
const to = gr && surer(gr.grade.status, up ? "probable" : mine.status) ? gr.grade.status : up ? "probable" : undefined;
|
|
3468
|
+
update(tree, owner, owner.startsWith("F") ? "family" : "person", (o) => ({ ...o, events: o.events.map((e) => (e.id === mine.id ? { ...e, citations: [...e.citations, ...add], ...(to ? { status: to } : {}) } : e)) }), {
|
|
3335
3469
|
op: "event.cite",
|
|
3336
|
-
summary: `${mine.id} cites ${add.map((x) => x.source).join(", ")}${up ? " — probable on the user's reading" : ""} (${reason})`,
|
|
3470
|
+
summary: `${mine.id} cites ${add.map((x) => x.source).join(", ")}${to && !up ? ` — ${to}: read ${gr.reads.join(", ")}` : up ? " — probable on the user's reading" : ""} (${reason})`,
|
|
3337
3471
|
});
|
|
3338
3472
|
applied.push({ do: "event.cite", id: mine.id, before: JSON.stringify({ status: mine.status, sources: add.map((x) => x.source) }) });
|
|
3339
3473
|
toVerify(owner, add);
|
|
3474
|
+
toCheck(owner, gr);
|
|
3340
3475
|
break;
|
|
3341
3476
|
}
|
|
3342
3477
|
case "fact.part": {
|
|
@@ -3801,8 +3936,32 @@ export function applySync(tree, plan, incoming, source, only) {
|
|
|
3801
3936
|
}, (id) => `+${id} task "read for itself what the user wrote of ${person}"`);
|
|
3802
3937
|
applied.push({ do: "task.add", id: t.id });
|
|
3803
3938
|
}
|
|
3939
|
+
// one task per person to check what one reader read, or readers read apart, or the person changed (the readings
|
|
3940
|
+
// are the research's own: a second, blind reading and its own look prove them or show a mistake)
|
|
3941
|
+
const openRead = tree.list("task").filter((t) => t.origin === READ_VERIFY_ORIGIN && ["open", "doing", "parked", "waiting"].includes(t.state));
|
|
3942
|
+
for (const [person, srcs] of actsAsArchive(tree) ? [] : readVerify) {
|
|
3943
|
+
const p = tree.get(person);
|
|
3944
|
+
if (!p || p.retracted || openRead.some((t) => t.subject.includes(person) && [...srcs].every((x) => t.subject.includes(x))))
|
|
3945
|
+
continue;
|
|
3946
|
+
const reads = [...new Set([...srcs].map((x) => tree.get(x)?.reading).filter((x) => !!x))];
|
|
3947
|
+
const images = [...new Set([...srcs].flatMap((x) => tree.get(x)?.clips?.map((k) => k.media) ?? []))];
|
|
3948
|
+
const t = create(tree, "task", {
|
|
3949
|
+
level: "verify",
|
|
3950
|
+
priority: 3,
|
|
3951
|
+
what: truncateText(phrase(tree.lang, "read.verify.what", { name: person, list: [...srcs].join(", ") }), 200),
|
|
3952
|
+
where: [...srcs, ...images],
|
|
3953
|
+
why: phrase(tree.lang, "read.verify.why", { read: reads.join(", "), source: source.id }),
|
|
3954
|
+
doneWhen: phrase(tree.lang, "read.verify.done", { read: reads[0] ?? "D…", source: [...srcs][0] }),
|
|
3955
|
+
subject: [person, ...srcs],
|
|
3956
|
+
state: "open",
|
|
3957
|
+
origin: READ_VERIFY_ORIGIN,
|
|
3958
|
+
}, (id) => `+${id} task "check the reading ${reads.join(", ")} of ${person}"`);
|
|
3959
|
+
applied.push({ do: "task.add", id: t.id });
|
|
3960
|
+
}
|
|
3804
3961
|
return { applied, changes: changes - skipped.length, skipped };
|
|
3805
3962
|
}
|
|
3963
|
+
/** Where a task to check a reading of a document confirmed in the app comes from (one per person and its sources). */
|
|
3964
|
+
export const READ_VERIFY_ORIGIN = "read:verify";
|
|
3806
3965
|
/** Where a task to read the user's transcripts again comes from (one per person at a time). */
|
|
3807
3966
|
export const VERIFY_ORIGIN = "sync:verify";
|
|
3808
3967
|
/** The Strom app's quality of a citation (GEDCOM QUAY) → what kind of record it is and its information. */
|
|
@@ -4218,6 +4377,15 @@ export function undoSync(tree, input) {
|
|
|
4218
4377
|
case "source.add":
|
|
4219
4378
|
update(tree, a.id, "source", (x) => ({ ...x, retracted: { at: now(), reason } }), { op: "source.retract", summary: `${a.id} retracted: ${reason}` });
|
|
4220
4379
|
break;
|
|
4380
|
+
case "media.add":
|
|
4381
|
+
update(tree, a.id, "media", (x) => ({ ...x, retracted: { at: now(), reason } }), { op: "media.retract", summary: `${a.id} retracted: ${reason}`, reason });
|
|
4382
|
+
break;
|
|
4383
|
+
case "input.sort": {
|
|
4384
|
+
// the material as it stood before the reading's source was made of it
|
|
4385
|
+
const b = JSON.parse(String(a.before ?? "{}"));
|
|
4386
|
+
update(tree, a.id, "input", ({ sorted: _s, ...x }) => ({ ...x, state: b.state ?? "new", ...(b.sorted ? { sorted: b.sorted } : {}) }), { op: "input.sort", summary: `${a.id} ${b.state ?? "new"} again: ${reason}`, reason });
|
|
4387
|
+
break;
|
|
4388
|
+
}
|
|
4221
4389
|
case "repo.add":
|
|
4222
4390
|
update(tree, a.id, "repository", (x) => ({ ...x, retracted: { at: now(), reason } }), { op: "repo.retract", summary: `${a.id} retracted: ${reason}`, reason });
|
|
4223
4391
|
break;
|
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.18.0-beta.
|
|
30
|
+
export const VERSION = "1.18.0-beta.4";
|
|
31
31
|
const GITIGNORE = `# Strom Research — local state and large files are not versioned
|
|
32
32
|
.strom/
|
|
33
33
|
/media/
|
package/dist/gedcom/export.js
CHANGED
|
@@ -411,6 +411,12 @@ export function exportGedcom(tree, opts = {}) {
|
|
|
411
411
|
}
|
|
412
412
|
if (opts.sourceReads && s.app?.verified)
|
|
413
413
|
w.line(1, "_STROM_VERIFIED", "Y");
|
|
414
|
+
// the reading of a document the source is an entry of: the app opens its result there (its pages, translation)
|
|
415
|
+
if (opts.readings && s.reading) {
|
|
416
|
+
w.line(1, "_STROM_READING", s.reading);
|
|
417
|
+
if (s.readingEntry)
|
|
418
|
+
w.line(2, "_STROM_ENTRY", s.readingEntry);
|
|
419
|
+
}
|
|
414
420
|
for (const cut of opts.excerpts?.(s) ?? []) {
|
|
415
421
|
const e = opts.turnsExcerpts ? cut : turned(cut);
|
|
416
422
|
w.line(1, "OBJE");
|