strom-research 1.18.0-beta.3 → 1.18.0-beta.5

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 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.3. Oddíl bude úplný s vydáním.*
10
+ *Připravuje se: bety do 1.18.0-beta.5. 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é. Dokument čtený znovu se ukazuje pod názvem svého souboru, nikdy pod kódem.
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.3. Der Abschnitt ist mit dem Release vollständig.*
10
+ *In Vorbereitung: die Betas bis 1.18.0-beta.5. 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,9 @@ 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. Ein erneut gelesenes Dokument wird unter dem Namen seiner
26
+ Datei gezeigt, nie unter einem Code.
25
27
  - **Fotos vom Handy.** `strom phone on`, das nur ein Mensch einschaltet, lässt ein Handy im Heimnetz (WLAN oder
26
28
  Kabel — nie ein Mobilfunknetz oder ein VPN) zu diesem Computer. Das Handy wird einmal per QR-Code gekoppelt; seine
27
29
  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.3. The section is complete with the release.*
10
+ *In preparation: the betas up to 1.18.0-beta.5. 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,9 @@ 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. A document read again is shown under
25
+ the name of its file, never a code.
24
26
  - **Photos from a phone.** `strom phone on`, turned on by the person alone, lets a phone on the home network (Wi-Fi
25
27
  or a cable — never a mobile network or a VPN) reach this computer. The phone is paired once by its QR code; its
26
28
  page then takes photos or picks them from the gallery and sends them whole into a tree of the research, with a
@@ -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}",
@@ -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}",
@@ -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}",
@@ -20,7 +20,7 @@ import { addReaders } from "../core/session.js";
20
20
  import { READER_MINUTES_HELP } from "../core/reader.js";
21
21
  import { stamp } from "../core/net.js";
22
22
  import { readers } from "./readers.js";
23
- import { alive, APP_READ_FROM, backBefore, beforeOf, buildResult, cancelPath, DOC_NEXT, DOC_PAGES, DOC_VIEWS, docPrompt, endState, FINAL_STATES, listReadings, loadState, newReadId, nextVersion, parseRegion, READ_ID, reportPath, resultPath, saveState, SMALL_PX, withoutDocNext, } from "../core/document.js";
23
+ import { alive, APP_READ_FROM, backBefore, beforeOf, buildResult, cancelPath, DOC_NEXT, DOC_PAGES, DOC_VIEWS, docPrompt, endState, FINAL_STATES, listReadings, loadState, newReadId, nextVersion, parseRegion, READ_ID, reportPath, resultPath, saveState, shownName, SMALL_PX, withoutDocNext, } from "../core/document.js";
24
24
  /** How often a reading at work looks whether it was cancelled (strom document cancel, from another process). */
25
25
  const CANCEL_LOOK_MS = 500;
26
26
  /** The image types a document is read from: a photo, a scan, a PDF (HEIC and PDF converted for the reader). */
@@ -73,6 +73,24 @@ function regionsOf(said) {
73
73
  }
74
74
  return out;
75
75
  }
