strom-research 1.18.0-beta.4 → 1.18.0-beta.6

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.4. Oddíl bude úplný s vydáním.*
10
+ *Připravuje se: bety do 1.18.0-beta.6. 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
@@ -21,7 +21,7 @@ Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou n
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
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
+ nečte podruhé. Dokument čtený znovu se ukazuje pod názvem svého souboru, nikdy pod kódem.
25
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
26
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á
27
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.4. Der Abschnitt ist mit dem Release vollständig.*
10
+ *In Vorbereitung: die Betas bis 1.18.0-beta.6. 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
@@ -22,7 +22,8 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
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
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
+ 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.
26
27
  - **Fotos vom Handy.** `strom phone on`, das nur ein Mensch einschaltet, lässt ein Handy im Heimnetz (WLAN oder
27
28
  Kabel — nie ein Mobilfunknetz oder ein VPN) zu diesem Computer. Das Handy wird einmal per QR-Code gekoppelt; seine
28
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.4. The section is complete with the release.*
10
+ *In preparation: the betas up to 1.18.0-beta.6. 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
@@ -21,7 +21,8 @@ The changes of each version in short. The full notes and the install lines are o
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
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
+ 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.
25
26
  - **Photos from a phone.** `strom phone on`, turned on by the person alone, lets a phone on the home network (Wi-Fi
26
27
  or a cable — never a mobile network or a VPN) reach this computer. The phone is paired once by its QR code; its
27
28
  page then takes photos or picks them from the gallery and sends them whole into a tree of the research, with a
@@ -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"))
@@ -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";
@@ -222,9 +222,13 @@ export function statusReads(seen, now = Date.now()) {
222
222
  .map((r) => r.read)
223
223
  .filter((r) => !FINAL_STATES.includes(r.state) || Date.parse(r.since) > now - STATUS_DAYS * DAY_MS);
224
224
  }
