memgineering 0.8.0 → 0.9.0

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/dist/index.js CHANGED
@@ -181,10 +181,29 @@ function quoteBlock(text, prefix = "> ") {
181
181
  return stripControl(text).split("\n").map((line) => prefix + line).join("\n");
182
182
  }
183
183
  function oneLine(text, max) {
184
- const flat = stripControl(text).replace(/\s+/g, " ").trim();
184
+ const flat = oneLineWhole(text);
185
185
  const chars = [...flat];
186
186
  if (chars.length <= max) return flat;
187
- return `${chars.slice(0, Math.max(1, max - 1)).join("").trimEnd()}\u2026`;
187
+ const budget = Math.max(1, max - 1);
188
+ let shown = chars.slice(0, budget).join("").trimEnd();
189
+ const next = chars[budget];
190
+ const cutsWord = next !== void 0 && /\S/.test(next) && /\S/.test(shown.at(-1) ?? "");
191
+ const lastSpace = shown.lastIndexOf(" ");
192
+ if (cutsWord && lastSpace >= Math.floor(budget * 0.6))
193
+ shown = shown.slice(0, lastSpace).trimEnd();
194
+ return `${shown}\u2026`;
195
+ }
196
+ function oneLineWhole(text) {
197
+ return stripControl(text).replace(/\s+/g, " ").trim();
198
+ }
199
+ function middleLine(text, max) {
200
+ const chars = [...oneLineWhole(text)];
201
+ if (chars.length <= max) return chars.join("");
202
+ if (max <= 1) return "\u2026";
203
+ const budget = max - 1;
204
+ const headLength = Math.max(1, Math.floor(budget * 0.4));
205
+ const tailLength = Math.max(1, budget - headLength);
206
+ return `${chars.slice(0, headLength).join("")}\u2026${chars.slice(-tailLength).join("")}`;
188
207
  }
