strom-research 1.25.1 → 1.25.2

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.
@@ -60,6 +60,7 @@ function view(k, p) {
60
60
  ...(why ? { why } : {}),
61
61
  ...(loc?.source ? { source: loc.source } : {}),
62
62
  ...(loc?.verified ? { verified: loc.verified } : {}),
63
+ ...(loc?.verifiedSource ? { verifiedSource: loc.verifiedSource } : {}),
63
64
  ...(loc?.checkWhy ? { checkWhy: loc.checkWhy } : {}),
64
65
  ...(!p.coords && p.unlocated && loc?.needs ? { needs: loc.needs } : {}),
65
66
  ...(q && (state === "proposed" || state === "conflict") ? { waiting: p.id } : {}),
@@ -77,7 +77,10 @@ export const SCHEMAS = {
77
77
  why: S({ max: 1000 }),
78
78
  source: { t: "object", of: { title: S({ req: true, max: 300 }), url: S({ max: 2000 }) } },
79
79
  verified: S(),
80
+ verifiedWhy: S({ max: 1000 }),
81
+ verifiedSource: { t: "object", of: { title: S({ req: true, max: 300 }), url: S({ max: 2000 }) } },
80
82
  checkWhy: S({ max: 1000 }),
83
+ checked: S(),
81
84
  needs: S({ max: 300 }),
82
85
  context: { t: "number", int: true, min: 0 },
83
86
  kept: { t: "object", of: { lat: { t: "number", req: true, min: -90, max: 90 }, lon: { t: "number", req: true, min: -180, max: 180 } } },
@@ -22,6 +22,7 @@ import { forgetLeftAlone } from "./leftalone.js";
22
22
  import { Settings } from "./config.js";
23
23
  import { forGaps, regionOfTask, takeRegion } from "./pastevents.js";
24
24
  import { placeOfTask, takePlace } from "./placehistory.js";
25
+ import { PLACES_IDENTIFY_ORIGIN, placesSessionCounts } from "./placeid.js";
25
26
  function currentFile(tree, env = tree.env) {
26
27
  const w = env.STROM_WORKER;
27
28
  return path.join(tree.root, ".strom", isWorkerId(w) ? `session-${w}.json` : "session.json");
@@ -236,6 +237,10 @@ export function closeSession(tree, s, input) {
236
237
  tree.put(back, { op: "task.continue", targets: [task.id, s.id], summary: `${task.id} back to the queue` });
237
238
  }
238
239
  }
240
+ // a session on the places: what it did counted by strom (identified, verified, asked …) before the agent's words
241
+ const counts = task?.origin === PLACES_IDENTIFY_ORIGIN ? placesSessionCounts(tree, s.id) : "";
242
+ if (counts)
243
+ input = { ...input, summary: input.summary.trim() ? `${counts} — ${input.summary.trim()}` : counts };
239
244
  const closed = {
240
245
  ...s,
241
246
  state: input.interrupted ? "interrupted" : "closed",
@@ -44,7 +44,7 @@ export function streetPlaceFixes(tree) {
44
44
  towns.set(b.key, t);
45
45
  continue;
46
46
  }
47
- const place = streets.townOf(s.town);
47
+ const place = streets.townOf(s.town, s.sentence);
48
48
  const had = e.house?.trim();
49
49
  // the house: the street and the number where the fact has none, the street before a number alone
50
50
  const house = houseWith(s.house, had);
@@ -56,6 +56,29 @@ export function streetPlaceFixes(tree) {
56
56
  out.fixes.push({ owner: o.id, event: e.id, before: placeHouse(e.place, e.house), after: placeHouse(place, house), ...(record ? { record } : {}) });
57
57
  }
58
58
  }
59
+ // what an older fix left of a sentence in the place ("v Lhotici v" of "v Lhotici v čp. 46", before prepositions were
60
+ // known): the place as the rules split it now, the fact's house kept — only where the fact is as that fix left it
61
+ const ownerOf = new Map();
62
+ for (const o of owners(tree)) {
63
+ if (o.retracted || o.mergedInto)
64
+ continue;
65
+ for (const e of o.events)
66
+ if (!e.retracted && e.status !== "retracted")
67
+ ownerOf.set(e.id, { owner: o, place: e.place, house: e.house });
68
+ }
69
+ const taken = new Set(out.fixes.map((f) => f.event));
70
+ for (const it of readStreetsFile(tree).runs.flatMap((r) => r.items)) {
71
+ const now = ownerOf.get(it.event);
72
+ if (!now || taken.has(it.event) || !same(now.place, now.house, it.after))
73
+ continue;
74
+ const s = streets.split(it.before.place);
75
+ const place = s ? streets.townOf(s.town, s.sentence) : undefined;
76
+ if (!place || place === it.after.place)
77
+ continue;
78
+ const record = recordOf(place);
79
+ out.fixes.push({ owner: now.owner.id, event: it.event, before: it.after, after: placeHouse(place, it.after.house), ...(record ? { record } : {}) });
80
+ taken.add(it.event);
81
+ }
59
82
  out.towns = [...towns]
60
83
  .map(([key, t]) => ({ town: t.town, key, fixes: t.fixes, places: [...t.places].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([p]) => p) }))
61
84
  .sort((a, b) => b.fixes.length - a.fixes.length || a.town.localeCompare(b.town));
@@ -36,6 +36,7 @@ export function streetTerms() {
36
36
  last: new Set((f.last ?? []).map((w) => foldText(w))),
37
37
  house: (f.house ?? []).map(words).filter((w) => w.length).sort((a, b) => b.length - a.length),
38
38
  numbered: new Set((f.numbered ?? []).map((w) => foldText(w))),
39
+ prepositions: new Set(Object.values(f.prepositions ?? {}).flat().map((w) => foldText(w))),
39
40
  };
40
41
  return terms;
41
42
  }
