strom-research 1.17.0-beta.2 → 1.17.0-beta.3

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,9 +7,14 @@ Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou n
7
7
 
8
8
  ## 1.17.0
9
9
 
10
- *Připravuje se: bety do 1.17.0-beta.2. Oddíl bude úplný s vydáním.*
10
+ *Připravuje se: bety do 1.17.0-beta.3. Oddíl bude úplný s vydáním.*
11
11
 
12
- - **Zatím nic nového v používání.** Zatím se změnilo jen to, jak se strom sestavuje a vydává.
12
+ - **Nejdřív, co by hypotézy spojovaly.** Úkoly doplnit, co by spojovaly verze otevřené hypotézy, jdou ve frontě
13
+ první — i ve stávajícím výzkumu po aktualizaci —, takže aplikace Strom brzy ukáže tyto vazby.
14
+ - **Přerušené stahování se ohlásí a uklidí.** Stahování, jehož proces skončil uprostřed, se ohlásí jako přerušené,
15
+ s tím, co se uložilo a co chybí; jeho pracovní složka se uklidí.
16
+ - **Nový materiál v jazyce výzkumu.** Co přišlo (zaregistrovaný materiál, co přibylo, pravděpodobné duplicity,
17
+ jména po sňatku, úkoly), se vypíše v jazyce výzkumu.
13
18
 
14
19
  ## 1.16.0
15
20
 
package/CHANGELOG.de.md CHANGED
@@ -7,9 +7,15 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
7
7
 
8
8
  ## 1.17.0
9
9
 
10
- *In Vorbereitung: die Betas bis 1.17.0-beta.2. Der Abschnitt ist mit dem Release vollständig.*
10
+ *In Vorbereitung: die Betas bis 1.17.0-beta.3. Der Abschnitt ist mit dem Release vollständig.*
11
11
 
12
- - **Noch nichts Neues in der Nutzung.** Bisher hat sich nur geändert, wie strom gebaut und veröffentlicht wird.
12
+ - **Zuerst, was Hypothesen verbinden würden.** Die Aufgaben, zu ergänzen, was die Varianten einer offenen Hypothese
13
+ verbinden würden, kommen in der Warteschlange zuerst — auch in einer bestehenden Forschung nach dem Update —, so
14
+ zeigt die Strom-App diese Verbindungen bald.
15
+ - **Ein unterbrochener Download wird gemeldet und aufgeräumt.** Ein Download, dessen Prozess mittendrin endete, wird
16
+ als unterbrochen gemeldet, mit dem, was gespeichert ist und was fehlt; sein Arbeitsordner wird entfernt.
17
+ - **Neues Material in der Sprache der Forschung.** Was hereinkam (registriertes Material, was hinzukam, wahrscheinliche
18
+ Duplikate, Namen nach der Heirat, Aufgaben), wird in der Sprache der Forschung gesagt.
13
19
 
14
20
  ## 1.16.0
15
21
 
package/CHANGELOG.md CHANGED
@@ -7,9 +7,15 @@ The changes of each version in short. The full notes and the install lines are o
7
7
 
8
8
  ## 1.17.0
9
9
 
10
- *In preparation: the betas up to 1.17.0-beta.2. The section is complete with the release.*
10
+ *In preparation: the betas up to 1.17.0-beta.3. The section is complete with the release.*
11
11
 
12
- - **Nothing new in use yet.** So far only the way strom is built and published has changed.
12
+ - **What hypotheses would connect, first.** The tasks to fill in what the variants of an open hypothesis would
13
+ connect go first in the queue — also in an existing research after the update — so the Strom app can show those
14
+ links soon.
15
+ - **An interrupted download is said and tidied.** A download whose process ended midway is reported as interrupted,
16
+ with what was saved and what is missing; its work folder is cleared away.
17
+ - **New material reported in the research's language.** What came in (registered material, what was added, likely
18
+ duplicates, married names, tasks) is said in the language of the research.
13
19
 
14
20
  ## 1.16.0
15
21
 
@@ -1549,6 +1549,13 @@
1549
1549
  "ui.intake.tree.matched": " – už ve výzkumu: {n}",
1550
1550
  "ui.intake.tree.back": "Je to rodokmen tohoto výzkumu, který se vrací: strom sync \"{file}\" ukáže i to, co se v něm změnilo.",
1551
1551
  "ui.intake.same": "Už tu bylo dřív: {n}.",
1552
+ "ui.intake.registered": "✓ Podklady zaevidované ve výzkumu: {n}.",
1553
+ "ui.intake.tree.unreadable": " · nečitelné řádky: {n}",
1554
+ "ui.intake.extended": "Doplněno k tomu, co výzkum má (vodítka, k ověření): {list}",
1555
+ "ui.intake.dupes": "Nejspíš už v rodokmenu: {list} (vypíše je úkol kontroly)",
1556
+ "ui.intake.married": "{id}: příjmení po sňatku, která uvádí a která se při převzetí vynechala: {n} → strom input names {id}",
1557
+ "ui.intake.tasks": "Úkoly k rozebrání podkladů: {n} → strom task next",
1558
+ "ui.intake.handed": "{task} dostal {inputs} a je zpět ve frontě – soubor se přečte jako jeho další krok (bez vlastního úkolu k rozebrání).",
1552
1559
  "ui.intake.chat": "Projít to s agentem hned?",
