@inerrata-corporation/errata 2.0.22 → 2.0.23-dev.4682

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.
@@ -1235,10 +1235,10 @@ function mergeDefs(...defs) {
1235
1235
  function cloneDef(schema) {
1236
1236
  return mergeDefs(schema._zod.def);
1237
1237
  }
1238
- function getElementAtPath(obj, path) {
1239
- if (!path)
1238
+ function getElementAtPath(obj, path2) {
1239
+ if (!path2)
1240
1240
  return obj;
1241
- return path.reduce((acc, key) => acc?.[key], obj);
1241
+ return path2.reduce((acc, key) => acc?.[key], obj);
1242
1242
  }
1243
1243
  function promiseAllObject(promisesObj) {
1244
1244
  const keys = Object.keys(promisesObj);
@@ -1621,11 +1621,11 @@ function aborted(x, startIndex = 0) {
1621
1621
  }
1622
1622
  return false;
1623
1623
  }
1624
- function prefixIssues(path, issues) {
1624
+ function prefixIssues(path2, issues) {
1625
1625
  return issues.map((iss) => {
1626
1626
  var _a2;
1627
1627
  (_a2 = iss).path ?? (_a2.path = []);
1628
- iss.path.unshift(path);
1628
+ iss.path.unshift(path2);
1629
1629
  return iss;
1630
1630
  });
1631
1631
  }
@@ -1808,7 +1808,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
1808
1808
  }
1809
1809
  function treeifyError(error48, mapper = (issue2) => issue2.message) {
1810
1810
  const result = { errors: [] };
1811
- const processError = (error49, path = []) => {
1811
+ const processError = (error49, path2 = []) => {
1812
1812
  var _a2, _b;
1813
1813
  for (const issue2 of error49.issues) {
1814
1814
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -1818,7 +1818,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
1818
1818
  } else if (issue2.code === "invalid_element") {
1819
1819
  processError({ issues: issue2.issues }, issue2.path);
1820
1820
  } else {
1821
- const fullpath = [...path, ...issue2.path];
1821
+ const fullpath = [...path2, ...issue2.path];
1822
1822
  if (fullpath.length === 0) {
1823
1823
  result.errors.push(mapper(issue2));
1824
1824
  continue;
@@ -1850,8 +1850,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
1850
1850
  }
1851
1851
  function toDotPath(_path) {
1852
1852
  const segs = [];
1853
- const path = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1854
- for (const seg of path) {
1853
+ const path2 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1854
+ for (const seg of path2) {
1855
1855
  if (typeof seg === "number")
1856
1856
  segs.push(`[${seg}]`);
1857
1857
  else if (typeof seg === "symbol")
@@ -13828,13 +13828,13 @@ function resolveRef(ref, ctx) {
13828
13828
  if (!ref.startsWith("#")) {
13829
13829
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
13830
13830
  }
13831
- const path = ref.slice(1).split("/").filter(Boolean);
13832
- if (path.length === 0) {
13831
+ const path2 = ref.slice(1).split("/").filter(Boolean);
13832
+ if (path2.length === 0) {
13833
13833
  return ctx.rootSchema;
13834
13834
  }
13835
13835
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
13836
- if (path[0] === defsKey) {
13837
- const key = path[1];
13836
+ if (path2[0] === defsKey) {
13837
+ const key = path2[1];
13838
13838
  if (!key || !ctx.defs[key]) {
13839
13839
  throw new Error(`Reference not found: ${ref}`);
13840
13840
  }
@@ -14697,7 +14697,7 @@ var CastaliaIngestPayloadSchema = external_exports.object({
14697
14697
  });
14698
14698
 
14699
14699
  // ../../packages/shared/src/history-read.ts
14700
- var CLOUD_ATTR_DUPLICATE_KEYS = ["attrsJson", "embedding"];
14700
+ var CLOUD_ATTR_DUPLICATE_KEYS = ["attrsJson", "embedding", "structEmbedding", "structEmbeddedAt"];
14701
14701
 
14702
14702
  // ../../packages/shared/src/history-revision.ts
14703
14703
  var Sha = external_exports.string().regex(/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/);
@@ -14712,6 +14712,17 @@ var HistoryRevisionRequestSchema = external_exports.strictObject({
14712
14712
  dependencyContext: HistoryDependencyContextSchema.optional()
14713
14713
  })).max(50)
14714
14714
  });
14715
+ var HistoryCarryRequestSchema = external_exports.strictObject({
14716
+ fromCommit: Sha,
14717
+ atCommit: Sha,
14718
+ parents: external_exports.array(external_exports.strictObject({ commit: Sha, parents: external_exports.array(Sha).max(32) })).max(8192),
14719
+ /** Every symbol in a file the range changed, both sides; `null` = too many to name, treat all as changed. */
14720
+ changedSymbols: external_exports.array(external_exports.string().regex(/^sym_[a-f0-9]{8,64}$/)).max(5e3).nullable(),
14721
+ /** Manifests the range changed; `null` = a lockfile (or too many manifests) changed, treat all as changed. */
14722
+ changedManifests: external_exports.array(external_exports.string().regex(/^hmanifest_[a-f0-9]{32}$/)).max(512).nullable()
14723
+ });
14724
+ var HISTORY_REVIEW_BUDGET = 48e3;
14725
+ var HISTORY_REVIEW_POLICY = `reviewer-view-${HISTORY_REVIEW_BUDGET}`;
14715
14726
 
14716
14727
  // ../../packages/shared/src/edit-placeholder.ts
14717
14728
  var AUTO_MINT_PREFIX = "addressed by an edit to ";
@@ -14773,13 +14784,13 @@ function loadNodeSqlite() {
14773
14784
  }
14774
14785
  }
14775
14786
  var DatabaseSync = loadNodeSqlite().DatabaseSync;
14776
- function openDatabase(path, opts) {
14777
- const db = opts ? new DatabaseSync(path, opts) : new DatabaseSync(path);
14787
+ function openDatabase(path2, opts) {
14788
+ const db = opts ? new DatabaseSync(path2, opts) : new DatabaseSync(path2);
14778
14789
  try {
14779
14790
  db.exec("PRAGMA foreign_keys = ON");
14780
14791
  } catch {
14781
14792
  }
14782
- if (path !== ":memory:" && !opts?.readOnly) {
14793
+ if (path2 !== ":memory:" && !opts?.readOnly) {
14783
14794
  try {
14784
14795
  db.exec("PRAGMA journal_mode = WAL");
14785
14796
  db.exec("PRAGMA synchronous = NORMAL");
@@ -14788,20 +14799,20 @@ function openDatabase(path, opts) {
14788
14799
  } catch {
14789
14800
  }
14790
14801
  }
14791
- const wrapStmt = (stmt) => ({
14802
+ const wrapStmt = (stmt2) => ({
14792
14803
  run: (...params) => {
14793
- const r = stmt.run(...coerceParams(params));
14804
+ const r = stmt2.run(...coerceParams(params));
14794
14805
  return {
14795
14806
  changes: Number(r.changes),
14796
14807
  lastInsertRowid: Number(r.lastInsertRowid)
14797
14808
  };
14798
14809
  },
14799
14810
  get: (...params) => {
14800
- const r = stmt.get(...coerceParams(params));
14811
+ const r = stmt2.get(...coerceParams(params));
14801
14812
  return r;
14802
14813
  },
14803
14814
  all: (...params) => {
14804
- const r = stmt.all(...coerceParams(params));
14815
+ const r = stmt2.all(...coerceParams(params));
14805
14816
  return r;
14806
14817
  }
14807
14818
  });
@@ -14855,6 +14866,584 @@ function normalizeParam(v) {
14855
14866
  return v;
14856
14867
  }
14857
14868
 
14869
+ // ../../packages/local-shared/src/tag-grammar.ts
14870
+ var TAG_CANONICAL_ID_PREFIXES = ["pat", "sol", "drc", "dcause", "dfix", "dprob", "dvuln", "prob", "claim", "err"];
14871
+ var TAG_HANDLE_SOURCE = String.raw`\[([a-z0-9][\w-]{0,80})\]|((?:${TAG_CANONICAL_ID_PREFIXES.join("|")})_[0-9a-f]{6,})`;
14872
+ var TAG_THREAD_SOURCE = String.raw`#([a-z][\w-]{0,63})`;
14873
+ var TAG_PLACEHOLDER_HANDLES = /* @__PURE__ */ new Set([
14874
+ "handle",
14875
+ "its-handle",
14876
+ "another",
14877
+ "a-third",
14878
+ "h",
14879
+ "h1",
14880
+ "h2",
14881
+ "id",
14882
+ "its-id",
14883
+ "a",
14884
+ "b",
14885
+ "\u2026",
14886
+ "..."
14887
+ ]);
14888
+ var TAG_EXAMPLES = {
14889
+ prior: (ref) => `([${ref}])`,
14890
+ problem: () => `[!one line on what's wrong]`,
14891
+ vulnerability: () => `[!vuln: who can do what; observed basis @ path:line]`,
14892
+ suspectedVulnerability: () => `[?vuln: consequence; missing check]`,
14893
+ vulnerabilityThread: () => `[!#v vuln: finding here]`,
14894
+ fix: (ref) => `(fix:[${ref}])`,
14895
+ // CONSTRAINT — the native capture path for a DIFFUSE problem (a constraint/tension/
14896
+ // conflict that shaped a decision). Free text, attaches to the decision sentence the
14897
+ // agent is already writing. Full rationale in the constraint note below + prior-tags.ts.
14898
+ constraint: () => `(constraint: the limitation that forced it)`,
14899
+ // TRIAGE — routes the problem/symptom in scope to its cause. `(cause:[handle])`
14900
+ // links to a cited RootCause/Solution node; `(cause: free text)` mints a RootCause
14901
+ // from the text. Harvested in prior-tags.ts → engine.ts (see the triage note below).
14902
+ triage: (ref) => `(cause:[${ref}])`,
14903
+ // Unlike a bare cause citation, this handle names the Problem being explained.
14904
+ triageExplain: (ref) => `(cause:[${ref}] the mechanism)`,
14905
+ // The MINT form — free-text cause that mints a RootCause. Prime-time surfacing
14906
+ // (recallForFile) pastes this next to an open Problem that has no cause yet, so the
14907
+ // diagnostic moment is cued when the agent is looking at the problem. Kept here so
14908
+ // the grammar can't drift from parseInlineTags' `(cause: …)` acceptance.
14909
+ triageMint: () => `(cause: why it happens)`,
14910
+ // THREAD forms — the witness-declared symptom↔cause/fix binding. A `#id` on the
14911
+ // flag names the diagnosis thread; the same `#id` on a later verb binds to it
14912
+ // explicitly (across sentences AND turns), replacing proximity guessing. Adjacent
14913
+ // tags never need one — the id is for diagnoses that unfold over distance.
14914
+ problemThread: () => `[!#slug one line on what's wrong]`,
14915
+ causeThread: () => `(cause:#slug the mechanism)`,
14916
+ fixThread: (thread = "slug") => `(fix:#${thread} what you changed)`,
14917
+ // ATTEMPT arc — what's being tried and what didn't work. A failed attempt is a
14918
+ // witnessed NEGATIVE result: it saves the next agent the same dead end.
14919
+ tried: () => `(tried: the approach you're attempting)`,
14920
+ failed: () => `(failed: what didn't work and the dead end it rules out)`,
14921
+ // `(failed:[h] …)` on a shown SOLUTION — the refute-by-failure form the context render teaches.
14922
+ failedCite: (ref) => `(failed:[${ref}] what came back)`,
14923
+ // LINK verbs (AC epic) — bind the problem in scope to CANONICAL priors and
14924
+ // abstractions. Multi-target by design: single links are weak; ~3 links to 3
14925
+ // DIFFERENT relevant priors is the target shape (see linkBullet).
14926
+ instance: (ref) => `(instance:[${ref}],[another],[a-third])`,
14927
+ pattern: () => `(pattern: name the general shape)`,
14928
+ // DOMAIN — the abstract area a problem is about; the concept layer an
14929
+ // anchor-less problem clusters on. Mints/resolves a Domain by name.
14930
+ domain: () => `(domain: The Area)`,
14931
+ // PACKAGE/COMPONENT — agent-named public anchors (OM-agent-anchors): the
14932
+ // package a problem is ABOUT (even when not a dependency) and the
14933
+ // framework/product-level unit it concerns. Public spine anchors → the
14934
+ // knowledge can cross to the collective.
14935
+ package: () => `(package: the-package-name)`,
14936
+ component: () => `(component: The Component)`,
14937
+ aids: (ref) => `(aids:[${ref}])`,
14938
+ // SOLUTION-STRUCTURE (SS-structure) — taught in the link bullet; Solution→Solution.
14939
+ buildsOn: (ref) => `(builds-on:[${ref}])`,
14940
+ supersedes: (ref) => `(supersedes:[${ref}])`,
14941
+ alternative: (ref) => `(alternative:[${ref}])`,
14942
+ // VERDICT (VS-verdict-tags) — a judgment on a SHOWN prior, distinct from leaning on it.
14943
+ // `confirm` rides the corroborate channel (it held); `refute` the contradict channel
14944
+ // (it's wrong / didn't hold) and REQUIRES a reason. This is the inline producer the
14945
+ // evidence channels lacked — refute sat dead (contradictionPressure 0) until now, while
14946
+ // lean-on corroboration lived, because only corroboration had an inline path. Grammar
14947
+ // single-sourced with parseInlineTags' CONFIRM_PREFIX/REFUTE_PREFIX.
14948
+ confirm: (ref) => `(confirm:[${ref}])`,
14949
+ refute: (ref) => `(refute:[${ref}] one line why)`,
14950
+ // RETRACT — "not a real problem": the errata-resolved block with a disposition
14951
+ // (parseDesignResolutions, turn.ts). No inline form exists; the block is the grammar.
14952
+ retract: (id) => ["```errata-resolved", `id: ${id}`, "disposition: false_positive", "reason: why it was never a real problem", "```"].join("\n")
14953
+ };
14954
+ var P = "Problem";
14955
+ var S = "Solution";
14956
+ var RC = "RootCause";
14957
+ var CITE_LOWERING = [
14958
+ { target: RC, writes: [{ from: P, edge: "CAUSED_BY", to: RC }] },
14959
+ { target: S, writes: [{ from: P, edge: "SOLVED_BY", to: S }] },
14960
+ { target: "Pattern", writes: [{ from: P, edge: "INSTANCE_OF", to: "Pattern" }, { from: "Pattern", edge: "ANCHORED_AT", to: "File" }], note: "cite-anchor promotion after 2 sessions", decision: "d", knownViolation: { edge: "ANCHORED_AT", finding: "motif cite-anchor promotion writes a refused ANCHORED_AT" } },
14961
+ { target: "Package", writes: [{ from: P, edge: "DEPENDS_ON", to: "Package" }] },
14962
+ { target: "Language", writes: [{ from: P, edge: "OCCURS_IN", to: "Language" }] },
14963
+ { target: "Tool", writes: [{ from: P, edge: "CONCERNS", to: "Tool" }] },
14964
+ { target: "Component", writes: [{ from: P, edge: "CONCERNS", to: "Component" }] },
14965
+ { target: P, writes: [{ from: P, edge: "RELATES_TO", to: P }, { from: P, edge: "ANCHORED_AT", to: "File" }], note: "corroboration witness + cite-anchor promotion" },
14966
+ { target: "AntiPattern", writes: [{ from: P, edge: "RELATES_TO", to: "AntiPattern" }, { from: "AntiPattern", edge: "ANCHORED_AT", to: "File" }], decision: "e", knownViolation: { edge: "ANCHORED_AT", finding: "motif cite-anchor promotion writes a refused ANCHORED_AT" } },
14967
+ { target: "Technique", writes: [{ from: P, edge: "RELATES_TO", to: "Technique" }, { from: "Technique", edge: "ANCHORED_AT", to: "File" }], decision: "e", knownViolation: { edge: "ANCHORED_AT", finding: "motif cite-anchor promotion writes a refused ANCHORED_AT" } },
14968
+ { target: "Weakness", writes: [{ from: P, edge: "RELATES_TO", to: "Weakness" }], decision: "e" },
14969
+ { target: "Domain", writes: [{ from: P, edge: "RELATES_TO", to: "Domain" }], decision: "e" },
14970
+ { target: "Claim", writes: [{ from: P, edge: "RELATES_TO", to: "Claim" }, { from: "Claim", edge: "ANCHORED_AT", to: "File" }] }
14971
+ ];
14972
+ var INSTANCE_LOWERING = [
14973
+ { target: "Pattern", writes: [{ from: P, edge: "INSTANCE_OF", to: "Pattern" }] },
14974
+ { target: P, writes: [{ from: P, edge: "RELATES_TO", to: P }] },
14975
+ { target: S, writes: [{ from: P, edge: "SOLVED_BY", to: S }], note: "an instance cite on a Solution claims the problem is solved by it", decision: "a" },
14976
+ { target: RC, writes: [{ from: P, edge: "CAUSED_BY", to: RC }] },
14977
+ { target: "AntiPattern", writes: [{ from: P, edge: "RELATES_TO", to: "AntiPattern" }], decision: "e" },
14978
+ { target: "Technique", writes: [{ from: P, edge: "RELATES_TO", to: "Technique" }], decision: "e" },
14979
+ { target: "Weakness", writes: [{ from: P, edge: "RELATES_TO", to: "Weakness" }], decision: "e" },
14980
+ { target: "Domain", writes: [{ from: P, edge: "RELATES_TO", to: "Domain" }], decision: "e" },
14981
+ { target: "Package", writes: [{ from: P, edge: "DEPENDS_ON", to: "Package" }] },
14982
+ { target: "Component", writes: [{ from: P, edge: "CONCERNS", to: "Component" }] }
14983
+ ];
14984
+ var TRIAGE_TO_RC = [
14985
+ { from: P, edge: "TRIAGED_BY", to: "Triage" },
14986
+ { from: "Triage", edge: "INDICATES", to: RC },
14987
+ { from: P, edge: "CAUSED_BY", to: RC }
14988
+ ];
14989
+ var TAG_VERBS = [
14990
+ {
14991
+ verb: "cite",
14992
+ form: "citation",
14993
+ opener: "(",
14994
+ kinds: ["prior"],
14995
+ args: "handles",
14996
+ thread: false,
14997
+ handleRequired: true,
14998
+ lowering: CITE_LOWERING,
14999
+ examples: ["prior"],
15000
+ hint: "cite a shown prior you leaned on: ([its-handle]) \u2014 handles only, no prose inside the parens",
15001
+ taught: "taught"
15002
+ },
15003
+ {
15004
+ verb: "!",
15005
+ form: "flag",
15006
+ opener: "[!",
15007
+ kinds: ["problem", "triage", "vulnerability", "fix", "prior"],
15008
+ args: "flag-body",
15009
+ thread: true,
15010
+ minText: 8,
15011
+ qualifiers: [
15012
+ { name: "vuln", taught: true, note: "[!vuln: \u2026] \u2192 Vulnerability (asserted)" },
15013
+ { name: "fixed", taught: false, note: "[!fixed: X] \u2192 read as a self-found fix" },
15014
+ { name: "resolved", taught: false, note: "[!resolved: X] \u2192 read as a self-found fix" }
15015
+ ],
15016
+ lowering: [
15017
+ { target: P, writes: [], note: "mints a Problem; `@ path[:line]` anchors it (ANCHORED_AT to code)" },
15018
+ { target: RC, writes: [...TRIAGE_TO_RC, { from: RC, edge: "CAUSED_BY", to: RC }], note: "`<-` segments: first \u2192 triage route, later \u2192 RootCause chain" },
15019
+ { target: "Vulnerability", writes: [], note: "[!vuln: \u2026]; `<-` causes stay on the finding (vulnerabilityCauses)" }
15020
+ ],
15021
+ examples: ["problem", "problemThread", "vulnerability", "vulnerabilityThread"],
15022
+ hint: "flag a problem on one line: [!what's wrong @ path:42]; fold causes with <- ; a fix goes in (fix: \u2026), not [!fixed: \u2026]",
15023
+ taught: "taught"
15024
+ },
15025
+ {
15026
+ verb: "?",
15027
+ form: "flag",
15028
+ opener: "[?",
15029
+ kinds: ["todo", "vulnerability"],
15030
+ args: "flag-body",
15031
+ thread: true,
15032
+ minText: 8,
15033
+ qualifiers: [{ name: "vuln", taught: true, note: "[?vuln: \u2026] \u2192 Vulnerability (suspected)" }],
15034
+ lowering: [{ target: P, writes: [], note: "mints a todo Problem; a fix can close it today", decision: "g" }],
15035
+ examples: ["suspectedVulnerability"],
15036
+ hint: "[?\u2026] marks an open question / todo; [?vuln: consequence; missing check] a suspected vulnerability",
15037
+ taught: "untaught",
15038
+ taughtBy: "only the [?vuln:] form is taught; the bare [?todo] form is not",
15039
+ decisions: ["g"]
15040
+ },
15041
+ {
15042
+ verb: "fix",
15043
+ form: "verb",
15044
+ opener: "(fix:",
15045
+ kinds: ["fix"],
15046
+ args: "handles-text",
15047
+ thread: true,
15048
+ minText: 12,
15049
+ lowering: [
15050
+ { target: P, writes: [{ from: P, edge: "SOLVED_BY", to: S }], note: "closes the Problem; a note \u226512 chars mints the Solution" },
15051
+ { target: "Vulnerability", writes: [{ from: "Vulnerability", edge: "SOLVED_BY", to: S }] },
15052
+ { target: RC, writes: [], note: "dropped: 'fix tag went nowhere' (not a Problem); no tag writes FIXED_BY", decision: "f" }
15053
+ ],
15054
+ examples: ["fix", "fixThread"],
15055
+ hint: "one fix per tag: (fix:[its-handle]) or (fix:#thread what you changed) \u2014 put the cause in its own (cause: \u2026)",
15056
+ taught: "taught",
15057
+ decisions: ["f"]
15058
+ },
15059
+ {
15060
+ verb: "cause",
15061
+ form: "verb",
15062
+ opener: "(cause:",
15063
+ kinds: ["triage"],
15064
+ args: "handles-text",
15065
+ thread: true,
15066
+ minText: 8,
15067
+ lowering: [
15068
+ { target: RC, writes: TRIAGE_TO_RC },
15069
+ { target: P, writes: [{ from: P, edge: "TRIAGED_BY", to: "Triage" }, { from: "Triage", edge: "INDICATES", to: RC }, { from: P, edge: "CAUSED_BY", to: RC }], note: "(cause:[problem] mechanism) \u2014 the cited Problem is the symptom; the mechanism mints the RootCause" },
15070
+ { target: S, writes: [{ from: P, edge: "TRIAGED_BY", to: "Triage" }, { from: "Triage", edge: "INDICATES", to: S }], decision: "b", knownViolation: { edge: "INDICATES", finding: "(cause:[solution]) writes a refused INDICATES \u2192 Solution" } }
15071
+ ],
15072
+ examples: ["triage", "triageExplain", "triageMint", "causeThread"],
15073
+ hint: "(cause: the mechanism you'd change) or (cause:[root-cause-handle]); bind a distant symptom with (cause:#thread \u2026)",
15074
+ taught: "taught",
15075
+ decisions: ["b"]
15076
+ },
15077
+ {
15078
+ verb: "constraint",
15079
+ form: "verb",
15080
+ opener: "(constraint:",
15081
+ kinds: ["constraint"],
15082
+ args: "text",
15083
+ thread: false,
15084
+ minText: 8,
15085
+ textRequired: true,
15086
+ lowering: [{ target: P, writes: [], note: "mints a constraint-kind Problem" }],
15087
+ examples: ["constraint"],
15088
+ hint: "(constraint: the limitation that forced the decision)",
15089
+ taught: "taught"
15090
+ },
15091
+ {
15092
+ verb: "tried",
15093
+ form: "verb",
15094
+ opener: "(tried:",
15095
+ kinds: ["attempt"],
15096
+ args: "text",
15097
+ thread: true,
15098
+ minText: 8,
15099
+ textRequired: true,
15100
+ lowering: [{ target: P, writes: [], note: "journals an attempt on the bound problem" }],
15101
+ examples: ["tried"],
15102
+ hint: "(tried: the approach) \u2014 bind with (tried:#thread \u2026)",
15103
+ taught: "taught"
15104
+ },
15105
+ {
15106
+ verb: "failed",
15107
+ form: "verb",
15108
+ opener: "(failed:",
15109
+ kinds: ["failure"],
15110
+ args: "handles-text",
15111
+ thread: true,
15112
+ minText: 8,
15113
+ textRequired: true,
15114
+ lowering: [
15115
+ { target: P, writes: [], note: "journals a failed attempt on the bound problem" },
15116
+ { target: S, writes: [], note: "(failed:[h]) refutes the cited prior (contradictionPressure); skips the label gate today", decision: "j" }
15117
+ ],
15118
+ examples: ["failed", "failedCite"],
15119
+ hint: "(failed: what didn't work); (failed:[solution] what came back) refutes a shown fix",
15120
+ taught: "taught",
15121
+ decisions: ["j"]
15122
+ },
15123
+ {
15124
+ verb: "instance",
15125
+ form: "verb",
15126
+ opener: "(instance:",
15127
+ kinds: ["instance"],
15128
+ args: "handles",
15129
+ thread: false,
15130
+ handleRequired: true,
15131
+ lowering: INSTANCE_LOWERING,
15132
+ examples: ["instance"],
15133
+ hint: "(instance:[h1],[h2],[h3]) \u2014 ~3 DIFFERENT shown priors this problem instantiates; handles only",
15134
+ taught: "taught",
15135
+ decisions: ["a", "e"]
15136
+ },
15137
+ {
15138
+ verb: "pattern",
15139
+ form: "verb",
15140
+ opener: "(pattern:",
15141
+ kinds: ["pattern"],
15142
+ args: "handle-or-text",
15143
+ thread: false,
15144
+ minText: 3,
15145
+ lowering: [{ target: "Pattern", writes: [{ from: P, edge: "INSTANCE_OF", to: "Pattern" }], note: "a handle must be a Pattern; free text resolves-or-mints one" }],
15146
+ examples: ["pattern"],
15147
+ hint: "(pattern: name the general shape) or (pattern:[shown-pattern])",
15148
+ taught: "taught"
15149
+ },
15150
+ {
15151
+ verb: "domain",
15152
+ form: "verb",
15153
+ opener: "(domain:",
15154
+ kinds: ["domain"],
15155
+ args: "text",
15156
+ thread: false,
15157
+ minText: 2,
15158
+ textRequired: true,
15159
+ lowering: [{ target: "Domain", writes: [{ from: P, edge: "PERTAIN_TO", to: "Domain" }], note: '(domain:#t X) mints a Domain named "#t X" today', decision: "k" }],
15160
+ examples: ["domain"],
15161
+ hint: "(domain: The Area) \u2014 Title Case, no thread",
15162
+ taught: "taught",
15163
+ decisions: ["k"]
15164
+ },
15165
+ {
15166
+ verb: "package",
15167
+ form: "verb",
15168
+ opener: "(package:",
15169
+ kinds: ["package"],
15170
+ args: "text",
15171
+ thread: false,
15172
+ minText: 2,
15173
+ textRequired: true,
15174
+ lowering: [{ target: "Package", writes: [{ from: P, edge: "DEPENDS_ON", to: "Package" }], note: "means 'concerns', written as DEPENDS_ON", decision: "c" }],
15175
+ examples: ["package"],
15176
+ hint: "(package: the-package-name) \u2014 the public package it's about",
15177
+ taught: "taught",
15178
+ decisions: ["c"]
15179
+ },
15180
+ {
15181
+ verb: "component",
15182
+ form: "verb",
15183
+ opener: "(component:",
15184
+ kinds: ["component"],
15185
+ args: "text",
15186
+ thread: false,
15187
+ minText: 2,
15188
+ textRequired: true,
15189
+ lowering: [{ target: "Component", writes: [{ from: P, edge: "CONCERNS", to: "Component" }] }],
15190
+ examples: ["component"],
15191
+ hint: "(component: The Component) \u2014 the framework/product unit",
15192
+ taught: "taught"
15193
+ },
15194
+ {
15195
+ verb: "aids",
15196
+ form: "verb",
15197
+ opener: "(aids:",
15198
+ kinds: ["transfer"],
15199
+ args: "handles",
15200
+ thread: true,
15201
+ handleRequired: true,
15202
+ lowering: [{ target: P, writes: [{ from: S, edge: "MAY_RESOLVE", to: P }], note: "source = the Solution in scope; non-Problem targets are skipped" }],
15203
+ examples: ["aids"],
15204
+ hint: "(aids:[problem-handle],\u2026) \u2014 other shown problems your fix might also resolve",
15205
+ taught: "taught"
15206
+ },
15207
+ {
15208
+ verb: "builds-on",
15209
+ form: "verb",
15210
+ opener: "(builds-on:",
15211
+ kinds: ["builds-on"],
15212
+ args: "handles",
15213
+ thread: true,
15214
+ handleRequired: true,
15215
+ lowering: [{ target: S, writes: [{ from: S, edge: "BUILDS_ON", to: S }] }],
15216
+ examples: ["buildsOn"],
15217
+ hint: "(builds-on:[shown-solution]) \u2014 your fix layers on it",
15218
+ taught: "taught",
15219
+ taughtBy: "linkBullet instruction text"
15220
+ },
15221
+ {
15222
+ verb: "supersedes",
15223
+ form: "verb",
15224
+ opener: "(supersedes:",
15225
+ kinds: ["supersedes"],
15226
+ args: "handles",
15227
+ thread: true,
15228
+ handleRequired: true,
15229
+ lowering: [{ target: S, writes: [{ from: S, edge: "SUPERSEDES", to: S }] }],
15230
+ examples: ["supersedes"],
15231
+ hint: "(supersedes:[shown-solution]) \u2014 your fix replaces it",
15232
+ taught: "taught",
15233
+ taughtBy: "linkBullet instruction text"
15234
+ },
15235
+ {
15236
+ verb: "alternative",
15237
+ form: "verb",
15238
+ opener: "(alternative:",
15239
+ kinds: ["alternative"],
15240
+ args: "handles",
15241
+ thread: true,
15242
+ handleRequired: true,
15243
+ lowering: [{ target: S, writes: [{ from: S, edge: "ALTERNATIVE_TO", to: S }] }],
15244
+ examples: ["alternative"],
15245
+ hint: "(alternative:[shown-solution]) \u2014 a different valid approach",
15246
+ taught: "taught",
15247
+ taughtBy: "linkBullet instruction text"
15248
+ },
15249
+ {
15250
+ verb: "confirm",
15251
+ form: "verb",
15252
+ opener: "(confirm:",
15253
+ kinds: ["confirm"],
15254
+ args: "handles",
15255
+ thread: false,
15256
+ handleRequired: true,
15257
+ lowering: [{ target: S, writes: [], note: "corroboration witness (corroborationCount), no edge" }],
15258
+ examples: ["confirm"],
15259
+ hint: "(confirm:[shown-prior]) \u2014 it held",
15260
+ taught: "taught",
15261
+ taughtBy: "verdictBullet + context render",
15262
+ decisions: ["j"]
15263
+ },
15264
+ {
15265
+ verb: "refute",
15266
+ form: "verb",
15267
+ opener: "(refute:",
15268
+ kinds: ["refute"],
15269
+ args: "handles-text",
15270
+ thread: false,
15271
+ minText: 4,
15272
+ handleRequired: true,
15273
+ textRequired: true,
15274
+ lowering: [{ target: S, writes: [], note: "contradiction witness (contradictionPressure), no edge; reason stays local" }],
15275
+ examples: ["refute"],
15276
+ hint: "(refute:[shown-prior] one line why) \u2014 the reason is required",
15277
+ taught: "taught",
15278
+ taughtBy: "verdictBullet + context render",
15279
+ decisions: ["j"]
15280
+ }
15281
+ ];
15282
+ var TAG_FENCES = [
15283
+ {
15284
+ fence: "errata-problem",
15285
+ fields: ["problem", "cause", "fix", "anchor", "kind"],
15286
+ required: [["problem"]],
15287
+ taught: "untaught",
15288
+ parser: "apps/daemon/src/turn.ts parseDesignFlags",
15289
+ decisions: ["h"],
15290
+ hint: "use an inline [!\u2026] flag instead (errata-problem cause ids never meet inline ids)"
15291
+ },
15292
+ {
15293
+ fence: "errata-resolved",
15294
+ fields: ["id", "problem", "fix", "disposition", "reason"],
15295
+ required: [["id", "problem"]],
15296
+ taught: "taught",
15297
+ taughtBy: "retract form only (TAG_EXAMPLE.retract); the fix form is untaught",
15298
+ parser: "apps/daemon/src/turn.ts parseDesignResolutions",
15299
+ hint: "```errata-resolved with id: and disposition: false_positive | invalid, reason: \u2026"
15300
+ },
15301
+ {
15302
+ fence: "errata-triage",
15303
+ fields: ["presenting", "cause", "test", "fix", "drift", "route"],
15304
+ required: [["presenting", "route"], ["cause", "test"]],
15305
+ taught: "taught",
15306
+ taughtBy: "MCP errata_triage tool copy",
15307
+ parser: "packages/local-graph/src/triage.ts parseTriageFences",
15308
+ hint: "```errata-triage with presenting: and cause: (or route: + test: to answer one)"
15309
+ },
15310
+ {
15311
+ fence: "errata-abstraction",
15312
+ fields: ["candidate", "principle", "covers"],
15313
+ required: [["candidate"], ["principle"], ["covers"]],
15314
+ taught: "taught",
15315
+ taughtBy: "MCP errata_pending_abstractions tool copy",
15316
+ parser: "packages/local-graph/src/abstraction.ts parseAbstractionFences",
15317
+ hint: "```errata-abstraction with candidate:, principle:, covers: (all required)"
15318
+ },
15319
+ {
15320
+ fence: "errata-intent",
15321
+ fields: ["intent"],
15322
+ required: [["intent"]],
15323
+ taught: "taught",
15324
+ parser: "packages/local-graph/src/intent.ts parseIntentFences",
15325
+ hint: "```errata-intent with intent: what you're about to do"
15326
+ },
15327
+ {
15328
+ fence: "errata-share",
15329
+ fields: ["priors"],
15330
+ required: [["priors"]],
15331
+ taught: "taught",
15332
+ taughtBy: "subagent hand-off copy",
15333
+ parser: "packages/local-graph/src/intent.ts parseShareFence",
15334
+ hint: "```errata-share with priors: all | [h1], [h2]"
15335
+ }
15336
+ ];
15337
+ var TAG_VERB_BY_NAME = new Map(
15338
+ TAG_VERBS.filter((v) => v.form === "verb").map((v) => [v.verb, v])
15339
+ );
15340
+ var TAG_PAREN_VERBS = [...TAG_VERB_BY_NAME.keys()].sort((a, b) => b.length - a.length);
15341
+ var TAG_GRAMMAR_OPENERS = [
15342
+ ...new Set(TAG_VERBS.filter((v) => v.form !== "citation").map((v) => v.opener))
15343
+ ];
15344
+ var TAG_QUOTED_PAREN_SOURCE = String.raw`\(\s*(?:${TAG_PAREN_VERBS.map((v) => v.replace(/-/g, "\\-")).join("|")})\s*:|\(\[`;
15345
+ var TAG_QUOTED_RE = new RegExp(String.raw`\[[!?]|` + TAG_QUOTED_PAREN_SOURCE, "i");
15346
+ var TAG_QUOTED_PAREN_RE = new RegExp(TAG_QUOTED_PAREN_SOURCE, "i");
15347
+ function tagGrammarCopy() {
15348
+ const copy = [];
15349
+ for (const ref of ["its-handle", "its-id", "h", "id"]) {
15350
+ for (const build of Object.values(TAG_EXAMPLES)) copy.push(build(ref));
15351
+ }
15352
+ for (const v of TAG_VERBS) {
15353
+ copy.push(v.hint);
15354
+ if (v.taughtBy) copy.push(v.taughtBy);
15355
+ for (const q of v.qualifiers ?? []) copy.push(q.note);
15356
+ for (const l of v.lowering) {
15357
+ if (l.note) copy.push(l.note);
15358
+ if (l.knownViolation) copy.push(l.knownViolation.finding);
15359
+ }
15360
+ }
15361
+ for (const f of TAG_FENCES) copy.push(f.hint);
15362
+ return copy;
15363
+ }
15364
+ var TEMPLATE_VERB_GROUP = new RegExp(
15365
+ String.raw`\(\s*(${TAG_PAREN_VERBS.map((v) => v.replace(/-/g, "\\-")).join("|")})\s*:((?:[^()\n]|\([^()\n]*\))*)\)`,
15366
+ "gi"
15367
+ );
15368
+ var BRACKET_TOKEN = /\[([^\]\n]{1,80})\]/g;
15369
+ var REAL_HANDLE_SHAPE = new RegExp(
15370
+ String.raw`^(?:[a-z0-9]+(?:-[a-z0-9]+)*-(?=[0-9a-f]{0,3}\d)[0-9a-f]{4}|(?:${TAG_CANONICAL_ID_PREFIXES.join("|")})_[0-9a-f]{6,})$`,
15371
+ "i"
15372
+ );
15373
+ function normalizeTemplateBody(body) {
15374
+ return body.replace(/…|\.{3}/g, " ").replace(/\s+/g, " ").trim().replace(/^[\s,;:—–-]+|[\s,;:.—–-]+$/g, "").toLowerCase();
15375
+ }
15376
+ function deriveTagTemplates(copy) {
15377
+ const bodies = /* @__PURE__ */ new Set();
15378
+ const handles = new Set([...TAG_PLACEHOLDER_HANDLES].map((h) => h.toLowerCase()));
15379
+ for (const text of copy) {
15380
+ for (const m of text.matchAll(BRACKET_TOKEN)) {
15381
+ const token = m[1].trim().toLowerCase();
15382
+ if (!/^[!?]/.test(token) && !REAL_HANDLE_SHAPE.test(token)) handles.add(token);
15383
+ }
15384
+ }
15385
+ for (const text of copy) {
15386
+ for (const m of text.matchAll(TEMPLATE_VERB_GROUP)) {
15387
+ const body = normalizeTemplateBody(m[2]);
15388
+ bodies.add(body);
15389
+ const unthreaded = normalizeTemplateBody(body.replace(/^#[a-z][\w-]*/i, ""));
15390
+ bodies.add(unthreaded);
15391
+ const rest = normalizeTemplateBody(unthreaded.replace(BRACKET_TOKEN, (t, h) => handles.has(h.trim().toLowerCase()) ? " " : t).replace(/,/g, " "));
15392
+ bodies.add(rest);
15393
+ }
15394
+ }
15395
+ bodies.delete("");
15396
+ return { bodies, handles };
15397
+ }
15398
+ var TAG_TEMPLATES = deriveTagTemplates(tagGrammarCopy());
15399
+ var VERB_WORDS_SOURCE = TAG_PAREN_VERBS.map((v) => v.replace(/-/g, "\\-")).join("|");
15400
+ var JOINED_VERB = new RegExp(String.raw`[,;]\s*(?=(?:${VERB_WORDS_SOURCE})\s*:)`, "i");
15401
+ var LEADING_VERB = new RegExp(String.raw`^\s*(${VERB_WORDS_SOURCE})\s*:`, "i");
15402
+ var ANY_VERB_WORD = new RegExp(String.raw`\b(?:${VERB_WORDS_SOURCE})\s*:`, "gi");
15403
+ var QUOTED_TAG_OPENER = new RegExp(String.raw`\(\s*(?:(${VERB_WORDS_SOURCE})\s*:|(?=\[))`, "gi");
15404
+
15405
+ // ../../packages/local-graph/src/wm-calibration.ts
15406
+ var WM_CALIBRATION_KEYS = [
15407
+ "shownCount",
15408
+ "evictedCount",
15409
+ "reinforceShownCount",
15410
+ "reinforceRecentCount",
15411
+ "reinforceEvictedCount",
15412
+ "reinforceUnshownCount",
15413
+ "reinforceUnshownQueryOnlyCount"
15414
+ ];
15415
+ var WM_COUNTED_PREDICATE = WM_CALIBRATION_KEYS.map((k) => `json_type(attrs_json, '$.${k}') IS NOT NULL`).join(" OR ");
15416
+ var wmCounterSql = (k) => `CASE WHEN json_type(attrs_json, '$.${k}') IN ('integer', 'real') THEN json_extract(attrs_json, '$.${k}') END`;
15417
+
15418
+ // ../../packages/local-graph/src/exposure-attrs.ts
15419
+ var EXPOSURE_ATTRS = [
15420
+ "shownCount",
15421
+ "evictedCount",
15422
+ "firstShownAt",
15423
+ "firstEvictedAt",
15424
+ "firstShownAtSeq",
15425
+ "lastShownAtSeq",
15426
+ "lastEvictedAtSeq",
15427
+ "shownSinceTouch",
15428
+ "shownSinceTouchAt",
15429
+ "exposureLog",
15430
+ "exposureLogStart",
15431
+ "exposureLogSince",
15432
+ "exposureFirst",
15433
+ "exposureTrimmed"
15434
+ ];
15435
+ var EXPOSURE_SCOPE = "exposure";
15436
+ var EXPOSURE_KEYS = new Set(EXPOSURE_ATTRS);
15437
+ var isExposureAttr = (key) => EXPOSURE_KEYS.has(key);
15438
+ function pickExposure(attrs) {
15439
+ const out = {};
15440
+ for (const k of EXPOSURE_ATTRS) if (attrs[k] !== void 0) out[k] = attrs[k];
15441
+ return out;
15442
+ }
15443
+ function exposureDocSql(key) {
15444
+ return `coalesce((SELECT x.attrs_json FROM node_exposure x WHERE x.id = nodes.id AND json_type(x.attrs_json, '$.${key}') IS NOT NULL), attrs_json)`;
15445
+ }
15446
+
14858
15447
  // ../../packages/local-graph/src/history-cache-floor.ts
14859
15448
  var HISTORY_CACHE_FLOOR_KEY = "history-cache-withdrawal-floor-3";
14860
15449
  function installHistoryCacheFloor(db) {
@@ -14895,7 +15484,7 @@ function installHistoryCacheFloor(db) {
14895
15484
  OR EXISTS(SELECT 1 FROM history_cache_withdrawals w WHERE w.id = nodes.id))
14896
15485
  AND (embedding_blob IS NOT NULL OR embedding_json <> '[]' OR is_landmark <> 0
14897
15486
  OR description <> ${description} OR attrs_json <> ${attrs});`;
14898
- const internal = `key IN ('orgId','teamId','groupId','projectId','aliasTexts','aliasCanonicalIds')
15487
+ const internal = `key IN ('orgId','teamId','groupId','projectId','aliasTexts','aliasCanonicalIds','residueAudienceVerified')
14899
15488
  OR key GLOB 'mergedFrom*' OR key GLOB '*WitnessKeys' OR key GLOB '*Sessions'
14900
15489
  OR key GLOB 'authoringSession*' OR key GLOB 'contributor*' OR key GLOB 'witnessKeys*'`;
14901
15490
  const stripMetadata = (scope) => `UPDATE nodes SET attrs_json=(
@@ -15029,7 +15618,9 @@ function installEditPlaceholderRetirement(db) {
15029
15618
  }
15030
15619
 
15031
15620
  // ../../packages/local-graph/src/cloud-attr-compaction.ts
15032
- var CLOUD_ATTR_COMPACTION_KEY = "cloud-attr-compaction-1";
15621
+ var CLOUD_ATTR_COMPACTION_KEY = "cloud-attr-compaction-2";
15622
+ var V1_KEY = "cloud-attr-compaction-1";
15623
+ var STRUCT_LABELS = "('Problem', 'Solution', 'RootCause')";
15033
15624
  var DUPES = CLOUD_ATTR_DUPLICATE_KEYS.map((k) => `'${k}'`).join(",");
15034
15625
  function compacted(row) {
15035
15626
  const packed = `json_extract(${row}.attrs_json, '$.attrsJson')`;
@@ -15048,14 +15639,17 @@ function dirty(row) {
15048
15639
  return `json_extract(${row}.attrs_json, '$.source') = 'cloud' AND (${has})`;
15049
15640
  }
15050
15641
  function installCloudAttrCompaction(db) {
15051
- if (db.prepare("SELECT value FROM store_meta WHERE key = ?").get(CLOUD_ATTR_COMPACTION_KEY)) return;
15642
+ const marked = (key) => Boolean(db.prepare("SELECT value FROM store_meta WHERE key = ?").get(key));
15643
+ if (marked(CLOUD_ATTR_COMPACTION_KEY)) return;
15644
+ const scope = marked(V1_KEY) ? ` AND label IN ${STRUCT_LABELS}` : "";
15052
15645
  db.transaction(() => {
15053
15646
  for (const operation of ["INSERT", "UPDATE"]) {
15054
- db.exec(`CREATE TRIGGER IF NOT EXISTS cloud_attr_compact_${operation.toLowerCase()}
15647
+ db.exec(`DROP TRIGGER IF EXISTS cloud_attr_compact_${operation.toLowerCase()}`);
15648
+ db.exec(`CREATE TRIGGER cloud_attr_compact_${operation.toLowerCase()}
15055
15649
  AFTER ${operation} ON nodes WHEN ${dirty("NEW")}
15056
15650
  BEGIN UPDATE nodes SET attrs_json = ${compacted("nodes")} WHERE id = NEW.id; END;`);
15057
15651
  }
15058
- db.exec(`UPDATE nodes SET attrs_json = ${compacted("nodes")} WHERE ${dirty("nodes")}`);
15652
+ db.exec(`UPDATE nodes SET attrs_json = ${compacted("nodes")} WHERE ${dirty("nodes")}${scope}`);
15059
15653
  db.prepare("INSERT INTO store_meta(key,value) VALUES (?,?)").run(CLOUD_ATTR_COMPACTION_KEY, "installed");
15060
15654
  });
15061
15655
  }
@@ -15093,6 +15687,313 @@ function installHistoryAttrSlim(db) {
15093
15687
  db.prepare("INSERT INTO store_meta(key,value) VALUES (?,?)").run(HISTORY_ATTR_SLIM_KEY, "done");
15094
15688
  }
15095
15689
 
15690
+ // ../../packages/local-graph/src/text-index.ts
15691
+ var TEXT_INDEX_KEY = "text-index-trigram-2";
15692
+ var TEXT_INDEX_MIN_TERM = 3;
15693
+ function installTextIndex(db) {
15694
+ if (db.prepare("SELECT value FROM store_meta WHERE key = ?").get(TEXT_INDEX_KEY)) return;
15695
+ try {
15696
+ db.exec(`CREATE VIRTUAL TABLE temp.fts5_probe USING fts5(x, tokenize='trigram'); DROP TABLE temp.fts5_probe`);
15697
+ } catch {
15698
+ db.prepare("INSERT INTO store_meta(key,value) VALUES (?,?)").run(TEXT_INDEX_KEY, "unavailable");
15699
+ return;
15700
+ }
15701
+ db.transaction(() => {
15702
+ db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS nodes_text
15703
+ USING fts5(description, content='nodes', content_rowid='rowid', tokenize='trigram')`);
15704
+ db.exec(`DROP TRIGGER IF EXISTS nodes_text_update_out; DROP TRIGGER IF EXISTS nodes_text_update_in;
15705
+ DROP TRIGGER IF EXISTS nodes_text_update`);
15706
+ db.exec(`CREATE TRIGGER IF NOT EXISTS nodes_text_insert AFTER INSERT ON nodes WHEN NEW.valid_to IS NULL
15707
+ BEGIN INSERT INTO nodes_text(rowid, description) VALUES (NEW.rowid, NEW.description); END;`);
15708
+ db.exec(`CREATE TRIGGER nodes_text_update AFTER UPDATE OF description, valid_to ON nodes
15709
+ WHEN OLD.description IS NOT NEW.description OR OLD.valid_to IS NOT NEW.valid_to
15710
+ BEGIN
15711
+ INSERT INTO nodes_text(nodes_text, rowid, description)
15712
+ SELECT 'delete', OLD.rowid, OLD.description WHERE OLD.valid_to IS NULL;
15713
+ INSERT INTO nodes_text(rowid, description)
15714
+ SELECT NEW.rowid, NEW.description WHERE NEW.valid_to IS NULL;
15715
+ END;`);
15716
+ db.exec(`CREATE TRIGGER IF NOT EXISTS nodes_text_delete AFTER DELETE ON nodes WHEN OLD.valid_to IS NULL
15717
+ BEGIN INSERT INTO nodes_text(nodes_text, rowid, description) VALUES ('delete', OLD.rowid, OLD.description); END;`);
15718
+ db.exec(`INSERT INTO nodes_text(nodes_text) VALUES ('delete-all')`);
15719
+ db.exec(`INSERT INTO nodes_text(rowid, description) SELECT rowid, description FROM nodes WHERE valid_to IS NULL`);
15720
+ db.prepare("INSERT OR REPLACE INTO store_meta(key,value) VALUES (?,?)").run("text-index-trigram-1", "installed");
15721
+ db.prepare("INSERT INTO store_meta(key,value) VALUES (?,?)").run(TEXT_INDEX_KEY, "installed");
15722
+ });
15723
+ }
15724
+ function textIndexReady(db) {
15725
+ return db.prepare("SELECT value FROM store_meta WHERE key = ?").get(TEXT_INDEX_KEY)?.["value"] === "installed";
15726
+ }
15727
+
15728
+ // ../../packages/local-graph/src/change-feed.ts
15729
+ var CHANGE_FEED_KEY = "change-feed";
15730
+ var ANY_CHANGE = "*";
15731
+ var nodeScope = (label) => `node:${label}`;
15732
+ var CHANGE_LOG_KEEP = 25e4;
15733
+ var PRUNED_TO_KEY = "change-log-pruned-to";
15734
+ function installChangeFeed(db) {
15735
+ if (changeFeedInstalled(db)) return;
15736
+ db.transaction(() => {
15737
+ db.exec("CREATE TABLE IF NOT EXISTS change_seq (scope TEXT PRIMARY KEY, seq INTEGER NOT NULL) WITHOUT ROWID");
15738
+ db.exec(`CREATE TABLE IF NOT EXISTS change_log (change_seq INTEGER PRIMARY KEY, kind TEXT NOT NULL, id TEXT NOT NULL,
15739
+ label TEXT, from_id TEXT, to_id TEXT)`);
15740
+ for (const name of ["nodes", "edges"].flatMap((t) => ["insert", "update", "delete"].map((op) => `change_feed_${t}_${op}`))) {
15741
+ db.exec(`DROP TRIGGER IF EXISTS ${name}`);
15742
+ }
15743
+ const changed = (table) => columns(db, table).map((c) => `OLD."${c}" IS NOT NEW."${c}"`).join(" OR ");
15744
+ const strip = (row) => `json_remove(${row}.attrs_json, ${EXPOSURE_ATTRS.map((a) => `'$.${a}'`).join(", ")})`;
15745
+ const exposureOnly = `(${columns(db, "nodes").filter((c) => c !== "attrs_json").map((c) => `OLD."${c}" IS NEW."${c}"`).join(" AND ")}
15746
+ AND CASE WHEN json_valid(OLD.attrs_json) AND json_valid(NEW.attrs_json) THEN ${strip("OLD")} IS ${strip("NEW")} ELSE 0 END)`;
15747
+ const bump = (...scopes) => `INSERT INTO change_seq(scope, seq) VALUES ${scopes.map((s) => `(${s}, 1)`).join(", ")}
15748
+ ON CONFLICT(scope) DO UPDATE SET seq = seq + 1;`;
15749
+ const seq = `(SELECT seq FROM change_seq WHERE scope = '*')`;
15750
+ const logNode = (row) => `INSERT INTO change_log(change_seq, kind, id, label) VALUES (${seq}, 'node', ${row}.id, ${row}.label);`;
15751
+ const logEdge = (row) => `INSERT INTO change_log(change_seq, kind, id, label, from_id, to_id) VALUES (${seq}, 'edge', ${row}.id, ${row}.type, ${row}.from_id, ${row}.to_id);`;
15752
+ const node = (row) => `'node:' || ${row}.label`;
15753
+ const edge = (row) => `'edge:' || ${row}.type`;
15754
+ db.exec(`CREATE TRIGGER change_feed_nodes_insert AFTER INSERT ON nodes BEGIN ${bump(`'*'`, node("NEW"))} ${logNode("NEW")} END;`);
15755
+ db.exec(`CREATE TRIGGER change_feed_nodes_update AFTER UPDATE ON nodes WHEN ${changed("nodes")} BEGIN
15756
+ ${bump(`'*'`)}
15757
+ INSERT INTO change_seq(scope, seq) SELECT CASE WHEN ${exposureOnly} THEN '${EXPOSURE_SCOPE}' ELSE ${node("NEW")} END, 1 WHERE 1
15758
+ ON CONFLICT(scope) DO UPDATE SET seq = seq + 1;
15759
+ INSERT INTO change_seq(scope, seq) SELECT ${node("OLD")}, 1 WHERE OLD.label IS NOT NEW.label
15760
+ ON CONFLICT(scope) DO UPDATE SET seq = seq + 1;
15761
+ ${logNode("NEW")}
15762
+ END;`);
15763
+ db.exec(`CREATE TRIGGER change_feed_nodes_delete AFTER DELETE ON nodes BEGIN ${bump(`'*'`, node("OLD"))} ${logNode("OLD")} END;`);
15764
+ db.exec(`CREATE TRIGGER change_feed_edges_insert AFTER INSERT ON edges BEGIN ${bump(`'*'`, edge("NEW"))} ${logEdge("NEW")} END;`);
15765
+ db.exec(`CREATE TRIGGER change_feed_edges_update AFTER UPDATE ON edges WHEN ${changed("edges")} BEGIN
15766
+ ${bump(`'*'`, edge("NEW"))}
15767
+ INSERT INTO change_seq(scope, seq) SELECT ${edge("OLD")}, 1 WHERE OLD.type IS NOT NEW.type
15768
+ ON CONFLICT(scope) DO UPDATE SET seq = seq + 1;
15769
+ ${logEdge("NEW")}
15770
+ END;`);
15771
+ db.exec(`CREATE TRIGGER change_feed_edges_delete AFTER DELETE ON edges BEGIN ${bump(`'*'`, edge("OLD"))} ${logEdge("OLD")} END;`);
15772
+ db.prepare("INSERT OR REPLACE INTO store_meta(key, value) VALUES (?, ?)").run(PRUNED_TO_KEY, String(readChangeSeq(db)));
15773
+ db.prepare("INSERT OR REPLACE INTO store_meta(key, value) VALUES (?, ?)").run(CHANGE_FEED_KEY, columnSignature(db));
15774
+ });
15775
+ }
15776
+ function readChangeSeq(db, scopes = [ANY_CHANGE]) {
15777
+ if (!scopes.length) return 0;
15778
+ const row = db.prepare(`SELECT coalesce(sum(seq), 0) AS n FROM change_seq WHERE scope IN (${scopes.map(() => "?").join(",")})`).get(...scopes);
15779
+ return Number(row?.n ?? 0);
15780
+ }
15781
+ function changeLogCovers(db, sinceSeq) {
15782
+ const prunedTo = Number(db.prepare("SELECT value FROM store_meta WHERE key = ?").get(PRUNED_TO_KEY)?.["value"] ?? NaN);
15783
+ return Number.isFinite(prunedTo) && sinceSeq >= prunedTo;
15784
+ }
15785
+ function readChangedNodeIds(db, sinceSeq, labels) {
15786
+ if (!changeLogCovers(db, sinceSeq)) return null;
15787
+ if (!labels.length) return [];
15788
+ const rows = db.prepare(`SELECT DISTINCT id FROM change_log WHERE change_seq > ? AND kind = 'node' AND label IN (${labels.map(() => "?").join(",")})`).all(sinceSeq, ...labels);
15789
+ return rows.map((r) => r.id);
15790
+ }
15791
+ function readChangedAmong(db, sinceSeq, ids) {
15792
+ if (!changeLogCovers(db, sinceSeq)) return null;
15793
+ const out = [];
15794
+ for (let i = 0; i < ids.length; i += 500) {
15795
+ const chunk = ids.slice(i, i + 500);
15796
+ const rows = db.prepare(`SELECT DISTINCT id FROM change_log WHERE change_seq > ? AND kind = 'node' AND id IN (${chunk.map(() => "?").join(",")})`).all(sinceSeq, ...chunk);
15797
+ for (const r of rows) out.push(r.id);
15798
+ }
15799
+ return out;
15800
+ }
15801
+ function readLineageChangedFiles(db, sinceSeq) {
15802
+ if (!changeLogCovers(db, sinceSeq)) return null;
15803
+ const rows = db.prepare(LINEAGE_CHANGED_FILES_SQL).all({ s: sinceSeq });
15804
+ return rows.map((r) => r.id);
15805
+ }
15806
+ var LINEAGE_CHANGED_FILES_SQL = `
15807
+ SELECT id FROM change_log WHERE change_seq > :s AND kind = 'node' AND label = 'File'
15808
+ UNION SELECT from_id FROM change_log WHERE change_seq > :s AND kind = 'edge' AND label IN ('DEFINES','CONTAINS')
15809
+ UNION SELECT e.from_id FROM (SELECT DISTINCT id FROM change_log WHERE change_seq > :s AND kind = 'node') c
15810
+ CROSS JOIN edges e ON e.to_id = c.id AND e.type IN ('DEFINES','CONTAINS')
15811
+ `;
15812
+ function pruneChangeLog(db) {
15813
+ const max = Number(db.prepare("SELECT coalesce(max(change_seq), 0) AS n FROM change_log").get()?.["n"] ?? 0);
15814
+ const cut = max - CHANGE_LOG_KEEP;
15815
+ if (cut <= 0) return 0;
15816
+ let removed = 0;
15817
+ db.transaction(() => {
15818
+ removed = Number(db.prepare("DELETE FROM change_log WHERE change_seq <= ?").run(cut).changes);
15819
+ const prev = Number(db.prepare("SELECT value FROM store_meta WHERE key = ?").get(PRUNED_TO_KEY)?.["value"] ?? 0);
15820
+ db.prepare("INSERT OR REPLACE INTO store_meta(key, value) VALUES (?, ?)").run(PRUNED_TO_KEY, String(Math.max(prev, cut)));
15821
+ });
15822
+ return removed;
15823
+ }
15824
+ function changeFeedInstalled(db) {
15825
+ return db.prepare("SELECT value FROM store_meta WHERE key = ?").get(CHANGE_FEED_KEY)?.["value"] === columnSignature(db);
15826
+ }
15827
+ function columns(db, table) {
15828
+ return db.prepare(`SELECT name FROM pragma_table_info('${table}') ORDER BY cid`).all().map((r) => r.name);
15829
+ }
15830
+ function columnSignature(db) {
15831
+ return `5|${columns(db, "nodes").join(",")}|${columns(db, "edges").join(",")}|${EXPOSURE_ATTRS.join(",")}`;
15832
+ }
15833
+
15834
+ // ../../packages/local-graph/src/lineage-integrity.ts
15835
+ var statements = /* @__PURE__ */ new WeakMap();
15836
+ function stmt(db, sql) {
15837
+ let byDb = statements.get(db);
15838
+ if (!byDb) statements.set(db, byDb = /* @__PURE__ */ new Map());
15839
+ let s = byDb.get(sql);
15840
+ if (!s) byDb.set(sql, s = db.prepare(sql));
15841
+ return s;
15842
+ }
15843
+ var HISTORY_EDGE_TYPES = [
15844
+ "SUPERSEDED_BY",
15845
+ "CONTINUES",
15846
+ "FORKED_FROM",
15847
+ "REGRESSION_OF",
15848
+ "SPLIT_INTO",
15849
+ "REVEALED_BY"
15850
+ ];
15851
+ var LINEAGE_INTEGRITY_KEY = "lineage-integrity-v1";
15852
+ var HISTORY_SQL = HISTORY_EDGE_TYPES.map((t) => `'${t}'`).join(",");
15853
+ var NOT_HISTORY = `type NOT IN (${HISTORY_SQL})`;
15854
+ var NOT_HISTORICAL_ROW = `json_type(attrs_json, '$.historical') IS NOT 'true'`;
15855
+ var RETAINS_EDGES = (n) => `(${n}.label = 'Problem'
15856
+ AND json_type(${n}.attrs_json, '$.resolvedAt') IS NOT NULL
15857
+ AND COALESCE(json_extract(${n}.attrs_json, '$.resolvedAs'), '') NOT IN ('duplicate', 'false_positive'))`;
15858
+ var PRESENT = (n) => `(${n}.valid_to IS NULL OR ${RETAINS_EDGES(n)})`;
15859
+ var STRANDED_EDGE_PREDICATE = `e.valid_to IS NULL AND e.${NOT_HISTORY}
15860
+ AND (NOT EXISTS (SELECT 1 FROM nodes f WHERE f.id = e.from_id AND ${PRESENT("f")})
15861
+ OR NOT EXISTS (SELECT 1 FROM nodes t WHERE t.id = e.to_id AND ${PRESENT("t")}))`;
15862
+ function countStrandedEdges(db) {
15863
+ const rows = db.prepare(`SELECT e.type AS type, count(*) AS n FROM edges e WHERE ${STRANDED_EDGE_PREDICATE} GROUP BY e.type ORDER BY n DESC`).all();
15864
+ const byType = {};
15865
+ let total = 0;
15866
+ for (const r of rows) {
15867
+ byType[r.type] = Number(r.n);
15868
+ total += Number(r.n);
15869
+ }
15870
+ return { total, byType };
15871
+ }
15872
+ function countLiveHeadConflicts(db) {
15873
+ const row = db.prepare(
15874
+ `SELECT count(*) AS n FROM (
15875
+ SELECT COALESCE(logical_id, id) AS lid FROM nodes
15876
+ WHERE valid_to IS NULL AND ${NOT_HISTORICAL_ROW}
15877
+ GROUP BY lid HAVING count(*) > 1)`
15878
+ ).get();
15879
+ return Number(row?.n ?? 0);
15880
+ }
15881
+ function closeIncidentEdges(db, id, t) {
15882
+ let closed = 0;
15883
+ for (const side of ["from_id", "to_id"]) {
15884
+ const r = stmt(db, `UPDATE edges SET valid_to = MAX(valid_from, ?) WHERE ${side} = ? AND valid_to IS NULL AND ${NOT_HISTORY}`).run(t, id);
15885
+ closed += Number(r.changes);
15886
+ }
15887
+ return closed;
15888
+ }
15889
+ function reopenCascadeClosedEdges(db, id, closedAt) {
15890
+ let reopened = 0;
15891
+ for (const [side, other] of [["from_id", "to_id"], ["to_id", "from_id"]]) {
15892
+ const r = stmt(
15893
+ db,
15894
+ `UPDATE edges SET valid_to = NULL
15895
+ WHERE ${side} = ? AND valid_to = ? AND ${NOT_HISTORY}
15896
+ AND EXISTS (SELECT 1 FROM nodes o WHERE o.id = edges.${other} AND o.valid_to IS NULL)`
15897
+ ).run(id, closedAt);
15898
+ reopened += Number(r.changes);
15899
+ }
15900
+ return reopened;
15901
+ }
15902
+ function collapseLiveHeads(db, logicalIds, t) {
15903
+ const headsOf = db.prepare(
15904
+ `SELECT id, version, valid_from FROM (
15905
+ SELECT id, version, valid_from, attrs_json FROM nodes WHERE logical_id = ?1 AND valid_to IS NULL
15906
+ UNION
15907
+ SELECT id, version, valid_from, attrs_json FROM nodes WHERE id = ?1 AND logical_id IS NULL AND valid_to IS NULL
15908
+ ) WHERE ${NOT_HISTORICAL_ROW}`
15909
+ );
15910
+ const continuesInto = db.prepare(
15911
+ `SELECT to_id FROM edges WHERE from_id = ? AND type = 'CONTINUES' AND valid_to IS NULL`
15912
+ );
15913
+ const rekeyHead = db.prepare(`UPDATE nodes SET logical_id = ?1 WHERE id = ?1`);
15914
+ const rekeyCopies = db.prepare(
15915
+ `UPDATE nodes SET logical_id = ?1 WHERE logical_id = ?2 AND substr(id, 1, length(?1) + 2) = ?1 || '@v'`
15916
+ );
15917
+ const lineageMembers = db.prepare(
15918
+ `SELECT id FROM nodes WHERE logical_id = ?1 UNION SELECT id FROM nodes WHERE id = ?1`
15919
+ );
15920
+ const closeContinues = db.prepare(
15921
+ `UPDATE edges SET valid_to = MAX(valid_from, ?) WHERE from_id = ? AND to_id = ? AND type = 'CONTINUES' AND valid_to IS NULL`
15922
+ );
15923
+ let split = 0;
15924
+ const queue = [...new Set(logicalIds)];
15925
+ for (let i = 0; i < queue.length; i++) {
15926
+ const lid = queue[i];
15927
+ const heads = headsOf.all(lid);
15928
+ if (heads.length < 2) continue;
15929
+ const members = new Set(lineageMembers.all(lid).map((r) => r.id));
15930
+ const splitOff = (h) => {
15931
+ for (const { to_id } of continuesInto.all(h.id)) {
15932
+ if (members.has(to_id)) closeContinues.run(t, h.id, to_id);
15933
+ }
15934
+ rekeyHead.run(h.id);
15935
+ rekeyCopies.run(h.id, lid);
15936
+ queue.push(h.id);
15937
+ split++;
15938
+ };
15939
+ const kept = new Set(heads.map((h) => h.id));
15940
+ for (const h of heads) {
15941
+ if (h.id === lid) continue;
15942
+ const refuted = continuesInto.all(h.id).some(
15943
+ (e) => e.to_id !== h.id && kept.has(e.to_id)
15944
+ );
15945
+ if (refuted && kept.size > 1) {
15946
+ splitOff(h);
15947
+ kept.delete(h.id);
15948
+ }
15949
+ }
15950
+ let remaining = heads.filter((h) => kept.has(h.id));
15951
+ if (remaining.length < 2) continue;
15952
+ remaining = [...remaining].sort(
15953
+ (a, b) => Number(b.id === lid) - Number(a.id === lid) || Number(b.version) - Number(a.version) || Number(b.valid_from) - Number(a.valid_from) || (a.id < b.id ? -1 : 1)
15954
+ );
15955
+ for (const h of remaining.slice(1)) splitOff(h);
15956
+ }
15957
+ return split;
15958
+ }
15959
+ function repairLineageIntegrity(db, now) {
15960
+ const historicalRowsClosed = Number(
15961
+ db.prepare(
15962
+ `UPDATE nodes SET valid_to = MAX(valid_from, COALESCE(
15963
+ (SELECT s.valid_from FROM edges s WHERE s.id = 'edge_superseded_' || nodes.id), ?))
15964
+ WHERE valid_to IS NULL AND json_type(attrs_json, '$.historical') = 'true'`
15965
+ ).run(now).changes
15966
+ );
15967
+ const conflicted = db.prepare(
15968
+ `SELECT COALESCE(logical_id, id) AS lid FROM nodes
15969
+ WHERE valid_to IS NULL AND ${NOT_HISTORICAL_ROW}
15970
+ GROUP BY lid HAVING count(*) > 1`
15971
+ ).all().map((r) => r.lid);
15972
+ const headsSplit = collapseLiveHeads(db, conflicted, now);
15973
+ const strandedEdgesClosed = Number(
15974
+ db.prepare(
15975
+ `UPDATE edges SET valid_to = MAX(valid_from, COALESCE(
15976
+ (SELECT min(v) FROM (
15977
+ SELECT f.valid_to AS v FROM nodes f WHERE f.id = edges.from_id AND NOT ${PRESENT("f")}
15978
+ UNION ALL
15979
+ SELECT t.valid_to FROM nodes t WHERE t.id = edges.to_id AND NOT ${PRESENT("t")})), ?))
15980
+ WHERE id IN (SELECT e.id FROM edges e WHERE ${STRANDED_EDGE_PREDICATE})`
15981
+ ).run(now).changes
15982
+ );
15983
+ return { historicalRowsClosed, headsSplit, strandedEdgesClosed };
15984
+ }
15985
+ function installLineageIntegrityRepair(db, now = Date.now()) {
15986
+ if (db.prepare("SELECT value FROM store_meta WHERE key = ?").get(LINEAGE_INTEGRITY_KEY)) return null;
15987
+ return db.transaction(() => {
15988
+ const report = repairLineageIntegrity(db, now);
15989
+ db.prepare("INSERT OR REPLACE INTO store_meta (key, value) VALUES (?, ?)").run(
15990
+ LINEAGE_INTEGRITY_KEY,
15991
+ JSON.stringify({ at: now, ...report })
15992
+ );
15993
+ return report;
15994
+ });
15995
+ }
15996
+
15096
15997
  // ../../packages/math/src/relevance.ts
15097
15998
  function pageRankScale(ranks) {
15098
15999
  const positive = [...ranks].filter((r) => Number.isFinite(r) && r > 0).sort((a, b) => a - b);
@@ -15103,6 +16004,177 @@ function pageRankScale(ranks) {
15103
16004
  var DEMAND_HOT = Math.log1p(10);
15104
16005
  var DEMAND_WARM = Math.log1p(5);
15105
16006
 
16007
+ // ../../packages/local-graph/src/node-select.ts
16008
+ var SELECT_INDEXES = [
16009
+ "nodes_live_local_community",
16010
+ "nodes_live_fold_candidate",
16011
+ "nodes_live_resolved",
16012
+ "nodes_live_fix_candidate",
16013
+ "nodes_live_anchor_hint"
16014
+ ];
16015
+ var EMBEDDING_DIM_SQL = "(CASE WHEN length(embedding_blob) >= 4 THEN length(embedding_blob) / 4 ELSE COALESCE(json_array_length(embedding_json), 0) END)";
16016
+ var KEY = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/;
16017
+ var COLUMNS = /* @__PURE__ */ new Set([
16018
+ "created_at",
16019
+ "last_updated_at",
16020
+ "page_rank",
16021
+ "created_at_seq",
16022
+ "last_reinforced_at_seq",
16023
+ "cumulative_hits",
16024
+ "extraction_confidence"
16025
+ ]);
16026
+ function path(key) {
16027
+ if (!KEY.test(key)) throw new Error(`selectNodes: bad attr key ${key}`);
16028
+ return `'$.${key}'`;
16029
+ }
16030
+ function selectIndex(i) {
16031
+ if (!SELECT_INDEXES.includes(i)) throw new Error(`selectNodes: bad index ${i}`);
16032
+ return i;
16033
+ }
16034
+ function column(c) {
16035
+ if (!COLUMNS.has(c)) throw new Error(`selectNodes: bad column ${c}`);
16036
+ return c;
16037
+ }
16038
+ function compileNodeWhere(q) {
16039
+ if (q.labels.length === 0) throw new Error("selectNodes: no labels");
16040
+ const params = [...q.labels];
16041
+ const byId = (q.where ?? []).some((w) => "idIn" in w || "idPrefix" in w && w.idPrefix.length > 0);
16042
+ const unindexed = byId ? "+" : "";
16043
+ const conds = [`${unindexed}label IN (${q.labels.map(() => "?").join(", ")})`];
16044
+ if (!q.includeClosed) conds.push(`${unindexed}valid_to IS NULL`);
16045
+ for (const w of q.where ?? []) {
16046
+ if ("embedding" in w) {
16047
+ conds.push(`${EMBEDDING_DIM_SQL} ${w.embedding === "present" ? ">" : "="} 0`);
16048
+ } else if ("idPrefix" in w) {
16049
+ conds.push("substr(id, 1, ?) = ?");
16050
+ params.push(w.idPrefix.length, w.idPrefix);
16051
+ if (w.idPrefix.length > 0) {
16052
+ const last = w.idPrefix.charCodeAt(w.idPrefix.length - 1);
16053
+ conds.push("id >= ? AND id < ?");
16054
+ params.push(w.idPrefix, w.idPrefix.slice(0, -1) + String.fromCharCode(last + 1));
16055
+ }
16056
+ } else if ("idIn" in w) {
16057
+ if (w.idIn.length === 0) conds.push("0");
16058
+ else {
16059
+ conds.push(`id IN (${w.idIn.map(() => "?").join(", ")})`);
16060
+ params.push(...w.idIn);
16061
+ }
16062
+ } else if ("descriptionIn" in w) {
16063
+ if (w.descriptionIn.length === 0) conds.push("0");
16064
+ else {
16065
+ conds.push(`description IN (${w.descriptionIn.map(() => "?").join(", ")})`);
16066
+ params.push(...w.descriptionIn);
16067
+ }
16068
+ } else if ("column" in w) {
16069
+ const c = column(w.column);
16070
+ for (const [op, sym] of [["gte", ">="], ["gt", ">"], ["lte", "<="], ["lt", "<"]]) {
16071
+ const v = w[op];
16072
+ if (v !== void 0) {
16073
+ conds.push(`${c} ${sym} ?`);
16074
+ params.push(v);
16075
+ }
16076
+ }
16077
+ } else {
16078
+ const p = path(w.attr);
16079
+ if ("absent" in w) conds.push(`json_type(attrs_json, ${p}) IS NULL`);
16080
+ else if ("present" in w) conds.push(`json_type(attrs_json, ${p}) IS NOT NULL`);
16081
+ else if ("nullish" in w) conds.push(`json_extract(attrs_json, ${p}) IS NULL`);
16082
+ else if ("notNullish" in w) conds.push(`json_extract(attrs_json, ${p}) IS NOT NULL`);
16083
+ else if ("eq" in w || "ne" in w) {
16084
+ const v = "eq" in w ? w.eq : w.ne;
16085
+ const neg = "ne" in w;
16086
+ if (typeof v === "boolean") {
16087
+ conds.push(`json_type(attrs_json, ${p}) ${neg ? "IS NOT" : "="} ?`);
16088
+ params.push(v ? "true" : "false");
16089
+ } else {
16090
+ conds.push(neg ? `json_extract(attrs_json, ${p}) IS NOT ?` : `json_extract(attrs_json, ${p}) = ?`);
16091
+ params.push(v);
16092
+ }
16093
+ }
16094
+ }
16095
+ }
16096
+ return { where: conds.join(" AND "), params };
16097
+ }
16098
+ function attrValueSql(p, doc = "attrs_json") {
16099
+ return `CASE json_type(${doc}, ${p}) WHEN 'integer' THEN CAST(json_extract(${doc}, ${p}) AS REAL) ELSE json_extract(${doc}, ${p}) END`;
16100
+ }
16101
+ function attrDocSql(key) {
16102
+ return isExposureAttr(key) ? exposureDocSql(key) : "attrs_json";
16103
+ }
16104
+ function nodeSelectFrom(q) {
16105
+ if (q.indexedBy) return `nodes INDEXED BY ${selectIndex(q.indexedBy)}`;
16106
+ const where = q.where ?? [];
16107
+ if (!q.includeClosed && where.some((w) => "descriptionIn" in w && w.descriptionIn.length > 0) && !where.some((w) => "idIn" in w)) {
16108
+ return "nodes INDEXED BY nodes_live_description";
16109
+ }
16110
+ return "nodes";
16111
+ }
16112
+ function compileNodeSelect(q) {
16113
+ const { where, params } = compileNodeWhere(q);
16114
+ const attrs = q.attrs ?? [];
16115
+ const attrCols = attrs.map((k, i) => `, ${attrValueSql(path(k), attrDocSql(k))} AS a${i}, json_type(${attrDocSql(k)}, ${path(k)}) AS t${i}`).join("");
16116
+ let order;
16117
+ if (q.orderBy && "column" in q.orderBy) {
16118
+ order = `${column(q.orderBy.column)} ${q.orderBy.desc ? "DESC" : "ASC"}, rowid`;
16119
+ } else if (q.orderBy) {
16120
+ const p = path(q.orderBy.attr);
16121
+ order = `json_extract(attrs_json, ${p}) IS NULL, json_extract(attrs_json, ${p}) ${q.orderBy.desc ? "DESC" : "ASC"}, rowid`;
16122
+ } else if (q.labels.length > 1) {
16123
+ order = `CASE label ${q.labels.map((_, i) => `WHEN ? THEN ${i}`).join(" ")} END, rowid`;
16124
+ } else {
16125
+ order = "rowid";
16126
+ }
16127
+ const orderParams = !q.orderBy && q.labels.length > 1 ? [...q.labels] : [];
16128
+ const limit = q.limit !== void 0 ? ` LIMIT ${Math.max(0, Math.floor(q.limit))}` : "";
16129
+ const sql = `SELECT id, label, description, created_at, last_updated_at, page_rank, cumulative_hits, extraction_confidence, extraction_source, community, created_at_seq, last_reinforced_at_seq, valid_to, logical_id, version, ${EMBEDDING_DIM_SQL} AS embedding_dim${q.withEmbedding ? ", embedding_blob, embedding_json" : ""}${attrCols} FROM ${nodeSelectFrom(q)} WHERE ${where} ORDER BY ${order}${limit}`;
16130
+ return { sql, params: [...params, ...orderParams] };
16131
+ }
16132
+ function decodeAttr(value, type) {
16133
+ switch (type) {
16134
+ case null:
16135
+ case void 0:
16136
+ return void 0;
16137
+ case "true":
16138
+ return true;
16139
+ case "false":
16140
+ return false;
16141
+ case "null":
16142
+ return null;
16143
+ case "object":
16144
+ case "array":
16145
+ return typeof value === "string" ? JSON.parse(value) : value;
16146
+ default:
16147
+ return value;
16148
+ }
16149
+ }
16150
+ function decodeSelectedRow(r, attrs, decodeVector) {
16151
+ const out = {};
16152
+ attrs.forEach((k, i) => {
16153
+ const v = decodeAttr(r[`a${i}`], r[`t${i}`]);
16154
+ if (v !== void 0) out[k] = v;
16155
+ });
16156
+ return {
16157
+ id: r["id"],
16158
+ label: r["label"],
16159
+ description: r["description"] ?? "",
16160
+ createdAt: r["created_at"],
16161
+ lastUpdatedAt: r["last_updated_at"],
16162
+ pageRank: r["page_rank"],
16163
+ cumulativeHits: r["cumulative_hits"],
16164
+ extractionConfidence: r["extraction_confidence"],
16165
+ extractionSource: r["extraction_source"],
16166
+ community: r["community"] ?? null,
16167
+ ...r["created_at_seq"] != null ? { createdAtSeq: r["created_at_seq"] } : {},
16168
+ ...r["last_reinforced_at_seq"] != null ? { lastReinforcedAtSeq: r["last_reinforced_at_seq"] } : {},
16169
+ validTo: r["valid_to"] ?? null,
16170
+ logicalId: r["logical_id"] ?? r["id"],
16171
+ version: Number(r["version"] ?? 1),
16172
+ embeddingDim: Number(r["embedding_dim"] ?? 0),
16173
+ ...decodeVector ? { embedding: decodeVector(r["embedding_blob"], r["embedding_json"] ?? "[]") } : {},
16174
+ attrs: out
16175
+ };
16176
+ }
16177
+
15106
16178
  // ../../packages/local-graph/src/store.ts
15107
16179
  var PAGE_RANK_SCALE_KEY = "pageRankScale.p95.v1";
15108
16180
  var TEXT_SEARCH_COLUMNS = `id, label, description, page_rank,
@@ -15157,6 +16229,7 @@ function localEdgeViolation(fromLabel, type, toLabel) {
15157
16229
  }
15158
16230
  var SCHEMA_VERSION = 6;
15159
16231
  var SESSION_REPAIR_KEY = "session-evidence-v1";
16232
+ var EXPOSURE_SIDE_KEY = "exposure-side-rows-1";
15160
16233
  var SESSION_EVIDENCE_PREDICATE = SESSION_EVIDENCE_FIELDS.map((field) => `instr(attrs_json, '${JSON.stringify(field)}') > 0`).join(" OR ");
15161
16234
  var SCHEMA_SQL = `
15162
16235
  CREATE TABLE IF NOT EXISTS schema_version (
@@ -15220,8 +16293,13 @@ CREATE TABLE IF NOT EXISTS nodes (
15220
16293
  );
15221
16294
 
15222
16295
  CREATE INDEX IF NOT EXISTS nodes_label ON nodes(label);
15223
- CREATE INDEX IF NOT EXISTS nodes_memory_tier ON nodes(memory_tier);
15224
- CREATE INDEX IF NOT EXISTS nodes_page_rank ON nodes(page_rank DESC);
16296
+ -- Dropped: no query plans through them (every prepared statement across the
16297
+ -- local-graph, indexer, context-writer, generalizer and daemon suites was
16298
+ -- planned with and without them, 2026-10-21), and each write that touches
16299
+ -- memory_tier / page_rank (every node upsert, every rescore) paid to maintain
16300
+ -- them. nodes_live_page_rank serves the live page-rank orderings.
16301
+ DROP INDEX IF EXISTS nodes_memory_tier;
16302
+ DROP INDEX IF EXISTS nodes_page_rank;
15225
16303
 
15226
16304
  CREATE TABLE IF NOT EXISTS edges (
15227
16305
  id TEXT PRIMARY KEY,
@@ -15243,7 +16321,10 @@ CREATE TABLE IF NOT EXISTS edges (
15243
16321
  FOREIGN KEY (to_id) REFERENCES nodes(id) ON DELETE CASCADE
15244
16322
  );
15245
16323
 
15246
- CREATE INDEX IF NOT EXISTS edges_from ON edges(from_id, type);
16324
+ -- Dropped: a prefix of edges_triple (from_id, type, to_id), which every
16325
+ -- from-side lookup and foreign-key check already plans through; it cost 47 MB
16326
+ -- and an index write on every edge write.
16327
+ DROP INDEX IF EXISTS edges_from;
15247
16328
  CREATE INDEX IF NOT EXISTS edges_to ON edges(to_id, type);
15248
16329
  CREATE INDEX IF NOT EXISTS edges_type ON edges(type);
15249
16330
  CREATE UNIQUE INDEX IF NOT EXISTS edges_triple ON edges(from_id, type, to_id);
@@ -15251,8 +16332,41 @@ CREATE UNIQUE INDEX IF NOT EXISTS edges_triple ON edges(from_id, type, to_id);
15251
16332
  var TEMPORAL_INDEX_SQL = `
15252
16333
  CREATE INDEX IF NOT EXISTS nodes_live ON nodes(label, valid_to);
15253
16334
  CREATE INDEX IF NOT EXISTS nodes_live_count ON nodes(valid_to);
15254
- CREATE INDEX IF NOT EXISTS nodes_live_recent ON nodes(label, last_updated_at DESC) WHERE valid_to IS NULL;
16335
+ -- Problems by recency (the only label read newest-first). Over every live row,
16336
+ -- each write that stamps last_updated_at moved a code symbol's entry from a
16337
+ -- scattered leaf: ~19 KB per position-only symbol rewrite on a 1.1 GB store.
16338
+ -- label stays the leading column so label = 'Problem' plans without a sort.
16339
+ DROP INDEX IF EXISTS nodes_live_recent;
16340
+ CREATE INDEX IF NOT EXISTS nodes_live_problem_recent ON nodes(label, last_updated_at DESC) WHERE valid_to IS NULL AND label = 'Problem';
16341
+ -- "Anything created since the watermark?" guards (the fold, regression and
16342
+ -- canonical-bind sweeps run one per session boundary). Without it the planner
16343
+ -- walks nodes_live and reads every live row of the label to test created_at:
16344
+ -- 56 MB per guard on a 1.1 GB store, to learn there was nothing to do. valid_to
16345
+ -- is a key column so the planner prefers this over nodes_live (one more term).
16346
+ CREATE INDEX IF NOT EXISTS nodes_live_created ON nodes(label, valid_to, created_at) WHERE valid_to IS NULL;
16347
+ -- Community members (the skills pull reads them twice per session boundary):
16348
+ -- ~15% of semantic nodes carry a localCommunity, and without this every live
16349
+ -- semantic row was read to find them (168 MB \u2192 18 MB per read).
16350
+ CREATE INDEX IF NOT EXISTS nodes_live_local_community ON nodes(label, valid_to, json_extract(attrs_json, '$.localCommunity')) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.localCommunity') IS NOT NULL;
16351
+ -- Problems parked for the fold judge (read every settle boundary): ~1 in 5 open
16352
+ -- Problems; without it every live Problem was read to find them (56 MB).
16353
+ CREATE INDEX IF NOT EXISTS nodes_live_fold_candidate ON nodes(label, valid_to) WHERE valid_to IS NULL AND json_type(attrs_json, '$.foldCandidate') IS NOT NULL;
16354
+ -- Render impressions, one row per node id (exposure-attrs.ts): a render bumps
16355
+ -- ~40 of these, which in the node row rewrote each whole row and its indexes.
16356
+ CREATE TABLE IF NOT EXISTS node_exposure (
16357
+ id TEXT PRIMARY KEY,
16358
+ attrs_json TEXT NOT NULL DEFAULT '{}'
16359
+ ) WITHOUT ROWID;
16360
+ -- The semantic-maintenance sweeps' own filters (closed, asked, hinted): each read
16361
+ -- every Problem row (~30 MB on a 1.1 GB store) for the few hundred that match,
16362
+ -- several times a run, and a run follows most writes (2026-10-20).
16363
+ CREATE INDEX IF NOT EXISTS nodes_live_resolved ON nodes(label, valid_to) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.resolvedAt') IS NOT NULL;
16364
+ CREATE INDEX IF NOT EXISTS nodes_live_fix_candidate ON nodes(label, valid_to) WHERE valid_to IS NULL AND json_type(attrs_json, '$.fixCandidateAt') IS NOT NULL;
16365
+ CREATE INDEX IF NOT EXISTS nodes_live_anchor_hint ON nodes(label, valid_to) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.anchorHint') IS NOT NULL;
15255
16366
  CREATE INDEX IF NOT EXISTS nodes_session_evidence ON nodes(id) WHERE ${SESSION_EVIDENCE_PREDICATE};
16367
+ -- Rows carrying a calibration counter, any version: the logical nodes wm
16368
+ -- calibration can count (~3k), found without reading every citable row.
16369
+ CREATE INDEX IF NOT EXISTS nodes_wm_counted ON nodes(logical_id, version, valid_to, label, ${WM_CALIBRATION_KEYS.map(wmCounterSql).join(", ")}) WHERE ${WM_COUNTED_PREDICATE};
15256
16370
  CREATE INDEX IF NOT EXISTS edges_session_evidence ON edges(id) WHERE ${SESSION_EVIDENCE_PREDICATE};
15257
16371
  -- Covering + partial index for labelFingerprint over the instance labels: the
15258
16372
  -- live-push probe runs it per workspace on a timer, and each hit used to read
@@ -15266,31 +16380,70 @@ CREATE INDEX IF NOT EXISTS nodes_instance_fingerprint_v2 ON nodes(label, last_up
15266
16380
  CREATE INDEX IF NOT EXISTS nodes_live_page_rank ON nodes(valid_to, page_rank);
15267
16381
  CREATE INDEX IF NOT EXISTS nodes_logical ON nodes(logical_id, version);
15268
16382
  CREATE INDEX IF NOT EXISTS edges_live ON edges(valid_to);
16383
+ -- Newest live edges first, so a "recent N matching edges" read stops early instead
16384
+ -- of matching and sorting every live edge (the hourly liveness check: 163 MB \u2192
16385
+ -- 69 MB). Named by recentEdgeAttrCoverage; the planner keeps edges_live otherwise.
16386
+ CREATE INDEX IF NOT EXISTS edges_live_created ON edges(created_at) WHERE valid_to IS NULL;
16387
+ -- Live prior-tag edges by recency: the liveness corroboration check reads the
16388
+ -- newest 100 of ~300, and walking all live edges newest-first to find them read
16389
+ -- 69 MB. Its predicate is the one recentEdgeAttrCoverage uses for that marker.
16390
+ CREATE INDEX IF NOT EXISTS edges_live_prior_tag ON edges(created_at) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.priorTag') = 1;
15269
16391
  -- Expression index on the per-node relPath (stored in attrs_json) so the
15270
16392
  -- incremental reindex can fetch just the changed files' symbols instead of
15271
16393
  -- scanning every versioned node. Partial: only live rows are ever queried.
15272
16394
  CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.relPath')) WHERE valid_to IS NULL;
16395
+ -- Symbols by qname and doc comments by the symbol they attach to: the drain's
16396
+ -- symbol lexicon resolves the names in the text it ships one at a time, instead
16397
+ -- of reading every Function/Method/Class/Interface/Const and Comment row on each
16398
+ -- live push (~300 MB of a 1.1 GB store per push, 2026-10-20).
16399
+ CREATE INDEX IF NOT EXISTS nodes_live_qname ON nodes(json_extract(attrs_json, '$.qname')) WHERE valid_to IS NULL;
16400
+ CREATE INDEX IF NOT EXISTS nodes_live_attached_to ON nodes(json_extract(attrs_json, '$.attachedTo')) WHERE valid_to IS NULL AND label = 'Comment';
16401
+ -- The attrs the cloud drain gates every instance row on, so it reads them from
16402
+ -- the index alone: pulled cloud rows are most of these labels (7,105 of 7,708
16403
+ -- Solutions on the live store) at ~10 KB of attrs each, and five full scans for
16404
+ -- two attrs read ~220 MB per push to find the few local rows that can ship.
16405
+ -- created_at leads so the index order is findNodeFieldsByLabel's.
16406
+ CREATE INDEX IF NOT EXISTS nodes_live_instance_gate ON nodes(label, created_at, json_extract(attrs_json, '$.source'), json_extract(attrs_json, '$.privateFinding'), json_extract(attrs_json, '$.resolvedAs'), id) WHERE valid_to IS NULL AND label IN ('Problem', 'Vulnerability', 'Solution', 'RootCause');
16407
+ -- Closed rows by relPath, for reviveReobserved (every incremental reindex): the
16408
+ -- live-only index above cannot serve it, so it scanned the whole table (~275 MB
16409
+ -- of a 1.1 GB store per reindex, 2026-10-20).
16410
+ CREATE INDEX IF NOT EXISTS nodes_closed_relpath ON nodes(json_extract(attrs_json, '$.relPath'), last_updated_at) WHERE valid_to IS NOT NULL;
15273
16411
  -- Live rows by name, for looking up the few symbols a statement mentions
15274
16412
  -- instead of hydrating every code symbol per minted problem.
15275
16413
  CREATE INDEX IF NOT EXISTS nodes_live_description ON nodes(description, label) WHERE valid_to IS NULL;
16414
+ -- The six single-attr lookups below are partial on the attr being present: each
16415
+ -- is read only by equality (or IN), which implies it. Keyed on NULL for the ~95%
16416
+ -- of rows that lack the attr, every write to such a row's attrs rewrote its NULL
16417
+ -- entry: ~25 KB per position-only symbol rewrite on a 1.1 GB store (2026-10-21).
16418
+ DROP INDEX IF EXISTS nodes_anchor_hint_path;
16419
+ DROP INDEX IF EXISTS nodes_command_signature;
16420
+ DROP INDEX IF EXISTS nodes_diag_command;
16421
+ DROP INDEX IF EXISTS nodes_lsp_rel_path;
16422
+ DROP INDEX IF EXISTS nodes_cloud_node_id;
16423
+ DROP INDEX IF EXISTS nodes_enrichment_pending;
15276
16424
  -- Problems by the file their anchor hint names: the harvest upgrades hints for
15277
16425
  -- every edited file of every turn, and a scan parses every Problem's attrs.
15278
- CREATE INDEX IF NOT EXISTS nodes_anchor_hint_path ON nodes(json_extract(attrs_json, '$.anchorHint.path')) WHERE valid_to IS NULL;
16426
+ CREATE INDEX IF NOT EXISTS nodes_anchor_hint_path_v2 ON nodes(json_extract(attrs_json, '$.anchorHint.path')) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.anchorHint.path') IS NOT NULL;
15279
16427
  -- Problems by the command / checker / file that opened them. The generalizer
15280
16428
  -- looked these up per shell or tool EVENT by hydrating every Problem: ~12s of
15281
16429
  -- every minute of daemon main-thread time on a 3.4k-Problem store (2026-10-05).
15282
- CREATE INDEX IF NOT EXISTS nodes_command_signature ON nodes(json_extract(attrs_json, '$.commandSignature')) WHERE valid_to IS NULL;
15283
- CREATE INDEX IF NOT EXISTS nodes_diag_command ON nodes(json_extract(attrs_json, '$.diagCommand')) WHERE valid_to IS NULL;
15284
- CREATE INDEX IF NOT EXISTS nodes_lsp_rel_path ON nodes(json_extract(attrs_json, '$.lspRelPath')) WHERE valid_to IS NULL;
16430
+ CREATE INDEX IF NOT EXISTS nodes_command_signature_v2 ON nodes(json_extract(attrs_json, '$.commandSignature')) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.commandSignature') IS NOT NULL;
16431
+ CREATE INDEX IF NOT EXISTS nodes_diag_command_v2 ON nodes(json_extract(attrs_json, '$.diagCommand')) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.diagCommand') IS NOT NULL;
16432
+ CREATE INDEX IF NOT EXISTS nodes_lsp_rel_path_v2 ON nodes(json_extract(attrs_json, '$.lspRelPath')) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.lspRelPath') IS NOT NULL;
15285
16433
  -- Local nodes by the cloud twin their upload receipt recorded. Every burst /
15286
16434
  -- search fusion folds the Network copy of the caller's own knowledge back onto
15287
16435
  -- the local node; unindexed, the reverse lookup was a ~300ms attrs scan per call.
15288
- CREATE INDEX IF NOT EXISTS nodes_cloud_node_id ON nodes(json_extract(attrs_json, '$.cloudNodeId')) WHERE valid_to IS NULL;
16436
+ CREATE INDEX IF NOT EXISTS nodes_cloud_node_id_v2 ON nodes(json_extract(attrs_json, '$.cloudNodeId')) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.cloudNodeId') IS NOT NULL;
15289
16437
  -- Nodes still waiting on a root-cause ask. The MCP server checks this on EVERY
15290
16438
  -- tool response (the enrichment piggyback), so it must not parse every
15291
- -- Solution's and Problem's attrs per call. Not partial: the Problem pull reads
16439
+ -- Solution's and Problem's attrs per call. Any version: the Problem pull reads
15292
16440
  -- closed versions too.
15293
- CREATE INDEX IF NOT EXISTS nodes_enrichment_pending ON nodes(json_extract(attrs_json, '$.enrichmentPending'));
16441
+ CREATE INDEX IF NOT EXISTS nodes_enrichment_pending_v2 ON nodes(json_extract(attrs_json, '$.enrichmentPending')) WHERE json_extract(attrs_json, '$.enrichmentPending') IS NOT NULL;
16442
+ -- Revisit flags, any label, any version: listNeedsRevisit's as-of read runs on
16443
+ -- every context render and otherwise json-checks every row (10.3 s on a 324k-row
16444
+ -- store, to return zero flags). The predicate must stay textually identical to
16445
+ -- nodesAsOfWhereTrue's or the planner will not use it.
16446
+ CREATE INDEX IF NOT EXISTS nodes_revisit_true ON nodes(valid_from) WHERE json_type(attrs_json, '$.revisit') = 'true';
15294
16447
  `;
15295
16448
  function encodeEmbedding(emb) {
15296
16449
  if (!emb || emb.length === 0) return null;
@@ -15351,12 +16504,39 @@ function rowToEdge(r) {
15351
16504
  }
15352
16505
  var FINGERPRINT_INDEXED_LABELS = ["Problem", "Vulnerability", "Solution", "RootCause"];
15353
16506
  var NODE_LABELS_CHUNK = 256;
16507
+ var NODE_UPSERT_COLUMNS = [
16508
+ "label",
16509
+ "description",
16510
+ "extraction_confidence",
16511
+ "embedding_json",
16512
+ "embedding_blob",
16513
+ "cumulative_surprise",
16514
+ "peak_surprise",
16515
+ "cumulative_hits",
16516
+ "last_updated_at",
16517
+ "memory_tier",
16518
+ "page_rank",
16519
+ "is_landmark",
16520
+ "community",
16521
+ "stability",
16522
+ "last_reinforced_at_seq",
16523
+ "attrs_json"
16524
+ ];
16525
+ var EDGE_UPSERT_COLUMNS = ["confidence", "last_seen_at", "nav_successes", "nav_failures", "valid_to", "attrs_json"];
16526
+ function sameSqlValue(a, b) {
16527
+ if (a === b || a == null && b == null) return true;
16528
+ if (a instanceof Uint8Array && b instanceof Uint8Array) return Buffer.compare(a, b) === 0;
16529
+ return false;
16530
+ }
16531
+ var PRIOR_TAG_MARKER = '"priorTag":true';
15354
16532
  var SqliteGraphStore = class {
15355
16533
  db;
15356
16534
  // Typed edge lookups build their SQL from the number of types asked for, and
15357
16535
  // used to prepare it on every call: ~600ms of a 2.5s drain build on a live
15358
16536
  // 225k-node store was statement compilation alone. Keyed by the SQL text.
15359
16537
  typedEdgeStmts = /* @__PURE__ */ new Map();
16538
+ /** Narrow merge updates, keyed by table and the columns they set. */
16539
+ changedColumnStmts = /* @__PURE__ */ new Map();
15360
16540
  stmts;
15361
16541
  /** Unified graph-mutation counter (see GraphStore.mutationCount). Bumped on
15362
16542
  * every node/edge upsert — code-index and capture alike — so the consolidation
@@ -15375,6 +16555,8 @@ var SqliteGraphStore = class {
15375
16555
  initialPageRankScale;
15376
16556
  constructor(opts) {
15377
16557
  this.db = openDatabase(opts.path);
16558
+ this.db.exec("PRAGMA cache_size = -32768");
16559
+ this.db.exec("PRAGMA mmap_size = 2147483648");
15378
16560
  this.db.exec(SCHEMA_SQL);
15379
16561
  const versionRow = this.db.prepare("SELECT version FROM schema_version LIMIT 1").get();
15380
16562
  if (!versionRow) {
@@ -15391,12 +16573,15 @@ var SqliteGraphStore = class {
15391
16573
  installEditPlaceholderRetirement(this.db);
15392
16574
  installCloudAttrCompaction(this.db);
15393
16575
  installHistoryAttrSlim(this.db);
16576
+ installTextIndex(this.db);
15394
16577
  if (!this.db.prepare("SELECT value FROM store_meta WHERE key = ?").get(SESSION_REPAIR_KEY)) {
15395
16578
  this.db.transaction(() => {
15396
16579
  this.repairSessionAliases();
15397
16580
  this.db.prepare("INSERT OR REPLACE INTO store_meta (key, value) VALUES (?, 'done')").run(SESSION_REPAIR_KEY);
15398
16581
  });
15399
16582
  }
16583
+ installLineageIntegrityRepair(this.db);
16584
+ installChangeFeed(this.db);
15400
16585
  this.stmts = {
15401
16586
  insertNode: this.db.prepare(`
15402
16587
  INSERT INTO nodes (
@@ -15435,7 +16620,19 @@ var SqliteGraphStore = class {
15435
16620
  last_reinforced_at_seq = excluded.last_reinforced_at_seq,
15436
16621
  attrs_json = excluded.attrs_json
15437
16622
  `),
15438
- getNode: this.db.prepare("SELECT * FROM nodes WHERE id = ?"),
16623
+ nodeUpsertTarget: this.db.prepare(`SELECT rowid AS rid, ${NODE_UPSERT_COLUMNS.join(", ")} FROM nodes WHERE id = ?`),
16624
+ edgeUpsertTarget: this.db.prepare(`SELECT rowid AS rid, ${EDGE_UPSERT_COLUMNS.join(", ")} FROM edges WHERE from_id = ? AND type = ? AND to_id = ?`),
16625
+ getNode: this.db.prepare("SELECT nodes.*, (SELECT x.attrs_json FROM node_exposure x WHERE x.id = nodes.id) AS exposure_json FROM nodes WHERE id = ?"),
16626
+ exposureOf: this.db.prepare("SELECT attrs_json FROM node_exposure WHERE id = ?"),
16627
+ writeExposure: this.db.prepare("INSERT INTO node_exposure(id, attrs_json) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET attrs_json = excluded.attrs_json"),
16628
+ lastUpdatedOf: this.db.prepare("SELECT last_updated_at AS t, attrs_json FROM nodes WHERE id = ?"),
16629
+ labelOf: this.db.prepare("SELECT label FROM nodes WHERE id = ?"),
16630
+ nodeContent: this.db.prepare(
16631
+ "SELECT label, description, extraction_confidence, extraction_source, stability, valid_to, attrs_json FROM nodes WHERE id = ?"
16632
+ ),
16633
+ edgeContent: this.db.prepare(
16634
+ "SELECT confidence, valid_to, attrs_json FROM edges WHERE from_id = ? AND type = ? AND to_id = ?"
16635
+ ),
15439
16636
  // Single-column fast paths for the nightly pass — writing 100k pageRank
15440
16637
  // values through the 18-column COALESCE updateNode took ~30s; these are ~10x
15441
16638
  // cheaper (one bound column, no COALESCE evaluation per row).
@@ -15592,6 +16789,7 @@ var SqliteGraphStore = class {
15592
16789
  "INSERT INTO store_meta (key, value) VALUES ('ingest_seq', :v) ON CONFLICT(key) DO UPDATE SET value = excluded.value"
15593
16790
  )
15594
16791
  };
16792
+ this.moveExposureToSideRows();
15595
16793
  const seqRow = this.db.prepare("SELECT value FROM store_meta WHERE key = 'ingest_seq'").get();
15596
16794
  this.ingestSeq = seqRow ? Number(seqRow.value) : 0;
15597
16795
  }
@@ -15607,6 +16805,93 @@ var SqliteGraphStore = class {
15607
16805
  AND (t.id IS NULL OR t.valid_to IS NOT NULL)
15608
16806
  `).all();
15609
16807
  }
16808
+ strandedSymbolsForFiles(fileIds) {
16809
+ const out = [];
16810
+ for (let i = 0; i < fileIds.length; i += 500) {
16811
+ const slice = fileIds.slice(i, i + 500);
16812
+ if (slice.length === 0) continue;
16813
+ out.push(...this.db.prepare(`
16814
+ SELECT f.id AS fileId, e.to_id AS symbolId
16815
+ FROM nodes f CROSS JOIN edges e ON e.from_id = f.id
16816
+ LEFT JOIN nodes t ON t.id = e.to_id
16817
+ WHERE f.id IN (${slice.map(() => "?").join(",")}) AND f.label = 'File' AND f.valid_to IS NULL AND e.valid_to IS NULL
16818
+ AND e.type IN ('DEFINES', 'CONTAINS')
16819
+ AND (t.id IS NULL OR t.valid_to IS NOT NULL)
16820
+ `).all(...slice));
16821
+ }
16822
+ return out;
16823
+ }
16824
+ firstLiveByNameOrQname(labels, name) {
16825
+ if (labels.length === 0) return null;
16826
+ const inLabels = labels.map(() => "?").join(",");
16827
+ const probe = (index, match) => this.db.prepare(
16828
+ `SELECT rowid AS rid, created_at AS at, label, description, json_extract(attrs_json, '$.qname') AS qname FROM nodes INDEXED BY ${index}
16829
+ WHERE ${match} = ? AND label IN (${inLabels}) AND valid_to IS NULL`
16830
+ ).all(name, ...labels);
16831
+ const rank = (r) => [labels.indexOf(r.label), r.at, r.rid];
16832
+ const before = (a, b) => a[0] < b[0] || a[0] === b[0] && (a[1] < b[1] || a[1] === b[1] && a[2] < b[2]);
16833
+ let best;
16834
+ for (const r of [...probe("nodes_live_description", "description"), ...probe("nodes_live_qname", "json_extract(attrs_json, '$.qname')")]) {
16835
+ if (!best || before(rank(r), rank(best))) best = r;
16836
+ }
16837
+ return best ? { label: best.label, description: best.description, qname: typeof best.qname === "string" ? best.qname : null } : null;
16838
+ }
16839
+ instanceGateRows(label) {
16840
+ return this.db.prepare(
16841
+ `SELECT id, json_extract(attrs_json, '$.source') AS source, json_extract(attrs_json, '$.privateFinding') AS privateFinding,
16842
+ json_extract(attrs_json, '$.resolvedAs') AS resolvedAs
16843
+ FROM nodes INDEXED BY nodes_live_instance_gate
16844
+ WHERE label IN ('Problem', 'Vulnerability', 'Solution', 'RootCause') AND label = ? AND valid_to IS NULL
16845
+ ORDER BY label, created_at, rowid`
16846
+ ).all(label);
16847
+ }
16848
+ wmCalibrationRows(labels) {
16849
+ const counted = this.db.prepare(
16850
+ `SELECT rowid AS rid, logical_id, version, valid_to, label, ${WM_CALIBRATION_KEYS.map((k, i) => `${wmCounterSql(k)} AS c${i}`).join(", ")}
16851
+ FROM nodes INDEXED BY nodes_wm_counted WHERE ${WM_COUNTED_PREDICATE}`
16852
+ ).all();
16853
+ const exposed = this.db.prepare(
16854
+ `SELECT n.rowid AS rid, n.logical_id, n.version, n.valid_to, n.label, x.attrs_json AS exposure
16855
+ FROM node_exposure x JOIN nodes n ON n.id = x.id`
16856
+ ).all();
16857
+ const others = this.db.prepare(
16858
+ `SELECT rowid AS rid, logical_id, version, valid_to, label FROM nodes WHERE rowid IN (
16859
+ SELECT n.rowid FROM nodes AS n INDEXED BY nodes_logical
16860
+ WHERE n.logical_id IN (SELECT logical_id FROM nodes INDEXED BY nodes_wm_counted WHERE ${WM_COUNTED_PREDICATE}
16861
+ UNION SELECT n2.logical_id FROM node_exposure x2 JOIN nodes n2 ON n2.id = x2.id)
16862
+ EXCEPT SELECT rowid FROM nodes INDEXED BY nodes_wm_counted WHERE ${WM_COUNTED_PREDICATE}
16863
+ EXCEPT SELECT n3.rowid FROM node_exposure x3 JOIN nodes n3 ON n3.id = x3.id)`
16864
+ ).all();
16865
+ const byRid = /* @__PURE__ */ new Map();
16866
+ for (const r of [...counted, ...others]) {
16867
+ const attrs = {};
16868
+ WM_CALIBRATION_KEYS.forEach((k, i) => {
16869
+ const v = r[`c${i}`];
16870
+ if (typeof v === "number") attrs[k] = v;
16871
+ });
16872
+ byRid.set(r.rid, { row: r, attrs });
16873
+ }
16874
+ for (const r of exposed) {
16875
+ const side = JSON.parse(r.exposure);
16876
+ const entry = byRid.get(r.rid) ?? { row: r, attrs: {} };
16877
+ for (const k of WM_CALIBRATION_KEYS) if (typeof side[k] === "number" && Number.isFinite(side[k])) entry.attrs[k] = side[k];
16878
+ byRid.set(r.rid, entry);
16879
+ }
16880
+ const order = new Map(labels.map((l, i) => [l, i]));
16881
+ return [...byRid.values()].filter(({ row }) => order.has(row.label)).sort((a, b) => order.get(a.row.label) - order.get(b.row.label) || a.row.rid - b.row.rid).map(({ row, attrs }) => ({ logicalId: row.logical_id, version: Number(row.version), validTo: row.valid_to ?? null, attrs }));
16882
+ }
16883
+ liveCommentsAttachedTo(name) {
16884
+ return this.db.prepare(
16885
+ `SELECT json_extract(attrs_json, '$.kind') AS kind, json_extract(attrs_json, '$.text') AS text, description FROM nodes INDEXED BY nodes_live_attached_to
16886
+ WHERE label = 'Comment' AND valid_to IS NULL AND json_extract(attrs_json, '$.attachedTo') = ? ORDER BY created_at, rowid`
16887
+ ).all(name);
16888
+ }
16889
+ strandedEdges() {
16890
+ return countStrandedEdges(this.db);
16891
+ }
16892
+ liveHeadConflicts() {
16893
+ return countLiveHeadConflicts(this.db);
16894
+ }
15610
16895
  fileIndexStates(ids) {
15611
16896
  if (ids.length === 0) return [];
15612
16897
  if (ids.length > 500) throw new RangeError("fileIndexStates accepts at most 500 ids per read");
@@ -15705,7 +16990,7 @@ var SqliteGraphStore = class {
15705
16990
  }
15706
16991
  mergeNode(node) {
15707
16992
  this.mutations++;
15708
- this.stmts.insertNode.run({
16993
+ const values = {
15709
16994
  id: node.id,
15710
16995
  label: node.label,
15711
16996
  description: node.description,
@@ -15731,8 +17016,37 @@ var SqliteGraphStore = class {
15731
17016
  logical_id: node.logicalId ?? node.id,
15732
17017
  created_at_seq: node.createdAtSeq ?? null,
15733
17018
  last_reinforced_at_seq: node.lastReinforcedAtSeq ?? null,
15734
- attrs_json: JSON.stringify(node.attrs ?? {})
15735
- });
17019
+ attrs_json: this.nodeAttrsJson(node.id, node.attrs ?? {})
17020
+ };
17021
+ const current = this.stmts.nodeUpsertTarget.get(node.id);
17022
+ if (current) this.writeChangedColumns("nodes", NODE_UPSERT_COLUMNS, current, values);
17023
+ else this.stmts.insertNode.run(values);
17024
+ }
17025
+ /** What the upsert's ON CONFLICT would write over `current`, setting only the
17026
+ * columns whose value moves (NODE_UPSERT_COLUMNS says why). */
17027
+ writeChangedColumns(table, columns2, current, next) {
17028
+ const changed = columns2.filter((c) => !sameSqlValue(current[c], next[c]));
17029
+ if (changed.length === 0) return;
17030
+ const key = `${table}:${changed.join(",")}`;
17031
+ let stmt2 = this.changedColumnStmts.get(key);
17032
+ if (!stmt2) {
17033
+ stmt2 = this.db.prepare(`UPDATE ${table} SET ${changed.map((c) => `${c} = :${c}`).join(", ")} WHERE rowid = :rid`);
17034
+ this.changedColumnStmts.set(key, stmt2);
17035
+ }
17036
+ stmt2.run({ rid: current["rid"], ...Object.fromEntries(changed.map((c) => [c, next[c]])) });
17037
+ }
17038
+ /** mergeNode, unless the live row already holds this content. Content is what
17039
+ * the columns a merge rewrites from the producer (label, description, attrs,
17040
+ * confidence, stability);
17041
+ * rank, community, landmark, surprise, hits, tier and the vector belong to
17042
+ * the passes that compute them, and a skipped write leaves them as they are
17043
+ * where a merge would have reset them to the producer's defaults. Returns
17044
+ * whether it wrote. */
17045
+ mergeNodeIfChanged(node) {
17046
+ const row = this.stmts.nodeContent.get(node.id);
17047
+ if (row && row.valid_to === null && row.label === node.label && row.description === node.description && row.extraction_confidence === node.extractionConfidence && row.stability === node.stability && !EXPOSURE_ATTRS.some((k) => node.attrs?.[k] !== void 0) && row.attrs_json === JSON.stringify(node.attrs ?? {})) return false;
17048
+ this.mergeNode(node);
17049
+ return true;
15736
17050
  }
15737
17051
  updateNode(id, patch) {
15738
17052
  const hasEmbedding = patch.embedding !== void 0 && patch.embedding.length > 0;
@@ -15757,7 +17071,37 @@ var SqliteGraphStore = class {
15757
17071
  logical_id: patch.logicalId ?? null,
15758
17072
  created_at_seq: patch.createdAtSeq ?? null,
15759
17073
  last_reinforced_at_seq: patch.lastReinforcedAtSeq ?? null,
15760
- attrs_json: patch.attrs ? JSON.stringify(patch.attrs) : null
17074
+ attrs_json: patch.attrs ? this.nodeAttrsJson(id, patch.attrs) : null
17075
+ });
17076
+ }
17077
+ /**
17078
+ * The node-row attrs of a write, after handing any exposure keys to the side
17079
+ * row (exposure-attrs.ts). `getNode` returns exposure merged into attrs, so a
17080
+ * read-modify-write carries it; kept in the row it would re-bloat the row and
17081
+ * drift from the side row. Unchanged exposure costs no write.
17082
+ */
17083
+ nodeAttrsJson(id, attrs) {
17084
+ if (!EXPOSURE_ATTRS.some((k) => attrs[k] !== void 0)) return JSON.stringify(attrs);
17085
+ const current = this.stmts.exposureOf.get(id);
17086
+ const next = JSON.stringify({ ...current ? JSON.parse(current.attrs_json) : {}, ...pickExposure(attrs) });
17087
+ if (current?.attrs_json !== next) this.stmts.writeExposure.run(id, next);
17088
+ const rest = { ...attrs };
17089
+ for (const k of EXPOSURE_ATTRS) delete rest[k];
17090
+ return JSON.stringify(rest);
17091
+ }
17092
+ /** One-time: exposure keys written to node rows before the side table existed
17093
+ * move to it. Only rows carrying a counter can carry exposure, and those are
17094
+ * what nodes_wm_counted holds, so this reads the index, not the table. */
17095
+ moveExposureToSideRows() {
17096
+ if (this.db.prepare("SELECT value FROM store_meta WHERE key = ?").get(EXPOSURE_SIDE_KEY)) return;
17097
+ const rows = this.db.prepare(`SELECT id, attrs_json FROM nodes INDEXED BY nodes_wm_counted WHERE ${WM_COUNTED_PREDICATE}`).all();
17098
+ const move = this.db.prepare("UPDATE nodes SET attrs_json = ? WHERE id = ?");
17099
+ this.db.transaction(() => {
17100
+ for (const r of rows) {
17101
+ const stripped = this.nodeAttrsJson(r.id, JSON.parse(r.attrs_json));
17102
+ if (stripped !== r.attrs_json) move.run(stripped, r.id);
17103
+ }
17104
+ this.db.prepare("INSERT OR REPLACE INTO store_meta(key, value) VALUES (?, 'done')").run(EXPOSURE_SIDE_KEY);
15761
17105
  });
15762
17106
  }
15763
17107
  /** Fast single-column writes for the nightly pass (avoid the 18-column COALESCE
@@ -15768,26 +17112,30 @@ var SqliteGraphStore = class {
15768
17112
  setLandmark(id, isLandmark) {
15769
17113
  this.stmts.setLandmark.run({ id, v: isLandmark ? 1 : 0 });
15770
17114
  }
15771
- setLocalCommunity(id, community, column) {
15772
- this.stmts.setLocalCommunity.run({ id, v: community, col: column ? 1 : 0 });
17115
+ setLocalCommunity(id, community, column2) {
17116
+ this.stmts.setLocalCommunity.run({ id, v: community, col: column2 ? 1 : 0 });
15773
17117
  }
15774
17118
  fieldStmts = /* @__PURE__ */ new Map();
15775
17119
  findNodeFieldsByLabel(label, attrKeys) {
15776
17120
  for (const k of attrKeys) if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k)) throw new Error(`findNodeFieldsByLabel: bad attr key ${k}`);
15777
17121
  const cacheKey = attrKeys.join(",");
15778
- let stmt = this.fieldStmts.get(cacheKey);
15779
- if (!stmt) {
15780
- const cols = attrKeys.map((k, i) => `, json_extract(attrs_json, '$.${k}') AS a${i}`).join("");
15781
- stmt = this.db.prepare(
15782
- `SELECT id, description, community, extraction_confidence${cols} FROM nodes WHERE label = ? AND valid_to IS NULL`
17122
+ let stmt2 = this.fieldStmts.get(cacheKey);
17123
+ if (!stmt2) {
17124
+ const cols = attrKeys.map((k, i) => `, ${attrValueSql(`'$.${k}'`)} AS a${i}, json_type(attrs_json, '$.${k}') AS t${i}`).join("");
17125
+ stmt2 = this.db.prepare(
17126
+ // Ordered, not left to the planner: readers take "first row wins" (the
17127
+ // symbol lexicon's name winner and doc pick), and the order otherwise
17128
+ // followed whichever partial index the planner chose. nodes_live_created
17129
+ // serves this order without a sort.
17130
+ `SELECT id, description, community, extraction_confidence${cols} FROM nodes WHERE label = ? AND valid_to IS NULL ORDER BY created_at, rowid`
15783
17131
  );
15784
- this.fieldStmts.set(cacheKey, stmt);
17132
+ this.fieldStmts.set(cacheKey, stmt2);
15785
17133
  }
15786
- return stmt.all(label).map((r) => {
17134
+ return stmt2.all(label).map((r) => {
15787
17135
  const attrs = {};
15788
17136
  attrKeys.forEach((k, i) => {
15789
- const v = r[`a${i}`];
15790
- if (v !== null && v !== void 0) attrs[k] = v;
17137
+ const v = decodeAttr(r[`a${i}`], r[`t${i}`]);
17138
+ if (v !== void 0) attrs[k] = v;
15791
17139
  });
15792
17140
  return {
15793
17141
  id: r["id"],
@@ -15798,6 +17146,15 @@ var SqliteGraphStore = class {
15798
17146
  };
15799
17147
  });
15800
17148
  }
17149
+ selectNodes(q) {
17150
+ const { sql, params } = compileNodeSelect(q);
17151
+ const rows = this.db.prepare(sql).all(...params);
17152
+ return rows.map((r) => decodeSelectedRow(r, q.attrs ?? [], q.withEmbedding ? decodeEmbedding : void 0));
17153
+ }
17154
+ countNodes(q) {
17155
+ const { where, params } = compileNodeWhere(q);
17156
+ return Number(this.db.prepare(`SELECT COUNT(*) AS n FROM ${nodeSelectFrom(q)} WHERE ${where}`).get(...params).n);
17157
+ }
15801
17158
  setCommunity(id, community) {
15802
17159
  this.stmts.setCommunity.run({ id, v: community });
15803
17160
  }
@@ -15810,7 +17167,7 @@ var SqliteGraphStore = class {
15810
17167
  return;
15811
17168
  }
15812
17169
  this.mutations++;
15813
- this.stmts.insertEdge.run({
17170
+ const values = {
15814
17171
  id: edge.id,
15815
17172
  from_id: edge.from,
15816
17173
  to_id: edge.to,
@@ -15824,8 +17181,33 @@ var SqliteGraphStore = class {
15824
17181
  valid_from: edge.validFrom ?? edge.createdAt,
15825
17182
  valid_to: edge.validTo ?? null,
15826
17183
  attrs_json: JSON.stringify(edge.attrs ?? {})
17184
+ };
17185
+ const current = this.stmts.edgeUpsertTarget.get(edge.from, edge.type, edge.to);
17186
+ if (!current) {
17187
+ this.stmts.insertEdge.run(values);
17188
+ return;
17189
+ }
17190
+ this.writeChangedColumns("edges", EDGE_UPSERT_COLUMNS, current, {
17191
+ ...values,
17192
+ confidence: Math.max(current["confidence"], edge.confidence),
17193
+ valid_to: null
15827
17194
  });
15828
17195
  }
17196
+ /** mergeEdge, unless the live edge already holds this content: same attrs, and
17197
+ * a confidence the upsert's MAX would keep. Its navigation posteriors and
17198
+ * last-seen stamp stay as they are where a merge would have reset them. The
17199
+ * ontology gate runs first either way, so refusals are recorded as before.
17200
+ * Returns whether it wrote (or refused). */
17201
+ mergeEdgeIfChanged(edge) {
17202
+ if (!this.edgeRuleViolation(edge)) {
17203
+ const row = this.stmts.edgeContent.get(edge.from, edge.type, edge.to);
17204
+ if (row && row.valid_to === null && row.confidence >= edge.confidence && row.attrs_json === JSON.stringify(edge.attrs ?? {})) {
17205
+ return false;
17206
+ }
17207
+ }
17208
+ this.mergeEdge(edge);
17209
+ return true;
17210
+ }
15829
17211
  /** EDGE_RULES check against the stored endpoints' labels, with the LOCAL
15830
17212
  * overlay consulted first. Returns the reason string on a documented-
15831
17213
  * forbidden combination, else null. Point lookups on the id PK — negligible
@@ -15870,11 +17252,13 @@ var SqliteGraphStore = class {
15870
17252
  }
15871
17253
  }
15872
17254
  edgeRuleViolation(edge) {
15873
- return localEdgeViolation(
15874
- this.getNode(edge.from)?.label,
15875
- edge.type,
15876
- this.getNode(edge.to)?.label
15877
- );
17255
+ return localEdgeViolation(this.labelOf(edge.from), edge.type, this.labelOf(edge.to));
17256
+ }
17257
+ /** An endpoint's label, any version, as `getNode(id)?.label` reads it. Every
17258
+ * edge write checks both endpoints, and hydrating them (attrs + vector) to
17259
+ * read one leading column was most of an incremental reindex's reads. */
17260
+ labelOf(id) {
17261
+ return this.stmts.labelOf.get(id)?.label;
15878
17262
  }
15879
17263
  updateEdge(id, patch) {
15880
17264
  this.stmts.updateEdge.run({
@@ -15890,18 +17274,38 @@ var SqliteGraphStore = class {
15890
17274
  }
15891
17275
  getNode(id) {
15892
17276
  const row = this.stmts.getNode.get(id);
15893
- return row ? rowToNode(row) : null;
17277
+ if (!row) return null;
17278
+ const node = rowToNode(row);
17279
+ if (row.exposure_json) Object.assign(node.attrs, JSON.parse(row.exposure_json));
17280
+ return node;
17281
+ }
17282
+ /** A node's render exposure: the side row's keys over the node row's own
17283
+ * (exposure-attrs.ts), plus the node's `lastUpdatedAt` (the "shown since
17284
+ * touch" counter is relative to it) — without reading the node's attrs. */
17285
+ exposureOf(id) {
17286
+ const row = this.stmts.lastUpdatedOf.get(id);
17287
+ if (!row) return null;
17288
+ const side = this.stmts.exposureOf.get(id);
17289
+ return {
17290
+ attrs: { ...pickExposure(JSON.parse(row.attrs_json)), ...side ? JSON.parse(side.attrs_json) : {} },
17291
+ lastUpdatedAt: row.t
17292
+ };
17293
+ }
17294
+ /** Replace a node's exposure (only EXPOSURE_ATTRS keys belong here). Touches
17295
+ * neither the node row nor its indexes, nor the change feed. */
17296
+ writeExposure(id, attrs) {
17297
+ this.stmts.writeExposure.run(id, JSON.stringify(attrs));
15894
17298
  }
15895
17299
  nodeLabels(ids) {
15896
17300
  const width = NODE_LABELS_CHUNK;
15897
- const stmt = this.typedEdgeStmt(
17301
+ const stmt2 = this.typedEdgeStmt(
15898
17302
  `SELECT id, label FROM nodes WHERE id IN (${Array.from({ length: width }, () => "?").join(",")})`
15899
17303
  );
15900
17304
  const labels = /* @__PURE__ */ new Map();
15901
17305
  for (let i = 0; i < ids.length; i += width) {
15902
17306
  const chunk = ids.slice(i, i + width);
15903
17307
  while (chunk.length < width) chunk.push(null);
15904
- for (const row of stmt.all(...chunk)) labels.set(row.id, row.label);
17308
+ for (const row of stmt2.all(...chunk)) labels.set(row.id, row.label);
15905
17309
  }
15906
17310
  return labels;
15907
17311
  }
@@ -15918,7 +17322,21 @@ var SqliteGraphStore = class {
15918
17322
  * per project per request; the materializing form held the daemon's event
15919
17323
  * loop for seconds at scale. */
15920
17324
  countNodesByLabel(label) {
15921
- return Number(this.stmts.countByLabel.get(label).n);
17325
+ return this.countAt(`label:${label}`, [nodeScope(label)], () => Number(this.stmts.countByLabel.get(label).n));
17326
+ }
17327
+ /** Counts asked for on every status poll (/health, `errata status`, the
17328
+ * dashboards): a live-node COUNT(*) reads the whole liveness index, ~3 MB per
17329
+ * call on a 1.1 GB store, to recount a set that moves only on a write. Each is
17330
+ * kept with the change-feed reading of the rows it counts — triggers bump it on
17331
+ * every write from any connection, so a hit is exact, not a TTL guess. */
17332
+ counts = /* @__PURE__ */ new Map();
17333
+ countAt(key, scopes, count) {
17334
+ const seq = readChangeSeq(this.db, scopes);
17335
+ const hit = this.counts.get(key);
17336
+ if (hit && hit.seq === seq) return hit.n;
17337
+ const n = count();
17338
+ this.counts.set(key, { seq, n });
17339
+ return n;
15922
17340
  }
15923
17341
  /** Live rows for a label with attrs and creation stamps only — for scans that
15924
17342
  * read attrs and never the vector. `findNodesByLabel` decodes every embedding
@@ -15927,16 +17345,39 @@ var SqliteGraphStore = class {
15927
17345
  findNodeStampsByLabel(label, attrKey) {
15928
17346
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(attrKey)) throw new Error(`findNodeStampsByLabel: bad attr key ${attrKey}`);
15929
17347
  const rows = this.db.prepare(
15930
- `SELECT id, created_at, created_at_seq, json_type(attrs_json, '$.${attrKey}') IS NOT NULL AS has
17348
+ `SELECT id, created_at, created_at_seq, json_type(${attrDocSql(attrKey)}, '$.${attrKey}') IS NOT NULL AS has,
17349
+ COALESCE(json_extract(attrs_json, '$.source') = 'cloud', 0) AS cloud
15931
17350
  FROM nodes WHERE label = ? AND valid_to IS NULL`
15932
17351
  ).all(label);
15933
17352
  return rows.map((r) => ({
15934
17353
  id: r.id,
15935
17354
  createdAt: r.created_at,
15936
17355
  ...r.created_at_seq != null ? { createdAtSeq: r.created_at_seq } : {},
15937
- has: r.has === 1
17356
+ has: r.has === 1,
17357
+ cloud: r.cloud === 1
15938
17358
  }));
15939
17359
  }
17360
+ findNodeStampsByLabelAttrs(label, attrKeys) {
17361
+ for (const k of attrKeys) if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k)) throw new Error(`findNodeStampsByLabelAttrs: bad attr key ${k}`);
17362
+ const keys = [...new Set(attrKeys)];
17363
+ const rows = this.db.prepare(
17364
+ `SELECT id, created_at, created_at_seq,
17365
+ ${keys.map((k, i) => `json_type(${attrDocSql(k)}, '$.${k}') IS NOT NULL AS h${i}`).join(", ")}${keys.length ? "," : ""}
17366
+ COALESCE(json_extract(attrs_json, '$.source') = 'cloud', 0) AS cloud
17367
+ FROM nodes WHERE label = ? AND valid_to IS NULL`
17368
+ ).all(label);
17369
+ const out = new Map(keys.map((k) => [k, []]));
17370
+ for (const r of rows) {
17371
+ keys.forEach((k, i) => out.get(k).push({
17372
+ id: r.id,
17373
+ createdAt: r.created_at,
17374
+ ...r.created_at_seq != null ? { createdAtSeq: r.created_at_seq } : {},
17375
+ has: r[`h${i}`] === 1,
17376
+ cloud: r.cloud === 1
17377
+ }));
17378
+ }
17379
+ return out;
17380
+ }
15940
17381
  findNodeAttrsByLabel(label) {
15941
17382
  const rows = this.stmts.attrsByLabel.all(label);
15942
17383
  return rows.map((r) => ({
@@ -15964,8 +17405,8 @@ var SqliteGraphStore = class {
15964
17405
  after = rows[rows.length - 1].rid;
15965
17406
  }
15966
17407
  }
15967
- recentNodeIds(label, limit) {
15968
- const rows = this.db.prepare("SELECT id FROM nodes WHERE label = ? AND valid_to IS NULL ORDER BY last_updated_at DESC LIMIT ?").all(label, Math.max(0, limit));
17408
+ recentProblemIds(limit) {
17409
+ const rows = this.db.prepare("SELECT id FROM nodes WHERE label = 'Problem' AND valid_to IS NULL ORDER BY last_updated_at DESC LIMIT ?").all(Math.max(0, limit));
15969
17410
  return rows.map((r) => r.id);
15970
17411
  }
15971
17412
  recentOpenProblemCandidates(opts) {
@@ -16045,6 +17486,20 @@ var SqliteGraphStore = class {
16045
17486
  }
16046
17487
  searchTextCandidates(terms) {
16047
17488
  if (terms.length === 0) return [];
17489
+ const indexed = terms.filter((t) => t.length >= TEXT_INDEX_MIN_TERM);
17490
+ if (indexed.length > 0 && textIndexReady(this.db)) {
17491
+ const short = terms.filter((t) => t.length < TEXT_INDEX_MIN_TERM).map((t) => t.toLowerCase());
17492
+ const rows2 = this.db.prepare(`WITH hits AS MATERIALIZED (
17493
+ SELECT t.rowid AS rid, COUNT(*) AS matched FROM json_each(:terms) q, nodes_text t
17494
+ WHERE t.description LIKE '%' || q.value || '%' GROUP BY t.rowid
17495
+ ) SELECT ${TEXT_SEARCH_COLUMNS}, hits.matched FROM hits JOIN nodes ON nodes.rowid = hits.rid
17496
+ WHERE nodes.valid_to IS NULL ORDER BY page_rank DESC, last_updated_at DESC`).all({ terms: JSON.stringify(indexed) });
17497
+ return rows2.map((row) => {
17498
+ const text = row.description.toLowerCase();
17499
+ const extra = short.filter((t) => text.includes(t)).length;
17500
+ return { node: textSearchNode(row), matched: row.matched + extra };
17501
+ });
17502
+ }
16048
17503
  const rows = this.db.prepare(`WITH matches AS MATERIALIZED (
16049
17504
  SELECT n.id, COUNT(*) AS matched FROM nodes n INDEXED BY nodes_live_description, json_each(:terms) t
16050
17505
  WHERE n.valid_to IS NULL AND n.description LIKE '%' || t.value || '%'
@@ -16053,6 +17508,18 @@ var SqliteGraphStore = class {
16053
17508
  JOIN matches USING (id) ORDER BY page_rank DESC, last_updated_at DESC`).all({ terms: JSON.stringify(terms) });
16054
17509
  return rows.map((row) => ({ node: textSearchNode(row), matched: row.matched }));
16055
17510
  }
17511
+ commentAnchors(commentIds) {
17512
+ const out = /* @__PURE__ */ new Map();
17513
+ if (commentIds.length === 0) return out;
17514
+ const rows = this.db.prepare(`WITH a AS MATERIALIZED (
17515
+ SELECT q.value AS cid, COALESCE(
17516
+ (SELECT to_id FROM edges WHERE from_id = q.value AND type IN ('EXPLAINS', 'ANNOTATES') AND valid_to IS NULL LIMIT 1),
17517
+ (SELECT from_id FROM edges WHERE to_id = q.value AND type = 'DOCUMENTED_BY' AND valid_to IS NULL LIMIT 1)
17518
+ ) AS aid FROM json_each(:ids) q
17519
+ ) SELECT a.cid AS cid, ${TEXT_SEARCH_COLUMNS} FROM a JOIN nodes ON nodes.id = a.aid AND nodes.valid_to IS NULL`).all({ ids: JSON.stringify(commentIds) });
17520
+ for (const row of rows) out.set(row.cid, textSearchNode(row));
17521
+ return out;
17522
+ }
16056
17523
  getTextSearchNode(id) {
16057
17524
  const row = this.db.prepare(`SELECT ${TEXT_SEARCH_COLUMNS} FROM nodes WHERE id = ? AND valid_to IS NULL`).get(id);
16058
17525
  return row ? textSearchNode(row) : null;
@@ -16072,12 +17539,12 @@ var SqliteGraphStore = class {
16072
17539
  return scale;
16073
17540
  }
16074
17541
  typedEdgeStmt(sql) {
16075
- let stmt = this.typedEdgeStmts.get(sql);
16076
- if (!stmt) {
16077
- stmt = this.db.prepare(sql);
16078
- this.typedEdgeStmts.set(sql, stmt);
17542
+ let stmt2 = this.typedEdgeStmts.get(sql);
17543
+ if (!stmt2) {
17544
+ stmt2 = this.db.prepare(sql);
17545
+ this.typedEdgeStmts.set(sql, stmt2);
16079
17546
  }
16080
- return stmt;
17547
+ return stmt2;
16081
17548
  }
16082
17549
  outEdges(nodeId, types) {
16083
17550
  if (!types || types.length === 0) {
@@ -16136,21 +17603,54 @@ var SqliteGraphStore = class {
16136
17603
  return out;
16137
17604
  }
16138
17605
  nodeCount() {
16139
- return Number(this.stmts.nodeCount.get().n);
17606
+ return this.countAt("nodes", [ANY_CHANGE], () => Number(this.stmts.nodeCount.get().n));
16140
17607
  }
16141
17608
  mutationCount() {
16142
17609
  return this.mutations;
16143
17610
  }
17611
+ changeStamp() {
17612
+ const { data_version } = this.db.pragma("data_version");
17613
+ const { n } = this.db.prepare("SELECT total_changes() AS n").get();
17614
+ return `${data_version}:${n}`;
17615
+ }
17616
+ changeSeq(scopes) {
17617
+ return readChangeSeq(this.db, scopes);
17618
+ }
17619
+ lineageChangedFiles(sinceSeq) {
17620
+ return readLineageChangedFiles(this.db, sinceSeq);
17621
+ }
17622
+ changedNodeIds(sinceSeq, labels) {
17623
+ return readChangedNodeIds(this.db, sinceSeq, labels);
17624
+ }
17625
+ changedAmong(sinceSeq, ids) {
17626
+ return readChangedAmong(this.db, sinceSeq, ids);
17627
+ }
17628
+ pruneChangeLog() {
17629
+ return pruneChangeLog(this.db);
17630
+ }
17631
+ fileRowStates(ids) {
17632
+ if (ids.length === 0) return [];
17633
+ if (ids.length > 500) throw new RangeError("fileRowStates accepts at most 500 ids per read");
17634
+ return this.db.prepare(`SELECT id, last_updated_at AS lastUpdatedAt, valid_to AS validTo FROM nodes WHERE id IN (${ids.map(() => "?").join(",")})`).all(...ids);
17635
+ }
17636
+ // Read and advanced in SQL, never from a per-connection copy: pass workers and
17637
+ // MCP processes open their own connections and advance the same row, so a
17638
+ // copy loaded at open went stale, and `copy + 1` wrote a duplicate or a
17639
+ // backwards seq from whichever connection had fallen behind.
16144
17640
  currentIngestSeq() {
17641
+ const row = this.db.prepare("SELECT value FROM store_meta WHERE key = 'ingest_seq'").get();
17642
+ this.ingestSeq = row ? Number(row.value) : 0;
16145
17643
  return this.ingestSeq;
16146
17644
  }
16147
17645
  nextIngestSeq() {
16148
- this.ingestSeq += 1;
16149
- this.stmts.setIngestSeq.run({ v: String(this.ingestSeq) });
17646
+ const row = this.db.prepare(
17647
+ "INSERT INTO store_meta (key, value) VALUES ('ingest_seq', '1') ON CONFLICT(key) DO UPDATE SET value = CAST(value AS INTEGER) + 1 RETURNING value"
17648
+ ).get();
17649
+ this.ingestSeq = Number(row.value);
16150
17650
  return this.ingestSeq;
16151
17651
  }
16152
17652
  edgeCount() {
16153
- return Number(this.stmts.edgeCount.get().n);
17653
+ return this.countAt("edges", [ANY_CHANGE], () => Number(this.stmts.edgeCount.get().n));
16154
17654
  }
16155
17655
  // Lightweight whole-graph scans — select only the columns the caller needs so a
16156
17656
  // 100k-node pass skips the per-row attrs JSON.parse + embedding decode rowToNode
@@ -16278,28 +17778,63 @@ var SqliteGraphStore = class {
16278
17778
  }
16279
17779
  }
16280
17780
  reviveReobserved(relPaths, workspaceId, since) {
16281
- let revived = 0;
17781
+ const revived = [];
16282
17782
  const CHUNK = 400;
16283
17783
  for (let i = 0; i < relPaths.length; i += CHUNK) {
16284
17784
  const slice = relPaths.slice(i, i + CHUNK);
16285
17785
  if (slice.length === 0) continue;
16286
- const r = this.db.prepare(
16287
- `UPDATE nodes SET valid_to = NULL
17786
+ const rows = this.db.prepare(
17787
+ `SELECT id, valid_to AS closedAt, COALESCE(logical_id, id) AS lid FROM nodes
16288
17788
  WHERE valid_to IS NOT NULL
16289
17789
  AND last_updated_at >= ?
16290
17790
  AND json_extract(attrs_json, '$.workspaceId') = ?
16291
- AND json_extract(attrs_json, '$.relPath') IN (${slice.map(() => "?").join(",")})`
16292
- ).run(since, workspaceId, ...slice);
16293
- revived += Number(r.changes);
16294
- }
16295
- if (revived > 0) this.mutations++;
16296
- return revived;
17791
+ AND json_extract(attrs_json, '$.relPath') IN (${slice.map(() => "?").join(",")})
17792
+ AND json_type(attrs_json, '$.historical') IS NOT 'true'`
17793
+ ).all(since, workspaceId, ...slice);
17794
+ revived.push(...rows);
17795
+ }
17796
+ if (revived.length === 0) return 0;
17797
+ const reopen = this.db.prepare("UPDATE nodes SET valid_to = NULL WHERE id = ? AND valid_to IS NOT NULL");
17798
+ for (const r of revived) reopen.run(r.id);
17799
+ for (const r of revived) reopenCascadeClosedEdges(this.db, r.id, Number(r.closedAt));
17800
+ collapseLiveHeads(this.db, revived.map((r) => r.lid), since);
17801
+ this.mutations++;
17802
+ return revived.length;
17803
+ }
17804
+ incomingEdgeCoverage(label, edgeTypes, cloudOnly) {
17805
+ if (edgeTypes.length === 0) return { count: 0, total: 0 };
17806
+ const r = this.db.prepare(
17807
+ `SELECT COUNT(*) AS total,
17808
+ COALESCE(SUM(CASE WHEN EXISTS (
17809
+ SELECT 1 FROM edges e WHERE e.to_id = n.id AND e.valid_to IS NULL
17810
+ AND e.type IN (${edgeTypes.map(() => "?").join(",")})
17811
+ ) THEN 1 ELSE 0 END), 0) AS count
17812
+ FROM nodes n
17813
+ WHERE n.label = ? AND n.valid_to IS NULL
17814
+ AND (? = 0 OR json_extract(n.attrs_json, '$.source') = 'cloud')`
17815
+ ).get(...edgeTypes, label, cloudOnly ? 1 : 0);
17816
+ return { count: Number(r.count), total: Number(r.total) };
17817
+ }
17818
+ liveEdgeTargets(edgeTypes) {
17819
+ if (edgeTypes.length === 0) return [];
17820
+ const rows = this.db.prepare(`SELECT DISTINCT to_id FROM edges INDEXED BY edges_type WHERE type IN (${edgeTypes.map(() => "?").join(",")}) AND valid_to IS NULL`).all(...edgeTypes);
17821
+ return rows.map((r) => r.to_id);
16297
17822
  }
16298
17823
  recentEdgeAttrCoverage(marker, attr, limit) {
17824
+ if (marker === PRIOR_TAG_MARKER) {
17825
+ const r2 = this.db.prepare(
17826
+ `SELECT COUNT(*) AS total,
17827
+ COALESCE(SUM(CASE WHEN json_extract(attrs_json, '$.' || ?) = 1 THEN 1 ELSE 0 END), 0) AS count
17828
+ FROM (SELECT attrs_json FROM edges INDEXED BY edges_live_prior_tag
17829
+ WHERE valid_to IS NULL AND json_extract(attrs_json, '$.priorTag') = 1
17830
+ ORDER BY created_at DESC LIMIT ?)`
17831
+ ).get(attr, limit);
17832
+ return { count: Number(r2.count), total: Number(r2.total) };
17833
+ }
16299
17834
  const r = this.db.prepare(
16300
17835
  `SELECT COUNT(*) AS total,
16301
17836
  COALESCE(SUM(CASE WHEN json_extract(attrs_json, '$.' || ?) = 1 THEN 1 ELSE 0 END), 0) AS count
16302
- FROM (SELECT attrs_json FROM edges
17837
+ FROM (SELECT attrs_json FROM edges INDEXED BY edges_live_created
16303
17838
  WHERE valid_to IS NULL AND attrs_json LIKE ?
16304
17839
  ORDER BY created_at DESC LIMIT ?)`
16305
17840
  ).get(attr, `%${marker}%`, limit);
@@ -16312,7 +17847,7 @@ var SqliteGraphStore = class {
16312
17847
  const slice = cloudIds.slice(i, i + CHUNK);
16313
17848
  if (slice.length === 0) continue;
16314
17849
  const rows = this.db.prepare(
16315
- `SELECT id, json_extract(attrs_json, '$.cloudNodeId') AS cloud FROM nodes INDEXED BY nodes_cloud_node_id WHERE json_extract(attrs_json, '$.cloudNodeId') IN (${slice.map(() => "?").join(",")}) AND valid_to IS NULL ORDER BY id`
17850
+ `SELECT id, json_extract(attrs_json, '$.cloudNodeId') AS cloud FROM nodes INDEXED BY nodes_cloud_node_id_v2 WHERE json_extract(attrs_json, '$.cloudNodeId') IN (${slice.map(() => "?").join(",")}) AND valid_to IS NULL ORDER BY id`
16316
17851
  ).all(...slice);
16317
17852
  for (const r of rows) out.set(r.cloud, [...out.get(r.cloud) ?? [], r.id]);
16318
17853
  }
@@ -16425,8 +17960,9 @@ var SqliteGraphStore = class {
16425
17960
  this.stmts.advanceLive.run({ live_id: liveId, t });
16426
17961
  return frozenId;
16427
17962
  }
16428
- closeNode(id, t) {
16429
- this.stmts.closeNode.run({ id, t });
17963
+ closeNode(id, t, opts = {}) {
17964
+ const r = this.stmts.closeNode.run({ id, t });
17965
+ if (Number(r.changes) > 0 && !opts.retainEdges) closeIncidentEdges(this.db, id, t);
16430
17966
  }
16431
17967
  closeEdge(id, t) {
16432
17968
  this.stmts.closeEdge.run({ id, t });
@@ -16446,9 +17982,11 @@ var SqliteGraphStore = class {
16446
17982
  * so it must be a plain identifier. */
16447
17983
  nodesAsOfWhereTrue(t, attr) {
16448
17984
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(attr)) throw new Error(`nodesAsOfWhereTrue: bad attr ${JSON.stringify(attr)}`);
17985
+ const indexed = attr === "revisit" ? " INDEXED BY nodes_revisit_true" : "";
16449
17986
  const rows = this.db.prepare(
16450
- `SELECT * FROM nodes WHERE valid_from <= :t AND (valid_to IS NULL OR valid_to > :t)
16451
- AND json_type(attrs_json, '$.${attr}') = 'true'`
17987
+ `SELECT * FROM nodes${indexed} WHERE valid_from <= :t AND (valid_to IS NULL OR valid_to > :t)
17988
+ AND json_type(attrs_json, '$.${attr}') = 'true'
17989
+ ORDER BY rowid`
16452
17990
  ).all({ t });
16453
17991
  return rows.map(rowToNode);
16454
17992
  }
@@ -17035,7 +18573,14 @@ function summaryFromDoc(doc, kind) {
17035
18573
  }
17036
18574
  if (verb !== null) return null;
17037
18575
  const noun = clipped.replace(/^(?:the|a|an)\s+/i, "");
17038
- return tidy(`${lead(kind)} for ${noun.charAt(0).toLowerCase()}${noun.slice(1)}`);
18576
+ const head = noun.split(/[\s-]/, 1)[0].replace(/[.,;:!?]+$/, "");
18577
+ return tidy(`${lead(kind)} for ${isOrdinaryCapital(head) ? noun.charAt(0).toLowerCase() + noun.slice(1) : noun}`);
18578
+ }
18579
+ var CONTRACTION_BASES = /* @__PURE__ */ new Set(["it", "that", "this", "there", "here", "what", "who", "where", "when", "why", "how", "let", "he", "she"]);
18580
+ function isOrdinaryCapital(word) {
18581
+ const contraction = /^([A-Za-z]+)['’]s$/.exec(word);
18582
+ if (contraction) return CONTRACTION_BASES.has(contraction[1].toLowerCase()) && /^[A-Z][a-z]*$/.test(contraction[1]);
18583
+ return /^[A-Z][a-z]+$/.test(word);
17039
18584
  }
17040
18585
  var VERB_PHRASE_KINDS = /* @__PURE__ */ new Set(["function", "method"]);
17041
18586
  function summaryFromName(name, kind) {
@@ -17152,6 +18697,62 @@ function buildSymbolLexicon(store) {
17152
18697
  }
17153
18698
  return lex;
17154
18699
  }
18700
+ function lazySymbolLexicon(store) {
18701
+ if (!store.firstLiveByNameOrQname || !store.liveCommentsAttachedTo) return null;
18702
+ const labels = SYMBOL_LABELS.map(([label]) => label);
18703
+ const kindOf = new Map(SYMBOL_LABELS.map(([label, kind]) => [label, kind]));
18704
+ const members = /* @__PURE__ */ new Map();
18705
+ const docs = /* @__PURE__ */ new Map();
18706
+ const entries = /* @__PURE__ */ new Map();
18707
+ const member = (name) => {
18708
+ if (!members.has(name)) {
18709
+ const hit = isDistinctiveIdentifier(name) ? store.firstLiveByNameOrQname(labels, name) : null;
18710
+ members.set(name, hit ? { kind: kindOf.get(hit.label), description: hit.description, qname: hit.qname } : null);
18711
+ }
18712
+ return members.get(name);
18713
+ };
18714
+ const docFor = (name) => {
18715
+ if (!docs.has(name)) {
18716
+ let found;
18717
+ for (const c of store.liveCommentsAttachedTo(name)) {
18718
+ if (typeof c.kind !== "string" || !DOC_COMMENT_KINDS.has(c.kind)) continue;
18719
+ const text = typeof c.text === "string" ? c.text : c.description;
18720
+ if (!text?.trim()) continue;
18721
+ found = text.trim().slice(0, MAX_DOC_CHARS);
18722
+ break;
18723
+ }
18724
+ docs.set(name, found);
18725
+ }
18726
+ return docs.get(name);
18727
+ };
18728
+ const isKnown = (tok) => member(tok) !== null;
18729
+ let anySymbol;
18730
+ return {
18731
+ has: isKnown,
18732
+ get(name) {
18733
+ if (!entries.has(name)) {
18734
+ const m = member(name);
18735
+ let entry = null;
18736
+ if (m) {
18737
+ entry = { kind: m.kind };
18738
+ const doc = (m.description ? docFor(m.description) : void 0) ?? (m.qname ? docFor(m.qname) : void 0);
18739
+ for (const phrase of summaryCandidates(name, m.kind, doc)) {
18740
+ if (summaryRejectionReason(phrase, isKnown) === null) {
18741
+ entry.summary = phrase;
18742
+ break;
18743
+ }
18744
+ }
18745
+ }
18746
+ entries.set(name, entry);
18747
+ }
18748
+ return entries.get(name) ?? void 0;
18749
+ },
18750
+ get size() {
18751
+ anySymbol ??= labels.some((label) => store.countNodesByLabel(label) > 0);
18752
+ return anySymbol ? 1 : 0;
18753
+ }
18754
+ };
18755
+ }
17155
18756
  var LEXICON_SYMBOL_LABELS = ["Function", "Method", "Class", "Interface", "Const", "Comment"];
17156
18757
  function codeGraphKey(store) {
17157
18758
  const files = store.labelFingerprint?.(["File"]);
@@ -17163,7 +18764,7 @@ function lexiconsFor(store) {
17163
18764
  const key = codeGraphKey(store);
17164
18765
  const hit = key !== null ? lexiconCache.get(store) : void 0;
17165
18766
  if (hit && hit.key === key) return hit;
17166
- const built = { symbols: buildSymbolLexicon(store), files: buildFileLexicon(store) };
18767
+ const built = { symbols: lazySymbolLexicon(store) ?? buildSymbolLexicon(store), files: buildFileLexicon(store) };
17167
18768
  if (key !== null) lexiconCache.set(store, { key, ...built });
17168
18769
  return built;
17169
18770
  }
@@ -17409,11 +19010,11 @@ function generalizeSymbols(text, lexicon, level = 1, fileLexicon) {
17409
19010
 
17410
19011
  // src/generalize-worker.ts
17411
19012
  var stores = /* @__PURE__ */ new Map();
17412
- function storeAt(path) {
17413
- let s = stores.get(path);
19013
+ function storeAt(path2) {
19014
+ let s = stores.get(path2);
17414
19015
  if (!s) {
17415
- s = openGraphStore({ path });
17416
- stores.set(path, s);
19016
+ s = openGraphStore({ path: path2 });
19017
+ stores.set(path2, s);
17417
19018
  }
17418
19019
  return s;
17419
19020
  }