76
+ /** --name [<file>:]<name>: the name each file is shown under in the reading (the first file's without a number). */
77
+ function namesOf(said, files) {
78
+ const out = new Map();
79
+ for (const v of Array.isArray(said) ? said : said === undefined ? [] : [said]) {
80
+ const m = /^(\d{1,2}):(.*)$/su.exec(String(v));
81
+ const at = m && Number(m[1]) >= 1 && Number(m[1]) <= files ? Number(m[1]) : 1;
82
+ const name = shownName(m && at === Number(m[1]) ? m[2] : String(v));
83
+ if (!name)
84
+ throw new UsageError(`invalid --name "${String(v).slice(0, 60)}"`, { hint: 'the file\'s name as the person knows it: --name "back of the photo.jpg" (the second file\'s: --name 2:<name>)' });
85
+ out.set(at, name);
86
+ }
87
+ return out;
88
+ }
89
+ /** An image's name for a person: the file it came as (its `from`: "the Strom app: x.jpg", "portal · x.jpg"), else its ID — never its hash. */
90
+ function mediaName(m) {
91
+ const last = m.from?.split(/: | · /u).at(-1)?.trim();
92
+ return (last && !last.includes(m.sha.slice(0, 12)) && shownName(last)) || m.id;
93
+ }
76
94
  function readersOf(said, dflt) {
77
95
  if (said === undefined)
78
96
  return dflt;
@@ -97,7 +115,7 @@ function filesOf(ctx, tree, args, persons, note) {
97
115
  const id = normId(a.trim(), a.trim().toUpperCase().startsWith("M") ? "media" : "input");
98
116
  const file = fileOf(tree, shared, id);
99
117
  const rec = id.startsWith("M") ? requireRecord(tree, id, "media") : requireRecord(tree, id, "input");
100
- records.push({ id, file, name: "name" in rec && rec.name ? rec.name : path.basename(file), ...(rec.sha ? { sha: rec.sha } : {}) });
118
+ records.push({ id, file, name: rec.type === "input" ? rec.name : mediaName(rec), ...(rec.sha ? { sha: rec.sha } : {}) });
101
119
  continue;
102
120
  }
103
121
  const p = ctx.resolvePath(a);
@@ -442,6 +460,7 @@ register({
442
460
  { name: "region", type: "string", multiple: true, value: "[<page>:]x,y,w,h", description: "read only this part of a page: fractions of it from its top left (repeatable, one per page)" },
443
461
  { name: "note", type: "string", value: "<text>", description: "what the person says the document is (\"the back of a photo\") — a description only, never names to find" },
444
462
  { name: "person", type: "string", multiple: true, value: "<P…>", description: "a person it is of, as the person says (repeatable)" },
463
+ { name: "name", type: "string", multiple: true, value: "[<file>:]<name>", description: "the name a file is shown under in the reading (default: its input's name, an image's original file name; repeatable, 2:<name> for the second file)" },
445
464
  { name: "model", type: "string", value: "<model>", description: "model of the readers (default: model.vision — never weaker for handwriting)" },
446
465
  { name: "minutes", type: "string", value: "<n>", description: READER_MINUTES_HELP, mistaken: ["timeout", "time", "time-limit", "max-minutes"] },
447
466
  { name: "queue", type: "boolean", description: "keep it to be read later, read nothing now (strom document read D… reads it)" },
@@ -465,6 +484,8 @@ register({
465
484
  tree.withTreeLock(() => {
466
485
  try {
467
486
  const { records, added } = filesOf(ctx, tree, args, persons, note);
487
+ for (const [at, name] of namesOf(opts.name, records.length))
488
+ records[at - 1].name = name;
468
489
  pages = pagesOf(tree, records, regions);
469
490
  for (const r of new Set(records.map((x) => x.id)))
470
491
  if (r.startsWith("I"))
@@ -562,20 +583,21 @@ register({
562
583
  tree: true,
563
584
  args: [{ name: "reading", description: "the reading: D…", required: true }],
564
585
  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
- },
586
+ run: (ctx, { args }) => showReading(ctx, readId(args[0])),
578
587
  });
588
+ /** A reading of a document as strom document show (and strom readings D…) says it: its result, or what its readers wrote so far. */
589
+ export function showReading(ctx, id) {
590
+ const tree = ctx.tree();
591
+ const s = loadState(tree.root, id);
592
+ let result;
593
+ if (s)
594
+ result = buildResult(tree, settled(tree, s));
595
+ else if (fs.existsSync(resultPath(tree.root, id)))
596
+ result = JSON.parse(fs.readFileSync(resultPath(tree.root, id), "utf8"));
597
+ else
598
+ throw new UsageError(`no reading ${id}`, { hint: "strom document list" });
599
+ return { text: said(ctx, result), data: result };
600
+ }
579
601
  register({
580
602
  path: ["document", "list"],
581
603
  summary: "The readings of documents of this research, newest first",
@@ -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
- text: all.length ? lines(table(page.items.map((i) => [i.id, i.kind, i.state, truncate(i.name, 60)])), moreLine(page, "strom input list")) : 'no inputs → strom intake <files or folder>',
252
- 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 })) },
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"],
@@ -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
- 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 } : {}) });
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…), or a reading: its report's name or the start of it (a day)" }],
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
- if (all.length === 0)
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
- const which = args[0]?.trim();
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 text = lines(...shown.flatMap(({ r, blocks }) => readingHead(r, blocks)[0].concat(r.question ? ` · "${truncate(r.question, 60)}"` : "")));
443
- return { text, data: { readings: shown.map(({ r, blocks }) => ({ stem: r.stem, question: r.question, reports: r.reports, images: blocks.map((b) => b.num) })) } };
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}`, ""])));
@@ -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.startsWith("source.") || c.kind === "fact.cite" || c.kind === "person.cite" || c.kind === "family.cite"
154
- ? `ui.sync.do.${c.kind}${c.reads || (c.kind === "source.new" && newReads(c, incoming)) ? ".read" : ""}`
155
- : c.kind === "fact.new" && c.reads
156
- ? "ui.sync.do.reading"
157
- : `ui.sync.do.${c.action}`;
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
  }
@@ -831,6 +831,10 @@ export function backBefore(s) {
831
831
  export function nextVersion(s) {
832
832
  return Math.max(s.version, ...s.reports.map((r) => r.version)) + 1;
833
833
  }
834
+ /** A file's name for a person: its last part, no control characters, at most 200 characters. */
835
+ export function shownName(name) {
836
+ return path.basename(name.normalize("NFC").replace(/[\\/]+/gu, "/")).replace(/[\p{Cc}]/gu, "").trim().slice(0, 200);
837
+ }
834
838
  /** Whoever asked a reading from the Strom app (its bridge). */
835
839
  export const APP_READ_FROM = "app:read";
836
840
  export function readsDir(root) {
@@ -15,7 +15,7 @@ import path from "node:path";
15
15
  import { spawn } from "node:child_process";
16
16
  import { newerTree, Tree } from "./tree.js";
17
17
  import { actsAsArchive } from "./state.js";
18
- import { APP_READ_FROM, alive, backBefore, buildResult, DOC_PAGES, FINAL_STATES, loadState, parseRegion, READ_ID, readsDir, reportPath, resultPath, saveState, statePath } from "./document.js";
18
+ import { APP_READ_FROM, alive, backBefore, buildResult, DOC_PAGES, FINAL_STATES, loadState, parseRegion, READ_ID, readsDir, reportPath, resultPath, saveState, shownName, statePath } from "./document.js";
19
19
  import { knownOriginal } from "./originals.js";
20
20
  import { readableTypes } from "./readable.js";
21
21
  import { makeView } from "./views.js";
@@ -297,6 +297,8 @@ function notReady(tree, settings, env, seen, now) {
297
297
  return undefined;
298
298
  }
299
299
  const BODY_MAX = 16 * 1024;
300
+ /** The names of files sent to be read kept until they are asked (the last ones). */
301
+ const NAMES_KEPT = 200;
300
302
  /**
301
303
  * The readings of the app: POST /read, /read/<id>/again, /read/<id>/cancel; GET /read/<id>, /read/<id>/page/<n>.jpg,
302
304
  * /reads; the queue read one at a time (tick, each tick of the bridge); what changed, for the event read.
@@ -309,11 +311,26 @@ export class AppReadings {
309
311
  starting = new Map();
310
312
  told = new Map();
311
313
  primed = false;
314
+ /** The name each file to read came under (X-Strom-Name of its last PUT): a file the research had already keeps its own. */
315
+ names = new Map();
312
316
  b;
313
317
  constructor(b) {
314
318
  this.b = b;
315
319
  this.seen = new ReadingsSeen(b.root, b.env);
316
320
  }
321
+ /** A file sent to be read (PUT /media with X-Strom-Read) under its name in the app: the reading of it shown so (sent with no name: its record's own). */
322
+ named(sha, name) {
323
+ this.names.delete(sha);
324
+ // cleaned as strom document read --name cleans it: nothing left (only slashes, control characters) = no name
325
+ const shown = name && shownName(name);
326
+ if (shown)
327
+ this.names.set(sha, shown);
328
+ for (const old of this.names.keys()) {
329
+ if (this.names.size <= NAMES_KEPT)
330
+ break;
331
+ this.names.delete(old);
332
+ }
333
+ }
317
334
  /** A reading of the app at work or queued (the bridge does not end idle meanwhile). */
318
335
  active() {
319
336
  return this.pending > 0 || this.appStates().some((s) => !FINAL_STATES.includes(s.state));
@@ -508,6 +525,8 @@ export class AppReadings {
508
525
  }
509
526
  const shared = this.b.settings().shared()?.value;
510
527
  const records = [];
528
+ // each file under the name the app sent it with last, else its record's own (never its hash)
529
+ const names = [];
511
530
  const types = readableTypes(this.b.env);
512
531
  for (const f of files) {
513
532
  const sha = f.toLowerCase();
@@ -521,13 +540,16 @@ export class AppReadings {
521
540
  if (known.bytes > READ_MAX_BYTES)
522
541
  return { code: 413, body: refusal(`larger than a reading takes (${READ_MAX_BYTES / 1024 / 1024} MB)`, "read.large", { sha, mb: String(READ_MAX_BYTES / 1024 / 1024) }) };
523
542
  records.push(known.id);
543
+ const name = this.names.get(sha);
544
+ if (name)
545
+ names.push(`--name=${records.length}:${name}`);
524
546
  }
525
547
  const limited = this.limited();
526
548
  if (limited)
527
549
  return limited;
528
550
  return {
529
551
  what: "a reading",
530
- args: ["document", "read", ...records, "--queue", "--readers", String(readers), "--into", lang, ...(person ? ["--person", person] : []), ...(note ? [`--note=${note}`] : []), ...(region ? [`--region=${region}`] : [])],
552
+ args: ["document", "read", ...records, "--queue", "--readers", String(readers), "--into", lang, ...(person ? ["--person", person] : []), ...(note ? [`--note=${note}`] : []), ...(region ? [`--region=${region}`] : []), ...names],
531
553
  asked: "read",
532
554
  };
533
555
  }
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,
@@ -1964,6 +1970,7 @@ export function serveLive(root, env) {
1964
1970
  return typeof v === "string" ? v : undefined;
1965
1971
  };
1966
1972
  let name;
1973
+ let named;
1967
1974
  let note;
1968
1975
  let meta;
1969
1976
  let region;
@@ -1985,7 +1992,8 @@ export function serveLive(root, env) {
1985
1992
  at = "X-Strom-Path";
1986
1993
  where = header("x-strom-path") ? batchPath(decodeURIComponent(header("x-strom-path"))) : undefined;
1987
1994
  at = "X-Strom-Name";
1988
- name = decodeURIComponent(header("x-strom-name") ?? "") || sha.slice(0, 12);
1995
+ named = decodeURIComponent(header("x-strom-name") ?? "").trim() || undefined;
1996
+ name = named ?? sha.slice(0, 12);
1989
1997
  at = "X-Strom-Note";
1990
1998
  note = header("x-strom-note") ? decodeURIComponent(header("x-strom-note")).slice(0, 500) : undefined;
1991
1999
  at = "X-Strom-Person";
@@ -2028,6 +2036,9 @@ export function serveLive(root, env) {
2028
2036
  if (no)
2029
2037
  return refuse(409, refusal(no.message, no.code === "task.no-file" ? "waiting.no-file" : "waiting.none", { id: forTask }));
2030
2038
  }
2039
+ // a file to read: its reading shown under the name it came with now — also when the research had it already
2040
+ if (read)
2041
+ reads.named(sha, named);
2031
2042
  receiveOriginal(receiver, req, res, { sha, name, note, persons: meta.persons, source: meta.source, region, batch, where, zip, task: forTask, ...(read ? { read } : {}) });
2032
2043
  };
2033
2044
  const closeBatchNow = (id, extra, reply) => closeBatchBy(receiver, id, extra, reply);