strom-research 1.19.0-beta.5 → 1.19.0-beta.7
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 +19 -12
- package/CHANGELOG.de.md +27 -17
- package/CHANGELOG.md +19 -12
- package/assets/lang/cs.json +11 -9
- package/assets/lang/de.json +11 -9
- package/assets/phone/phone.css +1 -1
- package/assets/phone/phone.js +30 -7
- package/dist/cli/main.js +8 -6
- package/dist/cli/menu.js +2 -2
- package/dist/cli/ui.js +9 -9
- package/dist/commands/fixchecks.js +153 -40
- package/dist/commands/fixes.js +25 -8
- package/dist/commands/pack.js +3 -3
- package/dist/commands/trees.js +3 -4
- package/dist/core/live-read.js +6 -2
- package/dist/core/phone.js +37 -12
- package/dist/core/phrases.js +2 -0
- package/dist/core/tree.js +1 -1
- 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.19.0
|
|
9
9
|
|
|
10
|
-
*Připravuje se: bety do 1.19.0-beta.
|
|
10
|
+
*Připravuje se: bety do 1.19.0-beta.7. Oddíl bude úplný s vydáním.*
|
|
11
11
|
|
|
12
12
|
- **Na archiv, který zmlkl, se čeká, místo opakovaných dotazů.** Když archiv přestane odpovídat, strom ho nechá na
|
|
13
13
|
pokoji hodinu — a když týž den mlčí dál, dvě a pak čtyři hodiny — a agent se dozví, do kdy: mezitím dělá
|
|
@@ -25,20 +25,26 @@ Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou n
|
|
|
25
25
|
- **Dokument se přečte spolehlivěji.** Čtenář zapisuje čtení do své zprávy průběžně, a čtení dvou čtenářů, kde četl
|
|
26
26
|
jen jeden, to tak řekne — nikdy, že se shodují.
|
|
27
27
|
- **Propojení s telefonem čeká na výzkum, neříká se jako chyba.** Než se po spuštění počítače spustí výzkum, `strom
|
|
28
|
-
phone` i nastavení řeknou, že propojení je zapnuté a jeho server se znovu spustí se spuštěním výzkumu.
|
|
28
|
+
phone` i nastavení řeknou, že propojení je zapnuté a jeho server se znovu spustí se spuštěním výzkumu. Na iPhonu
|
|
29
|
+
zůstane list fotek při psaní osoby nebo poznámky nad klávesnicí a nabízené osoby na očích; server telefonu se spustí
|
|
30
|
+
jednou, i když ho spouští několik naráz.
|
|
29
31
|
- **Opravy vlastních chyb stromu k rozhodnutí.** Když strom zjistí, že některá jeho dřívější verze ve výzkumu něco
|
|
30
32
|
udělala špatně, `strom fixes` a Nastavení → Údržba → Opravy řeknou, o co jde, co se našlo a co by napravení udělalo,
|
|
31
33
|
i jak je náročné; bez souhlasu se nestane nic a odmítnutí platí. První: slepá čtení ze zmenšeného snímku tam, kde
|
|
32
34
|
byla ostřejší část — přečtou se znovu vedle starých, nejdřív na několika, nic se nepřepíše, rozdíl se stane úkolem.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
Čtení se porovnají podle přečteného — jmen, dat a čísel přepisu, ne slov komentáře čtenáře (i v němčině, kde má
|
|
36
|
+
velké písmeno každé podstatné jméno) — a úkol jednou větou řekne, který údaj se liší; kde jméno může jen začínat
|
|
37
|
+
větu, čtení se za shodná nevydávají. Když není co číst znovu, `strom fixes run` to řekne. Druhá: po změně modelu nebo agenta pro čtení dřívější čtení, která stojí za nové přečtení
|
|
38
|
+
— s otevřenými místy, s neshodou čtenářů, s otevřeným rozporem, nepotvrzená u pramene; řečeno jako „četl model X,
|
|
39
|
+
teď se čte modelem Y“, nabídnuto jednou na agenta a model, každé se přečte znovu tak, jak se četlo (snímky jednoho
|
|
40
|
+
čtení spolu, zkouška i část vždy z celých čtení). Počítá se v čteních s jejich místy a náročnost ve voláních
|
|
41
|
+
čtenáře; nabízí se po stupních — jen sporná (rozchod čtenářů, otevřený rozpor, nepotvrzené u pramene), s přímou
|
|
42
|
+
linií, nebo všechna — každý s tím, co stojí, nejužší první; data a svědci jen řadí. Napravit může i jiný připravený
|
|
43
|
+
agent nebo model (`strom fixes run <id> --agent <a> --model <m>`), s tím, kdo četl dřív; ten, kdo tu číst nemůže, se
|
|
44
|
+
odmítne hned i s těmi, kdo mohou. Čas napravení se zaokrouhluje nahoru a po zkoušce se přepočítá podle čtení, jak
|
|
45
|
+
běžela souběžně.
|
|
46
|
+
- **Velikosti tak, jak je píše jazyk výzkumu.** `strom pack`, odebrání rodokmenu i menu uvádějí velikost balíku,
|
|
47
|
+
zálohy nebo snímků jako všude jinde (1,5 MB, u malých 24 kB), nikdy „0.0 MB“.
|
|
42
48
|
|
|
43
49
|
## 1.18.0
|
|
44
50
|
|
|
@@ -98,7 +104,8 @@ Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou n
|
|
|
98
104
|
poznámky: výzkum si nechá svůj text. Místo nebo jiný údaj delší než řádek souboru GEDCOM (255 bajtů) už nezastaví
|
|
99
105
|
jeho zápis a žádné emoji se nerozdělí tam, kde dlouhý řádek pokračuje. S `excerpts.for all` dostane v aplikaci Strom
|
|
100
106
|
výřez i pramen, který zatím nikdo necituje, pokud se vedle citovaných vejde do limitu – citované se kvůli němu
|
|
101
|
-
nezmenší; co se nevešlo, se řekne.
|
|
107
|
+
nezmenší; co se nevešlo, se řekne. Aplikace Strom se dozví, které obrazy pramene výzkum má, ale soubor je vynechal,
|
|
108
|
+
a na vyžádání si je z výzkumu načte — pramen po prameni, nebo všechny najednou i s tím, kolik jich je a jak asi velké.
|
|
102
109
|
|
|
103
110
|
## 1.17.0
|
|
104
111
|
|
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.19.0
|
|
9
9
|
|
|
10
|
-
*In Vorbereitung: die Betas bis 1.19.0-beta.
|
|
10
|
+
*In Vorbereitung: die Betas bis 1.19.0-beta.7. Der Abschnitt ist mit dem Release vollständig.*
|
|
11
11
|
|
|
12
12
|
- **Auf ein verstummtes Archiv wird gewartet, statt es immer wieder zu fragen.** Antwortet ein Archiv nicht mehr,
|
|
13
13
|
lässt strom es eine Stunde in Ruhe — zwei, dann vier, wenn es am selben Tag still bleibt — und der Agent erfährt, bis
|
|
@@ -29,22 +29,30 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
|
|
|
29
29
|
Lesung zweier Leser, bei der nur einer las, sagt das so — nie, dass sie übereinstimmen.
|
|
30
30
|
- **Die Verbindung mit dem Telefon wartet auf die Forschung, nicht als Fehler genannt.** Bevor die Forschung nach dem
|
|
31
31
|
Neustart des Computers startet, sagen `strom phone` und die Einstellungen, dass die Verbindung an ist und ihr Server
|
|
32
|
-
mit der Forschung wieder startet.
|
|
32
|
+
mit der Forschung wieder startet. Auf einem iPhone bleibt das Blatt der Fotos beim Tippen einer Person oder Notiz über
|
|
33
|
+
der Tastatur, die angebotenen Personen im Blick; der Server des Telefons startet einmal, auch wenn ihn mehrere
|
|
34
|
+
zugleich starten.
|
|
33
35
|
- **Korrekturen eigener Fehler von strom zur Entscheidung.** Findet strom, dass eine frühere Version von ihm in einer
|
|
34
|
-
Forschung etwas falsch gemacht hat, sagen `strom fixes` und Einstellungen → Wartung → Korrekturen, worum es geht,
|
|
35
|
-
gefunden wurde und was eine Behebung täte, auch wie aufwendig sie ist; ohne Ja geschieht nichts, ein Nein
|
|
36
|
-
Die erste: blinde Lesungen aus dem verkleinerten Scan, wo es einen schärferen Teil gab — neben den alten
|
|
37
|
-
gelesen, zuerst an einigen, nichts überschrieben, ein Unterschied wird zur Aufgabe.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
nicht bestätigt
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
36
|
+
Forschung etwas falsch gemacht hat, sagen `strom fixes` und Einstellungen → Wartung → Korrekturen, worum es geht,
|
|
37
|
+
was gefunden wurde und was eine Behebung täte, auch wie aufwendig sie ist; ohne Ja geschieht nichts, ein Nein
|
|
38
|
+
bleibt. Die erste: blinde Lesungen aus dem verkleinerten Scan, wo es einen schärferen Teil gab — neben den alten
|
|
39
|
+
noch einmal gelesen, zuerst an einigen, nichts überschrieben, ein Unterschied wird zur Aufgabe. Verglichen wird, was
|
|
40
|
+
gelesen wurde — die Namen, Daten und Zahlen der Abschrift, nicht die Worte des Kommentars (auch auf Deutsch, wo
|
|
41
|
+
jedes Substantiv groß geschrieben wird) —, und die Aufgabe sagt in einem Satz, welche Angabe abweicht; wo ein Name
|
|
42
|
+
nur einen Satz beginnen kann, gelten die Lesungen nicht als gleich. Gibt es nichts neu zu lesen, sagt `strom fixes
|
|
43
|
+
run` das. Die zweite: nach einem Wechsel des Lesemodells oder Agenten die früheren
|
|
44
|
+
Lesungen, die ein neues Lesen lohnen — mit offenen Stellen, uneinigen Lesern, einem offenen Widerspruch, an der
|
|
45
|
+
Quelle nicht bestätigt; gesagt als „gelesen von Modell X, jetzt liest Modell Y“, einmal je Agent und Modell gefragt,
|
|
46
|
+
jede so neu gelesen, wie sie gelesen wurde (die Bilder einer Lesung zusammen, ein Versuch oder ein Teil immer aus
|
|
47
|
+
ganzen Lesungen). Gezählt in Lesungen mit ihren Stellen, der Aufwand in Leseaufrufen; angeboten nach Stufen — nur
|
|
48
|
+
die strittigen (uneinige Leser, ein offener Widerspruch, an der Quelle nicht bestätigt), mit der direkten Linie oder
|
|
49
|
+
alle —, jede mit dem, was sie kostet, die engste zuerst; Daten und Zeugen ordnen nur. Beheben kann auch ein anderer
|
|
50
|
+
bereiter Agent oder ein anderes Modell (`strom fixes run <id> --agent <a> --model <m>`), mit dem, der vorher las;
|
|
51
|
+
einer, der hier nicht lesen kann, wird sofort abgelehnt, mit denen, die es können. Die Zeit einer Behebung wird
|
|
52
|
+
aufgerundet und nach dem Versuch nach den nebeneinander gelaufenen Lesungen neu gemessen.
|
|
53
|
+
- **Größen, wie die Sprache der Forschung sie schreibt.** `strom pack`, das Entfernen eines Stammbaums und das Menü
|
|
54
|
+
nennen die Größe eines Pakets, einer Sicherung oder der Bilder wie überall sonst (1,5 MB, bei kleinen 24 kB), nie
|
|
55
|
+
„0.0 MB“.
|
|
48
56
|
|
|
49
57
|
## 1.18.0
|
|
50
58
|
|
|
@@ -116,7 +124,9 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
|
|
|
116
124
|
GEDCOM-Datei (255 Bytes), hält das Schreiben der Datei nicht mehr auf, und kein Emoji wird dort geteilt, wo eine
|
|
117
125
|
lange Zeile weitergeht. Mit `excerpts.for all` bekommt auch eine Quelle, die noch niemand zitiert, in der Strom-App
|
|
118
126
|
ihren Ausschnitt, soweit er neben den zitierten unter die Grenze passt – die zitierten werden dafür nie kleiner; was
|
|
119
|
-
nicht passte, wird gesagt.
|
|
127
|
+
nicht passte, wird gesagt. Die Strom-App erfährt, welche Bilder einer Quelle die Forschung hat, ihre Datei aber
|
|
128
|
+
wegließ, und lädt sie bei Bedarf aus der Forschung — Quelle für Quelle oder alle auf einmal, mit Anzahl und etwa
|
|
129
|
+
ihrer Größe.
|
|
120
130
|
|
|
121
131
|
## 1.17.0
|
|
122
132
|
|
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.19.0
|
|
9
9
|
|
|
10
|
-
*In preparation: the betas up to 1.19.0-beta.
|
|
10
|
+
*In preparation: the betas up to 1.19.0-beta.7. The section is complete with the release.*
|
|
11
11
|
|
|
12
12
|
- **An archive that went silent is waited for, not asked again and again.** When an archive stops answering, strom
|
|
13
13
|
leaves it alone for an hour — two, then four when it stays silent the same day — and the agent hears until when:
|
|
@@ -28,20 +28,26 @@ The changes of each version in short. The full notes and the install lines are o
|
|
|
28
28
|
only one read says so, never that they agree.
|
|
29
29
|
- **The phone connection waits for the research, not said as an error.** Before the research starts after the computer
|
|
30
30
|
started again, `strom phone` and the settings say the connection is on and its server starts again with the research.
|
|
31
|
+
On an iPhone the sheet of photos stays above the keyboard while a person or a note is typed, the persons offered in
|
|
32
|
+
sight; the phone's server starts once, also when several start it at the same moment.
|
|
31
33
|
- **Fixes of strom's own mistakes, offered to decide.** When strom finds that an earlier version of its own did
|
|
32
34
|
something wrong in a research, `strom fixes` and Settings → Maintenance → Fixes say what it is about, what was found
|
|
33
35
|
and what a remedy would do, with how demanding it is; nothing happens without a yes, and a no is kept. The first:
|
|
34
36
|
blind readings made from the reduced scan where a sharper part existed — read again beside the old ones, first on a
|
|
35
|
-
few, nothing overwritten, a difference becomes a task.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
37
|
+
few, nothing overwritten, a difference becomes a task. Held against each other by what was read — the names, dates
|
|
38
|
+
and numbers of the transcript, not the words of the reader's comment (in German too, where every noun has a
|
|
39
|
+
capital) — the task says in one sentence which of them differs; where a name may only begin a sentence, the readings
|
|
40
|
+
are not said to agree. With nothing to read again, `strom fixes run` says so. The second: after the reading model or agent changed, the earlier readings worth reading again — with open
|
|
41
|
+
places, readers who disagreed, an open conflict, a reading not confirmed at the source; said as "read by model X,
|
|
42
|
+
now read by model Y", asked once per agent and model, each read again as it was read (the images of one reading
|
|
43
|
+
together, a trial or a part always of whole readings). Counted in readings with their places and what a remedy takes
|
|
44
|
+
in reader calls; offered by stage — the disputed alone (readers who differed, an open conflict, not confirmed at the
|
|
45
|
+
source), with the direct line, or all — each with what it takes, the narrowest first, dates and witnesses only
|
|
46
|
+
ordering them. The remedy may be read by another agent or model ready here (`strom fixes run <id> --agent <a>
|
|
47
|
+
--model <m>`), with which one read before said; one that cannot read here is refused at once, with those that can.
|
|
48
|
+
The time of a remedy is rounded up and, after the trial, measured again by the readings as they ran side by side.
|
|
49
|
+
- **Sizes as the research's language writes them.** `strom pack`, removing a family tree and the menu say how big a
|
|
50
|
+
package, a backup or the images are as everywhere else (1,5 MB in Czech, 24 kB for a small one), never "0.0 MB".
|
|
45
51
|
|
|
46
52
|
## 1.18.0
|
|
47
53
|
|
|
@@ -107,7 +113,8 @@ The changes of each version in short. The full notes and the install lines are o
|
|
|
107
113
|
value longer than a line of the GEDCOM file (255 bytes) no longer stops the file from being written, and no emoji is
|
|
108
114
|
split where a long line goes on. With `excerpts.for all` a source nobody cites yet gets its excerpt in the Strom app
|
|
109
115
|
too, wherever it fits under the limit beside the cited ones, which never get smaller for it; what did not fit is
|
|
110
|
-
said.
|
|
116
|
+
said. The Strom app is told which images of a source the research has but its file left out, and loads them from
|
|
117
|
+
the research on demand — a source at a time, or all at once with how many and about how big.
|
|
111
118
|
|
|
112
119
|
## 1.17.0
|
|
113
120
|
|
package/assets/lang/cs.json
CHANGED
|
@@ -2114,13 +2114,13 @@
|
|
|
2114
2114
|
"ui.trees.pack": "zabalit „{name}“ a poslat někomu (soubor ZIP)",
|
|
2115
2115
|
"ui.trees.unpack": "rodokmen, který někdo poslal (soubor ZIP)",
|
|
2116
2116
|
"ui.pack.which": "Které snímky přibalit?",
|
|
2117
|
-
"ui.pack.opt.records": "jen se zápisy – {
|
|
2118
|
-
"ui.pack.opt.all": "všechny – {
|
|
2119
|
-
"ui.pack.done": "Zabaleno „{name}“: {file} ({
|
|
2117
|
+
"ui.pack.opt.records": "jen se zápisy – {size} (prohledané stránky dá archiv znovu)",
|
|
2118
|
+
"ui.pack.opt.all": "všechny – {size}",
|
|
2119
|
+
"ui.pack.done": "Zabaleno „{name}“: {file} ({size})",
|
|
2120
2120
|
"ui.pack.saved": " rodokmen, jak byl naposledy uložený, i s historií",
|
|
2121
|
-
"ui.pack.images": " snímky: {n} ({
|
|
2122
|
-
"ui.pack.images.all": " snímky: všech {n} ({
|
|
2123
|
-
"ui.pack.left": "
|
|
2121
|
+
"ui.pack.images": " snímky: {n} ({size}) – se zápisy a ostatní, které nikdo znovu nestáhne",
|
|
2122
|
+
"ui.pack.images.all": " snímky: všech {n} ({size})",
|
|
2123
|
+
"ui.pack.left": " vynechané: {n, plural, one {# snímek} few {# snímky} other {# snímků}} ({size}) – prohledané stránky; až budou potřeba, strom je z archivu stáhne znovu",
|
|
2124
2124
|
"ui.pack.connectors": " konektory: {list}",
|
|
2125
2125
|
"ui.pack.noconnector": "⚠ některé vynechané snímky přišly konektorem, který na tomto počítači není – nikdo by je znovu nestáhl: strom pack --all-images",
|
|
2126
2126
|
"ui.pack.missing": "⚠ snímky, které rodokmen uvádí, ale na tomto počítači nejsou: {n} – v balíčku chybí",
|
|
@@ -2151,14 +2151,14 @@
|
|
|
2151
2151
|
"ui.remove.person": "rodokmen z počítače odebírá jen člověk u počítače",
|
|
2152
2152
|
"ui.remove.busy": "na rodokmenu „{name}“ se právě pracuje – odebrat ho jde, až práce skončí",
|
|
2153
2153
|
"ui.remove.what": "„{name}“ ({root}) · osoby: {persons}",
|
|
2154
|
-
"ui.remove.backup": "Nejdřív ho zabalit jako zálohu, se všemi snímky ({
|
|
2154
|
+
"ui.remove.backup": "Nejdřív ho zabalit jako zálohu, se všemi snímky ({size})? Kdykoli se vrátí přes strom unpack.",
|
|
2155
2155
|
"ui.remove.backup.done": " záloha: {file}",
|
|
2156
|
-
"ui.remove.images": "Poslat do koše i snímky, které žádný jiný rodokmen nepoužívá (snímky: {n}, {
|
|
2156
|
+
"ui.remove.images": "Poslat do koše i snímky, které žádný jiný rodokmen nepoužívá (snímky: {n}, {size})?",
|
|
2157
2157
|
"ui.remove.type": "Pro potvrzení napsat název rodokmenu ({name}) – cokoli jiného nic neodebere:",
|
|
2158
2158
|
"ui.remove.cancelled": "Nic se neodebralo.",
|
|
2159
2159
|
"ui.remove.done": "„{name}“ je v koši.",
|
|
2160
2160
|
"ui.remove.live": " aplikace Strom ho už živě nesleduje",
|
|
2161
|
-
"ui.remove.images.done": " jeho snímky v koši: {n} ({
|
|
2161
|
+
"ui.remove.images.done": " jeho snímky v koši: {n} ({size})",
|
|
2162
2162
|
"ui.remove.images.folder": "{name} – snímky",
|
|
2163
2163
|
"ui.remove.backup.back": " vrátí se ze zálohy: strom unpack „{file}“",
|
|
2164
2164
|
"ui.remove.trash.alone": " vrátí se z koše systému, dokud se koš nevysype",
|
|
@@ -2656,6 +2656,8 @@
|
|
|
2656
2656
|
"fix.model.why.differ": "Přečteno znovu modelem {now} (dříve: {before}) tam, kde něco zůstalo otevřené, a čtení se liší.",
|
|
2657
2657
|
"fix.model.why.refined": "Přečteno znovu modelem {now} (dříve: {before}) tam, kde něco zůstalo otevřené, a nové čtení přečetlo víc nebo jistěji.",
|
|
2658
2658
|
"fix.model.done": "obě čtení porovnaná mezi sebou ({show} {old} · {show} {now}) a s {sources}: co platí, zapsané u pramene, údaje na něm ověřené; nic odebrané bez důvodu",
|
|
2659
|
+
"fix.differs": "Liší se: dříve {before}, nově {now}.",
|
|
2660
|
+
"fix.more": "Nově přečteno navíc: {now}.",
|
|
2659
2661
|
"fix.model.unknown": "nezaznamenaný model",
|
|
2660
2662
|
"fix.model.own": "vlastní model"
|
|
2661
2663
|
}
|
package/assets/lang/de.json
CHANGED
|
@@ -2114,13 +2114,13 @@
|
|
|
2114
2114
|
"ui.trees.pack": "„{name}“ einpacken und jemandem schicken (eine ZIP-Datei)",
|
|
2115
2115
|
"ui.trees.unpack": "ein zugeschickter Stammbaum (eine ZIP-Datei)",
|
|
2116
2116
|
"ui.pack.which": "Welche Bilder kommen mit?",
|
|
2117
|
-
"ui.pack.opt.records": "die, auf denen die Einträge stehen – {
|
|
2118
|
-
"ui.pack.opt.all": "alle – {
|
|
2119
|
-
"ui.pack.done": "„{name}“ eingepackt: {file} ({
|
|
2117
|
+
"ui.pack.opt.records": "die, auf denen die Einträge stehen – {size} (die durchgesehenen Seiten gibt das Archiv wieder)",
|
|
2118
|
+
"ui.pack.opt.all": "alle – {size}",
|
|
2119
|
+
"ui.pack.done": "„{name}“ eingepackt: {file} ({size})",
|
|
2120
2120
|
"ui.pack.saved": " der Stammbaum, wie er zuletzt gespeichert wurde, mit seiner Geschichte",
|
|
2121
|
-
"ui.pack.images": " Bilder: {n} ({
|
|
2122
|
-
"ui.pack.images.all": " Bilder: alle {n} ({
|
|
2123
|
-
"ui.pack.left": " weggelassen: {n, plural, one {# Bild} other {# Bilder}} ({
|
|
2121
|
+
"ui.pack.images": " Bilder: {n} ({size}) – die, auf denen die Einträge stehen, und die, die niemand wieder laden kann",
|
|
2122
|
+
"ui.pack.images.all": " Bilder: alle {n} ({size})",
|
|
2123
|
+
"ui.pack.left": " weggelassen: {n, plural, one {# Bild} other {# Bilder}} ({size}) – durchgesehene Seiten; strom lädt sie aus dem Archiv wieder, wenn sie gebraucht werden",
|
|
2124
2124
|
"ui.pack.connectors": " Konnektoren: {list}",
|
|
2125
2125
|
"ui.pack.noconnector": "⚠ einige weggelassene Bilder kamen über einen Konnektor, der auf diesem Computer fehlt – niemand könnte sie wieder laden: strom pack --all-images",
|
|
2126
2126
|
"ui.pack.missing": "⚠ {n, plural, one {# Bild, das der Stammbaum nennt, ist} other {# Bilder, die der Stammbaum nennt, sind}} nicht auf diesem Computer – nicht im Paket",
|
|
@@ -2151,14 +2151,14 @@
|
|
|
2151
2151
|
"ui.remove.person": "einen Stammbaum nimmt nur die Person am Computer herunter",
|
|
2152
2152
|
"ui.remove.busy": "an „{name}“ wird gerade gearbeitet – er kann weg, wenn die Arbeit vorbei ist",
|
|
2153
2153
|
"ui.remove.what": "„{name}“ ({root}) · Personen: {persons}",
|
|
2154
|
-
"ui.remove.backup": "Zuerst als Sicherung einpacken, mit allen Bildern ({
|
|
2154
|
+
"ui.remove.backup": "Zuerst als Sicherung einpacken, mit allen Bildern ({size})? Er kommt jederzeit mit strom unpack zurück.",
|
|
2155
2155
|
"ui.remove.backup.done": " Sicherung: {file}",
|
|
2156
|
-
"ui.remove.images": "Auch die Bilder, die kein anderer Stammbaum nutzt, in den Papierkorb ({n}, {
|
|
2156
|
+
"ui.remove.images": "Auch die Bilder, die kein anderer Stammbaum nutzt, in den Papierkorb ({n}, {size})?",
|
|
2157
2157
|
"ui.remove.type": "Zur Bestätigung den Namen des Stammbaums ({name}) tippen – alles andere nimmt nichts weg:",
|
|
2158
2158
|
"ui.remove.cancelled": "Nichts wurde weggenommen.",
|
|
2159
2159
|
"ui.remove.done": "„{name}“ ist im Papierkorb.",
|
|
2160
2160
|
"ui.remove.live": " die Strom-App verfolgt ihn nicht mehr live",
|
|
2161
|
-
"ui.remove.images.done": " seine Bilder im Papierkorb: {n} ({
|
|
2161
|
+
"ui.remove.images.done": " seine Bilder im Papierkorb: {n} ({size})",
|
|
2162
2162
|
"ui.remove.images.folder": "{name} – Bilder",
|
|
2163
2163
|
"ui.remove.backup.back": " er kommt aus der Sicherung zurück: strom unpack „{file}“",
|
|
2164
2164
|
"ui.remove.trash.alone": " er kommt aus dem Papierkorb des Systems zurück, bis er geleert wird",
|
|
@@ -2656,6 +2656,8 @@
|
|
|
2656
2656
|
"fix.model.why.differ": "Von {now} erneut gelesen (zuvor: {before}), wo etwas offen blieb, und die Lesungen weichen ab.",
|
|
2657
2657
|
"fix.model.why.refined": "Von {now} erneut gelesen (zuvor: {before}), wo etwas offen blieb, und die neue Lesung liest mehr oder sicherer.",
|
|
2658
2658
|
"fix.model.done": "beide Lesungen miteinander verglichen ({show} {old} · {show} {now}) und mit {sources}: was gilt, an der Quelle vermerkt, die Angaben darauf geprüft; nichts ohne Grund entfernt",
|
|
2659
|
+
"fix.differs": "Abweichend: früher {before}, jetzt {now}.",
|
|
2660
|
+
"fix.more": "Jetzt zusätzlich gelesen: {now}.",
|
|
2659
2661
|
"fix.model.unknown": "ein nicht vermerktes Modell",
|
|
2660
2662
|
"fix.model.own": "eigenes Modell"
|
|
2661
2663
|
}
|
package/assets/phone/phone.css
CHANGED
|
@@ -122,7 +122,7 @@ button{font:inherit;color:inherit}
|
|
|
122
122
|
.strip.warn{color:var(--warning-text)}
|
|
123
123
|
|
|
124
124
|
/* the sheet of the photos to send */
|
|
125
|
-
.sheet{position:fixed;left:0;right:0;top:
|
|
125
|
+
.sheet{position:fixed;left:0;right:0;top:var(--vvt,0px);height:var(--vvh,100%);z-index:10;background:var(--bg);display:flex;flex-direction:column;max-width:560px;margin:0 auto}
|
|
126
126
|
.sheet-head{display:flex;align-items:flex-start;gap:8px;padding:max(12px,env(safe-area-inset-top)) 8px 10px 16px;background:var(--surface);border-bottom:1px solid var(--divider)}
|
|
127
127
|
.sheet-head>div{flex:1;min-width:0}
|
|
128
128
|
.sheet-head h2{margin:2px 0 2px}
|
package/assets/phone/phone.js
CHANGED
|
@@ -1158,16 +1158,17 @@
|
|
|
1158
1158
|
$("doneBox").hidden = !handed;
|
|
1159
1159
|
if (handed) $("doneText").textContent = t("commit.done", { n: b.done });
|
|
1160
1160
|
// the foot: the camera's photos only here, handing over
|
|
1161
|
-
|
|
1161
|
+
// while typing, only what ends the typing: the rest of the foot would take what the keyboard leaves of the sheet
|
|
1162
|
+
$("captureNote").hidden = !!S.typing || !items.some((i) => i.capture && i.blob);
|
|
1162
1163
|
$("photoRow").hidden = !!S.typing;
|
|
1163
1164
|
$("typingRow").hidden = !S.typing;
|
|
1164
1165
|
$("noPersonBtn").hidden = S.typing !== "person";
|
|
1165
1166
|
const committing = !!(b && b.commit);
|
|
1166
|
-
$("commitBtn").hidden = !items.length;
|
|
1167
|
+
$("commitBtn").hidden = !items.length || !!S.typing;
|
|
1167
1168
|
$("commitBtn").disabled = committing || !items.some((i) => !FINAL(i));
|
|
1168
1169
|
$("commitBtn").textContent = committing ? t("commit.wait") : t("commit");
|
|
1169
1170
|
$("commitCancel").hidden = !committing;
|
|
1170
|
-
$("commitLater").hidden = !items.length || committing;
|
|
1171
|
+
$("commitLater").hidden = !items.length || committing || !!S.typing;
|
|
1171
1172
|
};
|
|
1172
1173
|
|
|
1173
1174
|
const renderUnpaired = () => {
|
|
@@ -1275,6 +1276,17 @@
|
|
|
1275
1276
|
render();
|
|
1276
1277
|
};
|
|
1277
1278
|
|
|
1279
|
+
// the field typed in at the top of the sheet's body, what it offers under it (the keyboard leaves little of the sheet)
|
|
1280
|
+
const keepTyped = () => {
|
|
1281
|
+
const f = document.activeElement;
|
|
1282
|
+
if (!f || (f.id !== "person" && f.id !== "note") || $("sheet").hidden) return;
|
|
1283
|
+
const body = $("sheet").querySelector(".sheet-body");
|
|
1284
|
+
const field = (f.closest && f.closest(".field")) || f;
|
|
1285
|
+
if (!body) return;
|
|
1286
|
+
const off = field.getBoundingClientRect().top - body.getBoundingClientRect().top - 8;
|
|
1287
|
+
if (Math.abs(off) > 1) body.scrollTop += off;
|
|
1288
|
+
};
|
|
1289
|
+
|
|
1278
1290
|
let lookFor = 0;
|
|
1279
1291
|
let asked = "";
|
|
1280
1292
|
const findPeople = () => {
|
|
@@ -1317,6 +1329,7 @@
|
|
|
1317
1329
|
ul.append(li);
|
|
1318
1330
|
}
|
|
1319
1331
|
ul.hidden = !people.length;
|
|
1332
|
+
if (people.length) keepTyped();
|
|
1320
1333
|
}, 250);
|
|
1321
1334
|
};
|
|
1322
1335
|
|
|
@@ -1469,12 +1482,22 @@
|
|
|
1469
1482
|
renderPill();
|
|
1470
1483
|
};
|
|
1471
1484
|
window.addEventListener("scroll", () => S.newCount && window.scrollY <= 4 && renderPill(), { passive: true });
|
|
1472
|
-
// the keyboard: the sheet as high as what is seen of the page
|
|
1485
|
+
// the keyboard: the sheet as high as what is seen of the page and where it is seen (iOS Safari keeps the page as tall
|
|
1486
|
+
// and moves what is seen of it up above the keyboard: a sheet fixed at the page's top would end under the field), its
|
|
1487
|
+
// foot above the keyboard, the field typed in kept in sight; without visualViewport the sheet is the whole window
|
|
1473
1488
|
const vv = window.visualViewport;
|
|
1474
1489
|
if (vv) {
|
|
1475
|
-
const
|
|
1476
|
-
|
|
1477
|
-
|
|
1490
|
+
const place = () => {
|
|
1491
|
+
const root = document.documentElement.style;
|
|
1492
|
+
root.setProperty("--vvh", `${Math.round(vv.height)}px`);
|
|
1493
|
+
root.setProperty("--vvt", `${Math.max(0, Math.round(vv.offsetTop || 0))}px`);
|
|
1494
|
+
};
|
|
1495
|
+
vv.addEventListener("resize", () => {
|
|
1496
|
+
place();
|
|
1497
|
+
keepTyped();
|
|
1498
|
+
});
|
|
1499
|
+
vv.addEventListener("scroll", place);
|
|
1500
|
+
place();
|
|
1478
1501
|
}
|
|
1479
1502
|
|
|
1480
1503
|
// looked at again: asked at once; every 10 s while open
|
package/dist/cli/main.js
CHANGED
|
@@ -372,12 +372,14 @@ export async function main(argv, io, env, cwd) {
|
|
|
372
372
|
}
|
|
373
373
|
}
|
|
374
374
|
// …and the phone's server, when the connection is on (it starts again with this version by itself; one the
|
|
375
|
-
// installer ended to replace the program back)
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
375
|
+
// installer ended to replace the program back) — not by the server itself, which would start a second one beside it
|
|
376
|
+
if (def.path.join(" ") !== "phone serve") {
|
|
377
|
+
try {
|
|
378
|
+
revivePhone(env);
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
// its next bridge brings it back
|
|
382
|
+
}
|
|
381
383
|
}
|
|
382
384
|
}
|
|
383
385
|
// the folder of the program that runs with these settings (from a strom that ran here before it was kept, once):
|
package/dist/cli/menu.js
CHANGED
|
@@ -611,8 +611,8 @@ async function pickTree(ctx, run, lang) {
|
|
|
611
611
|
if (i === packAt) {
|
|
612
612
|
const tree = Tree.open(known[at].root, ctx.env);
|
|
613
613
|
const shared = ctx.settings.shared().value;
|
|
614
|
-
const size = (all) => (packPlan(tree, shared, all).media.reduce((s, m) => s + m.size, 0)
|
|
615
|
-
const which = await ctx.choose(ui(lang, "ui.pack.which"), [{ label: ui(lang, "ui.pack.opt.records", {
|
|
614
|
+
const size = (all) => mb(packPlan(tree, shared, all).media.reduce((s, m) => s + m.size, 0), lang);
|
|
615
|
+
const which = await ctx.choose(ui(lang, "ui.pack.which"), [{ label: ui(lang, "ui.pack.opt.records", { size: size(false) }) }, { label: ui(lang, "ui.pack.opt.all", { size: size(true) }) }], 0, { back: ui(lang, "ui.browse.back") });
|
|
616
616
|
if (which === undefined)
|
|
617
617
|
return;
|
|
618
618
|
await run(["pack", "--tree", tree.root, ...(which === 1 ? ["--all-images"] : [])]);
|
package/dist/cli/ui.js
CHANGED
|
@@ -1982,13 +1982,13 @@ export const UI = {
|
|
|
1982
1982
|
"ui.trees.pack": "pack \"{name}\" to send to someone (a ZIP file)",
|
|
1983
1983
|
"ui.trees.unpack": "a family tree from someone else (a ZIP file)",
|
|
1984
1984
|
"ui.pack.which": "Which images go along?",
|
|
1985
|
-
"ui.pack.opt.records": "the ones its records stand on — {
|
|
1986
|
-
"ui.pack.opt.all": "all of them — {
|
|
1987
|
-
"ui.pack.done": "Packed \"{name}\": {file} ({
|
|
1985
|
+
"ui.pack.opt.records": "the ones its records stand on — {size} (the pages searched through the archive gives again)",
|
|
1986
|
+
"ui.pack.opt.all": "all of them — {size}",
|
|
1987
|
+
"ui.pack.done": "Packed \"{name}\": {file} ({size})",
|
|
1988
1988
|
"ui.pack.saved": " the family tree as last saved, with its history",
|
|
1989
|
-
"ui.pack.images": " images: {n} ({
|
|
1990
|
-
"ui.pack.images.all": " images: all {n} ({
|
|
1991
|
-
"ui.pack.left": " left out: {n, plural, one {# image} other {# images}} ({
|
|
1989
|
+
"ui.pack.images": " images: {n} ({size}) — the ones its records stand on and the ones nobody can fetch again",
|
|
1990
|
+
"ui.pack.images.all": " images: all {n} ({size})",
|
|
1991
|
+
"ui.pack.left": " left out: {n, plural, one {# image} other {# images}} ({size}) — pages searched through; strom fetches them from the archive again when they are needed",
|
|
1992
1992
|
"ui.pack.connectors": " connectors: {list}",
|
|
1993
1993
|
"ui.pack.noconnector": "⚠ some images left out came through a connector that is not on this computer — nobody could fetch them again: strom pack --all-images",
|
|
1994
1994
|
"ui.pack.missing": "⚠ {n, plural, one {# image the family tree names is} other {# images the family tree names are}} not on this computer — not in the package",
|
|
@@ -2019,14 +2019,14 @@ export const UI = {
|
|
|
2019
2019
|
"ui.remove.person": "only the person at the computer takes a family tree off it",
|
|
2020
2020
|
"ui.remove.busy": "somebody is at work on \"{name}\" right now — it can go once the work is over",
|
|
2021
2021
|
"ui.remove.what": "\"{name}\" ({root}) · people: {persons}",
|
|
2022
|
-
"ui.remove.backup": "Pack it first as a backup, with all its images ({
|
|
2022
|
+
"ui.remove.backup": "Pack it first as a backup, with all its images ({size})? It comes back any time with strom unpack.",
|
|
2023
2023
|
"ui.remove.backup.done": " backup: {file}",
|
|
2024
|
-
"ui.remove.images": "Into the trash too the images no other family tree uses ({n}, {
|
|
2024
|
+
"ui.remove.images": "Into the trash too the images no other family tree uses ({n}, {size})?",
|
|
2025
2025
|
"ui.remove.type": "To confirm, type the name of the family tree ({name}) — anything else takes nothing off:",
|
|
2026
2026
|
"ui.remove.cancelled": "Nothing was taken off.",
|
|
2027
2027
|
"ui.remove.done": "\"{name}\" is in the trash.",
|
|
2028
2028
|
"ui.remove.live": " the Strom app no longer follows it live",
|
|
2029
|
-
"ui.remove.images.done": " its images in the trash: {n} ({
|
|
2029
|
+
"ui.remove.images.done": " its images in the trash: {n} ({size})",
|
|
2030
2030
|
"ui.remove.images.folder": "{name} – images",
|
|
2031
2031
|
"ui.remove.backup.back": " it comes back from the backup: strom unpack \"{file}\"",
|
|
2032
2032
|
"ui.remove.trash.alone": " it comes back from the system's trash, until it is emptied",
|
|
@@ -14,7 +14,7 @@ import { mediaOrient } from "../core/views.js";
|
|
|
14
14
|
import { median, readJournal } from "../core/readstats.js";
|
|
15
15
|
import { metricsDir } from "../core/metrics.js";
|
|
16
16
|
import { runGit } from "../core/git.js";
|
|
17
|
-
import {
|
|
17
|
+
import { READER_MS_MAX, readExpectDefault } from "../core/live-read.js";
|
|
18
18
|
import { foldText } from "../core/text.js";
|
|
19
19
|
import { loadReadings } from "./readings.js";
|
|
20
20
|
import { isArchive } from "../core/mode.js";
|
|
@@ -39,6 +39,8 @@ export const BLIND_REDUCED_FIXED_AT = "2026-10-21T13:42:49Z";
|
|
|
39
39
|
/** A view made for a reader this long before its reader began belongs to its reading. */
|
|
40
40
|
const VIEW_BEFORE_MS = 30 * 60_000;
|
|
41
41
|
const VIEW_AFTER_MS = 2 * 60_000;
|
|
42
|
+
/** strom makes a reader's views before it begins it: of a reader whose own start is known, only those (ms rounding). */
|
|
43
|
+
const VIEW_EXACT_MS = 100;
|
|
42
44
|
/** Views of one reading are made within moments of each other. */
|
|
43
45
|
const VIEW_CLUSTER_MS = 10_000;
|
|
44
46
|
/** A later reading from a sharper copy that covers this much of a place has read it again. */
|
|
@@ -96,6 +98,9 @@ function blindReducedDetect(tree, env) {
|
|
|
96
98
|
// when each reading began (its readers, this computer)
|
|
97
99
|
const began = new Map();
|
|
98
100
|
const capOf = new Map();
|
|
101
|
+
// when the reader of each image of a reading began, and every reader's start of an image (any reading)
|
|
102
|
+
const beganImage = new Map();
|
|
103
|
+
const startsOf = new Map();
|
|
99
104
|
for (const r of readJournal(path.join(metricsDir(tree.root), "readers.jsonl"))) {
|
|
100
105
|
if (r.kind !== "read" || typeof r.reader !== "string" || typeof r.at !== "string")
|
|
101
106
|
continue;
|
|
@@ -105,6 +110,17 @@ function blindReducedDetect(tree, env) {
|
|
|
105
110
|
began.set(stem, t);
|
|
106
111
|
if (num(r.cap))
|
|
107
112
|
capOf.set(stem, num(r.cap));
|
|
113
|
+
if (!Number.isFinite(t))
|
|
114
|
+
continue;
|
|
115
|
+
// the images it read (an older strom's record: those its reading is named after)
|
|
116
|
+
for (const id of Array.isArray(r.imageIds) ? r.imageIds : (stem.match(/M\d+/gu) ?? [])) {
|
|
117
|
+
if (typeof id !== "string")
|
|
118
|
+
continue;
|
|
119
|
+
const k = `${stem}|${id}`;
|
|
120
|
+
if (t < (beganImage.get(k) ?? Infinity))
|
|
121
|
+
beganImage.set(k, t);
|
|
122
|
+
startsOf.set(id, [...(startsOf.get(id) ?? []), t]);
|
|
123
|
+
}
|
|
108
124
|
}
|
|
109
125
|
// the views made for readers (this computer): a crop, the image it is of, when
|
|
110
126
|
const views = new Map();
|
|
@@ -132,13 +148,23 @@ function blindReducedDetect(tree, env) {
|
|
|
132
148
|
const times = r.reports.map((n) => added.get(n)).filter((t) => t !== undefined);
|
|
133
149
|
return times.length ? Math.min(...times) : undefined;
|
|
134
150
|
};
|
|
135
|
-
/**
|
|
136
|
-
|
|
137
|
-
|
|
151
|
+
/**
|
|
152
|
+
* The views of one reading of an image: those made last before its reader began, within moments of each other —
|
|
153
|
+
* never those of another reader of the same image (made before it began). Its reader's own start unknown here and
|
|
154
|
+
* views of more than one moment near: which it read is not sure (an estimate).
|
|
155
|
+
*/
|
|
156
|
+
const viewsOf = (stem, key, t) => {
|
|
157
|
+
const own = beganImage.get(`${stem}|${key}`);
|
|
158
|
+
const at = own ?? t;
|
|
159
|
+
const others = (startsOf.get(key) ?? []).filter((s) => s !== own);
|
|
160
|
+
const after = Math.min(at + (own !== undefined ? VIEW_EXACT_MS : VIEW_AFTER_MS), ...others.filter((s) => s > at));
|
|
161
|
+
const before = Math.max(at - VIEW_BEFORE_MS, ...others.filter((s) => s < at));
|
|
162
|
+
const near = (views.get(key) ?? []).filter((v) => v.t > before && v.t <= after);
|
|
138
163
|
if (!near.length)
|
|
139
|
-
return [];
|
|
164
|
+
return { views: [], estimated: false };
|
|
140
165
|
const last = Math.max(...near.map((v) => v.t));
|
|
141
|
-
|
|
166
|
+
const cluster = near.filter((v) => v.t >= last - VIEW_CLUSTER_MS);
|
|
167
|
+
return { views: cluster, estimated: own === undefined && cluster.length < near.length };
|
|
142
168
|
};
|
|
143
169
|
// places read again from a sharper copy since (a later reading): by whole image, fractions as the file lies
|
|
144
170
|
const readSharper = new Map();
|
|
@@ -167,7 +193,7 @@ function blindReducedDetect(tree, env) {
|
|
|
167
193
|
const m = byId.get(id);
|
|
168
194
|
if (!m || m.retracted)
|
|
169
195
|
continue;
|
|
170
|
-
const own = viewsOf(id, t);
|
|
196
|
+
const { views: own, estimated: unsure } = viewsOf(r.stem, id, t);
|
|
171
197
|
if (m.part) {
|
|
172
198
|
// a part read: the place in the whole image read sharper (when its views say where)
|
|
173
199
|
if (m.recordset !== undefined && m.image !== undefined) {
|
|
@@ -189,7 +215,7 @@ function blindReducedDetect(tree, env) {
|
|
|
189
215
|
const x1 = Math.max(...own.map((v) => v.region.x + v.region.w));
|
|
190
216
|
const y1 = Math.max(...own.map((v) => v.region.y + v.region.h));
|
|
191
217
|
const v = own[0];
|
|
192
|
-
places = [{ file: shownToFile({ x: x0 / v.W, y: y0 / v.H, w: (x1 - x0) / v.W, h: (y1 - y0) / v.H }, v.orient), estimated:
|
|
218
|
+
places = [{ file: shownToFile({ x: x0 / v.W, y: y0 / v.H, w: (x1 - x0) / v.W, h: (y1 - y0) / v.H }, v.orient), estimated: unsure }];
|
|
193
219
|
}
|
|
194
220
|
else if ((views.size === 0 || !views.has(id)) && r.blind && few && t < Date.parse(BLIND_REDUCED_FIXED_AT)) {
|
|
195
221
|
// no record of its views here (another computer): the places of the sources' entries on the image, an estimate
|
|
@@ -504,7 +530,7 @@ function newModelDetect(tree, env) {
|
|
|
504
530
|
const unclear = blocks.reduce((n, b) => n + Math.max(unclearOf(b), b.result === "unclear" ? 1 : 0), 0);
|
|
505
531
|
// a blind reading that reads otherwise than the researcher's reading of the image before it
|
|
506
532
|
const theirs = r.blind ? list.filter((x) => !x.r.blind && x.t <= t && x.r !== r).sort((a, b) => b.t - a.t)[0] : undefined;
|
|
507
|
-
const differ = !!theirs && compareReadings(theirs.blocks, blocks) === "differ";
|
|
533
|
+
const differ = !!theirs && compareReadings(theirs.blocks, blocks, tree.lang).result === "differ";
|
|
508
534
|
const reg = cropRegion(crop);
|
|
509
535
|
const src = reg && !m.part ? sourcesOf(sources, media, m, shownToFile(reg, mediaOrient(env.shared, m))) : sources.filter((s) => (s.media ?? []).includes(id) || (s.clips ?? []).some((c) => c.media === id)).map((s) => s.id).sort();
|
|
510
536
|
const reasons = [...(unclear ? ["unclear"] : []), ...(differ ? ["differ"] : []), ...(src.some((s) => conflicted.has(s)) ? ["conflict"] : [])];
|
|
@@ -671,13 +697,16 @@ export function decide(tree, id, decision) {
|
|
|
671
697
|
delete c.fixes;
|
|
672
698
|
}, { op: "fix.decide", summary: decision ? `fix ${id}: ${decision}` : `fix ${id}: asked again` });
|
|
673
699
|
}
|
|
674
|
-
/**
|
|
700
|
+
/**
|
|
701
|
+
* Each check with what it found here and the decision about it. Set right holds for what was read again: what a check
|
|
702
|
+
* still finds after (never read again — a few first, or a place strom took wrongly then) waits again, it alone.
|
|
703
|
+
*/
|
|
675
704
|
export function fixStates(tree, env) {
|
|
676
705
|
return FIX_CHECKS.map((check) => {
|
|
677
706
|
const found = check.detect(tree, env);
|
|
678
707
|
const key = decisionKeyOf(tree, check, env);
|
|
679
708
|
const decision = decisionOf(tree.config, key);
|
|
680
|
-
const state = decision ?
|
|
709
|
+
const state = decision?.decision === "declined" ? "declined" : found.items.length ? "pending" : decision ? "fixed" : "nothing";
|
|
681
710
|
return { check, key, found, ...(decision ? { decision } : {}), state };
|
|
682
711
|
});
|
|
683
712
|
}
|
|
@@ -691,7 +720,7 @@ function earlierReaders(root, agent, model) {
|
|
|
691
720
|
if (num(r.usd) !== undefined && !r.usdPartial)
|
|
692
721
|
usd.push(num(r.usd));
|
|
693
722
|
const ms = num(r.wallMs) ?? num(r.ms);
|
|
694
|
-
if (ms)
|
|
723
|
+
if (ms && ms > 0 && ms <= READER_MS_MAX)
|
|
695
724
|
secs.push(ms / 1000);
|
|
696
725
|
}
|
|
697
726
|
return { usd, secs };
|
|
@@ -710,7 +739,7 @@ export function estimateFix(root, o) {
|
|
|
710
739
|
const e = earlierReaders(root, o.agent, o.model);
|
|
711
740
|
const n = o.calls;
|
|
712
741
|
const mUsd = e.usd.length >= EARLIER_MIN ? median(e.usd) : undefined;
|
|
713
|
-
const one = (e.secs.length >= EARLIER_MIN ? median(e.secs) : undefined) ??
|
|
742
|
+
const one = (e.secs.length >= EARLIER_MIN ? median(e.secs) : undefined) ?? readExpectDefault(o.agent);
|
|
714
743
|
const secs = o.perReading !== undefined ? n * o.perReading : Math.ceil(n / Math.max(1, o.parallel)) * one;
|
|
715
744
|
const minutes = Math.max(1, Math.ceil(secs / 60));
|
|
716
745
|
if (mUsd !== undefined) {
|
|
@@ -719,39 +748,123 @@ export function estimateFix(root, o) {
|
|
|
719
748
|
}
|
|
720
749
|
return { level: n >= COST_COUNT.expensive ? "expensive" : n >= COST_COUNT.medium ? "medium" : "cheap", calls: n, minutes, basis: "count", earlier: 0 };
|
|
721
750
|
}
|
|
722
|
-
/**
|
|
723
|
-
|
|
724
|
-
|
|
751
|
+
/**
|
|
752
|
+
* Languages whose prose writes every noun with a capital ("Der Ausschnitt zeigt …"): there a lone capital is no name for
|
|
753
|
+
* sure, and the little words that begin a sentence there (an article, a pronoun) never one.
|
|
754
|
+
*/
|
|
755
|
+
const NOUNS_CAPITALIZED = {
|
|
756
|
+
de: new Set("der die das dem den des ein eine einen einem einer eines im am vom zum zur beim ins es er sie wir ich man dies diese dieser dieses hier da dort auch und aber oder nur noch so wie was wo nach unter von mit bei auf in an".split(" ")),
|
|
757
|
+
};
|
|
758
|
+
// a reader's quotation marks: what it quotes of the record is its transcript, the rest its comment
|
|
759
|
+
const QUOTED = /[„“”"«»「」『』]([^„“”"«»「」『』]*)[„“”"«»「」『』]/gu;
|
|
760
|
+
/** A word as compared: folded, the long s and the ezh of old hands as the letters they are. */
|
|
761
|
+
const wordKey = (w) => foldText(w).replace(/ſ/gu, "s").replace(/ʒ/gu, "z");
|
|
762
|
+
/**
|
|
763
|
+
* The facts of a transcript: the names (a word that begins with a capital, or any word of a script without capitals)
|
|
764
|
+
* and the numbers; a word broken over a line ("Wac- / lawa", "In= / mann", "Wac|lawa") whole, the marks of doubt and an editor's
|
|
765
|
+
* notes in brackets left out (a letter supplied in brackets, "Mi[cha]la", kept). Of a reader's own words (prose: nothing
|
|
766
|
+
* quoted), a capital that only begins a sentence ("The crop shows …", "Výřez ukazuje …") is a name only where the same
|
|
767
|
+
* word stands elsewhere as one; in a language that writes its nouns with a capital (`lang`), so is a lone capital — two or
|
|
768
|
+
* more together ("Jan Novak") and a month after a day ("5. Mai") stay names. Such a word is kept as maybe one.
|
|
769
|
+
*/
|
|
770
|
+
function readOf(text, unclear, found, prose = false, lang = "") {
|
|
771
|
+
const clean = text
|
|
772
|
+
.replace(/\[\?\]|\(\?\)/gu, "")
|
|
773
|
+
.replace(/\[[^\]]*\s[^\]]*\]/gu, " ")
|
|
774
|
+
.replace(/[[\]]/gu, "")
|
|
775
|
+
.replace(/([\p{L}\p{M}])(?:[-=¬]\s*[/|]?\s*|\|)(?=[\p{L}\p{M}])/gu, "$1");
|
|
776
|
+
const facts = new Map();
|
|
777
|
+
const words = new Set();
|
|
778
|
+
const tokens = [...clean.matchAll(/[\p{L}\p{M}\p{N}]+/gu)];
|
|
779
|
+
const little = prose ? NOUNS_CAPITALIZED[lang.toLowerCase().split(/[-_]/u)[0]] : undefined;
|
|
780
|
+
const capital = (i) => /^\p{Lu}/u.test(tokens[i]?.[0] ?? "") && !/\p{N}/u.test(tokens[i][0]) && !little?.has(wordKey(tokens[i][0]));
|
|
781
|
+
// the word before a token within its sentence (a sentence's end or a line's in between: none)
|
|
782
|
+
const joined = (a, b) => a >= 0 && b < tokens.length && !/[.!?…:;\n]/u.test(clean.slice(tokens[a].index + tokens[a][0].length, tokens[b].index).replace(/^\./u, (d) => (/\p{N}$/u.test(tokens[a][0]) ? "" : d)));
|
|
783
|
+
const maybeAt = [];
|
|
784
|
+
tokens.forEach((m, i) => {
|
|
785
|
+
const w = m[0];
|
|
786
|
+
const k = wordKey(w);
|
|
787
|
+
words.add(k);
|
|
788
|
+
const digits = /\p{N}/u.test(w);
|
|
789
|
+
const cased = /[\p{Lu}\p{Ll}]/u.test(w);
|
|
790
|
+
if (!digits && cased && prose && /^\p{Lu}/u.test(w)) {
|
|
791
|
+
if (little?.has(k))
|
|
792
|
+
return;
|
|
793
|
+
const lone = little ? !(joined(i - 1, i) && capital(i - 1)) && !(joined(i, i + 1) && capital(i + 1)) && !(joined(i - 1, i) && /^\p{N}+$/u.test(tokens[i - 1][0])) : opensSentence(clean, m.index, w.length);
|
|
794
|
+
if (lone) {
|
|
795
|
+
maybeAt.push({ word: w, key: k, at: i });
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
if (digits || (w.length > 1 && (!cased || /^\p{Lu}/u.test(w))))
|
|
800
|
+
if (!facts.has(k))
|
|
801
|
+
facts.set(k, w);
|
|
802
|
+
});
|
|
803
|
+
const near = (i) => (i >= 0 && i < tokens.length ? wordKey(tokens[i][0]) : "");
|
|
804
|
+
const maybe = maybeAt.filter((x) => x.word.length > 1).map((x) => ({ word: x.word, key: x.key, prev: joined(x.at - 1, x.at) ? near(x.at - 1) : "", next: joined(x.at, x.at + 1) ? near(x.at + 1) : "" }));
|
|
805
|
+
return { facts, words, unclear, found, maybe };
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* A word that begins a sentence of prose: at a line's start or after a sentence's end (never after a number's dot, "5. Mai"),
|
|
809
|
+
* the next word of its sentence in small letters ("Jan Novak", "Jan, son of" stay names).
|
|
810
|
+
*/
|
|
811
|
+
function opensSentence(text, at, length) {
|
|
812
|
+
let i = at - 1;
|
|
813
|
+
while (i >= 0 && /\s/u.test(text[i]) && text[i] !== "\n")
|
|
814
|
+
i--;
|
|
815
|
+
const start = i < 0 || text[i] === "\n" || (/[.!?…]/u.test(text[i]) && i > 0 && !/\p{N}/u.test(text[i - 1]));
|
|
816
|
+
return start && /^\s+\p{Ll}/u.test(text.slice(at + length));
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* What a reader read of its blocks: the parts of its entries it quotes from the record (when it quotes any), else its
|
|
820
|
+
* entries — never a line of possible readings with their chances or of letters with theirs (a "%" in it), nor how it
|
|
821
|
+
* describes the hand or how sure it is (hand, certainty); what it could not read only counts as unclear.
|
|
822
|
+
*/
|
|
823
|
+
function readOfBlocks(blocks, lang) {
|
|
824
|
+
const lines = blocks.flatMap((b) => b.entries ?? []).filter((l) => !/%/u.test(l));
|
|
825
|
+
const quoted = lines.flatMap((l) => [...l.matchAll(QUOTED)].map((m) => m[1]));
|
|
826
|
+
const unclear = blocks.reduce((n, x) => n + unclearOf(x), 0);
|
|
827
|
+
const found = blocks.some((b) => b.result === "found");
|
|
828
|
+
return quoted.length ? readOf(quoted.join(" "), unclear, found) : readOf(lines.join("\n"), unclear, found, true, lang);
|
|
829
|
+
}
|
|
830
|
+
/** What a reading of a document read: the people's names and ages, the events' dates and places (the reader's own fields). */
|
|
831
|
+
function readOfDocument(r) {
|
|
832
|
+
const { unclear } = docText(r);
|
|
833
|
+
const parts = r.entries.flatMap((e) => [...e.persons.flatMap((p) => [p.given, p.surname, p.age]), ...e.events.flatMap((v) => [v.date?.gedcom ?? v.date?.text, v.place?.text])]);
|
|
834
|
+
// every word of its fields counts: a document's reader says what each one is
|
|
835
|
+
const read = readOf(parts.filter(Boolean).join(" "), unclear, r.entries.length > 0);
|
|
836
|
+
for (const w of read.words)
|
|
837
|
+
if (!read.facts.has(w))
|
|
838
|
+
read.facts.set(w, w);
|
|
839
|
+
return { ...read, maybe: [] };
|
|
725
840
|
}
|
|
726
|
-
/** A reader's entries as one text, and its uncertain places. */
|
|
727
|
-
const readText = (blocks) => ({ text: blocks.map((b) => (b.entries?.length ? b.entries.join(" ") : b.text)).join(" "), unclear: blocks.reduce((n, x) => n + unclearOf(x), 0) });
|
|
728
841
|
/**
|
|
729
|
-
* The new reading of a place against the earlier one
|
|
730
|
-
*
|
|
842
|
+
* The new reading of a place against the earlier one, by what each read — the names and numbers of its transcript:
|
|
843
|
+
* the same (shoda); all the earlier one read with more beside it, fewer places unclear, or found where it was unclear
|
|
844
|
+
* (upřesněno); something the earlier one read that the new one does not — another name, date, number or place (rozchod).
|
|
845
|
+
* Of a reader's own words, a capital that may only begin a sentence or be a noun in another place of the same sentence
|
|
846
|
+
* ("Marie byla …" against "Anna byla …"): not sure, so refined, never the same. Nothing read: failed. `lang`: the language
|
|
847
|
+
* the readers wrote their own words in (the research language).
|
|
731
848
|
*/
|
|
732
|
-
export function compareReadings(before, now) {
|
|
733
|
-
return now.length ?
|
|
849
|
+
export function compareReadings(before, now, lang) {
|
|
850
|
+
return now.length ? compareRead(readOfBlocks(before, lang), readOfBlocks(now, lang)) : { result: "failed", before: [], now: [] };
|
|
734
851
|
}
|
|
735
|
-
/** A reading of a document again against the earlier one, as compareReadings (its
|
|
852
|
+
/** A reading of a document again against the earlier one, as compareReadings (its people and events). */
|
|
736
853
|
export function compareDocuments(before, now) {
|
|
737
854
|
if (!now || !now.entries.length)
|
|
738
|
-
return "failed";
|
|
739
|
-
return
|
|
740
|
-
}
|
|
741
|
-
function
|
|
742
|
-
const
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
return fewer ? "refined" : "same";
|
|
752
|
-
if ((alike >= 0.6 && fewer) || more)
|
|
753
|
-
return "refined";
|
|
754
|
-
return "differ";
|
|
855
|
+
return { result: "failed", before: [], now: [] };
|
|
856
|
+
return compareRead(readOfDocument(before), readOfDocument(now));
|
|
857
|
+
}
|
|
858
|
+
function compareRead(a, b) {
|
|
859
|
+
const before = [...a.facts].filter(([k]) => !b.words.has(k)).map(([, w]) => w);
|
|
860
|
+
const now = [...b.facts].filter(([k]) => !a.words.has(k)).map(([, w]) => w);
|
|
861
|
+
if (before.length)
|
|
862
|
+
return { result: "differ", before, now };
|
|
863
|
+
// a maybe-name of one where the other has another between the same words
|
|
864
|
+
const swapped = (x, y) => x.maybe.filter((m) => !x.facts.has(m.key) && !y.words.has(m.key) && y.maybe.some((o) => o.key !== m.key && o.prev === m.prev && o.next === m.next)).map((m) => m.word);
|
|
865
|
+
const maybeNow = swapped(b, a);
|
|
866
|
+
const surer = b.unclear < a.unclear || (b.found && !a.found);
|
|
867
|
+
return { result: now.length || maybeNow.length || surer ? "refined" : "same", before, now: [...now, ...maybeNow] };
|
|
755
868
|
}
|
|
756
869
|
// ── what waits ──────────────────────────────────────────────────────────────────────────────────────────────────
|
|
757
870
|
/** The checks that wait for the person's decision here (none in an archive: nobody reads there). */
|
package/dist/commands/fixes.js
CHANGED
|
@@ -202,6 +202,12 @@ function blocksOf(tree, stem, media) {
|
|
|
202
202
|
const r = loadReadings(tree).find((x) => x.stem === stem);
|
|
203
203
|
return (r?.blocks ?? []).filter((b) => b.media === media.id || (b.recordset === media.recordset && b.num !== undefined && b.num === media.image));
|
|
204
204
|
}
|
|
205
|
+
/** A comparison as a reading again keeps it: its result, and what differs where something does. */
|
|
206
|
+
function compared(c) {
|
|
207
|
+
if (!c)
|
|
208
|
+
return { result: "failed" };
|
|
209
|
+
return { result: c.result, ...(c.before.length || c.now.length ? { differs: { before: c.before, now: c.now } } : {}) };
|
|
210
|
+
}
|
|
205
211
|
/** Each place read again, blind with the earlier question, from the sharper copy; the readings held against each other. */
|
|
206
212
|
async function readAgain(ctx, tree, t, items, model) {
|
|
207
213
|
const { def } = resolveCommand(["read"]);
|
|
@@ -231,7 +237,8 @@ async function readAgain(ctx, tree, t, items, model) {
|
|
|
231
237
|
}
|
|
232
238
|
const before = loadDocResult(tree.root, item.reading);
|
|
233
239
|
const secs = (Date.now() - began) / 1000;
|
|
234
|
-
|
|
240
|
+
const c = before ? compareDocuments(before, now) : undefined;
|
|
241
|
+
return [{ item, ...compared(c), ...(now?.id ? { reading: now.id } : {}), secs, ...(error ? { error } : {}) }];
|
|
235
242
|
}
|
|
236
243
|
const opts = { question: item.question, ...(model ? { model } : {}), ...(item.crop ? { crop: item.crop } : {}), parallel: "1", ...(item.blind ? { blind: true } : {}), ...(item.context ? { context: item.context } : {}) };
|
|
237
244
|
let reading;
|
|
@@ -249,7 +256,7 @@ async function readAgain(ctx, tree, t, items, model) {
|
|
|
249
256
|
const m = byId.get(i.media);
|
|
250
257
|
const now = reading && m ? blocksOf(tree, reading, m) : [];
|
|
251
258
|
const before = m ? blocksOf(tree, i.reading, m) : [];
|
|
252
|
-
return { item: i,
|
|
259
|
+
return { item: i, ...compared(compareReadings(before, now, tree.lang)), ...(reading ? { reading } : {}), secs, ...(error ? { error } : {}) };
|
|
253
260
|
});
|
|
254
261
|
})).flat();
|
|
255
262
|
}
|
|
@@ -267,7 +274,7 @@ function tasksFor(tree, check, done, now) {
|
|
|
267
274
|
priority: d.result === "differ" ? 2 : 3,
|
|
268
275
|
what: phrase(lang, `${p}.what`, { media: i.document ? i.document.files.join(", ") : i.media, old: i.reading, now: d.reading }),
|
|
269
276
|
where: [...(i.document ? i.document.files : [i.media]), ...i.sources],
|
|
270
|
-
why: phrase(lang, `${p}.why.${d.result}`, { since: check.since, from: i.from ?? "", before: i.before ? label(i.before) : "", now: now ? label(now) : "" }),
|
|
277
|
+
why: [phrase(lang, `${p}.why.${d.result}`, { since: check.since, from: i.from ?? "", before: i.before ? label(i.before) : "", now: now ? label(now) : "" }), differsText(lang, d)].filter(Boolean).join(" "),
|
|
271
278
|
doneWhen: phrase(lang, `${p}.done`, { old: i.reading, now: d.reading, sources: i.sources.join(", ") || i.media, show: i.document ? "strom document show" : "strom readings" }),
|
|
272
279
|
subject: [...i.sources],
|
|
273
280
|
state: "open",
|
|
@@ -276,6 +283,15 @@ function tasksFor(tree, check, done, now) {
|
|
|
276
283
|
d.task = task.id;
|
|
277
284
|
}
|
|
278
285
|
}
|
|
286
|
+
/** One sentence of what differs (the facts each read that the other does not), or what the new reading read beside. */
|
|
287
|
+
function differsText(lang, d) {
|
|
288
|
+
const list = (ws) => (ws.length ? ws.slice(0, 6).join(", ") + (ws.length > 6 ? ", …" : "") : "—");
|
|
289
|
+
if (!d.differs)
|
|
290
|
+
return "";
|
|
291
|
+
if (d.result === "differ")
|
|
292
|
+
return phrase(lang, "fix.differs", { before: list(d.differs.before), now: list(d.differs.now) });
|
|
293
|
+
return d.differs.now.length ? phrase(lang, "fix.more", { now: list(d.differs.now) }) : "";
|
|
294
|
+
}
|
|
279
295
|
/** `secs`: the real time the readings took, readers at once and all. Its places: the same, refined, different. */
|
|
280
296
|
function summaryText(t, done, secs) {
|
|
281
297
|
const n = (r) => done.filter((d) => d.result === r).length;
|
|
@@ -532,14 +548,15 @@ register({
|
|
|
532
548
|
// who reads, before anything is offered: the agent asked, or with nothing asked, any here
|
|
533
549
|
if (typeof opts.model === "string" && !opts.model.trim())
|
|
534
550
|
throw new UsageError("--model names a model", { hint: `${set} --model <model>` });
|
|
535
|
-
if (ctx.settings.agent(tree.config).source === "flag" || typeof opts.model === "string" || !readyHere(ctx).length)
|
|
536
|
-
requireReady(ctx, tree, set);
|
|
537
|
-
personOnly(ctx, t, "Read places of this research again (paid readings)?", set);
|
|
538
551
|
const first = positive(opts.first, "first");
|
|
539
552
|
const tryN = positive(opts.try, "try", FIX_TRY);
|
|
540
553
|
const states = args[0] ? [stateOfCheck(ctx, tree, args[0])] : fixStates(tree, fixEnv(ctx, tree)).filter((s) => s.state === "pending");
|
|
541
|
-
|
|
542
|
-
|
|
554
|
+
// nothing to read again: said so, before who reads or whose choice it is
|
|
555
|
+
if (!states.some((s) => s.found.items.length))
|
|
556
|
+
return { text: t("ui.fixes.none"), data: { checks: states.map((s) => ({ id: s.check.id, read: 0, tasks: [] })) } };
|
|
557
|
+
if (ctx.settings.agent(tree.config).source === "flag" || typeof opts.model === "string" || !readyHere(ctx).length)
|
|
558
|
+
requireReady(ctx, tree, set);
|
|
559
|
+
personOnly(ctx, t, "Read places of this research again (paid readings)?", set);
|
|
543
560
|
const results = [];
|
|
544
561
|
for (const s of states) {
|
|
545
562
|
if (!s.found.items.length) {
|
package/dist/commands/pack.js
CHANGED
|
@@ -8,12 +8,12 @@ import { NeedsConsentError, UsageError } from "../core/errors.js";
|
|
|
8
8
|
import { findPack, freeRoot, INSTALL_UNIX, INSTALL_WINDOWS, packPlan, unpack, writePack } from "../core/pack.js";
|
|
9
9
|
import { desktopDir, userHome } from "../core/paths.js";
|
|
10
10
|
import { safeFolderName } from "../core/text.js";
|
|
11
|
+
import { mb } from "../core/tidy.js";
|
|
11
12
|
import { Tree } from "../core/tree.js";
|
|
12
13
|
import { syncAgentFiles } from "../agents/files.js";
|
|
13
14
|
import { setupWizard } from "../cli/wizard.js";
|
|
14
15
|
import { isAgent } from "../core/which.js";
|
|
15
16
|
import { ensureShared } from "./setup.js";
|
|
16
|
-
const mb = (bytes) => (bytes / 1e6 < 10 ? (bytes / 1e6).toFixed(1) : Math.round(bytes / 1e6).toString());
|
|
17
17
|
/** Where a package goes unless named: the desktop (else the home), the tree's name and the day, a free name. */
|
|
18
18
|
export function defaultPackFile(ctx, tree) {
|
|
19
19
|
const base = `${safeFolderName(tree.config.name) || "tree"} ${new Date().toISOString().slice(0, 10)}`;
|
|
@@ -60,11 +60,11 @@ register({
|
|
|
60
60
|
if (ctx.dryRun) {
|
|
61
61
|
const plan = packPlan(tree, shared, all);
|
|
62
62
|
const bytes = plan.media.reduce((s, m) => s + m.size, 0);
|
|
63
|
-
return { text: `dry run: would pack "${tree.config.name}" with ${counted(plan.media.length, "image")} (${mb(bytes)}
|
|
63
|
+
return { text: `dry run: would pack "${tree.config.name}" with ${counted(plan.media.length, "image")} (${mb(bytes)}) into ${ctx.display(out)} — nothing written`, data: { out, plan } };
|
|
64
64
|
}
|
|
65
65
|
const r = packTree(tree, shared, out, all);
|
|
66
66
|
const took = r.plan.media.reduce((s, m) => s + m.size, 0);
|
|
67
|
-
const text = lines(ui(lang, "ui.pack.done", { name: tree.config.name, file: ctx.display(r.file),
|
|
67
|
+
const text = lines(ui(lang, "ui.pack.done", { name: tree.config.name, file: ctx.display(r.file), size: mb(r.bytes, lang) }), ui(lang, "ui.pack.saved"), ui(lang, all ? "ui.pack.images.all" : "ui.pack.images", { n: r.plan.media.length, size: mb(took, lang) }), r.plan.left.count ? ui(lang, "ui.pack.left", { n: r.plan.left.count, size: mb(r.plan.left.bytes, lang) }) : undefined, r.plan.connectors.length ? ui(lang, "ui.pack.connectors", { list: r.plan.connectors.join(", ") }) : undefined, r.plan.left.count && r.plan.left.connectors ? ui(lang, "ui.pack.noconnector") : undefined, r.plan.missing.length ? ui(lang, "ui.pack.missing", { n: r.plan.missing.length }) : undefined, "", ui(lang, "ui.pack.send"));
|
|
68
68
|
return { text, data: { file: r.file, bytes: r.bytes, manifest: r.manifest, missing: r.plan.missing } };
|
|
69
69
|
},
|
|
70
70
|
});
|
package/dist/commands/trees.js
CHANGED
|
@@ -152,17 +152,16 @@ register({
|
|
|
152
152
|
const others = ctx.knownTrees().map((k) => k.root).filter((r) => path.resolve(r) !== path.resolve(root));
|
|
153
153
|
const only = onlyItsFiles(root, others, shared);
|
|
154
154
|
const onlyBytes = only.reduce((s, f) => s + f.size, 0);
|
|
155
|
-
const mb = (b) => (b / 1e6 < 10 ? (b / 1e6).toFixed(1) : Math.round(b / 1e6).toString());
|
|
156
155
|
ctx.io.stdout(ui(lang, "ui.remove.what", { name, root: ctx.display(root), persons: tree.list("person").length }) + "\n");
|
|
157
156
|
// a way back first
|
|
158
157
|
let backup;
|
|
159
158
|
const all = packPlan(tree, shared, true).media.reduce((s, m) => s + m.size, 0);
|
|
160
|
-
if (await ctx.confirm(ui(lang, "ui.remove.backup", {
|
|
159
|
+
if (await ctx.confirm(ui(lang, "ui.remove.backup", { size: mb(all, lang) }), true)) {
|
|
161
160
|
const out = defaultPackFile(ctx, tree);
|
|
162
161
|
backup = packTree(tree, shared, out, true).file;
|
|
163
162
|
ctx.io.stdout(ui(lang, "ui.remove.backup.done", { file: ctx.display(backup) }) + "\n");
|
|
164
163
|
}
|
|
165
|
-
const images = only.length > 0 && (await ctx.confirm(ui(lang, "ui.remove.images", { n: only.length,
|
|
164
|
+
const images = only.length > 0 && (await ctx.confirm(ui(lang, "ui.remove.images", { n: only.length, size: mb(onlyBytes, lang) }), backup !== undefined));
|
|
166
165
|
const typed = (await ctx.ask(ui(lang, "ui.remove.type", { name }))).trim();
|
|
167
166
|
if (!typed || foldText(typed) !== foldText(name))
|
|
168
167
|
return { text: ui(lang, "ui.remove.cancelled"), data: { removed: false, ...(backup ? { backup } : {}) } };
|
|
@@ -182,7 +181,7 @@ register({
|
|
|
182
181
|
moveToTrash(folder, ctx.env, ui(lang, "ui.remove.images.folder", { name }));
|
|
183
182
|
}
|
|
184
183
|
const cfg = ctx.settings.config;
|
|
185
|
-
const text = lines(ui(lang, "ui.remove.done", { name }), followed ? ui(lang, "ui.remove.live") : undefined, moved ? ui(lang, "ui.remove.images.done", { n: moved,
|
|
184
|
+
const text = lines(ui(lang, "ui.remove.done", { name }), followed ? ui(lang, "ui.remove.live") : undefined, moved ? ui(lang, "ui.remove.images.done", { n: moved, size: mb(onlyBytes, lang) }) : undefined, backup ? ui(lang, "ui.remove.backup.back", { file: ctx.display(backup) }) : undefined, ui(lang, backup ? "ui.remove.trash" : "ui.remove.trash.alone"), next ? ui(lang, "ui.remove.next", { name: next }) : undefined);
|
|
186
185
|
return { text, data: { removed: true, root, images: moved, ...(backup ? { backup } : {}), ...(next ? { current: cfg.currentTree } : {}) } };
|
|
187
186
|
},
|
|
188
187
|
});
|
package/dist/core/live-read.js
CHANGED
|
@@ -303,6 +303,10 @@ function notReady(tree, settings, env, seen, now) {
|
|
|
303
303
|
/** A reading's seconds before any of this agent's readers ended here (measured 2026-10-20 and 21): Grok's take minutes. */
|
|
304
304
|
export const READ_EXPECT_DEFAULT = { grok: 240 };
|
|
305
305
|
export const READ_EXPECT_OTHERS = 60;
|
|
306
|
+
/** The seconds a reading is expected to take with this agent before any of its readers ended here (any name: its own key only). */
|
|
307
|
+
export const readExpectDefault = (agent) => (Object.hasOwn(READ_EXPECT_DEFAULT, agent) ? READ_EXPECT_DEFAULT[agent] : READ_EXPECT_OTHERS);
|
|
308
|
+
/** A reader's time past this is no reader's (a journal written by hand, a clock gone wrong): never learned from. */
|
|
309
|
+
export const READER_MS_MAX = DAY_MS;
|
|
306
310
|
/** The readers of documents a reading's time is learned from: the last so many of the agent that read to an end. */
|
|
307
311
|
const EXPECT_LAST = 9;
|
|
308
312
|
const expectSeen = new Map();
|
|
@@ -322,7 +326,7 @@ function readerSecs(root, agent) {
|
|
|
322
326
|
const secs = new Map();
|
|
323
327
|
for (const r of readJournal(file)) {
|
|
324
328
|
const ms = typeof r.wallMs === "number" ? r.wallMs : typeof r.ms === "number" ? r.ms : NaN;
|
|
325
|
-
if (r.kind !== "document" || r.outcome !== "ok" || typeof r.agent !== "string" || !(ms >= 0))
|
|
329
|
+
if (r.kind !== "document" || r.outcome !== "ok" || typeof r.agent !== "string" || !(ms >= 0 && ms <= READER_MS_MAX))
|
|
326
330
|
continue;
|
|
327
331
|
secs.set(r.agent, [...(secs.get(r.agent) ?? []), ms / 1000].slice(-EXPECT_LAST));
|
|
328
332
|
}
|
|
@@ -339,7 +343,7 @@ function readerSecs(root, agent) {
|
|
|
339
343
|
export function expectSecs(root, agent, rounds = 1) {
|
|
340
344
|
const secs = readerSecs(root, agent).sort((a, b) => a - b);
|
|
341
345
|
const mid = secs.length >> 1;
|
|
342
|
-
const one = secs.length ? (secs.length % 2 ? secs[mid] : (secs[mid - 1] + secs[mid]) / 2) : (
|
|
346
|
+
const one = secs.length ? (secs.length % 2 ? secs[mid] : (secs[mid - 1] + secs[mid]) / 2) : readExpectDefault(agent);
|
|
343
347
|
return Math.max(1, Math.round(one * Math.max(1, rounds)));
|
|
344
348
|
}
|
|
345
349
|
/** A reading at work or queued: how long it is expected to take from its start (since, when it is reading). */
|
package/dist/core/phone.js
CHANGED
|
@@ -248,6 +248,19 @@ export function phoneRunning(env) {
|
|
|
248
248
|
const run = phoneRun(env);
|
|
249
249
|
return run && !run.ended && alive(run.pid) ? run : undefined;
|
|
250
250
|
}
|
|
251
|
+
/** How long a server just started may take to say itself (phone-run.json) before another may be started. */
|
|
252
|
+
const SPAWN_SAYS_MS = 20_000;
|
|
253
|
+
/**
|
|
254
|
+
* A server started a moment ago that has not said itself yet (Node starting, its addresses bound): its pid while it
|
|
255
|
+
* lives. Several asking at once (a bridge of each research, a strom just updated) start that one, not one each.
|
|
256
|
+
*/
|
|
257
|
+
function startedJustNow(env) {
|
|
258
|
+
const s = readSafe(file(env, "phone-spawn.json"));
|
|
259
|
+
if (!s || typeof s.pid !== "number" || typeof s.at !== "string")
|
|
260
|
+
return undefined;
|
|
261
|
+
const age = Date.now() - Date.parse(s.at);
|
|
262
|
+
return age >= 0 && age < SPAWN_SAYS_MS && alive(s.pid) ? s.pid : undefined;
|
|
263
|
+
}
|
|
251
264
|
function spawnPhone(env, extra = {}) {
|
|
252
265
|
const { command, args } = stromLauncher();
|
|
253
266
|
const dir = configDir(env);
|
|
@@ -270,6 +283,14 @@ function spawnPhone(env, extra = {}) {
|
|
|
270
283
|
child.unref();
|
|
271
284
|
if (typeof log === "number")
|
|
272
285
|
fs.closeSync(log);
|
|
286
|
+
if (child.pid) {
|
|
287
|
+
try {
|
|
288
|
+
writeJson(file(env, "phone-spawn.json"), { pid: child.pid, at: new Date().toISOString() });
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
// another may start one meanwhile: the one that gets the server's lock serves, the other ends at once
|
|
292
|
+
}
|
|
293
|
+
}
|
|
273
294
|
return child.pid;
|
|
274
295
|
}
|
|
275
296
|
/**
|
|
@@ -293,9 +314,13 @@ export function startPhone(env, opts = {}) {
|
|
|
293
314
|
const now = phoneRunning(env);
|
|
294
315
|
if (now)
|
|
295
316
|
return now;
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
317
|
+
// one started a moment ago and not said yet: waited for, none started beside it
|
|
318
|
+
let pid = startedJustNow(env);
|
|
319
|
+
if (!pid) {
|
|
320
|
+
if (opts.why)
|
|
321
|
+
notePhone(env, opts.why);
|
|
322
|
+
pid = spawnPhone(env);
|
|
323
|
+
}
|
|
299
324
|
const waitMs = opts.waitMs ?? 8000;
|
|
300
325
|
for (let waited = 0; pid && waited < waitMs; waited += 50) {
|
|
301
326
|
const run = phoneRun(env);
|
|
@@ -552,19 +577,19 @@ function pairMs(env) {
|
|
|
552
577
|
const t = Number(env.STROM_PHONE_PAIR_MS);
|
|
553
578
|
return phoneTest(env) && Number.isFinite(t) && t > 0 ? t : PAIR_MS;
|
|
554
579
|
}
|
|
555
|
-
function readPair(env) {
|
|
580
|
+
function readPair(env, clock = Date.now) {
|
|
556
581
|
const p = readSafe(file(env, "phone-pair.json"));
|
|
557
|
-
return p && typeof p.code === "string" && /^[A-Za-z0-9_-]{22}$/.test(p.code) && isTime(p.expiresAt) && Date.parse(p.expiresAt) >
|
|
582
|
+
return p && typeof p.code === "string" && /^[A-Za-z0-9_-]{22}$/.test(p.code) && isTime(p.expiresAt) && Date.parse(p.expiresAt) > clock() ? p : undefined;
|
|
558
583
|
}
|
|
559
584
|
/** When the code to pair a phone ends, while there is one (never the code). */
|
|
560
|
-
export function pairExpires(env) {
|
|
561
|
-
return readPair(env)?.expiresAt;
|
|
585
|
+
export function pairExpires(env, clock = Date.now) {
|
|
586
|
+
return readPair(env, clock)?.expiresAt;
|
|
562
587
|
}
|
|
563
588
|
/**
|
|
564
589
|
* The code to pair a phone: the one that holds, else a new one (128 bits, PAIR_MS long, kept for strom's user alone).
|
|
565
590
|
* `tree`: the tree whose QR code it is (the last one shown wins: the page opens it once paired).
|
|
566
591
|
*/
|
|
567
|
-
export function pairCode(env, opts = {}) {
|
|
592
|
+
export function pairCode(env, opts = {}, clock = Date.now) {
|
|
568
593
|
return withLock(file(env, "phone-pair.lock"), { owner: "the code to pair a phone" }, () => {
|
|
569
594
|
const f = file(env, "phone-pair.json");
|
|
570
595
|
const write = (p) => {
|
|
@@ -576,7 +601,7 @@ export function pairCode(env, opts = {}) {
|
|
|
576
601
|
// Windows: the user's folder is theirs
|
|
577
602
|
}
|
|
578
603
|
};
|
|
579
|
-
const now = readPair(env);
|
|
604
|
+
const now = readPair(env, clock);
|
|
580
605
|
if (now) {
|
|
581
606
|
// the last shown counts: one shown with no tree (outside a tree's folder) opens the tree the page chose
|
|
582
607
|
if (now.tree !== opts.tree) {
|
|
@@ -586,7 +611,7 @@ export function pairCode(env, opts = {}) {
|
|
|
586
611
|
return { code: now.code, expiresAt: now.expiresAt };
|
|
587
612
|
}
|
|
588
613
|
const code = crypto.randomBytes(16).toString("base64url");
|
|
589
|
-
const at = new Date();
|
|
614
|
+
const at = new Date(clock());
|
|
590
615
|
const expiresAt = new Date(at.getTime() + pairMs(env)).toISOString();
|
|
591
616
|
write({ code, at: at.toISOString(), expiresAt, ...(opts.tree ? { tree: opts.tree } : {}) });
|
|
592
617
|
notePhone(env, `a code to pair a phone made, it holds until ${expiresAt}`);
|
|
@@ -623,9 +648,9 @@ export function phoneName(ua) {
|
|
|
623
648
|
* The code given back by a phone's page, taken once: right and holding — the phone paired (its key, only now and only
|
|
624
649
|
* to it); anything else — none, and the code still holds for the phone it was shown to.
|
|
625
650
|
*/
|
|
626
|
-
export function pairPhone(env, code, info) {
|
|
651
|
+
export function pairPhone(env, code, info, clock = Date.now) {
|
|
627
652
|
const taken = withLock(file(env, "phone-pair.lock"), { owner: "the code to pair a phone" }, () => {
|
|
628
|
-
const p = readPair(env);
|
|
653
|
+
const p = readPair(env, clock);
|
|
629
654
|
const a = Buffer.from(p?.code ?? "");
|
|
630
655
|
const b = Buffer.from(code);
|
|
631
656
|
if (!p || a.length !== b.length || !crypto.timingSafeEqual(a, b))
|
package/dist/core/phrases.js
CHANGED
|
@@ -111,6 +111,8 @@ export const PHRASES = {
|
|
|
111
111
|
"fix.model.why.differ": "Read again by {now} (earlier: {before}) where something stayed open, and the readings differ.",
|
|
112
112
|
"fix.model.why.refined": "Read again by {now} (earlier: {before}) where something stayed open, and the new reading reads more or is surer.",
|
|
113
113
|
"fix.model.done": "both readings held against each other ({show} {old} · {show} {now}) and against {sources}: what holds kept on the source, the facts on it checked; nothing taken away without a reason",
|
|
114
|
+
"fix.differs": "What differs: earlier {before}, now {now}.",
|
|
115
|
+
"fix.more": "Read now beside the earlier: {now}.",
|
|
114
116
|
"fix.model.unknown": "a model not recorded",
|
|
115
117
|
"fix.model.own": "its own model",
|
|
116
118
|
"batch.unnamed": "material from the Strom app",
|
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.19.0-beta.
|
|
30
|
+
export const VERSION = "1.19.0-beta.7";
|
|
31
31
|
const GITIGNORE = `# Strom Research — local state and large files are not versioned
|
|
32
32
|
.strom/
|
|
33
33
|
/media/
|