1553
1560
  "ui.intake.say": "Do výzkumu jsem přidal(a) nové podklady – pojďme je projít.",
1554
1561
  "ui.research.kind": "Co zkoumat?",
@@ -1944,6 +1951,7 @@
1944
1951
  "ui.person.leadsonly": "jen vodítka",
1945
1952
  "ui.person.events": "údaje: {n}",
1946
1953
  "ui.list.more": "… dalších {n}: {command}",
1954
+ "ui.list.past": "(stránka {asked} není: poslední je {pages}, zobrazená výše)",
1947
1955
  "ui.error.archive": "Tento výzkum je Archiv Stromu: data přicházejí z aplikace Strom a nic se nezkoumá. Výzkum se zapíná v menu, první položka.",
1948
1956
  "ui.error.archive.hint": "nebo v terminálu: strom mode research",
1949
1957
  "ui.error.agent.missing": "Výzkum čeká na AI agenta ({name}), který na tomto počítači není: do té doby drží data z aplikace Strom jako Archiv Stromu. Agenta nainstalovat nebo vybrat jiného: menu, první položka.",
@@ -1549,6 +1549,13 @@
1549
1549
  "ui.intake.tree.matched": " – schon in der Forschung: {n}",
1550
1550
  "ui.intake.tree.back": "Es ist der Stammbaum dieser Forschung, der zurückkommt: strom sync \"{file}\" zeigt auch, was darin geändert wurde.",
1551
1551
  "ui.intake.same": "Schon vorher hier: {n}.",
1552
+ "ui.intake.registered": "✓ Unterlagen in der Forschung erfasst: {n}.",
1553
+ "ui.intake.tree.unreadable": " · unlesbare Zeilen: {n}",
1554
+ "ui.intake.extended": "Ergänzt zu dem, was die Forschung hat (Hinweise, zu prüfen): {list}",
1555
+ "ui.intake.dupes": "Wahrscheinlich schon im Stammbaum: {list} (die Prüfaufgabe listet sie auf)",
1556
+ "ui.intake.married": "{id}: Ehenamen, die sie nennt und die bei der Übernahme ausgelassen wurden: {n} → strom input names {id}",
1557
+ "ui.intake.tasks": "Aufgaben zum Auswerten der Unterlagen: {n} → strom task next",
1558
+ "ui.intake.handed": "{task} hat {inputs} bekommen und ist wieder in der Warteschlange – die Datei wird als nächster Schritt gelesen (keine eigene Aufgabe zum Auswerten).",
1552
1559
  "ui.intake.chat": "Jetzt mit dem Agenten durchgehen?",
1553
1560
  "ui.intake.say": "Ich habe der Forschung neue Unterlagen hinzugefügt – gehen wir sie durch.",
1554
1561
  "ui.research.kind": "Was soll erforscht werden?",
@@ -1944,6 +1951,7 @@
1944
1951
  "ui.person.leadsonly": "nur Hinweise",
1945
1952
  "ui.person.events": "Angaben: {n}",
1946
1953
  "ui.list.more": "… {n} weitere: {command}",
1954
+ "ui.list.past": "(keine Seite {asked}: die letzte ist {pages}, oben gezeigt)",
1947
1955
  "ui.error.archive": "Diese Forschung ist ein Strom-Archiv: die Daten kommen aus der Strom-App, und nichts wird erforscht. Die Forschung wird im Menü eingeschaltet, erster Punkt.",
1948
1956
  "ui.error.archive.hint": "oder im Terminal: strom mode research",
1949
1957
  "ui.error.agent.missing": "Die Forschung wartet auf ihren KI-Agenten ({name}), der auf diesem Computer nicht ist: bis dahin bewahrt sie die Daten aus der Strom-App als Strom-Archiv. Den Agenten installieren oder einen anderen wählen: das Menü, erster Punkt.",
@@ -39,7 +39,7 @@ import { ENRICH_PAGES, STORY_SOURCES_ORIGIN } from "../core/stories.js";
39
39
  import { sandboxOf, WITHOUT_READERS } from "../core/sandbox.js";
40
40
  import { WEB_SOFT } from "../core/metrics.js";
41
41
  import { indexBriefLines } from "../core/indexfirst.js";
42
- import { activeFetches, endedFetches } from "../core/fetching.js";
42
+ import { activeFetches, endedFetches, withRegistered } from "../core/fetching.js";
43
43
  /** The hosts of a connector, as its manifest names them (a wildcard without its star). */
44
44
  function connectorHosts(shared, name) {
45
45
  try {
@@ -628,13 +628,19 @@ function imagesLines(tree, b, route, shape, finder, noSharper = false, waiting)
628
628
  : ""),
629
629
  ].filter(Boolean);
630
630
  }
631
- /** The fetches at work for the research (each book: how far), and those in the background that ended lately with images left. */
632
- function fetchingLines(root) {
631
+ /** The fetches at work for the research (each book: how far), and those that ended lately with images left (in the background, or interrupted). */
632
+ function fetchingLines(tree) {
633
+ const root = tree.root;
633
634
  const active = activeFetches(root);
635
+ // an interrupted one: what it registered as the research has it
636
+ const has = (rs) => new Set(tree.list("media").filter((m) => m.recordset === rs && !m.part && !m.retracted && m.image !== undefined).map((m) => m.image));
634
637
  const out = active.map((m) => `- ${m.recordset}: ${m.registered}/${m.images.length} images registered (${m.background ? `in the background${m.task ? `, for ${m.task}` : ""}` : "in the foreground"}) — strom read ${m.recordset} --images ${runsArg(m.images)} reads them as they come; not fetched again`);
635
- for (const e of endedFetches(root))
638
+ for (const e of endedFetches(root).map((x) => withRegistered(x, has(x.recordset))))
636
639
  if (e.missed.length && !active.some((m) => m.recordset === e.recordset))
637
- out.push(`- ${e.recordset}: a fetch in the background ended with images ${runs(e.missed)} not fetched${e.stopped ? ` (stopped: ${e.stopped})` : ""} — strom fetch ${e.connector} ${shellArg(e.book)} --images ${runsArg(e.missed)} --recordset ${e.recordset} if they are needed`);
640
+ out.push(e.interrupted
641
+ ? // its process ended without a word (killed, the computer stopped): what it registered stays
642
+ `- ${e.recordset}: a fetch ${e.background ? "in the background" : "in the foreground"} was interrupted (${e.interrupted === "gone" ? "its process ended without a word" : "nothing heard of it for long"}) with images ${runs(e.missed)} not fetched — strom fetch ${e.connector} ${shellArg(e.book)} --images ${runsArg(e.missed)} --recordset ${e.recordset}${e.background ? " --background" : ""} if they are needed (what is registered is not fetched again)`
643
+ : `- ${e.recordset}: a fetch in the background ended with images ${runs(e.missed)} not fetched${e.stopped ? ` (stopped: ${e.stopped})` : ""} — strom fetch ${e.connector} ${shellArg(e.book)} --images ${runsArg(e.missed)} --recordset ${e.recordset} if they are needed`);
638
644
  return out;
639
645
  }
640
646
  /** What the user put in the shared inbox, waiting to be registered — by folder (one download each). */
@@ -1209,7 +1215,7 @@ export function buildBrief(tree, opts) {
1209
1215
  });
1210
1216
  // 7c. images being fetched for the research now (strom fetch --background of an earlier session, or another strom),
1211
1217
  // and what fetches in the background left unfetched lately: read as they come, never fetched twice
1212
- const fetching = fetchingLines(tree.root);
1218
+ const fetching = fetchingLines(tree);
1213
1219
  if (fetching.length)