@@ -50,6 +51,10 @@ function markerAt(toks, i) {
50
51
  return 0;
51
52
  }
52
53
  const isNumber = (t) => !!t && NUMBER.test(t.fold);
54
+ /** A house that says it is a house already ("č. p. 12", "Nr. 3", "nr domu 12"): a marker of a house number at its beginning. */
55
+ export function hasHouseMark(text) {
56
+ return markerAt(tokens(text.normalize("NFC")), 0) > 0;
57
+ }
53
58
  /** A house of numbers alone ("12", "13/2"). */
54
59
  export function numbersOnly(text) {
55
60
  const toks = tokens(text.normalize("NFC"));
@@ -119,6 +124,7 @@ export function splitStreet(place, known) {
119
124
  const keep = [];
120
125
  const house = [];
121
126
  let by = "word";
127
+ let sentence = false;
122
128
  for (const [i, part] of parts.entries()) {
123
129
  const toks = tokens(part);
124
130
  if (!toks.length)
@@ -150,7 +156,12 @@ export function splitStreet(place, known) {
150
156
  }
151
157
  // a house number with its marker at the end of a part ("Lhota č. p. 12", "Lhota, Horní Lhota Nr. 3"): the number
152
158
  if (tail && tail.at < tail.number && tail.at > 0) {
153
- keep.push(part.slice(0, toks[tail.at].start).replace(/[\s,;:–-]+$/u, "").trim());
159
+ const words = sentenceWords(part, toks.slice(0, tail.at));
160
+ if (!words.town)
161
+ continue;
162
+ if (words.sentence)
163
+ sentence = true;
164
+ keep.push(words.town);
154
165
  house.push(part.slice(toks[tail.number].start).trim());
155
166
  continue;
156
167
  }
@@ -158,7 +169,23 @@ export function splitStreet(place, known) {
158
169
  }
159
170
  if (!house.length || !keep.length || !keep[0])
160
171
  return undefined;
161
- return { town: keep.join(", "), house: house.join(" "), by };
172
+ return { town: keep.join(", "), house: house.join(" "), by, ...(sentence ? { sentence: true } : {}) };
173
+ }
174
+ /**
175
+ * The words before a house number without the sentence they stood in: a preposition at the end taken off ("v Lhotici v"
176
+ * of "v Lhotici v čp. 46"), and one in lower case at the beginning ("v Lhotici" → "Lhotici"; "U Lípy", a name, stays).
177
+ */
178
+ function sentenceWords(part, toks) {
179
+ const prep = streetTerms().prepositions;
180
+ let a = 0;
181
+ let b = toks.length;
182
+ while (b > a + 1 && prep.has(toks[b - 1].fold))
183
+ b--;
184
+ const first = toks[a];
185
+ const word = part.slice(first.start, first.end);
186
+ if (b > a + 1 && prep.has(first.fold) && word === word.toLowerCase() && word !== word.toUpperCase())
187
+ a++;
188
+ return { town: part.slice(toks[a].start, toks[b - 1].end).replace(/[\s,;:–-]+$/u, "").trim(), sentence: a > 0 || b < toks.length };
162
189
  }
163
190
  /**
164
191
  * A number alone at the end of the place's first part, after words, with no marker ("Lhota 16", "Lhota 16, okres
@@ -272,6 +299,11 @@ export function streetsOf(tree) {
272
299
  }
273
300
  for (const t of known.keys())
274
301
  partOf(t);
302
+ // the municipalities' names as the research writes them, by their name folded (the facts' spelling first)
303
+ const named = new Map();
304
+ for (const f of spellings.keys())
305
+ if (!f.includes(","))
306
+ named.set(f, f);
275
307
  for (const r of tree.list("place")) {
276
308
  if (r.retracted)
277
309
  continue;
@@ -280,6 +312,9 @@ export function streetsOf(tree) {
280
312
  partOf(n.name.split(",")[0]);
281
313
  else
282
314
  towns.add(foldText(n.name.split(",")[0]));
315
+ const f = foldText(n.name);
316
+ if (!f.includes(",") && !named.has(f))
317
+ named.set(f, n.name.normalize("NFC").trim());
283
318
  }
284
319
  }
285
320
  const bare = (place) => {
@@ -295,7 +330,7 @@ export function streetsOf(tree) {
295
330
  return { ...b, key, ...(a ? { houses: a.houses } : {}) };
296
331
  };
297
332
  const most = (m) => [...m].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))[0][0];
298
- const townOf = (town) => {
333
+ const townOf = (town, sentence) => {
299
334
  const f = foldText(town);
300
335
  const exact = spellings.get(f);
301
336
  if (exact)
@@ -304,6 +339,14 @@ export function streetsOf(tree) {
304
339
  const forms = byFirst.get(f);
305
340
  if (forms?.size === 1)
306
341
  return most(spellings.get([...forms][0]));
342
+ if (sentence && !named.has(f)) {
343
+ const like = [...named.keys()].filter((k) => otherCase(f, k));
344
+ if (like.length === 1) {
345
+ const k = like[0];
346
+ const m = spellings.get(k);
347
+ return m ? most(m) : named.get(k);
348
+ }
349
+ }
307
350
  }
308
351
  return town;
309
352
  };
@@ -313,12 +356,28 @@ export function streetsOf(tree) {
313
356
  townOf,
314
357
  placeOf: (place) => {
315
358
  const s = split(place);
316
- return s ? townOf(s.town) : place;
359
+ return s ? townOf(s.town, s.sentence) : place;
317
360
  },
318
361
  };
319
362
  memo.set(tree, { version: tree.version, answers: answersKey, streets });
320
363
  return streets;
321
364
  }
365
+ /**
366
+ * A municipality's name in a sentence in another case than the research writes it (folded): the same words but the
367
+ * ending of the last — the word as written ending in at most three letters of its own, the research's in at most one,
368
+ * three letters alike before ("lhotici" of "lhotice", "lhote" of "lhota", "lhotowie" of "lhotow"; never "lhote" of
369
+ * "lhotka", another municipality).
370
+ */
371
+ function otherCase(written, research) {
372
+ if (written === research)
373
+ return false;
374
+ let p = 0;
375
+ while (p < written.length && p < research.length && written[p] === research[p])
376
+ p++;
377
+ const a = written.slice(p);
378
+ const b = research.slice(p);
379
+ return p >= 3 && !/\s/u.test(written.slice(0, p).slice(-1)) && a.length <= 3 && b.length <= 1 && !/[\s,]/u.test(a + b);
380
+ }
322
381
  /**
323
382
  * Checks a place written now (strom event add|edit --place, person add --born-place|--died-place, family add
324
383
  * --married-place): a street or a house number in it is refused with what to write instead — the municipality as the
@@ -331,7 +390,7 @@ export function checkStreetPlace(tree, place, house, flag = "--place") {
331
390
  const s = streets.split(place);
332
391
  if (!s)
333
392
  return;
334
- const town = streets.townOf(s.town);
393
+ const town = streets.townOf(s.town, s.sentence);
335
394
  const given = house?.trim();
336
395
  const both = houseWith(s.house, given);
337
396
  const set = both ?? s.house;
package/dist/core/sync.js CHANGED
@@ -33,6 +33,7 @@ import { appAge, appDate } from "./appshapes.js";
33
33
  import { dateYears } from "./gdate.js";
34
34
  import { create, update } from "./records.js";
35
35
  import { decodeText, foldText, safeFolderName } from "./text.js";
36
+ import { hasHouseMark } from "./streets.js";
36
37
  import { roleWord } from "./roles.js";
37
38
  import { labels } from "../gedcom/labels.js";
38
39
  import { eventName, humanDate, humanDay } from "../cli/human.js";
@@ -3684,7 +3685,7 @@ export function applySync(tree, plan, incoming, source, only) {
3684
3685
  toCheck(owner, gr);
3685
3686
  }
3686
3687
  else {
3687
- const title = editTitle(c.name ?? owner, eventName(c.fact.kind, tree.lang, c.fact.label), factWords(tree, incoming, mine), factWords(tree, incoming, c.fact));
3688
+ const title = editTitle(tree.lang, c.name ?? owner, eventName(c.fact.kind, tree.lang, c.fact.label), factWords(tree, incoming, mine), factWords(tree, incoming, c.fact));
3688
3689
  conflictOfEdit(tree, applied, owner, c.fact.kind, title, [
3689
3690
  { ...(mine.citations[0] ? { source: mine.citations[0].source } : {}), value: describe(mine), note: `the research: ${mine.id}`, text: factWords(tree, incoming, mine) },
3690
3691
  { source: source.id, value: claimValue(c.fact), note: "the user's edit", text: factWords(tree, incoming, c.fact) },
@@ -3698,7 +3699,7 @@ export function applySync(tree, plan, incoming, source, only) {
3698
3699
  break;
3699
3700
  // picked without the state it was given: a record's fact is not overwritten
3700
3701
  if (c.action === "conflict" || (c.action === "pick" && recordBacked(tree, mine))) {
3701
- const title = editTitle(c.name ?? owner, eventName(c.fact.kind, tree.lang, c.fact.label), factWords(tree, incoming, mine), factWords(tree, incoming, c.fact));
3702
+ const title = editTitle(tree.lang, c.name ?? owner, eventName(c.fact.kind, tree.lang, c.fact.label), factWords(tree, incoming, mine), factWords(tree, incoming, c.fact));
3702
3703
  conflictOfEdit(tree, applied, owner, c.fact.kind, title, [
3703
3704
  { ...(mine.citations[0] ? { source: mine.citations[0].source } : {}), value: describe(mine), note: `the research: ${mine.id}`, text: factWords(tree, incoming, mine) },
3704
3705
  { source: source.id, value: describe(c.fact), note: "the user's edit", text: factWords(tree, incoming, c.fact) },
@@ -3748,7 +3749,7 @@ export function applySync(tree, plan, incoming, source, only) {
3748
3749
  const at = c.wasPart ? before.findIndex((x) => x.role === c.wasPart.role && (c.wasPart.person ? x.person === c.wasPart.person : !x.person && nameKey(x.name ?? "") === nameKey(c.wasPart.name ?? ""))) : -1;
3749
3750
  const kindOfRec = owner.startsWith("F") ? "family" : "person";
3750
3751
  if (c.action === "conflict") {
3751
- const title = editTitle(c.name ?? owner, eventName(mine.kind, tree.lang, mine.label), partWords(c.wasPart, incoming, tree.lang), partWords(c.part, incoming, tree.lang));
3752
+ const title = editTitle(tree.lang, c.name ?? owner, eventName(mine.kind, tree.lang, mine.label), partWords(c.wasPart, incoming, tree.lang), partWords(c.part, incoming, tree.lang));
3752
3753
  conflictOfEdit(tree, applied, owner, mine.kind, title, [
3753
3754
  { ...(mine.citations[0] ? { source: mine.citations[0].source } : {}), value: `${c.wasPart.role} ${c.wasPart.name ?? c.wasPart.person ?? "?"}`, note: `the research: ${mine.id}`, text: partWords(c.wasPart, incoming, tree.lang) },
3754
3755
  { source: source.id, value: partText(c.part, incoming), note: "the user's edit", text: partWords(c.part, incoming, tree.lang) },
@@ -3905,7 +3906,7 @@ export function applySync(tree, plan, incoming, source, only) {
3905
3906
  const day = humanDay(p.retracted.at, tree.lang);
3906
3907
  const ours = ui(tree.lang, "ui.conflict.removed.ours", { day, sync: c.removedBy ?? "—" });
3907
3908
  const theirs = ui(tree.lang, "ui.conflict.removed.theirs");
3908
- const { open } = conflictOfEdit(tree, applied, p.id, "PERSON", editTitle(name, ui(tree.lang, "ui.conflict.removed"), ours, theirs), [
3909
+ const { open } = conflictOfEdit(tree, applied, p.id, "PERSON", editTitle(tree.lang, name, ui(tree.lang, "ui.conflict.removed"), ours, theirs), [
3909
3910
  { value: `removed${c.removedBy ? ` ${c.removedBy}` : ""}`, note: "the research: removed by an earlier send", text: ours },
3910
3911
  { source: source.id, value: "edited", note: "the user's edit", text: theirs },
3911
3912
  ]);
@@ -3924,7 +3925,7 @@ export function applySync(tree, plan, incoming, source, only) {
3924
3925
  const ours = ui(tree.lang, by === "app" ? "ui.conflict.removing.ours" : `ui.conflict.removing.${by}`);
3925
3926
  const theirs = ui(tree.lang, "ui.conflict.removing.theirs");
3926
3927
  const where = by === "app" ? "in another copy" : by === "research" ? "in the research" : "in another copy and in the research";
3927
- const { open } = conflictOfEdit(tree, applied, p.id, "PERSON", editTitle(name, ui(tree.lang, "ui.conflict.removed"), ours, theirs), [
3928
+ const { open } = conflictOfEdit(tree, applied, p.id, "PERSON", editTitle(tree.lang, name, ui(tree.lang, "ui.conflict.removed"), ours, theirs), [
3928
3929
  { value: "edited", note: `the research: edited since ${where}`, text: ours },
3929
3930
  { source: source.id, value: "removed", note: "the user's edit", text: theirs },
3930
3931
  ]);
@@ -4009,7 +4010,7 @@ export function applySync(tree, plan, incoming, source, only) {
4009
4010
  else {
4010
4011
  // a record gives the name: the user decides; their name kept as another form meanwhile (sent again, it
4011
4012
  // asks nothing new)
4012
- const title = editTitle(c.wasName ?? owner, ui(tree.lang, "ui.conflict.name"), (c.wasName ?? "").replace(/\//g, "").replace(/\s+/g, " ").trim() || "—", c.text.replace(/\//g, "").replace(/\s+/g, " ").trim());
4013
+ const title = editTitle(tree.lang, c.wasName ?? owner, ui(tree.lang, "ui.conflict.name"), (c.wasName ?? "").replace(/\//g, "").replace(/\s+/g, " ").trim() || "—", c.text.replace(/\//g, "").replace(/\s+/g, " ").trim());
4013
4014
  const { was } = conflictOfEdit(tree, applied, owner, "NAME", title, [
4014
4015
  { ...(shown.citations?.[0] ? { source: shown.citations[0].source } : {}), value: c.wasName ?? "", note: "the research: the name the person is shown by", text: (c.wasName ?? "").replace(/\//g, "").replace(/\s+/g, " ").trim() || "—" },
4015
4016
  { source: source.id, value: c.text, note: "the user's edit", text: c.text.replace(/\//g, "").replace(/\s+/g, " ").trim() },
@@ -4034,7 +4035,7 @@ export function applySync(tree, plan, incoming, source, only) {
4034
4035
  if (c.action === "conflict") {
4035
4036
  const who = gedcomName(primaryName(p)).replace(/\//g, "").replace(/\s+/g, " ").trim();
4036
4037
  const said = (n) => (n ? formatName(n) : ui(tree.lang, "ui.conflict.primary.none"));
4037
- conflictOfEdit(tree, applied, owner, "NAME", editTitle(who, ui(tree.lang, "ui.conflict.primary"), said(was), said(target)), [
4038
+ conflictOfEdit(tree, applied, owner, "NAME", editTitle(tree.lang, who, ui(tree.lang, "ui.conflict.primary"), said(was), said(target)), [
4038
4039
  { value: was ? gedcomName(was) : "", note: "the research: the name it shows the person by", text: said(was) },
4039
4040
  { source: source.id, value: target ? gedcomName(target) : "", note: "the user's edit", text: said(target) },
4040
4041
  ], undefined, undefined, true);
@@ -4053,7 +4054,7 @@ export function applySync(tree, plan, incoming, source, only) {
4053
4054
  // a record gives the name: the user decides (strom conflict resolve --take user|research)
4054
4055
  const said = (v) => v || "—";
4055
4056
  const what = ui(tree.lang, c.title.part === "before" ? "ui.conflict.titleBefore" : "ui.conflict.titleAfter");
4056
- conflictOfEdit(tree, applied, owner, titleTag(c.title.part), editTitle(formatName(shown), what, said(c.title.was), said(now)), [
4057
+ conflictOfEdit(tree, applied, owner, titleTag(c.title.part), editTitle(tree.lang, formatName(shown), what, said(c.title.was), said(now)), [
4057
4058
  { ...(shown.citations?.[0] ? { source: shown.citations[0].source } : {}), value: c.title.was, note: "the research: the title of the name the person is shown by", text: said(c.title.was) },
4058
4059
  { source: source.id, value: now, note: "the user's edit", text: said(now) },
4059
4060
  ]);
@@ -4092,7 +4093,7 @@ export function applySync(tree, plan, incoming, source, only) {
4092
4093
  const p = tree.get(owner);
4093
4094
  if (c.action === "conflict") {
4094
4095
  const who = gedcomName(primaryName(p)).replace(/\//g, "").replace(/\s+/g, " ").trim();
4095
- conflictOfEdit(tree, applied, owner, "SEX", editTitle(who, ui(tree.lang, "ui.conflict.sex"), sexWord(p.sex, tree.lang), sexWord(c.text, tree.lang)), [
4096
+ conflictOfEdit(tree, applied, owner, "SEX", editTitle(tree.lang, who, ui(tree.lang, "ui.conflict.sex"), sexWord(p.sex, tree.lang), sexWord(c.text, tree.lang)), [
4096
4097
  { value: p.sex, note: "the research: the sex its records give", text: sexWord(p.sex, tree.lang) },
4097
4098
  { source: source.id, value: c.text, note: "the user's edit", text: sexWord(c.text, tree.lang) },
4098
4099
  ]);
@@ -4220,7 +4221,7 @@ export function applySync(tree, plan, incoming, source, only) {
4220
4221
  const partners = (c.partners ?? []).map((k) => who(k)).filter((k) => !!k);
4221
4222
  // the family of the user's parents: the research's the file named, or the one this send made — else made when taken
4222
4223
  const to = c.family ?? families.get(famKey(partners));
4223
- conflictOfEdit(tree, applied, child, "FAMC", editTitle(name, ui(tree.lang, "ui.conflict.parents"), ours, theirs), [
4224
+ conflictOfEdit(tree, applied, child, "FAMC", editTitle(tree.lang, name, ui(tree.lang, "ui.conflict.parents"), ours, theirs), [
4224
4225
  { value: `${born.id}: ${ours}`, note: "the research: the parents it gives", text: ours },
4225
4226
  { source: source.id, value: theirs, note: "the user's edit", text: theirs },
4226
4227
  ], undefined, partners.length ? { child, from: born.id, partners, ...(to ? { to } : {}) } : undefined);
@@ -4400,15 +4401,25 @@ function nameOfView(p, view) {
4400
4401
  const key = view.startsWith("married|") ? view.slice("married|".length) : undefined;
4401
4402
  return key === undefined ? undefined : p.names.find((n) => n !== first && nameKey(gedcomName(n)) === key);
4402
4403
  }
4403
- function editTitle(who, what, ours, theirs) {
4404
- return `${who.replace(/\//g, "").replace(/\s+/g, " ").trim()}: ${what.charAt(0).toUpperCase()}${what.slice(1)} — ${ours} × ${theirs}`;
4404
+ function editTitle(lang, who, what, ours, theirs) {
4405
+ return ui(lang, "ui.conflict.title", { who: who.replace(/\//g, "").replace(/\s+/g, " ").trim(), what: `${what.charAt(0).toUpperCase()}${what.slice(1)}`, ours, theirs });
4406
+ }
4407
+ /**
4408
+ * A fact's house as a person reads it, in the research's language: "čp. 12" of "12" — a house that says it is one
4409
+ * already ("nr domu 12", "Nr. 3") as it is, never "nr domu nr domu 12".
4410
+ */
4411
+ export function houseWords(lang, house) {
4412
+ const prefix = foldText(ui(lang, "ui.sync.house", { x: "" }).trim());
4413
+ if (hasHouseMark(house) || (prefix && foldText(house).startsWith(prefix)))
4414
+ return house;
4415
+ return ui(lang, "ui.sync.house", { x: house });
4405
4416
  }
4406
4417
  /** A fact as a conflict's title says it, in the research's language: "1904, Praha, čp. 12, věk 45 let". */
4407
4418
  function factWords(tree, incoming, f) {
4408
4419
  const lang = tree.lang;
4409
4420
  const given = (k) => (k.startsWith("x:") ? undefined : tree.get(k)?.names[0]?.given) ?? (incoming.persons.get(k)?.names[0]?.split("/")[0]?.trim() || k);
4410
4421
  const ages = Object.entries(f.ages ?? {}).map(([who, a]) => `${given(who)} ${humanAge(a, lang)}`);
4411
- return [f.value, humanDate(f.date, lang), f.place, f.house && ui(lang, "ui.sync.house", { x: f.house }), f.age && humanAge(f.age, lang), ...ages, f.cause && ui(lang, "ui.sync.cause", { x: f.cause })].filter(Boolean).join(", ") || "—";
4422
+ return [f.value, humanDate(f.date, lang), f.place, f.house && houseWords(lang, f.house), f.age && humanAge(f.age, lang), ...ages, f.cause && ui(lang, "ui.sync.cause", { x: f.cause })].filter(Boolean).join(", ") || "—";
4412
4423
  }
4413
4424
  /** Who the record names, as a conflict's title says it: "kmotr Jan Novák". */
4414
4425
  function partWords(p, snap, lang) {
package/dist/core/tree.js CHANGED
@@ -31,7 +31,7 @@ export const TREE_FILE = "strom.json";
31
31
  export const SEAL_FOREIGN = "seal.foreign";
32
32
  // between releases the candidate being made (a bridge run from the sources says what it is: the Strom app goes by it);
33
33
  // at a release the release's own (package.json)
34
- export const VERSION = "1.25.1";
34
+ export const VERSION = "1.25.2";
35
35
  const GITIGNORE = `# Strom Research — local state and large files are not versioned
36
36
  .strom/
37
37
  /media/
@@ -4,7 +4,7 @@ import { spawn } from "node:child_process";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { findAgent } from "../core/which.js";
7
- import { claudeEnv, isClaudeCommand } from "../core/claudedir.js";
7
+ import { forClaude, isClaudeCommand } from "../core/claudedir.js";
8
8
  /** A number the agent said, else nothing. */
9
9
  export function usageNumber(n) {
10
10
  return typeof n === "number" && Number.isFinite(n) ? n : undefined;
@@ -104,10 +104,10 @@ function cmdQuote(arg) {
104
104
  /**
105
105
  * Start an agent CLI found on PATH. npm installs agents on Windows as .cmd
106
106
  * launchers, which Node can only start through cmd.exe. Claude Code gets its folder (agent.claudeConfigDir) when the
107
- * environment says none: every `claude` strom starts goes through here.
107
+ * environment says none, and none when it says the default one (forClaude): every `claude` strom starts goes through here.
108
108
  */
109
109
  export function spawnAgent(command, args, given) {
110
- const opts = isClaudeCommand(command) ? { ...given, env: { ...given.env, ...claudeEnv(given.env) } } : given;
110
+ const opts = isClaudeCommand(command) ? { ...given, env: forClaude(given.env) } : given;
111
111
  const found = findAgent(command, opts.env) ?? command;
112
112
  if (process.platform === "win32" && /\.(cmd|bat)$/i.test(found)) {
113
113
  const line = [cmdQuote(found), ...args.map(cmdQuote)].join(" ");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strom-research",
3
- "version": "1.25.1",
3
+ "version": "1.25.2",
4
4
  "description": "Genealogical research toolkit for AI coding agents",
5
5
  "type": "module",
6
6
  "stromLayout": 2,