189
208
  function printBlock(text) {
190
209
  printHuman(stripControl(text));
@@ -216,7 +235,7 @@ var init_ui = __esm({
216
235
  /** A vault path. Longer, because the tail is the part that identifies it. */
217
236
  path: 120,
218
237
  /** An id or an `open:` handle. */
219
- handle: 64,
238
+ handle: 128,
220
239
  /** A claim, a summary, a server hint — a sentence rather than a label. */
221
240
  text: 200
222
241
  };
@@ -247,11 +266,12 @@ var init_errors = __esm({
247
266
 
248
267
  // ../../packages/memory-engine/src/schema.ts
249
268
  import { z } from "zod";
250
- var MappingModeSchema, ProvenanceSchema, MemoryStatusSchema, ClaimConfidenceSchema, MemoryKindSchema, ClaimSchema, MemorySchema, RelationTypeSchema, RelationSourceSchema, RelationSchema;
269
+ var MappingModeSchema, ScopeSourceSchema, ProvenanceSchema, MemoryStatusSchema, ClaimConfidenceSchema, MemoryKindSchema, ClaimSchema, MemorySchema, RelationTypeSchema, RelationSourceSchema, RelationSchema;
251
270
  var init_schema = __esm({
252
271
  "../../packages/memory-engine/src/schema.ts"() {
253
272
  "use strict";
254
273
  MappingModeSchema = z.enum(["explicit", "heuristic"]);
274
+ ScopeSourceSchema = z.enum(["explicit", "path", "none"]);
255
275
  ProvenanceSchema = z.object({
256
276
  /** Vault-relative path of the note this was mapped from. */
257
277
  sourcePath: z.string().min(1),
@@ -264,7 +284,9 @@ var init_schema = __esm({
264
284
  * here would be the engine learning one backend's internals.
265
285
  */
266
286
  sourceRevision: z.unknown(),
267
- mapping: MappingModeSchema
287
+ mapping: MappingModeSchema,
288
+ /** Field-level provenance needed before scope may lower a semantic threshold. */
289
+ scopeSource: ScopeSourceSchema.optional()
268
290
  });
269
291
  MemoryStatusSchema = z.enum(["active", "superseded", "retired"]);
270
292
  ClaimConfidenceSchema = z.enum(["stated", "observed", "inferred"]);
@@ -644,6 +666,8 @@ function mapNoteToMemory(input) {
644
666
  const ns = explicit ? nsRaw : {};
645
667
  const id = asString(ns["id"]) ?? path;
646
668
  const title = asString(ns["title"]) ?? asString(frontmatter?.["title"]) ?? h1Of(outline.intro) ?? titleFromPath(path);
669
+ const declaredScope = asString(ns["scope"]);
670
+ const scope = declaredScope ?? scopeFromPath(path);
647
671
  const summary = asString(ns["summary"]) ?? outline.description ?? firstParagraph(outline.intro) ?? firstSectionParagraph(outline.sections) ?? null;
648
672
  const memory = {
649
673
  id,
@@ -658,7 +682,7 @@ function mapNoteToMemory(input) {
658
682
  })(),
659
683
  title,
660
684
  summary,
661
- scope: asString(ns["scope"]) ?? scopeFromPath(path),
685
+ scope,
662
686
  current_claim: asString(ns["current_claim"]),
663
687
  status: statusOf(ns["status"], issues),
664
688
  // Only the author's own `binding: true`. Never inferred: guessing that a
@@ -676,7 +700,8 @@ function mapNoteToMemory(input) {
676
700
  provenance: {
677
701
  sourcePath: path,
678
702
  sourceRevision: revision,
679
- mapping: explicit ? "explicit" : "heuristic"
703
+ mapping: explicit ? "explicit" : "heuristic",
704
+ scopeSource: declaredScope !== null ? "explicit" : scope === null ? "none" : "path"
680
705
  }
681
706
  };
682
707
  const claims = [];
@@ -790,7 +815,7 @@ var init_cache_io = __esm({
790
815
  "../../packages/memory-engine/src/view/cache-io.ts"() {
791
816
  "use strict";
792
817
  init_schema();
793
- SCHEMA_VERSION = 5;
818
+ SCHEMA_VERSION = 6;
794
819
  POLICY_VERSION = 1;
795
820
  CacheEntrySchema = z2.object({
796
821
  path: z2.string().min(1),
@@ -1397,18 +1422,40 @@ var init_conflicts = __esm({
1397
1422
  });
1398
1423
 
1399
1424
  // ../../packages/memory-engine/src/recall/search.ts
1425
+ function splitMixedScript(token) {
1426
+ return token.split(LATIN_HANGUL_BOUNDARY).filter((part) => part.length > 0);
1427
+ }
1400
1428
  function terms(text) {
1401
- return text.normalize("NFC").toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((t) => t.length > 0);
1429
+ return [...text.normalize("NFC").toLowerCase().matchAll(TOKEN)].flatMap(
1430
+ (match) => splitMixedScript(match[0])
1431
+ );
1432
+ }
1433
+ function indexTerms(text) {
1434
+ return [...text.normalize("NFC").toLowerCase().matchAll(TOKEN)].flatMap((match) => {
1435
+ const token = match[0];
1436
+ const exact = splitMixedScript(token);
1437
+ if (!HAS_COMPONENT_SEPARATOR.test(token)) return exact;
1438
+ const components = token.split(COMPONENT_SEPARATOR).filter((part) => part.length > 0).flatMap(splitMixedScript);
1439
+ return [...exact, ...components];
1440
+ });
1441
+ }
1442
+ function exactOnly(term) {
1443
+ return HAS_COMPONENT_SEPARATOR.test(term) || new RegExp("\\p{N}", "u").test(term);
1402
1444
  }
1403
1445
  function startsWithFieldTerm(query, fieldTerms) {
1404
- if (query.length <= SUBSTRING_MIN) return false;
1446
+ if (query.length <= SUBSTRING_MIN || !HANGUL.test(query)) return false;
1405
1447
  for (const f of fieldTerms) {
1406
- if (f.length >= SUBSTRING_MIN && f.length < query.length && query.startsWith(f)) return true;
1448
+ if (HANGUL.test(f) && f.length >= SUBSTRING_MIN && f.length < query.length && query.startsWith(f))
1449
+ return true;
1407
1450
  }
1408
1451
  return false;
1409
1452
  }
1453
+ function particleSuffixedFieldTerm(query, firstFieldTerm) {
1454
+ if (!ONE_SYLLABLE_TOPIC_TERMS.has(query)) return false;
1455
+ return PARTICLES.some((particle) => firstFieldTerm === `${query}${particle}`);
1456
+ }
1410
1457
  function variantsOf(term) {
1411
- if (!HANGUL.test(term)) return [term];
1458
+ if (exactOnly(term) || !HANGUL.test(term)) return [term];
1412
1459
  for (const particle of PARTICLES) {
1413
1460
  if (!term.endsWith(particle)) continue;
1414
1461
  const stem = term.slice(0, -particle.length);
@@ -1416,19 +1463,22 @@ function variantsOf(term) {
1416
1463
  }
1417
1464
  return [term];
1418
1465
  }
1419
- function fieldScore(queryTerms, field, weight) {
1466
+ function fieldScore(queryTerms, field, weight, allowLeadingOneSyllable = false) {
1420
1467
  if (field === null || field === "") return 0;
1421
1468
  const normalized = field.normalize("NFC").toLowerCase();
1422
- const fieldTerms = new Set(terms(field));
1469
+ const orderedFieldTerms = indexTerms(field);
1470
+ const fieldTerms = new Set(orderedFieldTerms);
1423
1471
  let score = 0;
1424
- for (const q of queryTerms) {
1472
+ for (const [queryIndex, q] of queryTerms.entries()) {
1425
1473
  let best = 0;
1426
1474
  for (const variant of variantsOf(q)) {
1427
1475
  if (fieldTerms.has(variant)) {
1428
1476
  best = Math.max(best, weight);
1429
- } else if (variant.length >= SUBSTRING_MIN && normalized.includes(variant)) {
1477
+ } else if (allowLeadingOneSyllable && queryIndex === 0 && particleSuffixedFieldTerm(variant, orderedFieldTerms[0])) {
1478
+ best = Math.max(best, weight);
1479
+ } else if (!exactOnly(variant) && variant.length >= SUBSTRING_MIN && normalized.includes(variant)) {
1430
1480
  best = Math.max(best, weight / 2);
1431
- } else if (startsWithFieldTerm(variant, fieldTerms)) {
1481
+ } else if (!exactOnly(variant) && startsWithFieldTerm(variant, fieldTerms)) {
1432
1482
  best = Math.max(best, weight / 2);
1433
1483
  }
1434
1484
  }
@@ -1441,7 +1491,7 @@ function searchMemories(memories, query) {
1441
1491
  if (q.length === 0) return [];
1442
1492
  const scored = [];
1443
1493
  for (const memory of memories) {
1444
- let score = fieldScore(q, memory.title, WEIGHTS.title);
1494
+ let score = fieldScore(q, memory.title, WEIGHTS.title, true);
1445
1495
  for (const alias of memory.aliases) score += fieldScore(q, alias, WEIGHTS.alias);
1446
1496
  for (const entity of memory.entities) score += fieldScore(q, entity, WEIGHTS.entity);
1447
1497
  score += fieldScore(q, memory.summary, WEIGHTS.summary);
@@ -1452,11 +1502,15 @@ function searchMemories(memories, query) {
1452
1502
  (a, b) => b.score !== a.score ? b.score - a.score : a.memory.id < b.memory.id ? -1 : 1
1453
1503
  );
1454
1504
  }
1455
- var WEIGHTS, SUBSTRING_MIN, PARTICLES, HANGUL;
1505
+ var WEIGHTS, TOKEN, COMPONENT_SEPARATOR, HAS_COMPONENT_SEPARATOR, LATIN_HANGUL_BOUNDARY, SUBSTRING_MIN, PARTICLES, HANGUL, ONE_SYLLABLE_TOPIC_TERMS;
1456
1506
  var init_search = __esm({
1457
1507
  "../../packages/memory-engine/src/recall/search.ts"() {
1458
1508
  "use strict";
1459
1509
  WEIGHTS = { title: 6, alias: 4, entity: 3, summary: 2, scope: 1 };
1510
+ TOKEN = /[./]?[\p{L}\p{N}]+(?:[._/-][\p{L}\p{N}]+)*/gu;
1511
+ COMPONENT_SEPARATOR = /[._/-]+/u;
1512
+ HAS_COMPONENT_SEPARATOR = /[._/-]/u;
1513
+ LATIN_HANGUL_BOUNDARY = new RegExp("(?<=\\p{Script=Latin})(?=\\p{Script=Hangul})|(?<=\\p{Script=Hangul})(?=\\p{Script=Latin})", "u");
1460
1514
  SUBSTRING_MIN = 2;
1461
1515
  PARTICLES = [
1462
1516
  "\uC774\uB77C\uB3C4",
@@ -1494,6 +1548,7 @@ var init_search = __esm({
1494
1548
  "\uAED8"
1495
1549
  ];
1496
1550
  HANGUL = /[\uac00-\ud7a3]/;
1551
+ ONE_SYLLABLE_TOPIC_TERMS = /* @__PURE__ */ new Set(["\uD0A4"]);
1497
1552
  }
1498
1553
  });
1499
1554
 
@@ -1511,7 +1566,7 @@ var init_chunks = __esm({
1511
1566
  avgLength;
1512
1567
  constructor(refs) {
1513
1568
  for (const ref of refs) {
1514
- const tokens = terms(ref.text);
1569
+ const tokens = indexTerms(ref.text);
1515
1570
  if (tokens.length === 0) continue;
1516
1571
  const counts = /* @__PURE__ */ new Map();
1517
1572
  for (const token of tokens) counts.set(token, (counts.get(token) ?? 0) + 1);
@@ -1573,6 +1628,34 @@ var init_chunks = __esm({
1573
1628
  function isRecallDepth(value) {
1574
1629
  return typeof value === "string" && RECALL_DEPTHS.includes(value);
1575
1630
  }
1631
+ function isStrongDecisionMarker(term) {
1632
+ return /^(?:정하|정했|정한|정해|결정하|결정했|결정한|합의하|합의했|합의한|지키|지켜|따르|골랐|고른)/u.test(
1633
+ term
1634
+ ) || /^(?:decide[ds]?|deciding|settle[ds]?|settling|agree[ds]?|agreeing|choose[ds]?|choosing|chose|chosen|follow(?:s|ed|ing)?)$/u.test(
1635
+ term
1636
+ );
1637
+ }
1638
+ function variantsInclude(term, candidates) {
1639
+ return variantsOf(term).some((variant) => candidates.has(variant));
1640
+ }
1641
+ function isDecisionContext(term) {
1642
+ return variantsInclude(term, DECISION_CONTEXT) || term.startsWith("\uC720\uD6A8");
1643
+ }
1644
+ function shapeRecallQuery(query) {
1645
+ const queryTerms = terms(query);
1646
+ const strong = queryTerms.some(isStrongDecisionMarker);
1647
+ const noun = queryTerms.some((term) => variantsInclude(term, DECISION_NOUNS));
1648
+ const contextual = queryTerms.some(isDecisionContext);
1649
+ const decisionIntent = strong || noun && contextual;
1650
+ if (!decisionIntent) return { lexical: query, decisionIntent: false };
1651
+ const topical = queryTerms.filter(
1652
+ (term) => !isStrongDecisionMarker(term) && !variantsInclude(term, DECISION_NOUNS) && !variantsInclude(term, DECISION_FILLERS) && !term.startsWith("\uC720\uD6A8")
1653
+ );
1654
+ return {
1655
+ lexical: topical.length === 0 ? query : topical.join(" "),
1656
+ decisionIntent
1657
+ };
1658
+ }
1576
1659
  function freshnessOf(memory, claims, now) {
1577
1660
  if (memory.status === "superseded") return "superseded";
1578
1661
  if (memory.status === "retired") return "retired";
@@ -1599,12 +1682,14 @@ function recall(entries, query, opts = {}) {
1599
1682
  const claimsById = new Map(inScope.map((e) => [e.memory.id, e.claims]));
1600
1683
  const relations = inScope.flatMap((e) => e.relations);
1601
1684
  const allMemories = inScope.map((e) => e.memory);
1602
- const cardHits = searchMemories(allMemories, query);
1685
+ const allMemoryIds = new Set(allMemories.map((memory) => memory.id));
1686
+ const queryShape = shapeRecallQuery(query);
1687
+ const cardHits = searchMemories(allMemories, queryShape.lexical);
1603
1688
  const chunkHits = depth === "exact" ? [] : new ChunkIndex(
1604
1689
  inScope.flatMap(
1605
1690
  (e) => (e.chunks ?? []).map((c2) => ({ memoryId: e.memory.id, label: c2.label, text: c2.text }))
1606
1691
  )
1607
- ).search(query);
1692
+ ).search(queryShape.lexical);
1608
1693
  const semanticHits = depth === "exact" ? [] : opts.semantic ?? [];
1609
1694
  const scoreById = /* @__PURE__ */ new Map();
1610
1695
  for (const hit of cardHits) scoreById.set(hit.memory.id, hit.score);
@@ -1620,14 +1705,26 @@ function recall(entries, query, opts = {}) {
1620
1705
  const memory = memoryById.get(id);
1621
1706
  return memory === void 0 ? [] : [{ memory, score }];
1622
1707
  }).sort((a, b) => b.score !== a.score ? b.score - a.score : a.memory.id < b.memory.id ? -1 : 1);
1623
- const queryTerms = Math.max(1, terms(query).length);
1708
+ const authoritativeSupersedes = queryShape.decisionIntent ? relations.filter(
1709
+ (relation) => relation.type === "supersedes" && relation.source === "explicit" && relation.weight > 0 && allMemoryIds.has(relation.from) && allMemoryIds.has(relation.to)
1710
+ ) : [];
1711
+ const supersededByAnother = new Set(authoritativeSupersedes.map((relation) => relation.to));
1712
+ const supersedingIds = new Set(
1713
+ authoritativeSupersedes.map((relation) => relation.from).filter((id) => !supersededByAnother.has(id))
1714
+ );
1715
+ const queryTerms = Math.max(1, terms(queryShape.lexical).length);
1624
1716
  const scored = hits.map((hit) => ({
1625
1717
  memory: hit.memory,
1626
1718
  currency: CURRENCY_RANK[freshnessOf(hit.memory, claimsById.get(hit.memory.id) ?? [], now)],
1627
- score: hit.score + boostFor(hit.memory) / queryTerms
1719
+ score: hit.score + boostFor(hit.memory) / queryTerms,
1720
+ // Only a final tie-break, and only when the question asks for a standing
1721
+ // decision. A valid outgoing supersedes edge says this note is the declared
1722
+ // replacement; it does not say an off-topic replacement beats a better
1723
+ // topical match.
1724
+ superseding: supersedingIds.has(hit.memory.id)
1628
1725
  }));
1629
1726
  const ranked = scored.sort(
1630
- (a, b) => a.currency !== b.currency ? a.currency - b.currency : b.score !== a.score ? b.score - a.score : a.memory.id < b.memory.id ? -1 : 1
1727
+ (a, b) => a.currency !== b.currency ? a.currency - b.currency : b.score !== a.score ? b.score - a.score : a.superseding !== b.superseding ? a.superseding ? -1 : 1 : a.memory.id < b.memory.id ? -1 : 1
1631
1728
  );
1632
1729
  const selected = ranked.slice(0, limit).map((s) => s.memory);
1633
1730
  const byId = new Map(allMemories.map((m) => [m.id, m]));
@@ -1712,7 +1809,7 @@ function depthOf(value) {
1712
1809
  function boostFor(memory) {
1713
1810
  return (memory.binding === true ? BINDING_BOOST : 0) + (memory.kind === void 0 ? 0 : KIND_BOOST[memory.kind] ?? 0);
1714
1811
  }
1715
- var RECALL_DEPTHS, RecallOptionError, DEFAULTS, BODY_WEIGHT, SEMANTIC_WEIGHT, BINDING_BOOST, KIND_BOOST, CURRENCY_RANK, WIDEST_ESTIMATE;
1812
+ var RECALL_DEPTHS, RecallOptionError, DEFAULTS, BODY_WEIGHT, SEMANTIC_WEIGHT, BINDING_BOOST, KIND_BOOST, DECISION_NOUNS, DECISION_CONTEXT, DECISION_FILLERS, CURRENCY_RANK, WIDEST_ESTIMATE;
1716
1813
  var init_planner = __esm({
1717
1814
  "../../packages/memory-engine/src/recall/planner.ts"() {
1718
1815
  "use strict";
@@ -1742,6 +1839,60 @@ var init_planner = __esm({
1742
1839
  decision: BINDING_BOOST / 2,
1743
1840
  preference: BINDING_BOOST / 2
1744
1841
  };
1842
+ DECISION_NOUNS = /* @__PURE__ */ new Set([
1843
+ "\uACB0\uC815",
1844
+ "\uADDC\uCE59",
1845
+ "\uB8F0",
1846
+ "\uD569\uC758",
1847
+ "decision",
1848
+ "decisions",
1849
+ "rule",
1850
+ "rules",
1851
+ "agreement"
1852
+ ]);
1853
+ DECISION_CONTEXT = /* @__PURE__ */ new Set([
1854
+ "\uC9C0\uAE08",
1855
+ "\uD604\uC7AC",
1856
+ "\uC544\uC9C1",
1857
+ "\uC608\uC804\uC5D0",
1858
+ "\uBB58",
1859
+ "\uBB50",
1860
+ "\uBB34\uC5C7",
1861
+ "\uC5B4\uB5BB\uAC8C",
1862
+ "what",
1863
+ "which",
1864
+ "did",
1865
+ "now",
1866
+ "current",
1867
+ "currently",
1868
+ "still"
1869
+ ]);
1870
+ DECISION_FILLERS = /* @__PURE__ */ new Set([
1871
+ ...DECISION_CONTEXT,
1872
+ "\uB0B4\uAC00",
1873
+ "\uC6B0\uB9AC\uAC00",
1874
+ "\uC6B0\uB9AC",
1875
+ "\uB300\uD574",
1876
+ "\uAD00\uD574",
1877
+ "\uD558\uB294",
1878
+ "\uD588\uB358",
1879
+ "\uD560",
1880
+ "\uAC83",
1881
+ "\uBC18\uB4DC\uC2DC",
1882
+ "\uC911",
1883
+ "about",
1884
+ "on",
1885
+ "for",
1886
+ "to",
1887
+ "do",
1888
+ "does",
1889
+ "is",
1890
+ "the",
1891
+ "we",
1892
+ "i",
1893
+ "our",
1894
+ "my"
1895
+ ]);
1745
1896
  CURRENCY_RANK = {
1746
1897
  current: 0,
1747
1898
  expired: 1,
@@ -1984,18 +2135,21 @@ function detectConsolidation(entries, opts = {}) {
1984
2135
  scanned: 0,
1985
2136
  truncated: false
1986
2137
  };
1987
- const out = [
1988
- ...similarTitles(live, budget),
1989
- ...sharedContext(live, budget),
1990
- // Neither of these enumerates pairs — one walks declared edges, the other
1991
- // walks notes — so neither can be the thing that blows up, and neither
1992
- // spends the budget.
2138
+ const semanticExcluded = semanticGlobalExclusions(live, []);
2139
+ const semantic = opts.semantic === void 0 ? [] : boundedSemanticSimilarities(live, budget, opts.semantic, semanticExcluded);
2140
+ const titles = similarTitles(live, budget);
2141
+ const context = sharedContext(live, budget);
2142
+ const scored = [
2143
+ // Declared edges enumerate no pairs and spend no budget. Supersession is
2144
+ // first because it names both the winner and the still-active loser.
2145
+ ...declaredSupersessions(live),
1993
2146
  ...declaredContradictions(live),
1994
- ...kindPromotions(live)
2147
+ ...kindPromotions(live),
2148
+ ...semantic,
2149
+ ...context,
2150
+ ...titles
1995
2151
  ];
1996
- out.sort(
1997
- (a, b) => CONSOLIDATION_RULES.indexOf(a.rule) - CONSOLIDATION_RULES.indexOf(b.rule) || a.memory_id.localeCompare(b.memory_id) || (a.other_id ?? "").localeCompare(b.other_id ?? "")
1998
- );
2152
+ const out = rankCandidates(scored);
1999
2153
  return {
2000
2154
  candidates: out.slice(0, opts.limit ?? DEFAULT_LIMIT),
2001
2155
  scanned: budget.scanned,
@@ -2017,24 +2171,63 @@ function overlap(a, b) {
2017
2171
  }
2018
2172
  function similarTitles(entries, budget) {
2019
2173
  const termSets = entries.map((e) => titleTerms(e.memory.title));
2174
+ const cyclicKeys = entries.map((entry) => cyclicTitleKey(entry.memory.title));
2020
2175
  const buckets = /* @__PURE__ */ new Map();
2176
+ const byCyclicTitle = /* @__PURE__ */ new Map();
2021
2177
  termSets.forEach((set, i) => {
2178
+ const cyclicKey = cyclicKeys[i];
2179
+ if (cyclicKey !== null && cyclicKey !== void 0) addToBucket(byCyclicTitle, cyclicKey, i);
2022
2180
  for (const term of set) {
2023
2181
  const bucket = buckets.get(term);
2024
2182
  if (bucket) bucket.push(i);
2025
2183
  else buckets.set(term, [i]);
2026
2184
  }
2027
2185
  });
2028
- const out = [];
2186
+ const out = cyclicTitleSpanningCandidates(entries, byCyclicTitle);
2029
2187
  for (const [i, j] of pairsFrom(buckets, budget)) {
2030
2188
  const score = overlap(termSets[i], termSets[j]);
2031
2189
  if (score < TITLE_OVERLAP) continue;
2032
- out.push(
2033
- pairCandidate("similar_title", entries[i], entries[j], {
2190
+ out.push({
2191
+ candidate: pairCandidate("similar_title", entries[i], entries[j], {
2034
2192
  evidence: `titles overlap ${Math.round(score * 100)}%: "${entries[i].memory.title}" / "${entries[j].memory.title}"`,
2035
2193
  suggested_action: "supersede"
2036
- })
2194
+ }),
2195
+ // Exact lexical identity is a little stronger than the broad
2196
+ // scope+entity rule. This changes only ordering/evidence selection — the
2197
+ // same pair was already detected at the 2/3 threshold.
2198
+ confidence: score === 1 && cyclicKeys[i] !== null && cyclicKeys[i] === cyclicKeys[j] ? 2.01 : 1 + score
2199
+ });
2200
+ }
2201
+ return out;
2202
+ }
2203
+ function cyclicTitleKey(title) {
2204
+ const sequence = terms(title).map((term) => term.normalize("NFC").toLowerCase());
2205
+ if (sequence.length === 0) return null;
2206
+ if (sequence.length < 3) return sequence.join("\0");
2207
+ let best = null;
2208
+ for (let offset = 0; offset < sequence.length; offset += 1) {
2209
+ const key2 = [...sequence.slice(offset), ...sequence.slice(0, offset)].join("\0");
2210
+ if (best === null || key2 < best) best = key2;
2211
+ }
2212
+ return best;
2213
+ }
2214
+ function cyclicTitleSpanningCandidates(entries, buckets) {
2215
+ const out = [];
2216
+ for (const indices of buckets.values()) {
2217
+ if (indices.length < 2) continue;
2218
+ const ordered = [...indices].sort(
2219
+ (a, b) => entries[a].memory.id.localeCompare(entries[b].memory.id) || entries[a].path.localeCompare(entries[b].path)
2037
2220
  );
2221
+ const anchor = ordered[0];
2222
+ for (const other of ordered.slice(1)) {
2223
+ out.push({
2224
+ candidate: pairCandidate("similar_title", entries[anchor], entries[other], {
2225
+ evidence: `titles overlap 100%: "${entries[anchor].memory.title}" / "${entries[other].memory.title}"`,
2226
+ suggested_action: "supersede"
2227
+ }),
2228
+ confidence: 2.01
2229
+ });
2230
+ }
2038
2231
  }
2039
2232
  return out;
2040
2233
  }
@@ -2064,12 +2257,221 @@ function sharedContext(entries, budget) {
2064
2257
  }
2065
2258
  for (const [i, j] of pairsFrom(byEntity, budget)) {
2066
2259
  const shared = [...entitySets.get(i)].filter((e) => entitySets.get(j).has(e));
2067
- out.push(
2068
- pairCandidate("shared_context", entries[i], entries[j], {
2260
+ out.push({
2261
+ candidate: pairCandidate("shared_context", entries[i], entries[j], {
2069
2262
  evidence: `both in scope "${scope}" and both about: ${shared.sort().join(", ")}`,
2070
2263
  suggested_action: "supersede"
2071
- })
2072
- );
2264
+ }),
2265
+ confidence: 2
2266
+ });
2267
+ }
2268
+ }
2269
+ return out;
2270
+ }
2271
+ function boundedSemanticSimilarities(entries, budget, source, globalExcluded) {
2272
+ const semanticBudget = {
2273
+ left: Math.min(budget.left, SEMANTIC_PAIR_BUDGET),
2274
+ scanned: 0,
2275
+ truncated: false
2276
+ };
2277
+ const out = semanticSimilarities(entries, semanticBudget, source, globalExcluded);
2278
+ budget.left -= semanticBudget.scanned;
2279
+ budget.scanned += semanticBudget.scanned;
2280
+ budget.truncated ||= semanticBudget.truncated;
2281
+ return out;
2282
+ }
2283
+ function semanticSimilarities(entries, budget, source, globalExcluded) {
2284
+ const buckets = /* @__PURE__ */ new Map();
2285
+ const scopes = entries.map(declaredSemanticScope);
2286
+ const entities = entries.map(
2287
+ (entry) => new Set(entry.memory.entities.map((entity) => normalizeContext(entity)).filter(isPresent))
2288
+ );
2289
+ entries.forEach((entry, i) => {
2290
+ if (!source.has(entry.memory.id)) return;
2291
+ const scope = scopes[i];
2292
+ if (scope !== null) addToBucket(buckets, `scope:${scope}`, i);
2293
+ for (const entity of entities[i]) addToBucket(buckets, `entity:${entity}`, i);
2294
+ if (scope === null && entities[i].size === 0 && !globalExcluded.has(entry.memory.id)) {
2295
+ addToBucket(buckets, "global:unanchored", i);
2296
+ }
2297
+ });
2298
+ const observations = [];
2299
+ const byIndex = /* @__PURE__ */ new Map();
2300
+ for (const [i, j] of pairsFrom(buckets, budget)) {
2301
+ const sharedScope = scopes[i] !== null && scopes[i] === scopes[j] ? scopes[i] : null;
2302
+ const sharedEntities = [...entities[i]].filter((entity) => entities[j].has(entity)).sort();
2303
+ const contextual = sharedScope !== null || sharedEntities.length > 0;
2304
+ const raw = source.compare(entries[i].memory.id, entries[j].memory.id);
2305
+ if (raw === null || raw === void 0 || !Number.isFinite(raw)) continue;
2306
+ const score = Math.max(-1, Math.min(1, raw));
2307
+ const threshold = sharedScope !== null ? SAME_SCOPE_SEMANTIC_SIMILARITY : sharedEntities.length > 0 ? SHARED_ENTITY_SEMANTIC_SIMILARITY : GLOBAL_SEMANTIC_SIMILARITY;
2308
+ if (score < threshold) continue;
2309
+ const observation = {
2310
+ i,
2311
+ j,
2312
+ key: indexPairKey(i, j),
2313
+ score,
2314
+ context: !contextual ? "with no lexical context, corroborated by a three-note semantic cluster" : sharedScope === null ? `inside shared entity ${sharedEntities.join(", ")}` : `inside scope "${entries[i].memory.scope ?? sharedScope}"`,
2315
+ contextual
2316
+ };
2317
+ observations.push(observation);
2318
+ addObservation(byIndex, i, observation);
2319
+ addObservation(byIndex, j, observation);
2320
+ }
2321
+ if (budget.truncated) return [];
2322
+ const nearest = /* @__PURE__ */ new Map();
2323
+ for (const [i, adjacent] of byIndex) {
2324
+ const ranked = adjacent.sort((a, b) => b.score - a.score || a.key.localeCompare(b.key)).slice(0, RECIPROCAL_NEIGHBOURS);
2325
+ nearest.set(i, new Set(ranked.map((observation) => observation.key)));
2326
+ }
2327
+ const reciprocal = observations.filter(
2328
+ (observation) => nearest.get(observation.i)?.has(observation.key) === true && nearest.get(observation.j)?.has(observation.key) === true
2329
+ );
2330
+ const corroborated = triangleEdges(reciprocal);
2331
+ return reciprocal.filter((observation) => corroborated.has(observation.key)).map((observation) => ({
2332
+ candidate: pairCandidate(
2333
+ "semantic_similarity",
2334
+ entries[observation.i],
2335
+ entries[observation.j],
2336
+ {
2337
+ evidence: `indexed meaning is ${Math.round(observation.score * 100)}% similar ${observation.context} (reciprocal top-${RECIPROCAL_NEIGHBOURS}, three-note cluster)`,
2338
+ suggested_action: "supersede"
2339
+ }
2340
+ ),
2341
+ // Unanchored embedding evidence deliberately waits behind authored
2342
+ // context and strict lexical matches even after triangle corroboration.
2343
+ confidence: observation.contextual ? 3 + observation.score : 1.9
2344
+ }));
2345
+ }
2346
+ function triangleEdges(observations) {
2347
+ const adjacent = /* @__PURE__ */ new Map();
2348
+ for (const observation of observations) {
2349
+ addNeighbour(adjacent, observation.i, observation.j);
2350
+ addNeighbour(adjacent, observation.j, observation.i);
2351
+ }
2352
+ const out = /* @__PURE__ */ new Set();
2353
+ for (const observation of observations) {
2354
+ const left = adjacent.get(observation.i) ?? /* @__PURE__ */ new Set();
2355
+ const right = adjacent.get(observation.j) ?? /* @__PURE__ */ new Set();
2356
+ if ([...left].some((index) => index !== observation.j && right.has(index))) {
2357
+ out.add(observation.key);
2358
+ }
2359
+ }
2360
+ return out;
2361
+ }
2362
+ function addNeighbour(values, from, to) {
2363
+ const adjacent = values.get(from);
2364
+ if (adjacent) adjacent.add(to);
2365
+ else values.set(from, /* @__PURE__ */ new Set([to]));
2366
+ }
2367
+ function semanticGlobalExclusions(entries, titleCandidates) {
2368
+ const out = /* @__PURE__ */ new Set();
2369
+ const liveIds = new Set(entries.map((entry) => entry.memory.id));
2370
+ for (const entry of entries) {
2371
+ if (entry.relations.length === 0) continue;
2372
+ out.add(entry.memory.id);
2373
+ for (const relation of entry.relations) if (liveIds.has(relation.to)) out.add(relation.to);
2374
+ }
2375
+ for (const { candidate } of titleCandidates) {
2376
+ out.add(candidate.memory_id);
2377
+ if (candidate.other_id !== null) out.add(candidate.other_id);
2378
+ }
2379
+ return out;
2380
+ }
2381
+ function addObservation(byIndex, index, observation) {
2382
+ const values = byIndex.get(index);
2383
+ if (values) values.push(observation);
2384
+ else byIndex.set(index, [observation]);
2385
+ }
2386
+ function declaredSupersessions(entries) {
2387
+ const byId = new Map(entries.map((entry) => [entry.memory.id, entry]));
2388
+ const cyclicEdges = cyclicSupersessionEdges(entries, byId);
2389
+ const seen = /* @__PURE__ */ new Set();
2390
+ const out = [];
2391
+ for (const winner of entries) {
2392
+ for (const relation of winner.relations) {
2393
+ if (relation.type !== "supersedes") continue;
2394
+ const loser = byId.get(relation.to);
2395
+ if (!loser || loser.memory.id === winner.memory.id) continue;
2396
+ if (cyclicEdges.has(directedPairKey(winner.memory.id, loser.memory.id))) continue;
2397
+ const key2 = memoryPairKey(winner.memory.id, loser.memory.id);
2398
+ if (seen.has(key2)) continue;
2399
+ seen.add(key2);
2400
+ out.push({
2401
+ candidate: pairCandidate("declared_supersession", loser, winner, {
2402
+ evidence: `"${winner.memory.title}" already declares it supersedes "${loser.memory.title}", but the replaced note is still active`,
2403
+ suggested_action: "retire"
2404
+ }),
2405
+ confidence: 6
2406
+ });
2407
+ }
2408
+ }
2409
+ return out;
2410
+ }
2411
+ function cyclicSupersessionEdges(entries, byId) {
2412
+ const adjacent = /* @__PURE__ */ new Map();
2413
+ const reversed = /* @__PURE__ */ new Map();
2414
+ for (const id of byId.keys()) {
2415
+ adjacent.set(id, []);
2416
+ reversed.set(id, []);
2417
+ }
2418
+ for (const entry of entries) {
2419
+ for (const relation of entry.relations) {
2420
+ if (relation.type !== "supersedes" || relation.to === entry.memory.id) continue;
2421
+ if (!byId.has(relation.to)) continue;
2422
+ adjacent.get(entry.memory.id).push(relation.to);
2423
+ reversed.get(relation.to).push(entry.memory.id);
2424
+ }
2425
+ }
2426
+ const visited = /* @__PURE__ */ new Set();
2427
+ const finished = [];
2428
+ for (const start of byId.keys()) {
2429
+ if (visited.has(start)) continue;
2430
+ visited.add(start);
2431
+ const stack = [{ id: start, next: 0 }];
2432
+ while (stack.length > 0) {
2433
+ const frame = stack[stack.length - 1];
2434
+ const neighbours = adjacent.get(frame.id) ?? [];
2435
+ if (frame.next < neighbours.length) {
2436
+ const neighbour = neighbours[frame.next];
2437
+ frame.next += 1;
2438
+ if (!visited.has(neighbour)) {
2439
+ visited.add(neighbour);
2440
+ stack.push({ id: neighbour, next: 0 });
2441
+ }
2442
+ } else {
2443
+ finished.push(frame.id);
2444
+ stack.pop();
2445
+ }
2446
+ }
2447
+ }
2448
+ const componentById = /* @__PURE__ */ new Map();
2449
+ const componentSizes = /* @__PURE__ */ new Map();
2450
+ let component = 0;
2451
+ for (let index = finished.length - 1; index >= 0; index -= 1) {
2452
+ const start = finished[index];
2453
+ if (componentById.has(start)) continue;
2454
+ const stack = [start];
2455
+ componentById.set(start, component);
2456
+ let size = 0;
2457
+ while (stack.length > 0) {
2458
+ const id = stack.pop();
2459
+ size += 1;
2460
+ for (const neighbour of reversed.get(id) ?? []) {
2461
+ if (componentById.has(neighbour)) continue;
2462
+ componentById.set(neighbour, component);
2463
+ stack.push(neighbour);
2464
+ }
2465
+ }
2466
+ componentSizes.set(component, size);
2467
+ component += 1;
2468
+ }
2469
+ const out = /* @__PURE__ */ new Set();
2470
+ for (const [from, targets] of adjacent) {
2471
+ const fromComponent = componentById.get(from);
2472
+ if (fromComponent === void 0 || (componentSizes.get(fromComponent) ?? 0) < 2) continue;
2473
+ for (const to of targets) {
2474
+ if (componentById.get(to) === fromComponent) out.add(directedPairKey(from, to));
2073
2475
  }
2074
2476
  }
2075
2477
  return out;
@@ -2083,15 +2485,16 @@ function declaredContradictions(entries) {
2083
2485
  if (relation.type !== "contradicts") continue;
2084
2486
  const other = byId.get(relation.to);
2085
2487
  if (!other || other.memory.id === entry.memory.id) continue;
2086
- const key2 = [entry.memory.id, other.memory.id].sort().join("\0");
2488
+ const key2 = memoryPairKey(entry.memory.id, other.memory.id);
2087
2489
  if (seen.has(key2)) continue;
2088
2490
  seen.add(key2);
2089
- out.push(
2090
- pairCandidate("contradiction", entry, other, {
2491
+ out.push({
2492
+ candidate: pairCandidate("contradiction", entry, other, {
2091
2493
  evidence: `"${entry.memory.title}" declares it contradicts "${other.memory.title}" \u2014 recorded, never settled`,
2092
2494
  suggested_action: "supersede"
2093
- })
2094
- );
2495
+ }),
2496
+ confidence: 5
2497
+ });
2095
2498
  }
2096
2499
  }
2097
2500
  return out;
@@ -2102,20 +2505,88 @@ function kindPromotions(entries) {
2102
2505
  if (entry.memory.binding !== true) continue;
2103
2506
  if (entry.memory.kind === "decision") continue;
2104
2507
  out.push({
2105
- rule: "kind_promotion",
2106
- memory_id: entry.memory.id,
2107
- path: entry.path,
2108
- title: entry.memory.title,
2109
- other_id: null,
2110
- other_path: null,
2111
- other_title: null,
2112
- evidence: entry.memory.kind === void 0 ? "marked as binding but left unclassified \u2014 a memory that binds is a decision" : `marked as binding but classified as ${entry.memory.kind} \u2014 a memory that binds is a decision`,
2113
- suggested_action: "reclassify",
2114
- suggested_kind: "decision"
2508
+ candidate: {
2509
+ rule: "kind_promotion",
2510
+ memory_id: entry.memory.id,
2511
+ path: entry.path,
2512
+ title: entry.memory.title,
2513
+ other_id: null,
2514
+ other_path: null,
2515
+ other_title: null,
2516
+ evidence: entry.memory.kind === void 0 ? "marked as binding but left unclassified \u2014 a memory that binds is a decision" : `marked as binding but classified as ${entry.memory.kind} \u2014 a memory that binds is a decision`,
2517
+ suggested_action: "reclassify",
2518
+ suggested_kind: "decision"
2519
+ },
2520
+ confidence: 4
2115
2521
  });
2116
2522
  }
2117
2523
  return out;
2118
2524
  }
2525
+ function normalizeContext(value) {
2526
+ if (value === null) return null;
2527
+ const normalized = value.normalize("NFC").trim().toLowerCase();
2528
+ return normalized === "" ? null : normalized;
2529
+ }
2530
+ function declaredSemanticScope(entry) {
2531
+ const scope = normalizeContext(entry.memory.scope);
2532
+ if (scope === null) return null;
2533
+ if (entry.memory.provenance.scopeSource === "explicit") return scope;
2534
+ if (entry.memory.provenance.scopeSource === "path") return null;
2535
+ const parts = entry.path.split("/");
2536
+ const parent = parts.length >= 2 ? normalizeContext(parts[parts.length - 2] ?? null) : null;
2537
+ return scope === parent ? null : scope;
2538
+ }
2539
+ function isPresent(value) {
2540
+ return value !== null;
2541
+ }
2542
+ function addToBucket(buckets, key2, index) {
2543
+ const bucket = buckets.get(key2);
2544
+ if (bucket) bucket.push(index);
2545
+ else buckets.set(key2, [index]);
2546
+ }
2547
+ function memoryPairKey(left, right) {
2548
+ return left < right ? `${left}\0${right}` : `${right}\0${left}`;
2549
+ }
2550
+ function directedPairKey(from, to) {
2551
+ return `${from}\0${to}`;
2552
+ }
2553
+ function indexPairKey(left, right) {
2554
+ return left < right ? `${left}:${right}` : `${right}:${left}`;
2555
+ }
2556
+ function rankCandidates(scored) {
2557
+ const unique = /* @__PURE__ */ new Map();
2558
+ for (const item of scored) {
2559
+ const candidate = item.candidate;
2560
+ const key2 = candidate.other_id === null ? `single:${candidate.rule}:${candidate.memory_id}` : `pair:${memoryPairKey(candidate.memory_id, candidate.other_id)}`;
2561
+ const current = unique.get(key2);
2562
+ if (current === void 0 || compareScored(item, current) < 0) unique.set(key2, item);
2563
+ }
2564
+ const ordered = [...unique.values()].sort(compareScored);
2565
+ const tiers = /* @__PURE__ */ new Map();
2566
+ for (const item of ordered) addScored(tiers, Math.floor(item.confidence), item);
2567
+ const components = new CandidateComponents();
2568
+ const out = [];
2569
+ for (const tier of [...tiers.keys()].sort((a, b) => b - a)) {
2570
+ const useful = [];
2571
+ const cycles = [];
2572
+ for (const item of tiers.get(tier) ?? []) {
2573
+ const other = item.candidate.other_id;
2574
+ if (other === null || components.join(item.candidate.memory_id, other)) useful.push(item);
2575
+ else cycles.push(item);
2576
+ }
2577
+ for (const item of useful) out.push(item.candidate);
2578
+ for (const item of cycles) out.push(item.candidate);
2579
+ }
2580
+ return out;
2581
+ }
2582
+ function addScored(tiers, tier, item) {
2583
+ const values = tiers.get(tier);
2584
+ if (values) values.push(item);
2585
+ else tiers.set(tier, [item]);
2586
+ }
2587
+ function compareScored(a, b) {
2588
+ return b.confidence - a.confidence || CONSOLIDATION_RULES.indexOf(a.candidate.rule) - CONSOLIDATION_RULES.indexOf(b.candidate.rule) || a.candidate.memory_id.localeCompare(b.candidate.memory_id) || (a.candidate.other_id ?? "").localeCompare(b.candidate.other_id ?? "");
2589
+ }
2119
2590
  function pairCandidate(rule, a, b, rest) {
2120
2591
  return {
2121
2592
  rule,
@@ -2151,20 +2622,54 @@ function* pairsFrom(buckets, budget) {
2151
2622
  }
2152
2623
  }
2153
2624
  }
2154
- var CONSOLIDATION_RULES, DEFAULT_LIMIT, DEFAULT_PAIR_BUDGET, TITLE_OVERLAP;
2625
+ var CONSOLIDATION_RULES, DEFAULT_LIMIT, DEFAULT_PAIR_BUDGET, SEMANTIC_PAIR_BUDGET, TITLE_OVERLAP, SAME_SCOPE_SEMANTIC_SIMILARITY, SHARED_ENTITY_SEMANTIC_SIMILARITY, GLOBAL_SEMANTIC_SIMILARITY, RECIPROCAL_NEIGHBOURS, CandidateComponents;
2155
2626
  var init_detect = __esm({
2156
2627
  "../../packages/memory-engine/src/proposal/detect.ts"() {
2157
2628
  "use strict";
2158
2629
  init_search();
2159
2630
  CONSOLIDATION_RULES = [
2160
- "similar_title",
2161
- "shared_context",
2631
+ "declared_supersession",
2162
2632
  "contradiction",
2633
+ "semantic_similarity",
2634
+ "shared_context",
2635
+ "similar_title",
2163
2636
  "kind_promotion"
2164
2637
  ];
2165
2638
  DEFAULT_LIMIT = 50;
2166
2639
  DEFAULT_PAIR_BUDGET = 2e5;
2167
- TITLE_OVERLAP = 0.6;
2640
+ SEMANTIC_PAIR_BUDGET = 5e4;
2641
+ TITLE_OVERLAP = 2 / 3;
2642
+ SAME_SCOPE_SEMANTIC_SIMILARITY = 0.6;
2643
+ SHARED_ENTITY_SEMANTIC_SIMILARITY = 0.75;
2644
+ GLOBAL_SEMANTIC_SIMILARITY = 0.72;
2645
+ RECIPROCAL_NEIGHBOURS = 2;
2646
+ CandidateComponents = class {
2647
+ parent = /* @__PURE__ */ new Map();
2648
+ root(id) {
2649
+ const trail = [];
2650
+ let current = id;
2651
+ for (; ; ) {
2652
+ const parent = this.parent.get(current);
2653
+ if (parent === void 0) {
2654
+ this.parent.set(current, current);
2655
+ break;
2656
+ }
2657
+ if (parent === current) break;
2658
+ trail.push(current);
2659
+ current = parent;
2660
+ }
2661
+ for (const member of trail) this.parent.set(member, current);
2662
+ return current;
2663
+ }
2664
+ /** True when this edge connected two components rather than closing a cycle. */
2665
+ join(left, right) {
2666
+ const a = this.root(left);
2667
+ const b = this.root(right);
2668
+ if (a === b) return false;
2669
+ this.parent.set(b, a);
2670
+ return true;
2671
+ }
2672
+ };
2168
2673
  }
2169
2674
  });
2170
2675
 
@@ -2686,6 +3191,86 @@ function restoreUntouchedLines(original, rendered, changed) {
2686
3191
  return before.get(key2) ?? line;
2687
3192
  }).join("\n");
2688
3193
  }
3194
+ function yamlErrorLine(error, text) {
3195
+ const message = error instanceof Error ? error.message : String(error);
3196
+ let positioned = null;
3197
+ if (error && typeof error === "object") {
3198
+ const linePos = error.linePos;
3199
+ const line = linePos?.[0]?.line;
3200
+ if (typeof line === "number" && Number.isFinite(line) && line >= 1) positioned = line - 1;
3201
+ }
3202
+ const quote = /Missing closing (["'])quote/i.exec(message)?.[1];
3203
+ if (quote !== void 0) {
3204
+ const lines2 = text.split("\n");
3205
+ const end = positioned ?? lines2.length - 1;
3206
+ for (let i = 0; i <= end; i += 1) {
3207
+ const line = lines2[i] ?? "";
3208
+ const colon = line.indexOf(":");
3209
+ if (colon === -1) continue;
3210
+ const value = line.slice(colon + 1);
3211
+ let count = 0;
3212
+ for (let at = 0; at < value.length; at += 1) {
3213
+ if (value[at] !== quote) continue;
3214
+ let slashes = 0;
3215
+ for (let before = at - 1; before >= 0 && value[before] === "\\"; before -= 1) slashes += 1;
3216
+ if (slashes % 2 === 0) count += 1;
3217
+ }
3218
+ if (count % 2 === 1) return i;
3219
+ }
3220
+ }
3221
+ if (positioned !== null) return positioned;
3222
+ const namedLine = /\bat line (\d+)\b/i.exec(message)?.[1];
3223
+ if (namedLine !== void 0) return Math.max(0, Number(namedLine) - 1);
3224
+ const alias = /alias[^:]*:\s*([^\s]+)/i.exec(message)?.[1];
3225
+ if (alias !== void 0) {
3226
+ const found = text.split("\n").findIndex((line) => line.includes(`*${alias}`));
3227
+ if (found !== -1) return found;
3228
+ }
3229
+ return 0;
3230
+ }
3231
+ function yamlFieldAt(text, line, fallback) {
3232
+ const stack = [];
3233
+ const lines2 = text.split("\n");
3234
+ const end = Math.min(Math.max(line, 0), Math.max(lines2.length - 1, 0));
3235
+ for (let i = 0; i <= end; i += 1) {
3236
+ const match = ANY_INDENTED_KEY_RE.exec(lines2[i] ?? "");
3237
+ if (!match) continue;
3238
+ const whitespace = match[1] ?? "";
3239
+ const indent = whitespace.length;
3240
+ while ((stack.at(-1)?.indent ?? -1) >= indent) stack.pop();
3241
+ stack.push({ indent, key: (match[2] ?? "").trim() });
3242
+ }
3243
+ return stack.length > 0 ? stack.map(({ key: key2 }) => key2).join(".") : fallback;
3244
+ }
3245
+ function yamlFieldRefusal(text, error, fallback) {
3246
+ const message = error instanceof Error ? error.message : String(error);
3247
+ const localField = yamlFieldAt(text, yamlErrorLine(error, text), fallback);
3248
+ const field = fallback === NAMESPACE2 && localField !== NAMESPACE2 ? `${NAMESPACE2}.${localField}` : localField;
3249
+ const code = error && typeof error === "object" && typeof error.code === "string" ? error.code : "";
3250
+ let change;
3251
+ if (code === "TAB_AS_INDENT" || /tab/i.test(message)) {
3252
+ change = `Replace the tab indentation in ${field} with spaces`;
3253
+ } else if (/alias|anchor/i.test(message)) {
3254
+ change = `Replace the anchor or alias used by ${field} with its value inline`;
3255
+ } else if (code === "DUPLICATE_KEY" || /keys? must be unique/i.test(message)) {
3256
+ change = `Remove or rename the duplicate ${field} field`;
3257
+ } else {
3258
+ change = `Fix ${field} so it is valid YAML (close its quotes or brackets, and quote any value containing a colon)`;
3259
+ }
3260
+ return new PatchError(
3261
+ `the ${field} field could not be rewritten safely: ${message}`,
3262
+ `${change}, then try again. Nothing was written.`
3263
+ );
3264
+ }
3265
+ function assertReadableFrontmatter(frontmatter) {
3266
+ try {
3267
+ const doc = parseDocument(frontmatter);
3268
+ if (doc.errors[0]) throw doc.errors[0];
3269
+ doc.toJS();
3270
+ } catch (error) {
3271
+ throw yamlFieldRefusal(frontmatter, error, "frontmatter");
3272
+ }
3273
+ }
2689
3274
  function renderBlock(fields, existingText) {
2690
3275
  let body;
2691
3276
  if (existingText === null || existingText.trim() === "") {
@@ -2693,6 +3278,7 @@ function renderBlock(fields, existingText) {
2693
3278
  } else {
2694
3279
  try {
2695
3280
  const doc = parseDocument(existingText);
3281
+ if (doc.errors[0]) throw doc.errors[0];
2696
3282
  const changed = /* @__PURE__ */ new Set();
2697
3283
  for (const [key2, value] of Object.entries(fields)) {
2698
3284
  const current = doc.get(key2);
@@ -2706,14 +3292,21 @@ function renderBlock(fields, existingText) {
2706
3292
  }
2707
3293
  body = restoreUntouchedLines(
2708
3294
  existingText,
2709
- doc.toString({ lineWidth: 0 }).replace(/\n$/, ""),
3295
+ doc.toString({
3296
+ lineWidth: 0,
3297
+ // `yaml` turns a double-quoted scalar containing `\n` into three
3298
+ // physical lines once its JSON representation reaches 40 chars.
3299
+ // `restoreUntouchedLines` then sees the blank middle line, mistakes
3300
+ // the first one for a complete scalar, and restores it while
3301
+ // leaving the continuation behind. Keeping quoted newlines escaped
3302
+ // makes an untouched field one physical line on both sides, so the
3303
+ // byte-preserving restore and the YAML renderer agree on its span.
3304
+ doubleQuotedMinMultiLineLength: Number.POSITIVE_INFINITY
3305
+ }).replace(/\n$/, ""),
2710
3306
  changed
2711
3307
  );
2712
3308
  } catch (err) {
2713
- throw new PatchError(
2714
- `the memgineering block in this note could not be rewritten: ${err.message}`,
2715
- "Its YAML uses something this tool cannot rewrite safely \u2014 most often a tab indent, or an anchor/alias defined outside the block. Re-indent with spaces or inline the value, then try again. Nothing was written."
2716
- );
3309
+ throw yamlFieldRefusal(existingText, err, NAMESPACE2);
2717
3310
  }
2718
3311
  }
2719
3312
  return [`${NAMESPACE2}:`, ...body.split("\n").map((l) => l === "" ? l : ` ${l}`)];
@@ -2743,6 +3336,7 @@ ${block.join("\n")}
2743
3336
  };
2744
3337
  }
2745
3338
  const fenceBody = fenceMatch[1] ?? "";
3339
+ assertReadableFrontmatter(fenceBody);
2746
3340
  const fenceLines = splitLines(fenceBody);
2747
3341
  const span = findBlock(fenceLines);
2748
3342
  const existing = readNamespace(noteContent);
@@ -2908,7 +3502,7 @@ function reversePatch(content, patch) {
2908
3502
  }
2909
3503
  return null;
2910
3504
  }
2911
- var NAMESPACE2, FENCE_RE, LOOKS_FENCED_RE, PatchError, BLOCK_KEY_RE;
3505
+ var NAMESPACE2, FENCE_RE, LOOKS_FENCED_RE, PatchError, BLOCK_KEY_RE, ANY_INDENTED_KEY_RE;
2912
3506
  var init_patch = __esm({
2913
3507
  "../../packages/memory-engine/src/proposal/patch.ts"() {
2914
3508
  "use strict";
@@ -2926,6 +3520,7 @@ var init_patch = __esm({
2926
3520
  }
2927
3521
  };
2928
3522
  BLOCK_KEY_RE = /^([^\s#][^:]*?)\s*:/;
3523
+ ANY_INDENTED_KEY_RE = /^(\s*)([^\s#][^:]*?)\s*:/;
2929
3524
  }
2930
3525
  });
2931
3526
 
@@ -4460,11 +5055,13 @@ Nothing was indexed. This is what WOULD be stored:
4460
5055
  printHuman(
4461
5056
  c.gray(` ${oneLine(r.summary ?? "(no summary \u2014 title only)", WIDTH.text)}`)
4462
5057
  );
4463
- printHuman(c.gray(` from ${oneLine(r.path, WIDTH.path)}`));
5058
+ printHuman(c.gray(` from ${middleLine(r.path, WIDTH.path)}`));
4464
5059
  }
4465
5060
  printHuman(
4466
- c.gray(`
4467
- ${rows.length} note(s). Run without --dry-run to index them.`)
5061
+ c.gray(
5062
+ `
5063
+ ${rows.length} note(s). After the person approves this preview, run again with --yes to index them.`
5064
+ )
4468
5065
  );
4469
5066
  if (refusedWhilePreviewing.length > 0) {
4470
5067
  printHuman(
@@ -8350,6 +8947,9 @@ var cloudRules = (brain, args = {}) => call(brain, "POST", "/rules", {
8350
8947
  var cloudReindex = (brain, args = {}) => call(brain, "POST", "/reindex", {
8351
8948
  ...args.rebuild === true ? { rebuild: true } : {}
8352
8949
  });
8950
+ function findCloudBrain(brains, named) {
8951
+ return brains.find((brain) => brain.id === named) ?? brains.find((brain) => brain.name === named);
8952
+ }
8353
8953
  async function listCloudBrains(token, baseUrl, opts = {}) {
8354
8954
  const res = await apiRequest({
8355
8955
  method: "GET",
@@ -8529,13 +9129,13 @@ function isOpaqueId(id) {
8529
9129
  return id.length === OPAQUE_ID_LENGTH && /^[0-9ABCDEFGHJKMNPQRSTVWXYZ]+$/.test(id);
8530
9130
  }
8531
9131
  function displayHandle(id, allIds) {
8532
- if (!isOpaqueId(id)) return oneLine(id, WIDTH.handle);
9132
+ if (!isOpaqueId(id)) return oneLineWhole(id);
8533
9133
  for (let len = MIN_HANDLE_LENGTH; len < id.length; len++) {
8534
9134
  const prefix = id.slice(0, len);
8535
9135
  const matches = allIds.filter((other) => other.startsWith(prefix));
8536
9136
  if (matches.length === 1) return prefix;
8537
9137
  }
8538
- return oneLine(id, WIDTH.handle);
9138
+ return oneLineWhole(id);
8539
9139
  }
8540
9140
  function matchIds(ref, allIds) {
8541
9141
  const exact = allIds.filter((id) => id === ref);
@@ -8765,14 +9365,15 @@ init_config();
8765
9365
  init_ui();
8766
9366
  init_vault();
8767
9367
  async function resolveTarget(opts = {}) {
9368
+ if (opts.brain !== void 0) return namedCloudTarget({ ...opts, brain: opts.brain });
8768
9369
  if (opts.local !== true) {
8769
9370
  const cloud = await cloudTarget();
8770
9371
  if (cloud && cloud.kind === "cloud") {
8771
9372
  if (opts.vault !== void 0) {
8772
9373
  throw memgError(
8773
9374
  "invalid_input",
8774
- `\`--vault\` names a folder on this machine, and this machine reads the hosted brain \`${oneLine(cloud.name, 72)}\``,
8775
- `Nothing was read from \`${oneLine(opts.vault, 120)}\`, and nothing was changed.
9375
+ `\`--vault\` names a folder on this machine, and this machine reads the hosted brain \`${oneLine(cloud.name, WIDTH.title)}\``,
9376
+ `Nothing was read from \`${oneLine(opts.vault, WIDTH.path)}\`, and nothing was changed.
8776
9377
 
8777
9378
  To read that folder just this once: add \`--local\`
8778
9379
  To change which hosted brain this machine reads: \`memgineering use --brain <name>\`
@@ -8792,6 +9393,49 @@ brain silently reported another.`
8792
9393
  throw await accountAwareRefusal() ?? err;
8793
9394
  }
8794
9395
  }
9396
+ async function namedCloudTarget(opts) {
9397
+ const named = opts.brain;
9398
+ if (named.startsWith("--")) {
9399
+ throw memgError(
9400
+ "invalid_input",
9401
+ `\`--brain\` was given the flag \`${oneLine(named, WIDTH.title)}\` as its name`,
9402
+ "Nothing was read or changed. Put the hosted brain name or id immediately after `--brain`, then pass the other flag separately."
9403
+ );
9404
+ }
9405
+ const clashes = [
9406
+ opts.local === true ? "`--local`" : null,
9407
+ opts.vault !== void 0 ? "`--vault`" : null
9408
+ ].filter((value) => value !== null);
9409
+ if (clashes.length > 0) {
9410
+ throw memgError(
9411
+ "invalid_input",
9412
+ `\`--brain\` cannot be combined with ${clashes.join(" and ")}`,
9413
+ "`--brain <name>` reads an existing brain on the account. `--local` and `--vault <path>` read a folder on this machine. Nothing was read or changed; choose one target kind."
9414
+ );
9415
+ }
9416
+ const base = apiUrl();
9417
+ const credentials = await requireCredentials(base);
9418
+ const brains = await listCloudBrains(credentials.token, base);
9419
+ if (brains.length === 0) {
9420
+ throw memgError(
9421
+ "invalid_input",
9422
+ "this account has no hosted brains",
9423
+ "Nothing was read or changed. `--brain` only names an existing hosted brain; it never creates one."
9424
+ );
9425
+ }
9426
+ const found = findCloudBrain(brains, named);
9427
+ if (!found) {
9428
+ const available = brains.map((brain) => ` ${oneLine(brain.name, WIDTH.title)} (${oneLine(brain.id, WIDTH.handle)})`).join("\n");
9429
+ throw memgError(
9430
+ "invalid_input",
9431
+ `no hosted brain called "${oneLine(named, WIDTH.title)}"`,
9432
+ `Nothing was read or changed. This account has:
9433
+ ${available}
9434
+ Use one of those names or ids. No brain was created.`
9435
+ );
9436
+ }
9437
+ return { kind: "cloud", brainId: found.id, name: found.name, apiUrl: base };
9438
+ }
8795
9439
  var isNoVaultLinked = (err) => typeof err === "object" && err !== null && err.code === "no_vault_linked";
8796
9440
  async function accountAwareRefusal() {
8797
9441
  try {
@@ -8802,13 +9446,13 @@ async function accountAwareRefusal() {
8802
9446
  if (!cloud || cloud.api_url !== base) return null;
8803
9447
  return memgError(
8804
9448
  "not_signed_in",
8805
- `this machine is signed out of \`${oneLine(cloud.name, 72)}\``,
9449
+ `this machine is signed out of \`${oneLine(cloud.name, WIDTH.title)}\``,
8806
9450
  "Your notes are on that account \u2014 nothing is lost, and nothing here needs creating.\n\nSign back in: `memgineering login`\n\nDo NOT run `init` or `link` to fix this. They make a NEW empty brain on this\nmachine and leave the notes where they are."
8807
9451
  );
8808
9452
  }
8809
9453
  const brains = await listCloudBrains(credentials.token, base, { retries: 0 });
8810
9454
  if (brains.length === 0) return null;
8811
- const names = brains.map((b) => ` ${oneLine(b.name, 72)}`).join("\n");
9455
+ const names = brains.map((b) => ` ${oneLine(b.name, WIDTH.title)}`).join("\n");
8812
9456
  return memgError(
8813
9457
  "no_vault_linked",
8814
9458
  "this machine is not pointed at any brain yet",
@@ -9468,10 +10112,14 @@ async function assembleStage(adapter, entry, detail, sectionName) {
9468
10112
  init_vault();
9469
10113
  init_ui();
9470
10114
  function openCommand() {
9471
- return new Command4("open").description("open one memory, as deeply as you ask").argument("<ref>", "handle, id, path, or exact title").option("--vault <path>", "which brain to read").option("--local", "read the brain on this machine, even when signed in to a hosted one").option("--detail <level>", "card | summary | chunks | full (default: card)").option("--section <name>", "one section (implies --detail chunks)").action(
10115
+ return new Command4("open").description("open one memory, as deeply as you ask").argument("<ref>", "handle, id, path, or exact title").option("--vault <path>", "which brain to read").option("--local", "read the brain on this machine, even when signed in to a hosted one").option(
10116
+ "--brain <name>",
10117
+ "read from one existing hosted brain by name or id, without changing the pointer"
10118
+ ).option("--detail <level>", "card | summary | chunks | full (default: card)").option("--section <name>", "one section (implies --detail chunks)").action(
9472
10119
  async (ref, opts) => {
9473
10120
  const detail = parseDetail(opts.detail, opts.section ? "chunks" : "card");
9474
10121
  const target = await resolveTarget({
10122
+ ...opts.brain === void 0 ? {} : { brain: opts.brain },
9475
10123
  ...opts.local === void 0 ? {} : { local: opts.local },
9476
10124
  ...opts.vault === void 0 ? {} : { vault: opts.vault }
9477
10125
  });
@@ -10813,9 +11461,11 @@ function consolidateCommand() {
10813
11461
  [
10814
11462
  "",
10815
11463
  "What it looks for:",
10816
- " similar_title two notes titled with mostly the same words",
10817
- " shared_context two notes in one scope naming the same thing",
11464
+ " declared_supersession a replaced note that is still active",
10818
11465
  " contradiction a disagreement somebody declared and nobody settled",
11466
+ " semantic_similarity calibrated indexed meaning agrees reciprocally",
11467
+ " shared_context two notes in one scope naming the same thing",
11468
+ " similar_title two notes titled with mostly the same words",
10819
11469
  " kind_promotion a note marked as binding but filed as something other",
10820
11470
  " than a decision",
10821
11471
  "",
@@ -10844,7 +11494,7 @@ function consolidateCommand() {
10844
11494
  // Present on every answer, not only the empty one. A full list still
10845
11495
  // leaves the reader one step short — the candidates are evidence, and
10846
11496
  // the next command is the thing they cannot derive from evidence.
10847
- hint: truncated ? `This brain is large enough that detection stopped after ${report.scanned} pairs, so these are candidates rather than all of them. Narrow it \u2014 \`--vault\` a smaller brain, or run again after acting on these \u2014 and treat the list as a sample. Proposals are still written one at a time: \`memgineering propose <action> --memory <id> --claim "\u2026" --reason "\u2026" --excerpt "\u2026"\`.` : candidates.length ? 'None of these is a proposal yet. Decide what each pair means, then: `memgineering propose <action> --memory <id> --claim "\u2026" --reason "\u2026" --excerpt "\u2026"`. A duplicate is two proposals \u2014 supersede on the one that stays, retire on the one it replaces.' : "Nothing looked duplicated, contradicted, or misfiled. Detection reads titles, scope, entities and declared edges \u2014 a brain whose notes carry none of those has little for it to compare. `memgineering recall <topic>` is the way to find near-duplicates by hand."
11497
+ hint: truncated ? `This brain is large enough that detection stopped after ${report.scanned} pairs, so these are candidates rather than all of them. Narrow it \u2014 \`--vault\` a smaller brain, or run again after acting on these \u2014 and treat the list as a sample. Proposals are still written one at a time: \`memgineering propose <action> --memory <id> --claim "\u2026" --reason "\u2026" --excerpt "\u2026"\`.` : candidates.length ? 'None of these is a proposal yet. Decide what each pair means, then: `memgineering propose <action> --memory <id> --claim "\u2026" --reason "\u2026" --excerpt "\u2026"`. An inferred duplicate is two proposals \u2014 supersede the winner and retire the loser. `declared_supersession` already has the winner edge, so only the suggested loser retirement remains.' : "Nothing looked duplicated, contradicted, or misfiled. Detection reads titles, scope, entities and declared edges; a hosted brain also reuses a complete calibrated vector generation, with stricter corroboration when context is absent. `memgineering recall <topic>` remains the broader semantic search."
10848
11498
  },
10849
11499
  human: () => {
10850
11500
  if (candidates.length === 0) {
@@ -10871,7 +11521,7 @@ function consolidateCommand() {
10871
11521
  }
10872
11522
  printHuman(
10873
11523
  c.gray(
10874
- '\nNone of these is a proposal yet \u2014 nothing has been written.\nDecide what a pair means, then say so:\n memgineering propose <action> --memory <id> --claim "\u2026" --reason "\u2026" --excerpt "\u2026"\nA duplicate is two proposals: supersede on the one that stays, retire on the one\nit replaces.'
11524
+ '\nNone of these is a proposal yet \u2014 nothing has been written.\nDecide what a pair means, then say so:\n memgineering propose <action> --memory <id> --claim "\u2026" --reason "\u2026" --excerpt "\u2026"\nAn inferred duplicate is two proposals: supersede the winner and retire the loser.\nA declared_supersession already has its winner edge; only retire the named loser.'
10875
11525
  )
10876
11526
  );
10877
11527
  }
@@ -10886,7 +11536,7 @@ function noteLines(id, title, path) {
10886
11536
  // Backticked, as it was before the title moved onto the row above it. This
10887
11537
  // output is read by agents as markdown, and a path outside code formatting
10888
11538
  // is a path they have to guess the extent of.
10889
- c.gray(` \`${oneLine(path, WIDTH.path)}\``)
11539
+ c.gray(` \`${middleLine(path, WIDTH.path)}\``)
10890
11540
  ];
10891
11541
  }
10892
11542
  function renderCandidate(candidate) {
@@ -11532,8 +12182,12 @@ init_index_build();
11532
12182
  init_vault();
11533
12183
  init_ui();
11534
12184
  function evidenceCommand() {
11535
- return new Command10("evidence").description("how much a memory has been used, and why it last changed").argument("<ref>", "handle, id, path, or exact title").option("--vault <path>", "which brain to read").option("--local", "read the brain on this machine, even when signed in to a hosted one").action(async (ref, opts) => {
12185
+ return new Command10("evidence").description("how much a memory has been used, and why it last changed").argument("<ref>", "handle, id, path, or exact title").option("--vault <path>", "which brain to read").option("--local", "read the brain on this machine, even when signed in to a hosted one").option(
12186
+ "--brain <name>",
12187
+ "read from one existing hosted brain by name or id, without changing the pointer"
12188
+ ).action(async (ref, opts) => {
11536
12189
  const target = await resolveTarget({
12190
+ ...opts.brain === void 0 ? {} : { brain: opts.brain },
11537
12191
  ...opts.local === void 0 ? {} : { local: opts.local },
11538
12192
  ...opts.vault === void 0 ? {} : { vault: opts.vault }
11539
12193
  });
@@ -11542,7 +12196,11 @@ function evidenceCommand() {
11542
12196
  const report2 = await cloudEvidence(cloud, ref);
11543
12197
  printDual({
11544
12198
  json: { ...report2, brain: { hosted: true, name: target.name } },
11545
- human: () => renderEvidence(viewFromCloud(report2), { now: /* @__PURE__ */ new Date(), hosted: true })
12199
+ human: () => renderEvidence(viewFromCloud(report2), {
12200
+ now: /* @__PURE__ */ new Date(),
12201
+ hosted: true,
12202
+ brain: target.name
12203
+ })
11546
12204
  });
11547
12205
  return;
11548
12206
  }
@@ -11564,7 +12222,8 @@ function evidenceCommand() {
11564
12222
  });
11565
12223
  }
11566
12224
  function renderEvidence(report, opts) {
11567
- printHuman(`## evidence: ${oneLine(report.title, WIDTH.title)}
12225
+ const brainHeading = opts.brain === void 0 ? "" : ` \u2014 ${oneLine(opts.brain, WIDTH.title)}`;
12226
+ printHuman(`## evidence: ${oneLine(report.title, WIDTH.title)}${brainHeading}
11568
12227
  `);
11569
12228
  printHuman(
11570
12229
  `\`id: ${oneLine(report.id, WIDTH.handle)}\` \xB7 \`${oneLine(report.path, WIDTH.path)}\`
@@ -11987,8 +12646,12 @@ import { Command as Command12 } from "commander";
11987
12646
  init_vault();
11988
12647
  init_ui();
11989
12648
  function logCommand() {
11990
- return new Command12("log").description("what changed in this brain, and what can still be undone").option("--limit <n>", "how many operations", (v) => parseInt(v, 10), 20).option("--verbose", "include hashes and reverse patches").option("--vault <path>", "which brain to read").option("--local", "read the brain on this machine, even when signed in to a hosted one").action(async (opts) => {
12649
+ return new Command12("log").description("what changed in this brain, and what can still be undone").option("--limit <n>", "how many operations", (v) => parseInt(v, 10), 20).option("--verbose", "include hashes and reverse patches").option("--vault <path>", "which brain to read").option("--local", "read the brain on this machine, even when signed in to a hosted one").option(
12650
+ "--brain <name>",
12651
+ "read one existing hosted brain by name or id, without changing the pointer"
12652
+ ).action(async (opts) => {
11991
12653
  const target = await resolveTarget({
12654
+ ...opts.brain === void 0 ? {} : { brain: opts.brain },
11992
12655
  ...opts.local === void 0 ? {} : { local: opts.local },
11993
12656
  ...opts.vault === void 0 ? {} : { vault: opts.vault }
11994
12657
  });
@@ -12835,7 +13498,10 @@ init_errors();
12835
13498
  init_vault();
12836
13499
  init_ui();
12837
13500
  function recallCommand() {
12838
- return new Command18("recall").description("recall memory cards for a question").argument("<query>", "what you want to remember").option("--vault <path>", "which brain to search").option("--local", "search the brain on this machine, even when signed in to a hosted one").option("--scope <scope>", "restrict to one scope").option("--limit <n>", "max cards", (v) => parseInt(v, 10), 8).option(
13501
+ return new Command18("recall").description("recall memory cards for a question").argument("<query>", "what you want to remember").option("--vault <path>", "which brain to search").option("--local", "search the brain on this machine, even when signed in to a hosted one").option(
13502
+ "--brain <name>",
13503
+ "search one existing hosted brain by name or id, without changing the pointer"
13504
+ ).option("--scope <scope>", "restrict to one scope").option("--limit <n>", "max cards", (v) => parseInt(v, 10), 8).option(
12839
13505
  "--detail <level>",
12840
13506
  "title | card | summary | chunks | full (default: card) \u2014 a hosted brain answers title and card only; deeper needs a brain in a folder, or `open <handle>`"
12841
13507
  ).option("--depth <depth>", "exact | standard (default: standard, which also searches bodies)").option("--budget <tokens>", "max estimated size of the answer", (v) => parseInt(v, 10)).action(
@@ -12844,10 +13510,13 @@ function recallCommand() {
12844
13510
  const depth = parseDepth(opts.depth);
12845
13511
  const budget = parseBudget(opts.budget) ?? DEFAULT_TOKEN_BUDGET;
12846
13512
  const target = await resolveTarget({
13513
+ ...opts.brain === void 0 ? {} : { brain: opts.brain },
12847
13514
  ...opts.local === void 0 ? {} : { local: opts.local },
12848
13515
  ...opts.vault === void 0 ? {} : { vault: opts.vault }
12849
13516
  });
13517
+ const answeredBy = target.kind === "cloud" ? target.name : null;
12850
13518
  let rows;
13519
+ let unsearchedBrains = [];
12851
13520
  let isScaffolding;
12852
13521
  let reasons;
12853
13522
  let pathOf;
@@ -12875,6 +13544,9 @@ function recallCommand() {
12875
13544
  budget
12876
13545
  });
12877
13546
  rows = answered.memory_cards.map((card) => ({ card, handle: card.handle, stage: {} }));
13547
+ if (rows.length === 0) {
13548
+ unsearchedBrains = await listCloudBrains(cloud.token, cloud.apiUrl, { retries: 0 }).then((all) => all.filter((b) => b.id !== cloud.brainId)).catch(() => []);
13549
+ }
12878
13550
  isScaffolding = () => false;
12879
13551
  reasons = /* @__PURE__ */ new Map();
12880
13552
  pathOf = () => null;
@@ -12939,6 +13611,7 @@ function recallCommand() {
12939
13611
  json: {
12940
13612
  query,
12941
13613
  detail,
13614
+ ...answeredBy === null ? {} : { brain: { hosted: true, name: answeredBy } },
12942
13615
  // Echoed only when it was asked for. `detail` above is resolved
12943
13616
  // here because this CLI is what assembles the stages, but the depth
12944
13617
  // is the searching brain's to default — and on a hosted brain this
@@ -12971,6 +13644,7 @@ function recallCommand() {
12971
13644
  ...result.truncated === void 0 ? {} : { truncated: result.truncated }
12972
13645
  },
12973
13646
  human: () => {
13647
+ const brainHeading = answeredBy === null ? "" : ` \u2014 ${oneLine(answeredBy, WIDTH.title)}`;
12974
13648
  const reachedSomethingWritten = rows.some(({ card }) => !isScaffolding(card));
12975
13649
  const otherScript = reachedSomethingWritten ? null : entries ? unreachableScript(query, entries) : null;
12976
13650
  const sayWhy = () => {
@@ -12994,7 +13668,7 @@ function recallCommand() {
12994
13668
  );
12995
13669
  };
12996
13670
  if (rows.length === 0) {
12997
- printHuman(`## recall: ${query}
13671
+ printHuman(`## recall: ${query}${brainHeading}
12998
13672
  `);
12999
13673
  printHuman("_Nothing matched._\n");
13000
13674
  sayDegraded();
@@ -13002,14 +13676,38 @@ function recallCommand() {
13002
13676
  sayWhy();
13003
13677
  return;
13004
13678
  }
13679
+ if (unsearchedBrains.length > 0) {
13680
+ printHuman(
13681
+ c.yellow(
13682
+ `\u26A0 Only \`${oneLine(answeredBy ?? "", WIDTH.title)}\` was searched \u2014 this account has ${unsearchedBrains.length + 1} brains.`
13683
+ ) + c.gray(
13684
+ `
13685
+ ${unsearchedBrains.map(
13686
+ (brain) => ` memgineering recall ${JSON.stringify(query)} --brain ${JSON.stringify(brain.name)}`
13687
+ ).join(
13688
+ "\n"
13689
+ )}
13690
+ Safe to run \u2014 reading another brain does not change which one this machine points at.
13691
+ `
13692
+ )
13693
+ );
13694
+ }
13005
13695
  printHuman(
13006
13696
  c.gray(
13007
- depth === "exact" ? "`--depth exact` searches the card only \u2014 titles, aliases, entities, summaries and scope \u2014 try `--depth standard` to search note bodies too, or fewer words, or drop --scope." : "Titles, aliases, summaries and note bodies are all searched \u2014 try fewer or broader words, or drop --scope."
13697
+ // "Within this brain" is load-bearing when the warning above
13698
+ // has just said another brain went unsearched. The enumeration
13699
+ // reads as the whole surface — a recheck tester called this
13700
+ // the last line on screen and a false all-clear, sitting below
13701
+ // the one line that says otherwise.
13702
+ (depth === "exact" ? "Within this brain, `--depth exact` searches the card only \u2014 titles, aliases, entities, summaries and scope \u2014 try `--depth standard` to search note bodies too, or fewer words" : "Within this brain, titles, aliases, summaries and note bodies are all searched \u2014 try fewer or broader words") + // Only when the reader actually passed one. Telling somebody
13703
+ // to drop an argument they never supplied cost one tester a
13704
+ // whole command to `--help` to find out it was not their bug.
13705
+ (opts.scope ? ", or drop --scope." : ".")
13008
13706
  )
13009
13707
  );
13010
13708
  return;
13011
13709
  }
13012
- printHuman(`## recall: ${query} (${rows.length} cards)
13710
+ printHuman(`## recall: ${query}${brainHeading} (${rows.length} cards)
13013
13711
  `);
13014
13712
  sayDegraded();
13015
13713
  if (!reachedSomethingWritten) {
@@ -13025,7 +13723,22 @@ function recallCommand() {
13025
13723
  );
13026
13724
  for (const [i, { card, handle: handle2, stage }] of rows.entries()) {
13027
13725
  const mark = reachedSomethingWritten && isScaffolding(card);
13028
- printHuman(renderCard(i + 1, card, handle2, detail, stage, i === glossOn, mark));
13726
+ printHuman(
13727
+ renderCard(
13728
+ i + 1,
13729
+ card,
13730
+ handle2,
13731
+ detail,
13732
+ stage,
13733
+ i === glossOn,
13734
+ mark,
13735
+ // Only when `--brain` aimed this elsewhere. A recall of the
13736
+ // brain this machine already points at mints handles `open`
13737
+ // resolves by default, and repeating the name there would be
13738
+ // noise on the common path.
13739
+ opts.brain === void 0 ? void 0 : answeredBy ?? void 0
13740
+ )
13741
+ );
13029
13742
  }
13030
13743
  if (result.conflicts.length > 0) {
13031
13744
  printHuman(`### \u26A0 contradictions (${result.conflicts.length})
@@ -13130,7 +13843,7 @@ async function stageFor(adapter, entry, detail) {
13130
13843
  if (!entry) return {};
13131
13844
  return assembleStage(adapter, entry, detail);
13132
13845
  }
13133
- function renderCard(n, card, handle2, detail, stage, glossHere = false, scaffolding = false) {
13846
+ function renderCard(n, card, handle2, detail, stage, glossHere = false, scaffolding = false, qualifyWith) {
13134
13847
  const out = [];
13135
13848
  const standing = card.freshness === "current" ? "" : ` \xB7 ${card.freshness}`;
13136
13849
  const age = Number.isFinite(card.age_days) ? ` \xB7 ${ago2(Math.floor(card.age_days))}` : "";
@@ -13149,7 +13862,8 @@ function renderCard(n, card, handle2, detail, stage, glossHere = false, scaffold
13149
13862
  }
13150
13863
  if (detail !== "title") {
13151
13864
  if (card.summary) out.push("", quoteBlock(card.summary));
13152
- out.push("", `\`open: ${oneLine(handle2, WIDTH.handle)}\``);
13865
+ const openLine = qualifyWith === void 0 ? `open: ${oneLine(handle2, WIDTH.handle)}` : `memgineering open ${oneLine(handle2, WIDTH.handle)} --brain ${JSON.stringify(qualifyWith)}`;
13866
+ out.push("", `\`${openLine}\``);
13153
13867
  }
13154
13868
  if (stage.outline && stage.outline.length > 0) {
13155
13869
  out.push("", ...stage.outline.map((h) => `- ${h}`));
@@ -13414,6 +14128,9 @@ init_vault();
13414
14128
  init_ui();
13415
14129
  function resurfaceCommand() {
13416
14130
  return new Command20("resurface").description("what is worth having in view right now, without being asked").option("--context-dir <path>", "where the work is happening (default: cwd)").option("--limit <n>", "how many cards", (v) => parseInt(v, 10), 5).option("--vault <path>", "which brain").option("--local", "use the brain on this machine, even when signed in to a hosted one").option(
14131
+ "--brain <name>",
14132
+ "read one existing hosted brain by name or id, without changing the pointer"
14133
+ ).option(
13417
14134
  "--deadline <ms>",
13418
14135
  "give up and print nothing after this long",
13419
14136
  (v) => parseInt(v, 10)
@@ -13423,11 +14140,16 @@ function resurfaceCommand() {
13423
14140
  await bounded(async () => {
13424
14141
  const contextDir = opts.contextDir ?? process.cwd();
13425
14142
  const where = await resolveTarget({
14143
+ ...opts.brain === void 0 ? {} : { brain: opts.brain },
13426
14144
  ...opts.local === void 0 ? {} : { local: opts.local },
13427
14145
  ...opts.vault === void 0 ? {} : { vault: opts.vault }
13428
14146
  });
13429
14147
  if (where.kind === "cloud") {
13430
- await resurfaceHosted(where, opts.limit);
14148
+ await resurfaceHosted(
14149
+ where,
14150
+ opts.limit,
14151
+ opts.brain === void 0 ? void 0 : where.name
14152
+ );
13431
14153
  return;
13432
14154
  }
13433
14155
  const { brain, unreachableDefault } = await resolveBrain(opts.vault, contextDir);
@@ -13542,7 +14264,7 @@ ${quoteBlock(entry.memory.summary)}`);
13542
14264
  }
13543
14265
  );
13544
14266
  }
13545
- async function resurfaceHosted(where, limit) {
14267
+ async function resurfaceHosted(where, limit, qualifyWith) {
13546
14268
  const brain = await openCloudBrain(where);
13547
14269
  const result = await cloudResurface(brain, { limit });
13548
14270
  printDual({
@@ -13570,9 +14292,13 @@ async function resurfaceHosted(where, limit) {
13570
14292
  else if (card.why.length > 0) printHuman(oneLine(card.why.join(" \xB7 "), WIDTH.text));
13571
14293
  if (card.summary) printHuman(`
13572
14294
  ${quoteBlock(card.summary)}`);
13573
- printHuman(`
14295
+ printHuman(
14296
+ qualifyWith === void 0 ? `
13574
14297
  \`open: ${oneLine(card.handle, WIDTH.handle)}\`
13575
- `);
14298
+ ` : `
14299
+ \`memgineering open ${oneLine(card.handle, WIDTH.handle)} --brain ${JSON.stringify(qualifyWith)}\`
14300
+ `
14301
+ );
13576
14302
  }
13577
14303
  printBlock(
13578
14304
  c.gray(
@@ -13589,8 +14315,12 @@ exists only for a brain on this machine.`
13589
14315
  import { Command as Command21 } from "commander";
13590
14316
  init_ui();
13591
14317
  function rulesCommand() {
13592
- return new Command21("rules").description("the user\u2019s standing decisions \u2014 what binds you here").option("--vault <path>", "which brain").option("--local", "use the brain on this machine, even when signed in to a hosted one").action(async (opts) => {
14318
+ return new Command21("rules").description("the user\u2019s standing decisions \u2014 what binds you here").option("--vault <path>", "which brain").option("--local", "use the brain on this machine, even when signed in to a hosted one").option(
14319
+ "--brain <name>",
14320
+ "read one existing hosted brain by name or id, without changing the pointer"
14321
+ ).action(async (opts) => {
13593
14322
  const where = await resolveTarget({
14323
+ ...opts.brain === void 0 ? {} : { brain: opts.brain },
13594
14324
  ...opts.local === void 0 ? {} : { local: opts.local },
13595
14325
  ...opts.vault === void 0 ? {} : { vault: opts.vault }
13596
14326
  });
@@ -14252,6 +14982,7 @@ The text from before that change is still on this machine: ${join23(snapshotDir(
14252
14982
  init_errors();
14253
14983
  init_api_client();
14254
14984
  import { Command as Command25 } from "commander";
14985
+ import stringWidth from "string-width";
14255
14986
  init_config();
14256
14987
  init_ui();
14257
14988
  var NAME_WIDTH = 48;
@@ -14305,8 +15036,11 @@ function brainsCommand() {
14305
15036
  `## ${rows.length} brain${rows.length === 1 ? "" : "s"} on ${credentials.display_name === null ? "this account" : `${oneLine(credentials.display_name, NAME_WIDTH)}\u2019s account`}
14306
15037
  `
14307
15038
  );
14308
- for (const row of rows) {
14309
- const name = oneLine(row.name, NAME_WIDTH).padEnd(NAME_WIDTH);
15039
+ const shownNames = rows.map((row) => oneLine(row.name, NAME_WIDTH));
15040
+ const shownNameWidth = Math.max(...shownNames.map((name) => stringWidth(name)));
15041
+ for (const [index, row] of rows.entries()) {
15042
+ const shownName = shownNames[index] ?? "";
15043
+ const name = shownName + " ".repeat(shownNameWidth - stringWidth(shownName));
14310
15044
  const notes = row.note_count === null ? "" : `${String(row.note_count).padStart(6)} note(s)`;
14311
15045
  const made = row.created_at === null ? "" : ` made ${row.created_at.slice(0, 10)}`;
14312
15046
  printHuman(
@@ -14596,7 +15330,7 @@ async function pointAtHosted(named, opts, path) {
14596
15330
  "Nothing was changed. `memgineering push --brain <name>` creates one and fills it from a folder on this machine."
14597
15331
  );
14598
15332
  }
14599
- const found = brains.find((b) => b.id === named) ?? brains.find((b) => b.name === named);
15333
+ const found = findCloudBrain(brains, named);
14600
15334
  if (!found) {
14601
15335
  throw memgError(
14602
15336
  "invalid_input",