1214
1220
  sections.push({
1215
1221
  name: "fetching",
@@ -4,6 +4,7 @@ import { hasErrors } from "../core/check.js";
4
4
  import { guard } from "../core/guard.js";
5
5
  import { snapshot, verifyFast } from "../core/integrity.js";
6
6
  import { isArchive } from "../core/mode.js";
7
+ import { counted } from "./format.js";
7
8
  export function commitMessage(summaries, command) {
8
9
  const unique = [...new Set(summaries)];
9
10
  const subject = unique.length === 1 ? unique[0] : `${command}: ${unique.length} changes`;
@@ -17,34 +18,43 @@ export function autoCommit(ctx, def) {
17
18
  const ops = tree?.uncommitted() ?? [];
18
19
  if (!tree || tree.dryRun || ops.length === 0)
19
20
  return undefined;
20
- // Incremental gate: records were validated when written; here only what
21
- // changed since the last commit is checked, so the cost stays flat.
22
- const snap = snapshot(tree);
23
- const findings = [...verifyFast(tree, snap).findings, ...guard(tree, snap)];
24
- if (hasErrors(findings)) {
25
- const errors = findings.filter((f) => f.level === "error");
26
- return {
27
- message: [
28
- `error: written but NOT committed — ${errors.length} problem(s) in the data:`,
29
- ...errors.slice(0, 5).map((f) => ` ${f.id ?? f.file ?? ""} ${f.message}`),
30
- "→ strom check",
31
- ].join("\n"),
32
- problems: errors.map((f) => ({ ...(f.id ? { id: f.id } : {}), ...(f.file ? { file: f.file } : {}), message: f.message })),
33
- };
34
- }
35
- const message = commitMessage(ops.map((o) => o.summary), `strom ${def.path.join(" ")}`);
36
- // Only the files this command wrote, plus the free-form folders the agent
37
- // may edit — `git add` then never has to scan the whole data/ tree.
38
- const written = new Set(["data/_counters.json"]);
39
- for (const op of ops)
40
- for (const f of op.files)
41
- written.add(f.path);
42
- for (const f of tree.opsFilesTouched())
43
- written.add(f);
44
- // an archive: no agent edits its free-form folders — a person's own file in notes/ is theirs, never swept into a
45
- // commit of something else (found on Windows: strom mode committed a file of the user's) — nor by the switch itself
46
- const free = isArchive(tree) || def.path[0] === "mode" ? ["inputs", "output"] : ["notes", "tools", "inputs", "output"];
47
- tree.withTreeLock(() => tree.commit(message, [...written, ...free]));
21
+ // The check and the commit under one hold of the tree (a command that locks only while it writes reaches here
22
+ // without it): another strom in the middle of its commit — staged, its log not yet in HEAD — would otherwise be seen
23
+ // half done, its records as edited outside strom (found: a fetch in the foreground ending while one in the
24
+ // background committed its last batch).
25
+ const blocked = tree.withTreeLock(() => {
26
+ // Incremental gate: records were validated when written; here only what
27
+ // changed since the last commit is checked, so the cost stays flat.
28
+ const snap = snapshot(tree);
29
+ const findings = [...verifyFast(tree, snap).findings, ...guard(tree, snap)];
30
+ if (hasErrors(findings)) {
31
+ const errors = findings.filter((f) => f.level === "error");
32
+ return {
33
+ message: [
34
+ `error: written but NOT committed — ${counted(errors.length, "problem")} in the data:`,
35
+ ...errors.slice(0, 5).map((f) => ` ${f.id ?? f.file ?? ""} ${f.message}`),
36
+ "→ strom check",
37
+ ].join("\n"),
38
+ problems: errors.map((f) => ({ ...(f.id ? { id: f.id } : {}), ...(f.file ? { file: f.file } : {}), message: f.message })),
39
+ };
40
+ }
41
+ const message = commitMessage(ops.map((o) => o.summary), `strom ${def.path.join(" ")}`);
42
+ // Only the files this command wrote, plus the free-form folders the agent
43
+ // may edit — `git add` then never has to scan the whole data/ tree.
44
+ const written = new Set(["data/_counters.json"]);
45
+ for (const op of ops)
46
+ for (const f of op.files)
47
+ written.add(f.path);
48
+ for (const f of tree.opsFilesTouched())
49
+ written.add(f);
50
+ // an archive: no agent edits its free-form folders — a person's own file in notes/ is theirs, never swept into a
51
+ // commit of something else (found on Windows: strom mode committed a file of the user's) — nor by the switch itself
52
+ const free = isArchive(tree) || def.path[0] === "mode" ? ["inputs", "output"] : ["notes", "tools", "inputs", "output"];
53
+ tree.commit(message, [...written, ...free]);
54
+ return undefined;
55
+ });
56
+ if (blocked)
57
+ return blocked;
48
58
  tree.markCommitted();
49
59
  tree.settle();
50
60
  return undefined;
@@ -19,6 +19,7 @@ import { isAgent } from "../core/which.js";
19
19
  import { treeAgentHere } from "../core/state.js";
20
20
  import { canShowDialog, systemDialog, WAIT_SECONDS } from "../core/dialog.js";
21
21
  import { UI, ui } from "./ui.js";
22
+ import { DEFAULT_LIMIT } from "./format.js";
22
23
  export class Context {
23
24
  env;
24
25
  cwd;
@@ -46,7 +47,7 @@ export class Context {
46
47
  this.json = opts.json;
47
48
  this.yes = opts.yes;
48
49
  this.treeFlag = opts.tree;
49
- this.limit = opts.limit ?? 50;
50
+ this.limit = opts.limit ?? DEFAULT_LIMIT;
50
51
  this.page = opts.page ?? 1;
51
52
  }
52
53
  /** A context from parsed command-line values (global options and settings flags). */
@@ -16,10 +16,12 @@ export function table(rows, opts = {}) {
16
16
  .trimEnd())
17
17
  .join("\n");
18
18
  }
19
+ /** The rows of a page when --limit is not given. */
20
+ export const DEFAULT_LIMIT = 50;
19
21
  export function paginate(items, limit, page) {
20
22
  const pages = Math.max(1, Math.ceil(items.length / limit));
21
23
  const p = Math.min(Math.max(1, page), pages);
22
- return { items: items.slice((p - 1) * limit, p * limit), total: items.length, page: p, pages, limit };
24
+ return { items: items.slice((p - 1) * limit, p * limit), total: items.length, page: p, pages, limit, ...(page > pages && items.length ? { asked: page } : {}) };
23
25
  }
24
26
  /**
25
27
  * The most rows a page of a long listing gives, whatever --limit says: a row is at most ~170 bytes, a page under ~20 kB
@@ -28,10 +30,14 @@ export function paginate(items, limit, page) {
28
30
  export const LIST_PAGE_MAX = 100;
29
31
  /** Footer telling the caller how to see more — in the research's language where the listing is (lang), else English. */
30
32
  export function moreLine(page, command, lang) {
33
+ // a page past the last: the last one shown, and said so (found on Windows: --page 99 gave the last page silently)
34
+ if (page.asked)
35
+ return lang ? ui(lang, "ui.list.past", { asked: String(page.asked), pages: String(page.pages) }) : `(no page ${page.asked}: the last is ${page.pages}, shown above)`;
31
36
  if (page.page >= page.pages)
32
37
  return "";
33
38
  const left = page.total - page.page * page.limit;
34
- const next = `${command} --page ${page.page + 1}`;
39
+ // the next page of the same size: a --limit of its own goes along (found on Windows: --page 2 alone took 50 rows)
40
+ const next = `${command}${page.limit !== DEFAULT_LIMIT ? ` --limit ${page.limit}` : ""} --page ${page.page + 1}`;
35
41
  return lang ? ui(lang, "ui.list.more", { n: String(left), command: next }) : `… ${left} more: ${next}`;
36
42
  }
37
43
  export function truncate(text, max) {
package/dist/cli/main.js CHANGED
@@ -16,7 +16,7 @@ import { assertIntact } from "../core/integrity.js";
16
16
  import { resetCache } from "../core/git.js";
17
17
  import { newerTree, Tree, VERSION } from "../core/tree.js";
18
18
  import { settleArchive } from "../core/mode.js";
19
- import { settleHypothesisLinks } from "../core/hypolinks.js";
19
+ import { raiseHypothesisLinkTasks, settleHypothesisLinks } from "../core/hypolinks.js";
20
20
  import { liveRunning, reviveLive, startLive } from "../core/live.js";
21
21
  import { fireHooks, HOOK_INTERFACE } from "../core/hooks.js";
22
22
  import { prependPath } from "../runners/runner.js";
@@ -314,7 +314,14 @@ export async function main(argv, io, env, cwd) {
314
314
  catch {
315
315
  // another computer's seal, a tree at work: its next bridge or switch settles it
316
316
  }
317
- // …and older hypotheses get the links of their variants their claims say beyond doubt, the rest a task (once)
317
+ // …and the tasks an older strom made to say those links come first in the queue (once), then older hypotheses get
318
+ // the links of their variants their claims say beyond doubt, the rest a task (once)
319
+ try {
320
+ raiseHypothesisLinkTasks(Tree.open(k.root, env));
321
+ }
322
+ catch {
323
+ // another computer's seal, a tree at work: its bridge's start raises them
324
+ }
318
325
  try {
319
326
  settleHypothesisLinks(Tree.open(k.root, env));
320
327
  }
package/dist/cli/ui.js CHANGED
@@ -1462,6 +1462,13 @@ export const UI = {
1462
1462
  "ui.intake.tree.matched": " – already in the research: {n}",
1463
1463
  "ui.intake.tree.back": "It is this research's own family tree coming back: strom sync \"{file}\" shows what was changed in it too.",
1464
1464
  "ui.intake.same": "Already here before: {n}.",
1465
+ "ui.intake.registered": "✓ Material registered in the research: {n}.",
1466
+ "ui.intake.tree.unreadable": " · unreadable lines: {n}",
1467
+ "ui.intake.extended": "Added to what the research has (leads, to be checked): {list}",
1468
+ "ui.intake.dupes": "Probably in the tree already: {list} (the review task lists them)",
1469
+ "ui.intake.married": "{id}: married names it gives, left out when it was imported: {n} → strom input names {id}",
1470
+ "ui.intake.tasks": "Tasks to go through the material: {n} → strom task next",
1471
+ "ui.intake.handed": "{task} got {inputs} and is back in the queue — the file is read as its next step (no task of its own to go through it).",
1465
1472
  "ui.intake.chat": "Go through it with the agent now?",
1466
1473
  "ui.intake.say": "I added new material to the research — let us go through it.",
1467
1474
  "ui.research.kind": "What should be found out?",
@@ -1748,6 +1755,7 @@ export const UI = {
1748
1755
  "ui.person.leadsonly": "leads only",
1749
1756
  "ui.person.events": "{n} ev",
1750
1757
  "ui.list.more": "… {n} more: {command}",
1758
+ "ui.list.past": "(no page {asked}: the last is {pages}, shown above)",
1751
1759
  "ui.error.archive": "This research is a Strom archive: its data come from the Strom app and nothing is researched. Research is switched on in the menu, its first item.",
1752
1760
  "ui.error.archive.hint": "or in a terminal: strom mode research",
1753
1761
  "ui.error.agent.missing": "The research waits for its AI agent ({name}), which is not on this computer: until then it keeps the data from the Strom app as a Strom archive. Installing the agent or choosing another: the menu, its first item.",
@@ -1,7 +1,7 @@
1
1
  // search · searched · conflict · hypothesis · lesson · find · gaps
2
2
  import { register } from "../cli/registry.js";
3
3
  import { ui } from "../cli/ui.js";
4
- import { lines, moreLine, paginate, table, truncate } from "../cli/format.js";
4
+ import { counted, lines, moreLine, paginate, table, truncate } from "../cli/format.js";
5
5
  import { UsageError } from "../core/errors.js";
6
6
  import { LESSON_DETAIL_MAX, LESSON_MAX, LESSON_SCOPES, RECORD_TYPES, SEARCH_METHODS, SEARCH_RESULTS, VARIANT_LINK_KINDS, } from "../core/model.js";
7
7
  import { create, csvOpt, listOpt, normId, requireRecord, update } from "../core/records.js";
@@ -19,6 +19,7 @@ import { childLink, LINK_HOW, linkHints, linkText, personsLink, recordedLinks, r
19
19
  import { yearsOption, yearsOverlap } from "../core/years.js";
20
20
  import { searchedAs } from "../core/evidence.js";
21
21
  import { refuseNegativeOverUnclear } from "./readings.js";
22
+ import { hypothesisPeople } from "../core/directions.js";
22
23
  function written(tree) {
23
24
  return lines(...tree.written.map((o) => o.summary));
24
25
  }
@@ -72,6 +73,17 @@ function conflictConcerns(tree, id) {
72
73
  (c.edit !== undefined && (ids.has(c.edit.event) || (c.edit.cites ?? []).some((ci) => ids.has(ci.source)))) ||
73
74
  (c.parents !== undefined && [c.parents.child, c.parents.from, c.parents.to ?? "", ...c.parents.partners].some((s) => ids.has(s)));
74
75
  }
76
+ /**
77
+ * Whether a hypothesis is about a person or family (strom hypothesis list --about): named in its subject, among the
78
+ * people it is about (hypothesisPeople: its subject's, those its variants would connect or name), or in what its
79
+ * variants' links have named; a person also through the families they are a partner of.
80
+ */
81
+ function hypothesisConcerns(tree, id) {
82
+ const ids = new Set([id, ...(typeOfId(id) === "person" ? familiesAsPartner(tree, id).map((f) => f.id) : [])]);
83
+ return (h) => h.subject.some((s) => ids.has(s)) ||
84
+ hypothesisPeople(tree, h).some((p) => ids.has(p)) ||
85
+ h.variants.some((v) => (v.linked ?? []).some((x) => ids.has(x)) || (v.links ?? []).some((l) => l.kind === "child" && l.family !== undefined && ids.has(l.family)));
86
+ }
75
87
  /** The tasks that work on a conflict or hypothesis (--about X…/H…). */
76
88
  function tasksAbout(tree, id) {
77
89
  return tree.list("task").filter((t) => t.subject.includes(id));
@@ -380,7 +392,7 @@ register({
380
392
  // a negative of an index is the index's: the book itself not searched in vain by it
381
393
  const ofIndex = hits.filter((s) => s.result === "negative" && s.method === "index").length;
382
394
  const text = hits.length
383
- ? lines(`${hits.length} search(es) for ${what}${years ? ` in ${years}` : ""} — ${negative} negative${ofIndex ? ` (${ofIndex} in an index only — not the book searched in vain)` : ""}, ${hits.length - negative} with results`, table(hits.map(searchLine)))
395
+ ? lines(`${counted(hits.length, "search", "searches")} for ${what}${years ? ` in ${years}` : ""} — ${negative} negative${ofIndex ? ` (${ofIndex} in an index only — not the book searched in vain)` : ""}, ${hits.length - negative} with results`, table(hits.map(searchLine)))
384
396
  : `nothing searched yet for ${what}${years ? ` in ${years}` : ""}`;
385
397
  return { text: lines(text, ...said), data: { searches: hits } };
386
398
  },
@@ -753,21 +765,22 @@ register({
753
765
  options: [
754
766
  { name: "all", type: "boolean", description: "include decided and abandoned" },
755
767
  { name: "unlinked", type: "boolean", description: "only those no variant of which says what it would connect (strom hypothesis link), but those whose every variant naming people says what the tree records already" },
768
+ { name: "about", type: "string", value: "<P…|F…>", description: "only the hypotheses about this person or family: its subject, or the people and families its variants would connect" },
756
769
  ],
757
- examples: ["strom hypothesis list", "strom hypothesis list --unlinked"],
770
+ examples: ["strom hypothesis list", "strom hypothesis list --unlinked", "strom hypothesis list --about P0001"],
758
771
  run(ctx, { opts }) {
759
772
  const tree = ctx.tree();
760
773
  // a hypothesis whose variants say only what the tree records already has nothing to link
761
774
  const linked = (h) => h.variants.some((v) => v.links?.length) || (h.variants.some((v) => recordedLinks(tree, h, v).length) && !h.variants.some((v) => toLink(tree, h, v)));
762
- const all = tree.list("hypothesis").filter((h) => (opts.all || h.state === "open") && !(opts.unlinked && linked(h)));
775
+ const about = typeof opts.about === "string" ? anyRefs(tree, [opts.about])[0] : undefined;
776
+ const concerns = about ? hypothesisConcerns(tree, about) : undefined;
777
+ const all = tree.list("hypothesis").filter((h) => (opts.all || h.state === "open") && !(opts.unlinked && linked(h)) && (!concerns || concerns(h)));
763
778
  const links = (h) => h.variants.reduce((n, v) => n + (v.links?.length ?? 0), 0);
764
779
  return {
765
780
  text: all.length
766
781
  ? table(all.map((h) => [h.id, h.state, truncate(h.question, 60), h.variants.map((v) => v.label).join("/"), links(h) ? `${links(h)} link${links(h) === 1 ? "" : "s"}` : ""]))
767
- : opts.unlinked
768
- ? "no open hypothesis without links"
769
- : "no open hypotheses",
770
- data: { hypotheses: all },
782
+ : `no ${opts.all ? "" : "open "}${opts.unlinked ? "hypothesis without links" : about ? "hypothesis" : "hypotheses"}${about ? ` about ${about} → all of them: strom hypothesis list${opts.all ? "" : " --all"}` : ""}`,
783
+ data: { ...(about ? { about } : {}), hypotheses: all },
771
784
  };
772
785
  },
773
786
  }, {
@@ -6,7 +6,7 @@
6
6
  import path from "node:path";
7
7
  import { register } from "../cli/registry.js";
8
8
  import { ui } from "../cli/ui.js";
9
- import { lines, secondsIn, shellArg, table } from "../cli/format.js";
9
+ import { counted, lines, secondsIn, shellArg, table } from "../cli/format.js";
10
10
  import { EXIT, StromError, UsageError } from "../core/errors.js";
11
11
  import { isAgent } from "../core/which.js";
12
12
  import { canShowDialog, systemChoice, WAIT_SECONDS } from "../core/dialog.js";
@@ -558,7 +558,7 @@ register({
558
558
  return { text: `${hosts.join(", ")}: kept as before — ${milder.map((p) => `${p.connector} ${p.from}`).join(", ")}`, data: { hosts, kept: kept.map((c) => c.name) } };
559
559
  }
560
560
  if (opts.accept) {
561
- ctx.requireHuman(`Accept what ${connectors.map((c) => c.name).join(", ")} say(s) of ${hosts.join(", ")} now (${connectors.map((c) => claimOf(c)).join(", ")}), its pace too?`, `strom allow archive ${target} --accept`, `archive:accept:${hosts.join(",")}`, ui(lang, "ui.archive.accept", { names: connectors.map((c) => `${c.manifest.title} (${levelWords(lang, claimOf(c))})`).join(", ") }));
561
+ ctx.requireHuman(`Accept what ${connectors.map((c) => c.name).join(", ")} ${connectors.length === 1 ? "says" : "say"} of ${hosts.join(", ")} now (${connectors.map((c) => claimOf(c)).join(", ")}), its pace too?`, `strom allow archive ${target} --accept`, `archive:accept:${hosts.join(",")}`, ui(lang, "ui.archive.accept", { names: connectors.map((c) => `${c.manifest.title} (${levelWords(lang, claimOf(c))})`).join(", ") }));
562
562
  acceptConnectors(ctx.env, connectors);
563
563
  return { text: `${hosts.join(", ")}: the connectors' readings accepted — ${connectors.map((c) => `${c.name} ${claimOf(c)}`).join(", ")}`, data: { hosts, accepted: connectors.map((c) => c.name) } };
564
564
  }
@@ -8,7 +8,7 @@
8
8
  // creates; later lines use it as "@name".
9
9
  import fs from "node:fs";
10
10
  import { register } from "../cli/registry.js";
11
- import { lines, table } from "../cli/format.js";
11
+ import { counted, lines, table } from "../cli/format.js";
12
12
  import { AmbiguousError, StromError, UsageError } from "../core/errors.js";
13
13
  /** A label: a word in any script ("anna", "šimon", "otec_2"), its accents composed or not. */
14
14
  const LABEL = "[\\p{L}_][\\p{L}\\p{M}\\p{N}_-]*";
@@ -254,7 +254,7 @@ register({
254
254
  throw errors[0];
255
255
  if (errors.length)
256
256
  throw manyWrong(errors, batch.length);
257
- const text = lines(table(report.flatMap((r) => r.summaries.map((s, i) => [i === 0 ? `${r.line}` : "", s, i === 0 && r.label ? `#${r.label}` : ""]))), `${report.length} command(s) as one change`);
257
+ const text = lines(table(report.flatMap((r) => r.summaries.map((s, i) => [i === 0 ? `${r.line}` : "", s, i === 0 && r.label ? `#${r.label}` : ""]))), `${counted(report.length, "command")} as one change`);
258
258
  return { text, data: { lines: report, labels: Object.fromEntries(labels) } };
259
259
  },
260
260
  });
@@ -4,7 +4,7 @@ import { actsAsArchive } from "../core/state.js";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { register } from "../cli/registry.js";
7
- import { lines, table } from "../cli/format.js";
7
+ import { counted, lines, table } from "../cli/format.js";
8
8
  import { check } from "../core/check.js";
9
9
  import { guard } from "../core/guard.js";
10
10
  import { unprovenPeople } from "../core/review.js";
@@ -21,7 +21,7 @@ export function report(findings, okText, lang) {
21
21
  const sum = { errors: errors.length, warnings: findings.length - errors.length };
22
22
  const text = findings.length === 0
23
23
  ? okText
24
- : lines(table(findings.map((f) => [f.level === "error" ? "ERROR" : "warn", f.code, f.id ?? f.file ?? "", f.message, f.hint ? `→ ${f.hint}` : ""])), "", lang ? ui(lang, "ui.check.found", sum) : `${sum.errors} error(s), ${sum.warnings} warning(s)`);
24
+ : lines(table(findings.map((f) => [f.level === "error" ? "ERROR" : "warn", f.code, f.id ?? f.file ?? "", f.message, f.hint ? `→ ${f.hint}` : ""])), "", lang ? ui(lang, "ui.check.found", sum) : `${counted(sum.errors, "error")}, ${counted(sum.warnings, "warning")}`);
25
25
  return { text, data: { ok: errors.length === 0, findings }, exitCode: errors.length ? 1 : 0 };
26
26
  }
27
27
  register({
@@ -127,7 +127,7 @@ register({
127
127
  const after = verifyFast(tree).findings.filter((f) => f.level === "error");
128
128
  const text = r.restored.length + r.quarantined.length === 0
129
129
  ? "nothing to repair — every record matches its seal"
130
- : lines(...r.restored.map((f) => `restored ${f}`), ...r.quarantined.map((f) => `removed ${f}`), `sealed state: ${r.from.slice(0, 8)} · copies of replaced files: .strom/quarantine/`, after.length ? `still ${after.length} problem(s) → strom verify` : undefined);
130
+ : lines(...r.restored.map((f) => `restored ${f}`), ...r.quarantined.map((f) => `removed ${f}`), `sealed state: ${r.from.slice(0, 8)} · copies of replaced files: .strom/quarantine/`, after.length ? `still ${counted(after.length, "problem")} → strom verify` : undefined);
131
131
  return { text, data: { ...r, ok: after.length === 0 }, exitCode: after.length ? 1 : 0 };
132
132
  },
133
133
  }, {
@@ -6,7 +6,7 @@
6
6
  import fs from "node:fs";
7
7
  import path from "node:path";
8
8
  import { register } from "../cli/registry.js";
9
- import { lines, truncate } from "../cli/format.js";
9
+ import { counted, lines, truncate } from "../cli/format.js";
10
10
  import { UsageError } from "../core/errors.js";
11
11
  import { normId, requireRecord, update } from "../core/records.js";
12
12
  import { makeView } from "../core/views.js";
@@ -90,10 +90,10 @@ register({
90
90
  throw new UsageError("--batch must be a positive number");
91
91
  const groups = batches(todo, size);
92
92
  if (!todo.length)
93
- return { text: lines("nothing to do: every source on images has its clip", away.length ? `${away.length} source(s) on images not on this computer (or not JPEG/PNG)` : undefined), data: { sources: 0 } };
93
+ return { text: lines("nothing to do: every source on images has its clip", away.length ? `${counted(away.length, "source")} on images not on this computer (or not JPEG/PNG)` : undefined), data: { sources: 0 } };
94
94
  if (tree.dryRun)
95
95
  return {
96
- text: lines(`${todo.length} source(s) to find on their images — ${groups.length} reader(s) to find them, as many to check`, away.length ? `${away.length} more on images not on this computer (or not JPEG/PNG)` : undefined, "(dry run — no reader started)"),
96
+ text: lines(`${counted(todo.length, "source")} to find on their images — ${counted(groups.length, "reader")} to find them, as many to check`, away.length ? `${away.length} more on images not on this computer (or not JPEG/PNG)` : undefined, "(dry run — no reader started)"),
97
97
  data: { sources: todo.length, readers: groups.length, away: away.map((s) => s.id) },
98
98
  };
99
99
  const r0 = readers(ctx, tree, shared, "clips", "find", opts);
@@ -174,7 +174,7 @@ register({
174
174
  // a search over many entries, not one entry: no clip, and said so
175
175
  const many = [...new Set(locs.filter((l) => l.result === "many").map((l) => l.source))];
176
176
  const missed = todo.filter((s) => !clipsOf.has(s.id));
177
- const text = lines(`${clipsOf.size} of ${todo.length} source(s) got their clip${model ? ` · ${model}` : ""}${r0.cost() ? ` · $${r0.cost().toFixed(2)}` : ""}`, many.length ? `not one entry but a search over many (no clip — a page of it would show nothing): ${many.join(", ")}` : undefined, rejected.length ? `the check turned down: ${rejected.map((v) => `${v.source} (${v.verdict}${v.why ? `: ${truncate(v.why, 40)}` : ""})`).join(", ")}` : undefined, missed.length ? `without a clip: ${missed.map((s) => s.id).slice(0, 20).join(", ")}${missed.length > 20 ? ` … (${missed.length})` : ""} — strom source edit S… --clip M…@x,y,w,h, or strom clips again` : undefined, away.length ? `on images not on this computer (or not JPEG/PNG): ${away.length}` : undefined, failed.length ? `readers that failed: ${failed.join(", ")}` : undefined, "the entries cut out for the Strom app: strom app (output/tree-strom.ged)");
177
+ const text = lines(`${clipsOf.size} of ${counted(todo.length, "source")} got their clip${model ? ` · ${model}` : ""}${r0.cost() ? ` · $${r0.cost().toFixed(2)}` : ""}`, many.length ? `not one entry but a search over many (no clip — a page of it would show nothing): ${many.join(", ")}` : undefined, rejected.length ? `the check turned down: ${rejected.map((v) => `${v.source} (${v.verdict}${v.why ? `: ${truncate(v.why, 40)}` : ""})`).join(", ")}` : undefined, missed.length ? `without a clip: ${missed.map((s) => s.id).slice(0, 20).join(", ")}${missed.length > 20 ? ` … (${missed.length})` : ""} — strom source edit S… --clip M…@x,y,w,h, or strom clips again` : undefined, away.length ? `on images not on this computer (or not JPEG/PNG): ${away.length}` : undefined, failed.length ? `readers that failed: ${failed.join(", ")}` : undefined, "the entries cut out for the Strom app: strom app (output/tree-strom.ged)");
178
178
  return {
179
179
  text,
180
180
  data: {