225
- /** GET /reads: the last 90 days, and every reading no source of the research is of. */
225
+ /**
226
+ * GET /reads: the last 90 days, and every reading no source of the research is of. inSource: a source of this tree —
227
+ * the only one a reading can be of (its input, state and result are the tree's; the sync takes no other's) — and not
228
+ * retracted (a sync undone, a source merged away): the reading is then to be come back to again.
229
+ */
226
230
  export function listedReads(tree, seen, now = Date.now()) {
227
- const inSources = new Set(tree.list("source").map((s) => s.reading).filter((r) => typeof r === "string"));
231
+ const inSources = new Set(tree.list("source").flatMap((s) => (!s.retracted && typeof s.reading === "string" ? [s.reading] : [])));
228
232
  return seen
229
233
  .all()
230
234
  .map((r) => ({ ...r.read, inSource: inSources.has(r.read.id) }))
@@ -297,6 +301,8 @@ function notReady(tree, settings, env, seen, now) {
297
301
  return undefined;
298
302
  }
299
303
  const BODY_MAX = 16 * 1024;
304
+ /** The names of files sent to be read kept until they are asked (the last ones). */
305
+ const NAMES_KEPT = 200;
300
306
  /**
301
307
  * The readings of the app: POST /read, /read/<id>/again, /read/<id>/cancel; GET /read/<id>, /read/<id>/page/<n>.jpg,
302
308
  * /reads; the queue read one at a time (tick, each tick of the bridge); what changed, for the event read.
@@ -309,11 +315,26 @@ export class AppReadings {
309
315
  starting = new Map();
310
316
  told = new Map();
311
317
  primed = false;
318
+ /** The name each file to read came under (X-Strom-Name of its last PUT): a file the research had already keeps its own. */
319
+ names = new Map();
312
320
  b;
313
321
  constructor(b) {
314
322
  this.b = b;
315
323
  this.seen = new ReadingsSeen(b.root, b.env);
316
324
  }
325
+ /** 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). */
326
+ named(sha, name) {
327
+ this.names.delete(sha);
328
+ // cleaned as strom document read --name cleans it: nothing left (only slashes, control characters) = no name
329
+ const shown = name && shownName(name);
330
+ if (shown)
331
+ this.names.set(sha, shown);
332
+ for (const old of this.names.keys()) {
333
+ if (this.names.size <= NAMES_KEPT)
334
+ break;
335
+ this.names.delete(old);
336
+ }
337
+ }
317
338
  /** A reading of the app at work or queued (the bridge does not end idle meanwhile). */
318
339
  active() {
319
340
  return this.pending > 0 || this.appStates().some((s) => !FINAL_STATES.includes(s.state));
@@ -508,6 +529,8 @@ export class AppReadings {
508
529
  }
509
530
  const shared = this.b.settings().shared()?.value;
510
531
  const records = [];
532
+ // each file under the name the app sent it with last, else its record's own (never its hash)
533
+ const names = [];
511
534
  const types = readableTypes(this.b.env);
512
535
  for (const f of files) {
513
536
  const sha = f.toLowerCase();
@@ -521,13 +544,16 @@ export class AppReadings {
521
544
  if (known.bytes > READ_MAX_BYTES)
522
545
  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
546
  records.push(known.id);
547
+ const name = this.names.get(sha);
548
+ if (name)
549
+ names.push(`--name=${records.length}:${name}`);
524
550
  }
525
551
  const limited = this.limited();
526
552
  if (limited)
527
553
  return limited;
528
554
  return {
529
555
  what: "a reading",
530
- args: ["document", "read", ...records, "--queue", "--readers", String(readers), "--into", lang, ...(person ? ["--person", person] : []), ...(note ? [`--note=${note}`] : []), ...(region ? [`--region=${region}`] : [])],
556
+ args: ["document", "read", ...records, "--queue", "--readers", String(readers), "--into", lang, ...(person ? ["--person", person] : []), ...(note ? [`--note=${note}`] : []), ...(region ? [`--region=${region}`] : []), ...names],
531
557
  asked: "read",
532
558
  };
533
559
  }
package/dist/core/live.js CHANGED
@@ -1970,6 +1970,7 @@ export function serveLive(root, env) {
1970
1970
  return typeof v === "string" ? v : undefined;
1971
1971
  };
1972
1972
  let name;
1973
+ let named;
1973
1974
  let note;
1974
1975
  let meta;
1975
1976
  let region;
@@ -1991,7 +1992,8 @@ export function serveLive(root, env) {
1991
1992
  at = "X-Strom-Path";
1992
1993
  where = header("x-strom-path") ? batchPath(decodeURIComponent(header("x-strom-path"))) : undefined;
1993
1994
  at = "X-Strom-Name";
1994
- 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);
1995
1997
  at = "X-Strom-Note";
1996
1998
  note = header("x-strom-note") ? decodeURIComponent(header("x-strom-note")).slice(0, 500) : undefined;
1997
1999
  at = "X-Strom-Person";
@@ -2034,6 +2036,9 @@ export function serveLive(root, env) {
2034
2036
  if (no)
2035
2037
  return refuse(409, refusal(no.message, no.code === "task.no-file" ? "waiting.no-file" : "waiting.none", { id: forTask }));
2036
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);
2037
2042
  receiveOriginal(receiver, req, res, { sha, name, note, persons: meta.persons, source: meta.source, region, batch, where, zip, task: forTask, ...(read ? { read } : {}) });
2038
2043
  };
2039
2044
  const closeBatchNow = (id, extra, reply) => closeBatchBy(receiver, id, extra, reply);
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.4";
30
+ export const VERSION = "1.18.0-beta.6";
31
31
  const GITIGNORE = `# Strom Research — local state and large files are not versioned
32
32
  .strom/
33
33
  /media/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strom-research",
3
- "version": "1.18.0-beta.4",
3
+ "version": "1.18.0-beta.6",
4
4
  "description": "Genealogical research toolkit for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {