@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.
@@ -792,9 +792,13 @@ function predicateIsTheEntity(text, entityId, entityCount) {
792
792
  }
793
793
  function canonicalClaimId(text, scope) {
794
794
  const s = claimStructure(text, scope);
795
- return digest(
796
- s.raw !== void 0 ? { raw: s.raw, scope: s.scope, stance: s.stance } : { entities: s.entities, predicate: s.predicate, stance: s.stance, scope: s.scope }
797
- );
795
+ return s.raw !== void 0 ? conceptDigest(s.raw, s.scope, s.stance) : digest({ entities: s.entities, predicate: s.predicate, stance: s.stance, scope: s.scope });
796
+ }
797
+ function conceptClaimId(text, scope) {
798
+ return conceptDigest(conceptBag(text), scope ?? {}, detectStance(text));
799
+ }
800
+ function conceptDigest(raw, scope, stance) {
801
+ return digest({ raw, scope, stance });
798
802
  }
799
803
  var init_claim_id = __esm({
800
804
  "../../packages/shared/src/nlp/claim-id.ts"() {
@@ -822,14 +826,14 @@ function identityId(input) {
822
826
  statement: canonicalClaimId(input.statement.trim())
823
827
  });
824
828
  case "DesignProblem":
825
- return `${DESIGN_PROBLEM_PREFIX}${canonicalClaimId(input.statement.trim())}`.slice(
829
+ return `${DESIGN_PROBLEM_PREFIX}${conceptClaimId(input.statement.trim())}`.slice(
826
830
  0,
827
831
  DESIGN_PROBLEM_ID_LENGTH
828
832
  );
829
833
  case "DiagnosticProblem":
830
834
  return `${DIAGNOSTIC_PROBLEM_PREFIX}${digest({ key: input.key }).slice(0, DIAGNOSTIC_HASH_LENGTH)}`;
831
835
  case "Triage":
832
- return `${TRIAGE_PREFIX}${canonicalClaimId(input.statement.trim())}`.slice(
836
+ return `${TRIAGE_PREFIX}${conceptClaimId(input.statement.trim())}`.slice(
833
837
  0,
834
838
  TRIAGE_ID_LENGTH
835
839
  );
@@ -1114,10 +1118,10 @@ function mergeDefs(...defs) {
1114
1118
  function cloneDef(schema) {
1115
1119
  return mergeDefs(schema._zod.def);
1116
1120
  }
1117
- function getElementAtPath(obj, path) {
1118
- if (!path)
1121
+ function getElementAtPath(obj, path2) {
1122
+ if (!path2)
1119
1123
  return obj;
1120
- return path.reduce((acc, key) => acc?.[key], obj);
1124
+ return path2.reduce((acc, key) => acc?.[key], obj);
1121
1125
  }
1122
1126
  function promiseAllObject(promisesObj) {
1123
1127
  const keys = Object.keys(promisesObj);
@@ -1429,11 +1433,11 @@ function aborted(x, startIndex = 0) {
1429
1433
  }
1430
1434
  return false;
1431
1435
  }
1432
- function prefixIssues(path, issues) {
1436
+ function prefixIssues(path2, issues) {
1433
1437
  return issues.map((iss) => {
1434
1438
  var _a2;
1435
1439
  (_a2 = iss).path ?? (_a2.path = []);
1436
- iss.path.unshift(path);
1440
+ iss.path.unshift(path2);
1437
1441
  return iss;
1438
1442
  });
1439
1443
  }
@@ -1675,7 +1679,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
1675
1679
  }
1676
1680
  function treeifyError(error48, mapper = (issue2) => issue2.message) {
1677
1681
  const result = { errors: [] };
1678
- const processError = (error49, path = []) => {
1682
+ const processError = (error49, path2 = []) => {
1679
1683
  var _a2, _b;
1680
1684
  for (const issue2 of error49.issues) {
1681
1685
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -1685,7 +1689,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
1685
1689
  } else if (issue2.code === "invalid_element") {
1686
1690
  processError({ issues: issue2.issues }, issue2.path);
1687
1691
  } else {
1688
- const fullpath = [...path, ...issue2.path];
1692
+ const fullpath = [...path2, ...issue2.path];
1689
1693
  if (fullpath.length === 0) {
1690
1694
  result.errors.push(mapper(issue2));
1691
1695
  continue;
@@ -1717,8 +1721,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
1717
1721
  }
1718
1722
  function toDotPath(_path) {
1719
1723
  const segs = [];
1720
- const path = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1721
- for (const seg of path) {
1724
+ const path2 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1725
+ for (const seg of path2) {
1722
1726
  if (typeof seg === "number")
1723
1727
  segs.push(`[${seg}]`);
1724
1728
  else if (typeof seg === "symbol")
@@ -14412,13 +14416,13 @@ function resolveRef(ref, ctx) {
14412
14416
  if (!ref.startsWith("#")) {
14413
14417
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
14414
14418
  }
14415
- const path = ref.slice(1).split("/").filter(Boolean);
14416
- if (path.length === 0) {
14419
+ const path2 = ref.slice(1).split("/").filter(Boolean);
14420
+ if (path2.length === 0) {
14417
14421
  return ctx.rootSchema;
14418
14422
  }
14419
14423
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
14420
- if (path[0] === defsKey) {
14421
- const key = path[1];
14424
+ if (path2[0] === defsKey) {
14425
+ const key = path2[1];
14422
14426
  if (!key || !ctx.defs[key]) {
14423
14427
  throw new Error(`Reference not found: ${ref}`);
14424
14428
  }
@@ -15737,8 +15741,8 @@ function normalizeSymbolPath(relPath) {
15737
15741
  return relPath.replace(/\\/g, "/").replace(/\/+/g, "/").replace(/^\.\//, "").replace(/^\//, "");
15738
15742
  }
15739
15743
  function projectSymbolId(projectSalt, projectId, relPath, qname, kind) {
15740
- const path = normalizeSymbolPath(relPath);
15741
- return `sym_${createHmac("sha256", projectSalt).update(`${projectId}:${path}:${qname}:${kind}`).digest("hex").slice(0, 24)}`;
15744
+ const path2 = normalizeSymbolPath(relPath);
15745
+ return `sym_${createHmac("sha256", projectSalt).update(`${projectId}:${path2}:${qname}:${kind}`).digest("hex").slice(0, 24)}`;
15742
15746
  }
15743
15747
  var init_project_symbol = __esm({
15744
15748
  "../../packages/shared/src/project-symbol.ts"() {
@@ -15774,7 +15778,7 @@ var init_history_read = __esm({
15774
15778
  "../../packages/shared/src/history-read.ts"() {
15775
15779
  "use strict";
15776
15780
  init_history_dependencies();
15777
- CLOUD_ATTR_DUPLICATE_KEYS = ["attrsJson", "embedding"];
15781
+ CLOUD_ATTR_DUPLICATE_KEYS = ["attrsJson", "embedding", "structEmbedding", "structEmbeddedAt"];
15778
15782
  }
15779
15783
  });
15780
15784
 
@@ -15786,7 +15790,7 @@ var init_history_temporal = __esm({
15786
15790
  });
15787
15791
 
15788
15792
  // ../../packages/shared/src/history-revision.ts
15789
- var Sha, HistoryRevisionRequestSchema;
15793
+ var Sha, HistoryRevisionRequestSchema, HistoryCarryRequestSchema, HISTORY_REVIEW_BUDGET, HISTORY_REVIEW_POLICY;
15790
15794
  var init_history_revision = __esm({
15791
15795
  "../../packages/shared/src/history-revision.ts"() {
15792
15796
  "use strict";
@@ -15806,6 +15810,17 @@ var init_history_revision = __esm({
15806
15810
  dependencyContext: HistoryDependencyContextSchema.optional()
15807
15811
  })).max(50)
15808
15812
  });
15813
+ HistoryCarryRequestSchema = external_exports.strictObject({
15814
+ fromCommit: Sha,
15815
+ atCommit: Sha,
15816
+ parents: external_exports.array(external_exports.strictObject({ commit: Sha, parents: external_exports.array(Sha).max(32) })).max(8192),
15817
+ /** Every symbol in a file the range changed, both sides; `null` = too many to name, treat all as changed. */
15818
+ changedSymbols: external_exports.array(external_exports.string().regex(/^sym_[a-f0-9]{8,64}$/)).max(5e3).nullable(),
15819
+ /** Manifests the range changed; `null` = a lockfile (or too many manifests) changed, treat all as changed. */
15820
+ changedManifests: external_exports.array(external_exports.string().regex(/^hmanifest_[a-f0-9]{32}$/)).max(512).nullable()
15821
+ });
15822
+ HISTORY_REVIEW_BUDGET = 48e3;
15823
+ HISTORY_REVIEW_POLICY = `reviewer-view-${HISTORY_REVIEW_BUDGET}`;
15809
15824
  }
15810
15825
  });
15811
15826
 
@@ -15995,13 +16010,13 @@ function loadNodeSqlite() {
15995
16010
  process.emitWarning = original;
15996
16011
  }
15997
16012
  }
15998
- function openDatabase(path, opts) {
15999
- const db = opts ? new DatabaseSync(path, opts) : new DatabaseSync(path);
16013
+ function openDatabase(path2, opts) {
16014
+ const db = opts ? new DatabaseSync(path2, opts) : new DatabaseSync(path2);
16000
16015
  try {
16001
16016
  db.exec("PRAGMA foreign_keys = ON");
16002
16017
  } catch {
16003
16018
  }
16004
- if (path !== ":memory:" && !opts?.readOnly) {
16019
+ if (path2 !== ":memory:" && !opts?.readOnly) {
16005
16020
  try {
16006
16021
  db.exec("PRAGMA journal_mode = WAL");
16007
16022
  db.exec("PRAGMA synchronous = NORMAL");
@@ -16010,20 +16025,20 @@ function openDatabase(path, opts) {
16010
16025
  } catch {
16011
16026
  }
16012
16027
  }
16013
- const wrapStmt = (stmt) => ({
16028
+ const wrapStmt = (stmt2) => ({
16014
16029
  run: (...params) => {
16015
- const r = stmt.run(...coerceParams(params));
16030
+ const r = stmt2.run(...coerceParams(params));
16016
16031
  return {
16017
16032
  changes: Number(r.changes),
16018
16033
  lastInsertRowid: Number(r.lastInsertRowid)
16019
16034
  };
16020
16035
  },
16021
16036
  get: (...params) => {
16022
- const r = stmt.get(...coerceParams(params));
16037
+ const r = stmt2.get(...coerceParams(params));
16023
16038
  return r;
16024
16039
  },
16025
16040
  all: (...params) => {
16026
- const r = stmt.all(...coerceParams(params));
16041
+ const r = stmt2.all(...coerceParams(params));
16027
16042
  return r;
16028
16043
  }
16029
16044
  });
@@ -16084,6 +16099,548 @@ var init_sqlite_adapter = __esm({
16084
16099
  }
16085
16100
  });
16086
16101
 
16102
+ // ../../packages/local-shared/src/tag-grammar.ts
16103
+ function tagGrammarCopy() {
16104
+ const copy = [];
16105
+ for (const ref of ["its-handle", "its-id", "h", "id"]) {
16106
+ for (const build of Object.values(TAG_EXAMPLES)) copy.push(build(ref));
16107
+ }
16108
+ for (const v of TAG_VERBS) {
16109
+ copy.push(v.hint);
16110
+ if (v.taughtBy) copy.push(v.taughtBy);
16111
+ for (const q of v.qualifiers ?? []) copy.push(q.note);
16112
+ for (const l of v.lowering) {
16113
+ if (l.note) copy.push(l.note);
16114
+ if (l.knownViolation) copy.push(l.knownViolation.finding);
16115
+ }
16116
+ }
16117
+ for (const f of TAG_FENCES) copy.push(f.hint);
16118
+ return copy;
16119
+ }
16120
+ function normalizeTemplateBody(body) {
16121
+ return body.replace(/…|\.{3}/g, " ").replace(/\s+/g, " ").trim().replace(/^[\s,;:—–-]+|[\s,;:.—–-]+$/g, "").toLowerCase();
16122
+ }
16123
+ function deriveTagTemplates(copy) {
16124
+ const bodies = /* @__PURE__ */ new Set();
16125
+ const handles = new Set([...TAG_PLACEHOLDER_HANDLES].map((h) => h.toLowerCase()));
16126
+ for (const text of copy) {
16127
+ for (const m of text.matchAll(BRACKET_TOKEN)) {
16128
+ const token = m[1].trim().toLowerCase();
16129
+ if (!/^[!?]/.test(token) && !REAL_HANDLE_SHAPE.test(token)) handles.add(token);
16130
+ }
16131
+ }
16132
+ for (const text of copy) {
16133
+ for (const m of text.matchAll(TEMPLATE_VERB_GROUP)) {
16134
+ const body = normalizeTemplateBody(m[2]);
16135
+ bodies.add(body);
16136
+ const unthreaded = normalizeTemplateBody(body.replace(/^#[a-z][\w-]*/i, ""));
16137
+ bodies.add(unthreaded);
16138
+ const rest = normalizeTemplateBody(unthreaded.replace(BRACKET_TOKEN, (t, h) => handles.has(h.trim().toLowerCase()) ? " " : t).replace(/,/g, " "));
16139
+ bodies.add(rest);
16140
+ }
16141
+ }
16142
+ bodies.delete("");
16143
+ return { bodies, handles };
16144
+ }
16145
+ var TAG_CANONICAL_ID_PREFIXES, TAG_HANDLE_SOURCE, TAG_THREAD_SOURCE, TAG_PLACEHOLDER_HANDLES, TAG_EXAMPLES, P, S, RC, CITE_LOWERING, INSTANCE_LOWERING, TRIAGE_TO_RC, TAG_VERBS, TAG_FENCES, TAG_VERB_BY_NAME, TAG_PAREN_VERBS, TAG_GRAMMAR_OPENERS, TAG_QUOTED_PAREN_SOURCE, TAG_QUOTED_RE, TAG_QUOTED_PAREN_RE, TEMPLATE_VERB_GROUP, BRACKET_TOKEN, REAL_HANDLE_SHAPE, TAG_TEMPLATES, VERB_WORDS_SOURCE, JOINED_VERB, LEADING_VERB, ANY_VERB_WORD, QUOTED_TAG_OPENER;
16146
+ var init_tag_grammar = __esm({
16147
+ "../../packages/local-shared/src/tag-grammar.ts"() {
16148
+ "use strict";
16149
+ TAG_CANONICAL_ID_PREFIXES = ["pat", "sol", "drc", "dcause", "dfix", "dprob", "dvuln", "prob", "claim", "err"];
16150
+ TAG_HANDLE_SOURCE = String.raw`\[([a-z0-9][\w-]{0,80})\]|((?:${TAG_CANONICAL_ID_PREFIXES.join("|")})_[0-9a-f]{6,})`;
16151
+ TAG_THREAD_SOURCE = String.raw`#([a-z][\w-]{0,63})`;
16152
+ TAG_PLACEHOLDER_HANDLES = /* @__PURE__ */ new Set([
16153
+ "handle",
16154
+ "its-handle",
16155
+ "another",
16156
+ "a-third",
16157
+ "h",
16158
+ "h1",
16159
+ "h2",
16160
+ "id",
16161
+ "its-id",
16162
+ "a",
16163
+ "b",
16164
+ "\u2026",
16165
+ "..."
16166
+ ]);
16167
+ TAG_EXAMPLES = {
16168
+ prior: (ref) => `([${ref}])`,
16169
+ problem: () => `[!one line on what's wrong]`,
16170
+ vulnerability: () => `[!vuln: who can do what; observed basis @ path:line]`,
16171
+ suspectedVulnerability: () => `[?vuln: consequence; missing check]`,
16172
+ vulnerabilityThread: () => `[!#v vuln: finding here]`,
16173
+ fix: (ref) => `(fix:[${ref}])`,
16174
+ // CONSTRAINT — the native capture path for a DIFFUSE problem (a constraint/tension/
16175
+ // conflict that shaped a decision). Free text, attaches to the decision sentence the
16176
+ // agent is already writing. Full rationale in the constraint note below + prior-tags.ts.
16177
+ constraint: () => `(constraint: the limitation that forced it)`,
16178
+ // TRIAGE — routes the problem/symptom in scope to its cause. `(cause:[handle])`
16179
+ // links to a cited RootCause/Solution node; `(cause: free text)` mints a RootCause
16180
+ // from the text. Harvested in prior-tags.ts → engine.ts (see the triage note below).
16181
+ triage: (ref) => `(cause:[${ref}])`,
16182
+ // Unlike a bare cause citation, this handle names the Problem being explained.
16183
+ triageExplain: (ref) => `(cause:[${ref}] the mechanism)`,
16184
+ // The MINT form — free-text cause that mints a RootCause. Prime-time surfacing
16185
+ // (recallForFile) pastes this next to an open Problem that has no cause yet, so the
16186
+ // diagnostic moment is cued when the agent is looking at the problem. Kept here so
16187
+ // the grammar can't drift from parseInlineTags' `(cause: …)` acceptance.
16188
+ triageMint: () => `(cause: why it happens)`,
16189
+ // THREAD forms — the witness-declared symptom↔cause/fix binding. A `#id` on the
16190
+ // flag names the diagnosis thread; the same `#id` on a later verb binds to it
16191
+ // explicitly (across sentences AND turns), replacing proximity guessing. Adjacent
16192
+ // tags never need one — the id is for diagnoses that unfold over distance.
16193
+ problemThread: () => `[!#slug one line on what's wrong]`,
16194
+ causeThread: () => `(cause:#slug the mechanism)`,
16195
+ fixThread: (thread = "slug") => `(fix:#${thread} what you changed)`,
16196
+ // ATTEMPT arc — what's being tried and what didn't work. A failed attempt is a
16197
+ // witnessed NEGATIVE result: it saves the next agent the same dead end.
16198
+ tried: () => `(tried: the approach you're attempting)`,
16199
+ failed: () => `(failed: what didn't work and the dead end it rules out)`,
16200
+ // `(failed:[h] …)` on a shown SOLUTION — the refute-by-failure form the context render teaches.
16201
+ failedCite: (ref) => `(failed:[${ref}] what came back)`,
16202
+ // LINK verbs (AC epic) — bind the problem in scope to CANONICAL priors and
16203
+ // abstractions. Multi-target by design: single links are weak; ~3 links to 3
16204
+ // DIFFERENT relevant priors is the target shape (see linkBullet).
16205
+ instance: (ref) => `(instance:[${ref}],[another],[a-third])`,
16206
+ pattern: () => `(pattern: name the general shape)`,
16207
+ // DOMAIN — the abstract area a problem is about; the concept layer an
16208
+ // anchor-less problem clusters on. Mints/resolves a Domain by name.
16209
+ domain: () => `(domain: The Area)`,
16210
+ // PACKAGE/COMPONENT — agent-named public anchors (OM-agent-anchors): the
16211
+ // package a problem is ABOUT (even when not a dependency) and the
16212
+ // framework/product-level unit it concerns. Public spine anchors → the
16213
+ // knowledge can cross to the collective.
16214
+ package: () => `(package: the-package-name)`,
16215
+ component: () => `(component: The Component)`,
16216
+ aids: (ref) => `(aids:[${ref}])`,
16217
+ // SOLUTION-STRUCTURE (SS-structure) — taught in the link bullet; Solution→Solution.
16218
+ buildsOn: (ref) => `(builds-on:[${ref}])`,
16219
+ supersedes: (ref) => `(supersedes:[${ref}])`,
16220
+ alternative: (ref) => `(alternative:[${ref}])`,
16221
+ // VERDICT (VS-verdict-tags) — a judgment on a SHOWN prior, distinct from leaning on it.
16222
+ // `confirm` rides the corroborate channel (it held); `refute` the contradict channel
16223
+ // (it's wrong / didn't hold) and REQUIRES a reason. This is the inline producer the
16224
+ // evidence channels lacked — refute sat dead (contradictionPressure 0) until now, while
16225
+ // lean-on corroboration lived, because only corroboration had an inline path. Grammar
16226
+ // single-sourced with parseInlineTags' CONFIRM_PREFIX/REFUTE_PREFIX.
16227
+ confirm: (ref) => `(confirm:[${ref}])`,
16228
+ refute: (ref) => `(refute:[${ref}] one line why)`,
16229
+ // RETRACT — "not a real problem": the errata-resolved block with a disposition
16230
+ // (parseDesignResolutions, turn.ts). No inline form exists; the block is the grammar.
16231
+ retract: (id) => ["```errata-resolved", `id: ${id}`, "disposition: false_positive", "reason: why it was never a real problem", "```"].join("\n")
16232
+ };
16233
+ P = "Problem";
16234
+ S = "Solution";
16235
+ RC = "RootCause";
16236
+ CITE_LOWERING = [
16237
+ { target: RC, writes: [{ from: P, edge: "CAUSED_BY", to: RC }] },
16238
+ { target: S, writes: [{ from: P, edge: "SOLVED_BY", to: S }] },
16239
+ { 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" } },
16240
+ { target: "Package", writes: [{ from: P, edge: "DEPENDS_ON", to: "Package" }] },
16241
+ { target: "Language", writes: [{ from: P, edge: "OCCURS_IN", to: "Language" }] },
16242
+ { target: "Tool", writes: [{ from: P, edge: "CONCERNS", to: "Tool" }] },
16243
+ { target: "Component", writes: [{ from: P, edge: "CONCERNS", to: "Component" }] },
16244
+ { target: P, writes: [{ from: P, edge: "RELATES_TO", to: P }, { from: P, edge: "ANCHORED_AT", to: "File" }], note: "corroboration witness + cite-anchor promotion" },
16245
+ { 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" } },
16246
+ { 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" } },
16247
+ { target: "Weakness", writes: [{ from: P, edge: "RELATES_TO", to: "Weakness" }], decision: "e" },
16248
+ { target: "Domain", writes: [{ from: P, edge: "RELATES_TO", to: "Domain" }], decision: "e" },
16249
+ { target: "Claim", writes: [{ from: P, edge: "RELATES_TO", to: "Claim" }, { from: "Claim", edge: "ANCHORED_AT", to: "File" }] }
16250
+ ];
16251
+ INSTANCE_LOWERING = [
16252
+ { target: "Pattern", writes: [{ from: P, edge: "INSTANCE_OF", to: "Pattern" }] },
16253
+ { target: P, writes: [{ from: P, edge: "RELATES_TO", to: P }] },
16254
+ { 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" },
16255
+ { target: RC, writes: [{ from: P, edge: "CAUSED_BY", to: RC }] },
16256
+ { target: "AntiPattern", writes: [{ from: P, edge: "RELATES_TO", to: "AntiPattern" }], decision: "e" },
16257
+ { target: "Technique", writes: [{ from: P, edge: "RELATES_TO", to: "Technique" }], decision: "e" },
16258
+ { target: "Weakness", writes: [{ from: P, edge: "RELATES_TO", to: "Weakness" }], decision: "e" },
16259
+ { target: "Domain", writes: [{ from: P, edge: "RELATES_TO", to: "Domain" }], decision: "e" },
16260
+ { target: "Package", writes: [{ from: P, edge: "DEPENDS_ON", to: "Package" }] },
16261
+ { target: "Component", writes: [{ from: P, edge: "CONCERNS", to: "Component" }] }
16262
+ ];
16263
+ TRIAGE_TO_RC = [
16264
+ { from: P, edge: "TRIAGED_BY", to: "Triage" },
16265
+ { from: "Triage", edge: "INDICATES", to: RC },
16266
+ { from: P, edge: "CAUSED_BY", to: RC }
16267
+ ];
16268
+ TAG_VERBS = [
16269
+ {
16270
+ verb: "cite",
16271
+ form: "citation",
16272
+ opener: "(",
16273
+ kinds: ["prior"],
16274
+ args: "handles",
16275
+ thread: false,
16276
+ handleRequired: true,
16277
+ lowering: CITE_LOWERING,
16278
+ examples: ["prior"],
16279
+ hint: "cite a shown prior you leaned on: ([its-handle]) \u2014 handles only, no prose inside the parens",
16280
+ taught: "taught"
16281
+ },
16282
+ {
16283
+ verb: "!",
16284
+ form: "flag",
16285
+ opener: "[!",
16286
+ kinds: ["problem", "triage", "vulnerability", "fix", "prior"],
16287
+ args: "flag-body",
16288
+ thread: true,
16289
+ minText: 8,
16290
+ qualifiers: [
16291
+ { name: "vuln", taught: true, note: "[!vuln: \u2026] \u2192 Vulnerability (asserted)" },
16292
+ { name: "fixed", taught: false, note: "[!fixed: X] \u2192 read as a self-found fix" },
16293
+ { name: "resolved", taught: false, note: "[!resolved: X] \u2192 read as a self-found fix" }
16294
+ ],
16295
+ lowering: [
16296
+ { target: P, writes: [], note: "mints a Problem; `@ path[:line]` anchors it (ANCHORED_AT to code)" },
16297
+ { target: RC, writes: [...TRIAGE_TO_RC, { from: RC, edge: "CAUSED_BY", to: RC }], note: "`<-` segments: first \u2192 triage route, later \u2192 RootCause chain" },
16298
+ { target: "Vulnerability", writes: [], note: "[!vuln: \u2026]; `<-` causes stay on the finding (vulnerabilityCauses)" }
16299
+ ],
16300
+ examples: ["problem", "problemThread", "vulnerability", "vulnerabilityThread"],
16301
+ hint: "flag a problem on one line: [!what's wrong @ path:42]; fold causes with <- ; a fix goes in (fix: \u2026), not [!fixed: \u2026]",
16302
+ taught: "taught"
16303
+ },
16304
+ {
16305
+ verb: "?",
16306
+ form: "flag",
16307
+ opener: "[?",
16308
+ kinds: ["todo", "vulnerability"],
16309
+ args: "flag-body",
16310
+ thread: true,
16311
+ minText: 8,
16312
+ qualifiers: [{ name: "vuln", taught: true, note: "[?vuln: \u2026] \u2192 Vulnerability (suspected)" }],
16313
+ lowering: [{ target: P, writes: [], note: "mints a todo Problem; a fix can close it today", decision: "g" }],
16314
+ examples: ["suspectedVulnerability"],
16315
+ hint: "[?\u2026] marks an open question / todo; [?vuln: consequence; missing check] a suspected vulnerability",
16316
+ taught: "untaught",
16317
+ taughtBy: "only the [?vuln:] form is taught; the bare [?todo] form is not",
16318
+ decisions: ["g"]
16319
+ },
16320
+ {
16321
+ verb: "fix",
16322
+ form: "verb",
16323
+ opener: "(fix:",
16324
+ kinds: ["fix"],
16325
+ args: "handles-text",
16326
+ thread: true,
16327
+ minText: 12,
16328
+ lowering: [
16329
+ { target: P, writes: [{ from: P, edge: "SOLVED_BY", to: S }], note: "closes the Problem; a note \u226512 chars mints the Solution" },
16330
+ { target: "Vulnerability", writes: [{ from: "Vulnerability", edge: "SOLVED_BY", to: S }] },
16331
+ { target: RC, writes: [], note: "dropped: 'fix tag went nowhere' (not a Problem); no tag writes FIXED_BY", decision: "f" }
16332
+ ],
16333
+ examples: ["fix", "fixThread"],
16334
+ hint: "one fix per tag: (fix:[its-handle]) or (fix:#thread what you changed) \u2014 put the cause in its own (cause: \u2026)",
16335
+ taught: "taught",
16336
+ decisions: ["f"]
16337
+ },
16338
+ {
16339
+ verb: "cause",
16340
+ form: "verb",
16341
+ opener: "(cause:",
16342
+ kinds: ["triage"],
16343
+ args: "handles-text",
16344
+ thread: true,
16345
+ minText: 8,
16346
+ lowering: [
16347
+ { target: RC, writes: TRIAGE_TO_RC },
16348
+ { 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" },
16349
+ { 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" } }
16350
+ ],
16351
+ examples: ["triage", "triageExplain", "triageMint", "causeThread"],
16352
+ hint: "(cause: the mechanism you'd change) or (cause:[root-cause-handle]); bind a distant symptom with (cause:#thread \u2026)",
16353
+ taught: "taught",
16354
+ decisions: ["b"]
16355
+ },
16356
+ {
16357
+ verb: "constraint",
16358
+ form: "verb",
16359
+ opener: "(constraint:",
16360
+ kinds: ["constraint"],
16361
+ args: "text",
16362
+ thread: false,
16363
+ minText: 8,
16364
+ textRequired: true,
16365
+ lowering: [{ target: P, writes: [], note: "mints a constraint-kind Problem" }],
16366
+ examples: ["constraint"],
16367
+ hint: "(constraint: the limitation that forced the decision)",
16368
+ taught: "taught"
16369
+ },
16370
+ {
16371
+ verb: "tried",
16372
+ form: "verb",
16373
+ opener: "(tried:",
16374
+ kinds: ["attempt"],
16375
+ args: "text",
16376
+ thread: true,
16377
+ minText: 8,
16378
+ textRequired: true,
16379
+ lowering: [{ target: P, writes: [], note: "journals an attempt on the bound problem" }],
16380
+ examples: ["tried"],
16381
+ hint: "(tried: the approach) \u2014 bind with (tried:#thread \u2026)",
16382
+ taught: "taught"
16383
+ },
16384
+ {
16385
+ verb: "failed",
16386
+ form: "verb",
16387
+ opener: "(failed:",
16388
+ kinds: ["failure"],
16389
+ args: "handles-text",
16390
+ thread: true,
16391
+ minText: 8,
16392
+ textRequired: true,
16393
+ lowering: [
16394
+ { target: P, writes: [], note: "journals a failed attempt on the bound problem" },
16395
+ { target: S, writes: [], note: "(failed:[h]) refutes the cited prior (contradictionPressure); skips the label gate today", decision: "j" }
16396
+ ],
16397
+ examples: ["failed", "failedCite"],
16398
+ hint: "(failed: what didn't work); (failed:[solution] what came back) refutes a shown fix",
16399
+ taught: "taught",
16400
+ decisions: ["j"]
16401
+ },
16402
+ {
16403
+ verb: "instance",
16404
+ form: "verb",
16405
+ opener: "(instance:",
16406
+ kinds: ["instance"],
16407
+ args: "handles",
16408
+ thread: false,
16409
+ handleRequired: true,
16410
+ lowering: INSTANCE_LOWERING,
16411
+ examples: ["instance"],
16412
+ hint: "(instance:[h1],[h2],[h3]) \u2014 ~3 DIFFERENT shown priors this problem instantiates; handles only",
16413
+ taught: "taught",
16414
+ decisions: ["a", "e"]
16415
+ },
16416
+ {
16417
+ verb: "pattern",
16418
+ form: "verb",
16419
+ opener: "(pattern:",
16420
+ kinds: ["pattern"],
16421
+ args: "handle-or-text",
16422
+ thread: false,
16423
+ minText: 3,
16424
+ lowering: [{ target: "Pattern", writes: [{ from: P, edge: "INSTANCE_OF", to: "Pattern" }], note: "a handle must be a Pattern; free text resolves-or-mints one" }],
16425
+ examples: ["pattern"],
16426
+ hint: "(pattern: name the general shape) or (pattern:[shown-pattern])",
16427
+ taught: "taught"
16428
+ },
16429
+ {
16430
+ verb: "domain",
16431
+ form: "verb",
16432
+ opener: "(domain:",
16433
+ kinds: ["domain"],
16434
+ args: "text",
16435
+ thread: false,
16436
+ minText: 2,
16437
+ textRequired: true,
16438
+ lowering: [{ target: "Domain", writes: [{ from: P, edge: "PERTAIN_TO", to: "Domain" }], note: '(domain:#t X) mints a Domain named "#t X" today', decision: "k" }],
16439
+ examples: ["domain"],
16440
+ hint: "(domain: The Area) \u2014 Title Case, no thread",
16441
+ taught: "taught",
16442
+ decisions: ["k"]
16443
+ },
16444
+ {
16445
+ verb: "package",
16446
+ form: "verb",
16447
+ opener: "(package:",
16448
+ kinds: ["package"],
16449
+ args: "text",
16450
+ thread: false,
16451
+ minText: 2,
16452
+ textRequired: true,
16453
+ lowering: [{ target: "Package", writes: [{ from: P, edge: "DEPENDS_ON", to: "Package" }], note: "means 'concerns', written as DEPENDS_ON", decision: "c" }],
16454
+ examples: ["package"],
16455
+ hint: "(package: the-package-name) \u2014 the public package it's about",
16456
+ taught: "taught",
16457
+ decisions: ["c"]
16458
+ },
16459
+ {
16460
+ verb: "component",
16461
+ form: "verb",
16462
+ opener: "(component:",
16463
+ kinds: ["component"],
16464
+ args: "text",
16465
+ thread: false,
16466
+ minText: 2,
16467
+ textRequired: true,
16468
+ lowering: [{ target: "Component", writes: [{ from: P, edge: "CONCERNS", to: "Component" }] }],
16469
+ examples: ["component"],
16470
+ hint: "(component: The Component) \u2014 the framework/product unit",
16471
+ taught: "taught"
16472
+ },
16473
+ {
16474
+ verb: "aids",
16475
+ form: "verb",
16476
+ opener: "(aids:",
16477
+ kinds: ["transfer"],
16478
+ args: "handles",
16479
+ thread: true,
16480
+ handleRequired: true,
16481
+ lowering: [{ target: P, writes: [{ from: S, edge: "MAY_RESOLVE", to: P }], note: "source = the Solution in scope; non-Problem targets are skipped" }],
16482
+ examples: ["aids"],
16483
+ hint: "(aids:[problem-handle],\u2026) \u2014 other shown problems your fix might also resolve",
16484
+ taught: "taught"
16485
+ },
16486
+ {
16487
+ verb: "builds-on",
16488
+ form: "verb",
16489
+ opener: "(builds-on:",
16490
+ kinds: ["builds-on"],
16491
+ args: "handles",
16492
+ thread: true,
16493
+ handleRequired: true,
16494
+ lowering: [{ target: S, writes: [{ from: S, edge: "BUILDS_ON", to: S }] }],
16495
+ examples: ["buildsOn"],
16496
+ hint: "(builds-on:[shown-solution]) \u2014 your fix layers on it",
16497
+ taught: "taught",
16498
+ taughtBy: "linkBullet instruction text"
16499
+ },
16500
+ {
16501
+ verb: "supersedes",
16502
+ form: "verb",
16503
+ opener: "(supersedes:",
16504
+ kinds: ["supersedes"],
16505
+ args: "handles",
16506
+ thread: true,
16507
+ handleRequired: true,
16508
+ lowering: [{ target: S, writes: [{ from: S, edge: "SUPERSEDES", to: S }] }],
16509
+ examples: ["supersedes"],
16510
+ hint: "(supersedes:[shown-solution]) \u2014 your fix replaces it",
16511
+ taught: "taught",
16512
+ taughtBy: "linkBullet instruction text"
16513
+ },
16514
+ {
16515
+ verb: "alternative",
16516
+ form: "verb",
16517
+ opener: "(alternative:",
16518
+ kinds: ["alternative"],
16519
+ args: "handles",
16520
+ thread: true,
16521
+ handleRequired: true,
16522
+ lowering: [{ target: S, writes: [{ from: S, edge: "ALTERNATIVE_TO", to: S }] }],
16523
+ examples: ["alternative"],
16524
+ hint: "(alternative:[shown-solution]) \u2014 a different valid approach",
16525
+ taught: "taught",
16526
+ taughtBy: "linkBullet instruction text"
16527
+ },
16528
+ {
16529
+ verb: "confirm",
16530
+ form: "verb",
16531
+ opener: "(confirm:",
16532
+ kinds: ["confirm"],
16533
+ args: "handles",
16534
+ thread: false,
16535
+ handleRequired: true,
16536
+ lowering: [{ target: S, writes: [], note: "corroboration witness (corroborationCount), no edge" }],
16537
+ examples: ["confirm"],
16538
+ hint: "(confirm:[shown-prior]) \u2014 it held",
16539
+ taught: "taught",
16540
+ taughtBy: "verdictBullet + context render",
16541
+ decisions: ["j"]
16542
+ },
16543
+ {
16544
+ verb: "refute",
16545
+ form: "verb",
16546
+ opener: "(refute:",
16547
+ kinds: ["refute"],
16548
+ args: "handles-text",
16549
+ thread: false,
16550
+ minText: 4,
16551
+ handleRequired: true,
16552
+ textRequired: true,
16553
+ lowering: [{ target: S, writes: [], note: "contradiction witness (contradictionPressure), no edge; reason stays local" }],
16554
+ examples: ["refute"],
16555
+ hint: "(refute:[shown-prior] one line why) \u2014 the reason is required",
16556
+ taught: "taught",
16557
+ taughtBy: "verdictBullet + context render",
16558
+ decisions: ["j"]
16559
+ }
16560
+ ];
16561
+ TAG_FENCES = [
16562
+ {
16563
+ fence: "errata-problem",
16564
+ fields: ["problem", "cause", "fix", "anchor", "kind"],
16565
+ required: [["problem"]],
16566
+ taught: "untaught",
16567
+ parser: "apps/daemon/src/turn.ts parseDesignFlags",
16568
+ decisions: ["h"],
16569
+ hint: "use an inline [!\u2026] flag instead (errata-problem cause ids never meet inline ids)"
16570
+ },
16571
+ {
16572
+ fence: "errata-resolved",
16573
+ fields: ["id", "problem", "fix", "disposition", "reason"],
16574
+ required: [["id", "problem"]],
16575
+ taught: "taught",
16576
+ taughtBy: "retract form only (TAG_EXAMPLE.retract); the fix form is untaught",
16577
+ parser: "apps/daemon/src/turn.ts parseDesignResolutions",
16578
+ hint: "```errata-resolved with id: and disposition: false_positive | invalid, reason: \u2026"
16579
+ },
16580
+ {
16581
+ fence: "errata-triage",
16582
+ fields: ["presenting", "cause", "test", "fix", "drift", "route"],
16583
+ required: [["presenting", "route"], ["cause", "test"]],
16584
+ taught: "taught",
16585
+ taughtBy: "MCP errata_triage tool copy",
16586
+ parser: "packages/local-graph/src/triage.ts parseTriageFences",
16587
+ hint: "```errata-triage with presenting: and cause: (or route: + test: to answer one)"
16588
+ },
16589
+ {
16590
+ fence: "errata-abstraction",
16591
+ fields: ["candidate", "principle", "covers"],
16592
+ required: [["candidate"], ["principle"], ["covers"]],
16593
+ taught: "taught",
16594
+ taughtBy: "MCP errata_pending_abstractions tool copy",
16595
+ parser: "packages/local-graph/src/abstraction.ts parseAbstractionFences",
16596
+ hint: "```errata-abstraction with candidate:, principle:, covers: (all required)"
16597
+ },
16598
+ {
16599
+ fence: "errata-intent",
16600
+ fields: ["intent"],
16601
+ required: [["intent"]],
16602
+ taught: "taught",
16603
+ parser: "packages/local-graph/src/intent.ts parseIntentFences",
16604
+ hint: "```errata-intent with intent: what you're about to do"
16605
+ },
16606
+ {
16607
+ fence: "errata-share",
16608
+ fields: ["priors"],
16609
+ required: [["priors"]],
16610
+ taught: "taught",
16611
+ taughtBy: "subagent hand-off copy",
16612
+ parser: "packages/local-graph/src/intent.ts parseShareFence",
16613
+ hint: "```errata-share with priors: all | [h1], [h2]"
16614
+ }
16615
+ ];
16616
+ TAG_VERB_BY_NAME = new Map(
16617
+ TAG_VERBS.filter((v) => v.form === "verb").map((v) => [v.verb, v])
16618
+ );
16619
+ TAG_PAREN_VERBS = [...TAG_VERB_BY_NAME.keys()].sort((a, b) => b.length - a.length);
16620
+ TAG_GRAMMAR_OPENERS = [
16621
+ ...new Set(TAG_VERBS.filter((v) => v.form !== "citation").map((v) => v.opener))
16622
+ ];
16623
+ TAG_QUOTED_PAREN_SOURCE = String.raw`\(\s*(?:${TAG_PAREN_VERBS.map((v) => v.replace(/-/g, "\\-")).join("|")})\s*:|\(\[`;
16624
+ TAG_QUOTED_RE = new RegExp(String.raw`\[[!?]|` + TAG_QUOTED_PAREN_SOURCE, "i");
16625
+ TAG_QUOTED_PAREN_RE = new RegExp(TAG_QUOTED_PAREN_SOURCE, "i");
16626
+ TEMPLATE_VERB_GROUP = new RegExp(
16627
+ String.raw`\(\s*(${TAG_PAREN_VERBS.map((v) => v.replace(/-/g, "\\-")).join("|")})\s*:((?:[^()\n]|\([^()\n]*\))*)\)`,
16628
+ "gi"
16629
+ );
16630
+ BRACKET_TOKEN = /\[([^\]\n]{1,80})\]/g;
16631
+ REAL_HANDLE_SHAPE = new RegExp(
16632
+ 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,})$`,
16633
+ "i"
16634
+ );
16635
+ TAG_TEMPLATES = deriveTagTemplates(tagGrammarCopy());
16636
+ VERB_WORDS_SOURCE = TAG_PAREN_VERBS.map((v) => v.replace(/-/g, "\\-")).join("|");
16637
+ JOINED_VERB = new RegExp(String.raw`[,;]\s*(?=(?:${VERB_WORDS_SOURCE})\s*:)`, "i");
16638
+ LEADING_VERB = new RegExp(String.raw`^\s*(${VERB_WORDS_SOURCE})\s*:`, "i");
16639
+ ANY_VERB_WORD = new RegExp(String.raw`\b(?:${VERB_WORDS_SOURCE})\s*:`, "gi");
16640
+ QUOTED_TAG_OPENER = new RegExp(String.raw`\(\s*(?:(${VERB_WORDS_SOURCE})\s*:|(?=\[))`, "gi");
16641
+ }
16642
+ });
16643
+
16087
16644
  // ../../packages/local-shared/src/index.ts
16088
16645
  var init_src2 = __esm({
16089
16646
  "../../packages/local-shared/src/index.ts"() {
@@ -16097,6 +16654,7 @@ var init_src2 = __esm({
16097
16654
  init_review();
16098
16655
  init_anchorable();
16099
16656
  init_sqlite_adapter();
16657
+ init_tag_grammar();
16100
16658
  }
16101
16659
  });
16102
16660
 
@@ -16267,6 +16825,48 @@ import { parentPort } from "node:worker_threads";
16267
16825
  init_src2();
16268
16826
  init_src();
16269
16827
 
16828
+ // ../../packages/local-graph/src/wm-calibration.ts
16829
+ var WM_CALIBRATION_KEYS = [
16830
+ "shownCount",
16831
+ "evictedCount",
16832
+ "reinforceShownCount",
16833
+ "reinforceRecentCount",
16834
+ "reinforceEvictedCount",
16835
+ "reinforceUnshownCount",
16836
+ "reinforceUnshownQueryOnlyCount"
16837
+ ];
16838
+ var WM_COUNTED_PREDICATE = WM_CALIBRATION_KEYS.map((k) => `json_type(attrs_json, '$.${k}') IS NOT NULL`).join(" OR ");
16839
+ var wmCounterSql = (k) => `CASE WHEN json_type(attrs_json, '$.${k}') IN ('integer', 'real') THEN json_extract(attrs_json, '$.${k}') END`;
16840
+
16841
+ // ../../packages/local-graph/src/exposure-attrs.ts
16842
+ var EXPOSURE_ATTRS = [
16843
+ "shownCount",
16844
+ "evictedCount",
16845
+ "firstShownAt",
16846
+ "firstEvictedAt",
16847
+ "firstShownAtSeq",
16848
+ "lastShownAtSeq",
16849
+ "lastEvictedAtSeq",
16850
+ "shownSinceTouch",
16851
+ "shownSinceTouchAt",
16852
+ "exposureLog",
16853
+ "exposureLogStart",
16854
+ "exposureLogSince",
16855
+ "exposureFirst",
16856
+ "exposureTrimmed"
16857
+ ];
16858
+ var EXPOSURE_SCOPE = "exposure";
16859
+ var EXPOSURE_KEYS = new Set(EXPOSURE_ATTRS);
16860
+ var isExposureAttr = (key) => EXPOSURE_KEYS.has(key);
16861
+ function pickExposure(attrs) {
16862
+ const out = {};
16863
+ for (const k of EXPOSURE_ATTRS) if (attrs[k] !== void 0) out[k] = attrs[k];
16864
+ return out;
16865
+ }
16866
+ function exposureDocSql(key) {
16867
+ 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)`;
16868
+ }
16869
+
16270
16870
  // ../../packages/local-graph/src/history-cache-floor.ts
16271
16871
  var HISTORY_CACHE_FLOOR_KEY = "history-cache-withdrawal-floor-3";
16272
16872
  function installHistoryCacheFloor(db) {
@@ -16307,7 +16907,7 @@ function installHistoryCacheFloor(db) {
16307
16907
  OR EXISTS(SELECT 1 FROM history_cache_withdrawals w WHERE w.id = nodes.id))
16308
16908
  AND (embedding_blob IS NOT NULL OR embedding_json <> '[]' OR is_landmark <> 0
16309
16909
  OR description <> ${description} OR attrs_json <> ${attrs});`;
16310
- const internal = `key IN ('orgId','teamId','groupId','projectId','aliasTexts','aliasCanonicalIds')
16910
+ const internal = `key IN ('orgId','teamId','groupId','projectId','aliasTexts','aliasCanonicalIds','residueAudienceVerified')
16311
16911
  OR key GLOB 'mergedFrom*' OR key GLOB '*WitnessKeys' OR key GLOB '*Sessions'
16312
16912
  OR key GLOB 'authoringSession*' OR key GLOB 'contributor*' OR key GLOB 'witnessKeys*'`;
16313
16913
  const stripMetadata = (scope) => `UPDATE nodes SET attrs_json=(
@@ -16443,7 +17043,9 @@ function installEditPlaceholderRetirement(db) {
16443
17043
 
16444
17044
  // ../../packages/local-graph/src/cloud-attr-compaction.ts
16445
17045
  init_src2();
16446
- var CLOUD_ATTR_COMPACTION_KEY = "cloud-attr-compaction-1";
17046
+ var CLOUD_ATTR_COMPACTION_KEY = "cloud-attr-compaction-2";
17047
+ var V1_KEY = "cloud-attr-compaction-1";
17048
+ var STRUCT_LABELS = "('Problem', 'Solution', 'RootCause')";
16447
17049
  var DUPES = CLOUD_ATTR_DUPLICATE_KEYS.map((k) => `'${k}'`).join(",");
16448
17050
  function compacted(row) {
16449
17051
  const packed = `json_extract(${row}.attrs_json, '$.attrsJson')`;
@@ -16462,14 +17064,17 @@ function dirty(row) {
16462
17064
  return `json_extract(${row}.attrs_json, '$.source') = 'cloud' AND (${has})`;
16463
17065
  }
16464
17066
  function installCloudAttrCompaction(db) {
16465
- if (db.prepare("SELECT value FROM store_meta WHERE key = ?").get(CLOUD_ATTR_COMPACTION_KEY)) return;
17067
+ const marked = (key) => Boolean(db.prepare("SELECT value FROM store_meta WHERE key = ?").get(key));
17068
+ if (marked(CLOUD_ATTR_COMPACTION_KEY)) return;
17069
+ const scope = marked(V1_KEY) ? ` AND label IN ${STRUCT_LABELS}` : "";
16466
17070
  db.transaction(() => {
16467
17071
  for (const operation of ["INSERT", "UPDATE"]) {
16468
- db.exec(`CREATE TRIGGER IF NOT EXISTS cloud_attr_compact_${operation.toLowerCase()}
17072
+ db.exec(`DROP TRIGGER IF EXISTS cloud_attr_compact_${operation.toLowerCase()}`);
17073
+ db.exec(`CREATE TRIGGER cloud_attr_compact_${operation.toLowerCase()}
16469
17074
  AFTER ${operation} ON nodes WHEN ${dirty("NEW")}
16470
17075
  BEGIN UPDATE nodes SET attrs_json = ${compacted("nodes")} WHERE id = NEW.id; END;`);
16471
17076
  }
16472
- db.exec(`UPDATE nodes SET attrs_json = ${compacted("nodes")} WHERE ${dirty("nodes")}`);
17077
+ db.exec(`UPDATE nodes SET attrs_json = ${compacted("nodes")} WHERE ${dirty("nodes")}${scope}`);
16473
17078
  db.prepare("INSERT INTO store_meta(key,value) VALUES (?,?)").run(CLOUD_ATTR_COMPACTION_KEY, "installed");
16474
17079
  });
16475
17080
  }
@@ -16508,6 +17113,313 @@ function installHistoryAttrSlim(db) {
16508
17113
  db.prepare("INSERT INTO store_meta(key,value) VALUES (?,?)").run(HISTORY_ATTR_SLIM_KEY, "done");
16509
17114
  }
16510
17115
 
17116
+ // ../../packages/local-graph/src/text-index.ts
17117
+ var TEXT_INDEX_KEY = "text-index-trigram-2";
17118
+ var TEXT_INDEX_MIN_TERM = 3;
17119
+ function installTextIndex(db) {
17120
+ if (db.prepare("SELECT value FROM store_meta WHERE key = ?").get(TEXT_INDEX_KEY)) return;
17121
+ try {
17122
+ db.exec(`CREATE VIRTUAL TABLE temp.fts5_probe USING fts5(x, tokenize='trigram'); DROP TABLE temp.fts5_probe`);
17123
+ } catch {
17124
+ db.prepare("INSERT INTO store_meta(key,value) VALUES (?,?)").run(TEXT_INDEX_KEY, "unavailable");
17125
+ return;
17126
+ }
17127
+ db.transaction(() => {
17128
+ db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS nodes_text
17129
+ USING fts5(description, content='nodes', content_rowid='rowid', tokenize='trigram')`);
17130
+ db.exec(`DROP TRIGGER IF EXISTS nodes_text_update_out; DROP TRIGGER IF EXISTS nodes_text_update_in;
17131
+ DROP TRIGGER IF EXISTS nodes_text_update`);
17132
+ db.exec(`CREATE TRIGGER IF NOT EXISTS nodes_text_insert AFTER INSERT ON nodes WHEN NEW.valid_to IS NULL
17133
+ BEGIN INSERT INTO nodes_text(rowid, description) VALUES (NEW.rowid, NEW.description); END;`);
17134
+ db.exec(`CREATE TRIGGER nodes_text_update AFTER UPDATE OF description, valid_to ON nodes
17135
+ WHEN OLD.description IS NOT NEW.description OR OLD.valid_to IS NOT NEW.valid_to
17136
+ BEGIN
17137
+ INSERT INTO nodes_text(nodes_text, rowid, description)
17138
+ SELECT 'delete', OLD.rowid, OLD.description WHERE OLD.valid_to IS NULL;
17139
+ INSERT INTO nodes_text(rowid, description)
17140
+ SELECT NEW.rowid, NEW.description WHERE NEW.valid_to IS NULL;
17141
+ END;`);
17142
+ db.exec(`CREATE TRIGGER IF NOT EXISTS nodes_text_delete AFTER DELETE ON nodes WHEN OLD.valid_to IS NULL
17143
+ BEGIN INSERT INTO nodes_text(nodes_text, rowid, description) VALUES ('delete', OLD.rowid, OLD.description); END;`);
17144
+ db.exec(`INSERT INTO nodes_text(nodes_text) VALUES ('delete-all')`);
17145
+ db.exec(`INSERT INTO nodes_text(rowid, description) SELECT rowid, description FROM nodes WHERE valid_to IS NULL`);
17146
+ db.prepare("INSERT OR REPLACE INTO store_meta(key,value) VALUES (?,?)").run("text-index-trigram-1", "installed");
17147
+ db.prepare("INSERT INTO store_meta(key,value) VALUES (?,?)").run(TEXT_INDEX_KEY, "installed");
17148
+ });
17149
+ }
17150
+ function textIndexReady(db) {
17151
+ return db.prepare("SELECT value FROM store_meta WHERE key = ?").get(TEXT_INDEX_KEY)?.["value"] === "installed";
17152
+ }
17153
+
17154
+ // ../../packages/local-graph/src/change-feed.ts
17155
+ var CHANGE_FEED_KEY = "change-feed";
17156
+ var ANY_CHANGE = "*";
17157
+ var nodeScope = (label) => `node:${label}`;
17158
+ var CHANGE_LOG_KEEP = 25e4;
17159
+ var PRUNED_TO_KEY = "change-log-pruned-to";
17160
+ function installChangeFeed(db) {
17161
+ if (changeFeedInstalled(db)) return;
17162
+ db.transaction(() => {
17163
+ db.exec("CREATE TABLE IF NOT EXISTS change_seq (scope TEXT PRIMARY KEY, seq INTEGER NOT NULL) WITHOUT ROWID");
17164
+ db.exec(`CREATE TABLE IF NOT EXISTS change_log (change_seq INTEGER PRIMARY KEY, kind TEXT NOT NULL, id TEXT NOT NULL,
17165
+ label TEXT, from_id TEXT, to_id TEXT)`);
17166
+ for (const name of ["nodes", "edges"].flatMap((t) => ["insert", "update", "delete"].map((op) => `change_feed_${t}_${op}`))) {
17167
+ db.exec(`DROP TRIGGER IF EXISTS ${name}`);
17168
+ }
17169
+ const changed = (table) => columns(db, table).map((c) => `OLD."${c}" IS NOT NEW."${c}"`).join(" OR ");
17170
+ const strip = (row) => `json_remove(${row}.attrs_json, ${EXPOSURE_ATTRS.map((a) => `'$.${a}'`).join(", ")})`;
17171
+ const exposureOnly = `(${columns(db, "nodes").filter((c) => c !== "attrs_json").map((c) => `OLD."${c}" IS NEW."${c}"`).join(" AND ")}
17172
+ AND CASE WHEN json_valid(OLD.attrs_json) AND json_valid(NEW.attrs_json) THEN ${strip("OLD")} IS ${strip("NEW")} ELSE 0 END)`;
17173
+ const bump = (...scopes) => `INSERT INTO change_seq(scope, seq) VALUES ${scopes.map((s) => `(${s}, 1)`).join(", ")}
17174
+ ON CONFLICT(scope) DO UPDATE SET seq = seq + 1;`;
17175
+ const seq = `(SELECT seq FROM change_seq WHERE scope = '*')`;
17176
+ const logNode = (row) => `INSERT INTO change_log(change_seq, kind, id, label) VALUES (${seq}, 'node', ${row}.id, ${row}.label);`;
17177
+ 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);`;
17178
+ const node = (row) => `'node:' || ${row}.label`;
17179
+ const edge = (row) => `'edge:' || ${row}.type`;
17180
+ db.exec(`CREATE TRIGGER change_feed_nodes_insert AFTER INSERT ON nodes BEGIN ${bump(`'*'`, node("NEW"))} ${logNode("NEW")} END;`);
17181
+ db.exec(`CREATE TRIGGER change_feed_nodes_update AFTER UPDATE ON nodes WHEN ${changed("nodes")} BEGIN
17182
+ ${bump(`'*'`)}
17183
+ INSERT INTO change_seq(scope, seq) SELECT CASE WHEN ${exposureOnly} THEN '${EXPOSURE_SCOPE}' ELSE ${node("NEW")} END, 1 WHERE 1
17184
+ ON CONFLICT(scope) DO UPDATE SET seq = seq + 1;
17185
+ INSERT INTO change_seq(scope, seq) SELECT ${node("OLD")}, 1 WHERE OLD.label IS NOT NEW.label
17186
+ ON CONFLICT(scope) DO UPDATE SET seq = seq + 1;
17187
+ ${logNode("NEW")}
17188
+ END;`);
17189
+ db.exec(`CREATE TRIGGER change_feed_nodes_delete AFTER DELETE ON nodes BEGIN ${bump(`'*'`, node("OLD"))} ${logNode("OLD")} END;`);
17190
+ db.exec(`CREATE TRIGGER change_feed_edges_insert AFTER INSERT ON edges BEGIN ${bump(`'*'`, edge("NEW"))} ${logEdge("NEW")} END;`);
17191
+ db.exec(`CREATE TRIGGER change_feed_edges_update AFTER UPDATE ON edges WHEN ${changed("edges")} BEGIN
17192
+ ${bump(`'*'`, edge("NEW"))}
17193
+ INSERT INTO change_seq(scope, seq) SELECT ${edge("OLD")}, 1 WHERE OLD.type IS NOT NEW.type
17194
+ ON CONFLICT(scope) DO UPDATE SET seq = seq + 1;
17195
+ ${logEdge("NEW")}
17196
+ END;`);
17197
+ db.exec(`CREATE TRIGGER change_feed_edges_delete AFTER DELETE ON edges BEGIN ${bump(`'*'`, edge("OLD"))} ${logEdge("OLD")} END;`);
17198
+ db.prepare("INSERT OR REPLACE INTO store_meta(key, value) VALUES (?, ?)").run(PRUNED_TO_KEY, String(readChangeSeq(db)));
17199
+ db.prepare("INSERT OR REPLACE INTO store_meta(key, value) VALUES (?, ?)").run(CHANGE_FEED_KEY, columnSignature(db));
17200
+ });
17201
+ }
17202
+ function readChangeSeq(db, scopes = [ANY_CHANGE]) {
17203
+ if (!scopes.length) return 0;
17204
+ const row = db.prepare(`SELECT coalesce(sum(seq), 0) AS n FROM change_seq WHERE scope IN (${scopes.map(() => "?").join(",")})`).get(...scopes);
17205
+ return Number(row?.n ?? 0);
17206
+ }
17207
+ function changeLogCovers(db, sinceSeq) {
17208
+ const prunedTo = Number(db.prepare("SELECT value FROM store_meta WHERE key = ?").get(PRUNED_TO_KEY)?.["value"] ?? NaN);
17209
+ return Number.isFinite(prunedTo) && sinceSeq >= prunedTo;
17210
+ }
17211
+ function readChangedNodeIds(db, sinceSeq, labels) {
17212
+ if (!changeLogCovers(db, sinceSeq)) return null;
17213
+ if (!labels.length) return [];
17214
+ 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);
17215
+ return rows.map((r) => r.id);
17216
+ }
17217
+ function readChangedAmong(db, sinceSeq, ids) {
17218
+ if (!changeLogCovers(db, sinceSeq)) return null;
17219
+ const out = [];
17220
+ for (let i = 0; i < ids.length; i += 500) {
17221
+ const chunk = ids.slice(i, i + 500);
17222
+ 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);
17223
+ for (const r of rows) out.push(r.id);
17224
+ }
17225
+ return out;
17226
+ }
17227
+ function readLineageChangedFiles(db, sinceSeq) {
17228
+ if (!changeLogCovers(db, sinceSeq)) return null;
17229
+ const rows = db.prepare(LINEAGE_CHANGED_FILES_SQL).all({ s: sinceSeq });
17230
+ return rows.map((r) => r.id);
17231
+ }
17232
+ var LINEAGE_CHANGED_FILES_SQL = `
17233
+ SELECT id FROM change_log WHERE change_seq > :s AND kind = 'node' AND label = 'File'
17234
+ UNION SELECT from_id FROM change_log WHERE change_seq > :s AND kind = 'edge' AND label IN ('DEFINES','CONTAINS')
17235
+ UNION SELECT e.from_id FROM (SELECT DISTINCT id FROM change_log WHERE change_seq > :s AND kind = 'node') c
17236
+ CROSS JOIN edges e ON e.to_id = c.id AND e.type IN ('DEFINES','CONTAINS')
17237
+ `;
17238
+ function pruneChangeLog(db) {
17239
+ const max = Number(db.prepare("SELECT coalesce(max(change_seq), 0) AS n FROM change_log").get()?.["n"] ?? 0);
17240
+ const cut = max - CHANGE_LOG_KEEP;
17241
+ if (cut <= 0) return 0;
17242
+ let removed = 0;
17243
+ db.transaction(() => {
17244
+ removed = Number(db.prepare("DELETE FROM change_log WHERE change_seq <= ?").run(cut).changes);
17245
+ const prev = Number(db.prepare("SELECT value FROM store_meta WHERE key = ?").get(PRUNED_TO_KEY)?.["value"] ?? 0);
17246
+ db.prepare("INSERT OR REPLACE INTO store_meta(key, value) VALUES (?, ?)").run(PRUNED_TO_KEY, String(Math.max(prev, cut)));
17247
+ });
17248
+ return removed;
17249
+ }
17250
+ function changeFeedInstalled(db) {
17251
+ return db.prepare("SELECT value FROM store_meta WHERE key = ?").get(CHANGE_FEED_KEY)?.["value"] === columnSignature(db);
17252
+ }
17253
+ function columns(db, table) {
17254
+ return db.prepare(`SELECT name FROM pragma_table_info('${table}') ORDER BY cid`).all().map((r) => r.name);
17255
+ }
17256
+ function columnSignature(db) {
17257
+ return `5|${columns(db, "nodes").join(",")}|${columns(db, "edges").join(",")}|${EXPOSURE_ATTRS.join(",")}`;
17258
+ }
17259
+
17260
+ // ../../packages/local-graph/src/lineage-integrity.ts
17261
+ var statements = /* @__PURE__ */ new WeakMap();
17262
+ function stmt(db, sql) {
17263
+ let byDb = statements.get(db);
17264
+ if (!byDb) statements.set(db, byDb = /* @__PURE__ */ new Map());
17265
+ let s = byDb.get(sql);
17266
+ if (!s) byDb.set(sql, s = db.prepare(sql));
17267
+ return s;
17268
+ }
17269
+ var HISTORY_EDGE_TYPES = [
17270
+ "SUPERSEDED_BY",
17271
+ "CONTINUES",
17272
+ "FORKED_FROM",
17273
+ "REGRESSION_OF",
17274
+ "SPLIT_INTO",
17275
+ "REVEALED_BY"
17276
+ ];
17277
+ var LINEAGE_INTEGRITY_KEY = "lineage-integrity-v1";
17278
+ var HISTORY_SQL = HISTORY_EDGE_TYPES.map((t) => `'${t}'`).join(",");
17279
+ var NOT_HISTORY = `type NOT IN (${HISTORY_SQL})`;
17280
+ var NOT_HISTORICAL_ROW = `json_type(attrs_json, '$.historical') IS NOT 'true'`;
17281
+ var RETAINS_EDGES = (n) => `(${n}.label = 'Problem'
17282
+ AND json_type(${n}.attrs_json, '$.resolvedAt') IS NOT NULL
17283
+ AND COALESCE(json_extract(${n}.attrs_json, '$.resolvedAs'), '') NOT IN ('duplicate', 'false_positive'))`;
17284
+ var PRESENT = (n) => `(${n}.valid_to IS NULL OR ${RETAINS_EDGES(n)})`;
17285
+ var STRANDED_EDGE_PREDICATE = `e.valid_to IS NULL AND e.${NOT_HISTORY}
17286
+ AND (NOT EXISTS (SELECT 1 FROM nodes f WHERE f.id = e.from_id AND ${PRESENT("f")})
17287
+ OR NOT EXISTS (SELECT 1 FROM nodes t WHERE t.id = e.to_id AND ${PRESENT("t")}))`;
17288
+ function countStrandedEdges(db) {
17289
+ 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();
17290
+ const byType = {};
17291
+ let total = 0;
17292
+ for (const r of rows) {
17293
+ byType[r.type] = Number(r.n);
17294
+ total += Number(r.n);
17295
+ }
17296
+ return { total, byType };
17297
+ }
17298
+ function countLiveHeadConflicts(db) {
17299
+ const row = db.prepare(
17300
+ `SELECT count(*) AS n FROM (
17301
+ SELECT COALESCE(logical_id, id) AS lid FROM nodes
17302
+ WHERE valid_to IS NULL AND ${NOT_HISTORICAL_ROW}
17303
+ GROUP BY lid HAVING count(*) > 1)`
17304
+ ).get();
17305
+ return Number(row?.n ?? 0);
17306
+ }
17307
+ function closeIncidentEdges(db, id, t) {
17308
+ let closed = 0;
17309
+ for (const side of ["from_id", "to_id"]) {
17310
+ 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);
17311
+ closed += Number(r.changes);
17312
+ }
17313
+ return closed;
17314
+ }
17315
+ function reopenCascadeClosedEdges(db, id, closedAt) {
17316
+ let reopened = 0;
17317
+ for (const [side, other] of [["from_id", "to_id"], ["to_id", "from_id"]]) {
17318
+ const r = stmt(
17319
+ db,
17320
+ `UPDATE edges SET valid_to = NULL
17321
+ WHERE ${side} = ? AND valid_to = ? AND ${NOT_HISTORY}
17322
+ AND EXISTS (SELECT 1 FROM nodes o WHERE o.id = edges.${other} AND o.valid_to IS NULL)`
17323
+ ).run(id, closedAt);
17324
+ reopened += Number(r.changes);
17325
+ }
17326
+ return reopened;
17327
+ }
17328
+ function collapseLiveHeads(db, logicalIds, t) {
17329
+ const headsOf = db.prepare(
17330
+ `SELECT id, version, valid_from FROM (
17331
+ SELECT id, version, valid_from, attrs_json FROM nodes WHERE logical_id = ?1 AND valid_to IS NULL
17332
+ UNION
17333
+ SELECT id, version, valid_from, attrs_json FROM nodes WHERE id = ?1 AND logical_id IS NULL AND valid_to IS NULL
17334
+ ) WHERE ${NOT_HISTORICAL_ROW}`
17335
+ );
17336
+ const continuesInto = db.prepare(
17337
+ `SELECT to_id FROM edges WHERE from_id = ? AND type = 'CONTINUES' AND valid_to IS NULL`
17338
+ );
17339
+ const rekeyHead = db.prepare(`UPDATE nodes SET logical_id = ?1 WHERE id = ?1`);
17340
+ const rekeyCopies = db.prepare(
17341
+ `UPDATE nodes SET logical_id = ?1 WHERE logical_id = ?2 AND substr(id, 1, length(?1) + 2) = ?1 || '@v'`
17342
+ );
17343
+ const lineageMembers = db.prepare(
17344
+ `SELECT id FROM nodes WHERE logical_id = ?1 UNION SELECT id FROM nodes WHERE id = ?1`
17345
+ );
17346
+ const closeContinues = db.prepare(
17347
+ `UPDATE edges SET valid_to = MAX(valid_from, ?) WHERE from_id = ? AND to_id = ? AND type = 'CONTINUES' AND valid_to IS NULL`
17348
+ );
17349
+ let split = 0;
17350
+ const queue = [...new Set(logicalIds)];
17351
+ for (let i = 0; i < queue.length; i++) {
17352
+ const lid = queue[i];
17353
+ const heads = headsOf.all(lid);
17354
+ if (heads.length < 2) continue;
17355
+ const members = new Set(lineageMembers.all(lid).map((r) => r.id));
17356
+ const splitOff = (h) => {
17357
+ for (const { to_id } of continuesInto.all(h.id)) {
17358
+ if (members.has(to_id)) closeContinues.run(t, h.id, to_id);
17359
+ }
17360
+ rekeyHead.run(h.id);
17361
+ rekeyCopies.run(h.id, lid);
17362
+ queue.push(h.id);
17363
+ split++;
17364
+ };
17365
+ const kept = new Set(heads.map((h) => h.id));
17366
+ for (const h of heads) {
17367
+ if (h.id === lid) continue;
17368
+ const refuted = continuesInto.all(h.id).some(
17369
+ (e) => e.to_id !== h.id && kept.has(e.to_id)
17370
+ );
17371
+ if (refuted && kept.size > 1) {
17372
+ splitOff(h);
17373
+ kept.delete(h.id);
17374
+ }
17375
+ }
17376
+ let remaining = heads.filter((h) => kept.has(h.id));
17377
+ if (remaining.length < 2) continue;
17378
+ remaining = [...remaining].sort(
17379
+ (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)
17380
+ );
17381
+ for (const h of remaining.slice(1)) splitOff(h);
17382
+ }
17383
+ return split;
17384
+ }
17385
+ function repairLineageIntegrity(db, now) {
17386
+ const historicalRowsClosed = Number(
17387
+ db.prepare(
17388
+ `UPDATE nodes SET valid_to = MAX(valid_from, COALESCE(
17389
+ (SELECT s.valid_from FROM edges s WHERE s.id = 'edge_superseded_' || nodes.id), ?))
17390
+ WHERE valid_to IS NULL AND json_type(attrs_json, '$.historical') = 'true'`
17391
+ ).run(now).changes
17392
+ );
17393
+ const conflicted = db.prepare(
17394
+ `SELECT COALESCE(logical_id, id) AS lid FROM nodes
17395
+ WHERE valid_to IS NULL AND ${NOT_HISTORICAL_ROW}
17396
+ GROUP BY lid HAVING count(*) > 1`
17397
+ ).all().map((r) => r.lid);
17398
+ const headsSplit = collapseLiveHeads(db, conflicted, now);
17399
+ const strandedEdgesClosed = Number(
17400
+ db.prepare(
17401
+ `UPDATE edges SET valid_to = MAX(valid_from, COALESCE(
17402
+ (SELECT min(v) FROM (
17403
+ SELECT f.valid_to AS v FROM nodes f WHERE f.id = edges.from_id AND NOT ${PRESENT("f")}
17404
+ UNION ALL
17405
+ SELECT t.valid_to FROM nodes t WHERE t.id = edges.to_id AND NOT ${PRESENT("t")})), ?))
17406
+ WHERE id IN (SELECT e.id FROM edges e WHERE ${STRANDED_EDGE_PREDICATE})`
17407
+ ).run(now).changes
17408
+ );
17409
+ return { historicalRowsClosed, headsSplit, strandedEdgesClosed };
17410
+ }
17411
+ function installLineageIntegrityRepair(db, now = Date.now()) {
17412
+ if (db.prepare("SELECT value FROM store_meta WHERE key = ?").get(LINEAGE_INTEGRITY_KEY)) return null;
17413
+ return db.transaction(() => {
17414
+ const report = repairLineageIntegrity(db, now);
17415
+ db.prepare("INSERT OR REPLACE INTO store_meta (key, value) VALUES (?, ?)").run(
17416
+ LINEAGE_INTEGRITY_KEY,
17417
+ JSON.stringify({ at: now, ...report })
17418
+ );
17419
+ return report;
17420
+ });
17421
+ }
17422
+
16511
17423
  // ../../packages/math/src/constants.ts
16512
17424
  var NEMORI_DEDUP_COSINE = 0.88;
16513
17425
 
@@ -16695,6 +17607,177 @@ function detectCommunitiesLeiden(input, opts = {}) {
16695
17607
  var DEMAND_HOT = Math.log1p(10);
16696
17608
  var DEMAND_WARM = Math.log1p(5);
16697
17609
 
17610
+ // ../../packages/local-graph/src/node-select.ts
17611
+ var SELECT_INDEXES = [
17612
+ "nodes_live_local_community",
17613
+ "nodes_live_fold_candidate",
17614
+ "nodes_live_resolved",
17615
+ "nodes_live_fix_candidate",
17616
+ "nodes_live_anchor_hint"
17617
+ ];
17618
+ var EMBEDDING_DIM_SQL = "(CASE WHEN length(embedding_blob) >= 4 THEN length(embedding_blob) / 4 ELSE COALESCE(json_array_length(embedding_json), 0) END)";
17619
+ var KEY = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/;
17620
+ var COLUMNS = /* @__PURE__ */ new Set([
17621
+ "created_at",
17622
+ "last_updated_at",
17623
+ "page_rank",
17624
+ "created_at_seq",
17625
+ "last_reinforced_at_seq",
17626
+ "cumulative_hits",
17627
+ "extraction_confidence"
17628
+ ]);
17629
+ function path(key) {
17630
+ if (!KEY.test(key)) throw new Error(`selectNodes: bad attr key ${key}`);
17631
+ return `'$.${key}'`;
17632
+ }
17633
+ function selectIndex(i) {
17634
+ if (!SELECT_INDEXES.includes(i)) throw new Error(`selectNodes: bad index ${i}`);
17635
+ return i;
17636
+ }
17637
+ function column(c) {
17638
+ if (!COLUMNS.has(c)) throw new Error(`selectNodes: bad column ${c}`);
17639
+ return c;
17640
+ }
17641
+ function compileNodeWhere(q) {
17642
+ if (q.labels.length === 0) throw new Error("selectNodes: no labels");
17643
+ const params = [...q.labels];
17644
+ const byId = (q.where ?? []).some((w) => "idIn" in w || "idPrefix" in w && w.idPrefix.length > 0);
17645
+ const unindexed = byId ? "+" : "";
17646
+ const conds = [`${unindexed}label IN (${q.labels.map(() => "?").join(", ")})`];
17647
+ if (!q.includeClosed) conds.push(`${unindexed}valid_to IS NULL`);
17648
+ for (const w of q.where ?? []) {
17649
+ if ("embedding" in w) {
17650
+ conds.push(`${EMBEDDING_DIM_SQL} ${w.embedding === "present" ? ">" : "="} 0`);
17651
+ } else if ("idPrefix" in w) {
17652
+ conds.push("substr(id, 1, ?) = ?");
17653
+ params.push(w.idPrefix.length, w.idPrefix);
17654
+ if (w.idPrefix.length > 0) {
17655
+ const last = w.idPrefix.charCodeAt(w.idPrefix.length - 1);
17656
+ conds.push("id >= ? AND id < ?");
17657
+ params.push(w.idPrefix, w.idPrefix.slice(0, -1) + String.fromCharCode(last + 1));
17658
+ }
17659
+ } else if ("idIn" in w) {
17660
+ if (w.idIn.length === 0) conds.push("0");
17661
+ else {
17662
+ conds.push(`id IN (${w.idIn.map(() => "?").join(", ")})`);
17663
+ params.push(...w.idIn);
17664
+ }
17665
+ } else if ("descriptionIn" in w) {
17666
+ if (w.descriptionIn.length === 0) conds.push("0");
17667
+ else {
17668
+ conds.push(`description IN (${w.descriptionIn.map(() => "?").join(", ")})`);
17669
+ params.push(...w.descriptionIn);
17670
+ }
17671
+ } else if ("column" in w) {
17672
+ const c = column(w.column);
17673
+ for (const [op, sym] of [["gte", ">="], ["gt", ">"], ["lte", "<="], ["lt", "<"]]) {
17674
+ const v = w[op];
17675
+ if (v !== void 0) {
17676
+ conds.push(`${c} ${sym} ?`);
17677
+ params.push(v);
17678
+ }
17679
+ }
17680
+ } else {
17681
+ const p = path(w.attr);
17682
+ if ("absent" in w) conds.push(`json_type(attrs_json, ${p}) IS NULL`);
17683
+ else if ("present" in w) conds.push(`json_type(attrs_json, ${p}) IS NOT NULL`);
17684
+ else if ("nullish" in w) conds.push(`json_extract(attrs_json, ${p}) IS NULL`);
17685
+ else if ("notNullish" in w) conds.push(`json_extract(attrs_json, ${p}) IS NOT NULL`);
17686
+ else if ("eq" in w || "ne" in w) {
17687
+ const v = "eq" in w ? w.eq : w.ne;
17688
+ const neg = "ne" in w;
17689
+ if (typeof v === "boolean") {
17690
+ conds.push(`json_type(attrs_json, ${p}) ${neg ? "IS NOT" : "="} ?`);
17691
+ params.push(v ? "true" : "false");
17692
+ } else {
17693
+ conds.push(neg ? `json_extract(attrs_json, ${p}) IS NOT ?` : `json_extract(attrs_json, ${p}) = ?`);
17694
+ params.push(v);
17695
+ }
17696
+ }
17697
+ }
17698
+ }
17699
+ return { where: conds.join(" AND "), params };
17700
+ }
17701
+ function attrValueSql(p, doc = "attrs_json") {
17702
+ return `CASE json_type(${doc}, ${p}) WHEN 'integer' THEN CAST(json_extract(${doc}, ${p}) AS REAL) ELSE json_extract(${doc}, ${p}) END`;
17703
+ }
17704
+ function attrDocSql(key) {
17705
+ return isExposureAttr(key) ? exposureDocSql(key) : "attrs_json";
17706
+ }
17707
+ function nodeSelectFrom(q) {
17708
+ if (q.indexedBy) return `nodes INDEXED BY ${selectIndex(q.indexedBy)}`;
17709
+ const where = q.where ?? [];
17710
+ if (!q.includeClosed && where.some((w) => "descriptionIn" in w && w.descriptionIn.length > 0) && !where.some((w) => "idIn" in w)) {
17711
+ return "nodes INDEXED BY nodes_live_description";
17712
+ }
17713
+ return "nodes";
17714
+ }
17715
+ function compileNodeSelect(q) {
17716
+ const { where, params } = compileNodeWhere(q);
17717
+ const attrs = q.attrs ?? [];
17718
+ const attrCols = attrs.map((k, i) => `, ${attrValueSql(path(k), attrDocSql(k))} AS a${i}, json_type(${attrDocSql(k)}, ${path(k)}) AS t${i}`).join("");
17719
+ let order;
17720
+ if (q.orderBy && "column" in q.orderBy) {
17721
+ order = `${column(q.orderBy.column)} ${q.orderBy.desc ? "DESC" : "ASC"}, rowid`;
17722
+ } else if (q.orderBy) {
17723
+ const p = path(q.orderBy.attr);
17724
+ order = `json_extract(attrs_json, ${p}) IS NULL, json_extract(attrs_json, ${p}) ${q.orderBy.desc ? "DESC" : "ASC"}, rowid`;
17725
+ } else if (q.labels.length > 1) {
17726
+ order = `CASE label ${q.labels.map((_, i) => `WHEN ? THEN ${i}`).join(" ")} END, rowid`;
17727
+ } else {
17728
+ order = "rowid";
17729
+ }
17730
+ const orderParams = !q.orderBy && q.labels.length > 1 ? [...q.labels] : [];
17731
+ const limit = q.limit !== void 0 ? ` LIMIT ${Math.max(0, Math.floor(q.limit))}` : "";
17732
+ 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}`;
17733
+ return { sql, params: [...params, ...orderParams] };
17734
+ }
17735
+ function decodeAttr(value, type) {
17736
+ switch (type) {
17737
+ case null:
17738
+ case void 0:
17739
+ return void 0;
17740
+ case "true":
17741
+ return true;
17742
+ case "false":
17743
+ return false;
17744
+ case "null":
17745
+ return null;
17746
+ case "object":
17747
+ case "array":
17748
+ return typeof value === "string" ? JSON.parse(value) : value;
17749
+ default:
17750
+ return value;
17751
+ }
17752
+ }
17753
+ function decodeSelectedRow(r, attrs, decodeVector) {
17754
+ const out = {};
17755
+ attrs.forEach((k, i) => {
17756
+ const v = decodeAttr(r[`a${i}`], r[`t${i}`]);
17757
+ if (v !== void 0) out[k] = v;
17758
+ });
17759
+ return {
17760
+ id: r["id"],
17761
+ label: r["label"],
17762
+ description: r["description"] ?? "",
17763
+ createdAt: r["created_at"],
17764
+ lastUpdatedAt: r["last_updated_at"],
17765
+ pageRank: r["page_rank"],
17766
+ cumulativeHits: r["cumulative_hits"],
17767
+ extractionConfidence: r["extraction_confidence"],
17768
+ extractionSource: r["extraction_source"],
17769
+ community: r["community"] ?? null,
17770
+ ...r["created_at_seq"] != null ? { createdAtSeq: r["created_at_seq"] } : {},
17771
+ ...r["last_reinforced_at_seq"] != null ? { lastReinforcedAtSeq: r["last_reinforced_at_seq"] } : {},
17772
+ validTo: r["valid_to"] ?? null,
17773
+ logicalId: r["logical_id"] ?? r["id"],
17774
+ version: Number(r["version"] ?? 1),
17775
+ embeddingDim: Number(r["embedding_dim"] ?? 0),
17776
+ ...decodeVector ? { embedding: decodeVector(r["embedding_blob"], r["embedding_json"] ?? "[]") } : {},
17777
+ attrs: out
17778
+ };
17779
+ }
17780
+
16698
17781
  // ../../packages/local-graph/src/store.ts
16699
17782
  var PAGE_RANK_SCALE_KEY = "pageRankScale.p95.v1";
16700
17783
  var TEXT_SEARCH_COLUMNS = `id, label, description, page_rank,
@@ -16749,6 +17832,7 @@ function localEdgeViolation(fromLabel, type, toLabel) {
16749
17832
  }
16750
17833
  var SCHEMA_VERSION = 6;
16751
17834
  var SESSION_REPAIR_KEY = "session-evidence-v1";
17835
+ var EXPOSURE_SIDE_KEY = "exposure-side-rows-1";
16752
17836
  var SESSION_EVIDENCE_PREDICATE = SESSION_EVIDENCE_FIELDS.map((field) => `instr(attrs_json, '${JSON.stringify(field)}') > 0`).join(" OR ");
16753
17837
  var SCHEMA_SQL = `
16754
17838
  CREATE TABLE IF NOT EXISTS schema_version (
@@ -16812,8 +17896,13 @@ CREATE TABLE IF NOT EXISTS nodes (
16812
17896
  );
16813
17897
 
16814
17898
  CREATE INDEX IF NOT EXISTS nodes_label ON nodes(label);
16815
- CREATE INDEX IF NOT EXISTS nodes_memory_tier ON nodes(memory_tier);
16816
- CREATE INDEX IF NOT EXISTS nodes_page_rank ON nodes(page_rank DESC);
17899
+ -- Dropped: no query plans through them (every prepared statement across the
17900
+ -- local-graph, indexer, context-writer, generalizer and daemon suites was
17901
+ -- planned with and without them, 2026-10-21), and each write that touches
17902
+ -- memory_tier / page_rank (every node upsert, every rescore) paid to maintain
17903
+ -- them. nodes_live_page_rank serves the live page-rank orderings.
17904
+ DROP INDEX IF EXISTS nodes_memory_tier;
17905
+ DROP INDEX IF EXISTS nodes_page_rank;
16817
17906
 
16818
17907
  CREATE TABLE IF NOT EXISTS edges (
16819
17908
  id TEXT PRIMARY KEY,
@@ -16835,7 +17924,10 @@ CREATE TABLE IF NOT EXISTS edges (
16835
17924
  FOREIGN KEY (to_id) REFERENCES nodes(id) ON DELETE CASCADE
16836
17925
  );
16837
17926
 
16838
- CREATE INDEX IF NOT EXISTS edges_from ON edges(from_id, type);
17927
+ -- Dropped: a prefix of edges_triple (from_id, type, to_id), which every
17928
+ -- from-side lookup and foreign-key check already plans through; it cost 47 MB
17929
+ -- and an index write on every edge write.
17930
+ DROP INDEX IF EXISTS edges_from;
16839
17931
  CREATE INDEX IF NOT EXISTS edges_to ON edges(to_id, type);
16840
17932
  CREATE INDEX IF NOT EXISTS edges_type ON edges(type);
16841
17933
  CREATE UNIQUE INDEX IF NOT EXISTS edges_triple ON edges(from_id, type, to_id);
@@ -16843,8 +17935,41 @@ CREATE UNIQUE INDEX IF NOT EXISTS edges_triple ON edges(from_id, type, to_id);
16843
17935
  var TEMPORAL_INDEX_SQL = `
16844
17936
  CREATE INDEX IF NOT EXISTS nodes_live ON nodes(label, valid_to);
16845
17937
  CREATE INDEX IF NOT EXISTS nodes_live_count ON nodes(valid_to);
16846
- CREATE INDEX IF NOT EXISTS nodes_live_recent ON nodes(label, last_updated_at DESC) WHERE valid_to IS NULL;
17938
+ -- Problems by recency (the only label read newest-first). Over every live row,
17939
+ -- each write that stamps last_updated_at moved a code symbol's entry from a
17940
+ -- scattered leaf: ~19 KB per position-only symbol rewrite on a 1.1 GB store.
17941
+ -- label stays the leading column so label = 'Problem' plans without a sort.
17942
+ DROP INDEX IF EXISTS nodes_live_recent;
17943
+ CREATE INDEX IF NOT EXISTS nodes_live_problem_recent ON nodes(label, last_updated_at DESC) WHERE valid_to IS NULL AND label = 'Problem';
17944
+ -- "Anything created since the watermark?" guards (the fold, regression and
17945
+ -- canonical-bind sweeps run one per session boundary). Without it the planner
17946
+ -- walks nodes_live and reads every live row of the label to test created_at:
17947
+ -- 56 MB per guard on a 1.1 GB store, to learn there was nothing to do. valid_to
17948
+ -- is a key column so the planner prefers this over nodes_live (one more term).
17949
+ CREATE INDEX IF NOT EXISTS nodes_live_created ON nodes(label, valid_to, created_at) WHERE valid_to IS NULL;
17950
+ -- Community members (the skills pull reads them twice per session boundary):
17951
+ -- ~15% of semantic nodes carry a localCommunity, and without this every live
17952
+ -- semantic row was read to find them (168 MB \u2192 18 MB per read).
17953
+ 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;
17954
+ -- Problems parked for the fold judge (read every settle boundary): ~1 in 5 open
17955
+ -- Problems; without it every live Problem was read to find them (56 MB).
17956
+ 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;
17957
+ -- Render impressions, one row per node id (exposure-attrs.ts): a render bumps
17958
+ -- ~40 of these, which in the node row rewrote each whole row and its indexes.
17959
+ CREATE TABLE IF NOT EXISTS node_exposure (
17960
+ id TEXT PRIMARY KEY,
17961
+ attrs_json TEXT NOT NULL DEFAULT '{}'
17962
+ ) WITHOUT ROWID;
17963
+ -- The semantic-maintenance sweeps' own filters (closed, asked, hinted): each read
17964
+ -- every Problem row (~30 MB on a 1.1 GB store) for the few hundred that match,
17965
+ -- several times a run, and a run follows most writes (2026-10-20).
17966
+ 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;
17967
+ 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;
17968
+ 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;
16847
17969
  CREATE INDEX IF NOT EXISTS nodes_session_evidence ON nodes(id) WHERE ${SESSION_EVIDENCE_PREDICATE};
17970
+ -- Rows carrying a calibration counter, any version: the logical nodes wm
17971
+ -- calibration can count (~3k), found without reading every citable row.
17972
+ 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};
16848
17973
  CREATE INDEX IF NOT EXISTS edges_session_evidence ON edges(id) WHERE ${SESSION_EVIDENCE_PREDICATE};
16849
17974
  -- Covering + partial index for labelFingerprint over the instance labels: the
16850
17975
  -- live-push probe runs it per workspace on a timer, and each hit used to read
@@ -16858,31 +17983,70 @@ CREATE INDEX IF NOT EXISTS nodes_instance_fingerprint_v2 ON nodes(label, last_up
16858
17983
  CREATE INDEX IF NOT EXISTS nodes_live_page_rank ON nodes(valid_to, page_rank);
16859
17984
  CREATE INDEX IF NOT EXISTS nodes_logical ON nodes(logical_id, version);
16860
17985
  CREATE INDEX IF NOT EXISTS edges_live ON edges(valid_to);
17986
+ -- Newest live edges first, so a "recent N matching edges" read stops early instead
17987
+ -- of matching and sorting every live edge (the hourly liveness check: 163 MB \u2192
17988
+ -- 69 MB). Named by recentEdgeAttrCoverage; the planner keeps edges_live otherwise.
17989
+ CREATE INDEX IF NOT EXISTS edges_live_created ON edges(created_at) WHERE valid_to IS NULL;
17990
+ -- Live prior-tag edges by recency: the liveness corroboration check reads the
17991
+ -- newest 100 of ~300, and walking all live edges newest-first to find them read
17992
+ -- 69 MB. Its predicate is the one recentEdgeAttrCoverage uses for that marker.
17993
+ 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;
16861
17994
  -- Expression index on the per-node relPath (stored in attrs_json) so the
16862
17995
  -- incremental reindex can fetch just the changed files' symbols instead of
16863
17996
  -- scanning every versioned node. Partial: only live rows are ever queried.
16864
17997
  CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.relPath')) WHERE valid_to IS NULL;
17998
+ -- Symbols by qname and doc comments by the symbol they attach to: the drain's
17999
+ -- symbol lexicon resolves the names in the text it ships one at a time, instead
18000
+ -- of reading every Function/Method/Class/Interface/Const and Comment row on each
18001
+ -- live push (~300 MB of a 1.1 GB store per push, 2026-10-20).
18002
+ CREATE INDEX IF NOT EXISTS nodes_live_qname ON nodes(json_extract(attrs_json, '$.qname')) WHERE valid_to IS NULL;
18003
+ CREATE INDEX IF NOT EXISTS nodes_live_attached_to ON nodes(json_extract(attrs_json, '$.attachedTo')) WHERE valid_to IS NULL AND label = 'Comment';
18004
+ -- The attrs the cloud drain gates every instance row on, so it reads them from
18005
+ -- the index alone: pulled cloud rows are most of these labels (7,105 of 7,708
18006
+ -- Solutions on the live store) at ~10 KB of attrs each, and five full scans for
18007
+ -- two attrs read ~220 MB per push to find the few local rows that can ship.
18008
+ -- created_at leads so the index order is findNodeFieldsByLabel's.
18009
+ 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');
18010
+ -- Closed rows by relPath, for reviveReobserved (every incremental reindex): the
18011
+ -- live-only index above cannot serve it, so it scanned the whole table (~275 MB
18012
+ -- of a 1.1 GB store per reindex, 2026-10-20).
18013
+ CREATE INDEX IF NOT EXISTS nodes_closed_relpath ON nodes(json_extract(attrs_json, '$.relPath'), last_updated_at) WHERE valid_to IS NOT NULL;
16865
18014
  -- Live rows by name, for looking up the few symbols a statement mentions
16866
18015
  -- instead of hydrating every code symbol per minted problem.
16867
18016
  CREATE INDEX IF NOT EXISTS nodes_live_description ON nodes(description, label) WHERE valid_to IS NULL;
18017
+ -- The six single-attr lookups below are partial on the attr being present: each
18018
+ -- is read only by equality (or IN), which implies it. Keyed on NULL for the ~95%
18019
+ -- of rows that lack the attr, every write to such a row's attrs rewrote its NULL
18020
+ -- entry: ~25 KB per position-only symbol rewrite on a 1.1 GB store (2026-10-21).
18021
+ DROP INDEX IF EXISTS nodes_anchor_hint_path;
18022
+ DROP INDEX IF EXISTS nodes_command_signature;
18023
+ DROP INDEX IF EXISTS nodes_diag_command;
18024
+ DROP INDEX IF EXISTS nodes_lsp_rel_path;
18025
+ DROP INDEX IF EXISTS nodes_cloud_node_id;
18026
+ DROP INDEX IF EXISTS nodes_enrichment_pending;
16868
18027
  -- Problems by the file their anchor hint names: the harvest upgrades hints for
16869
18028
  -- every edited file of every turn, and a scan parses every Problem's attrs.
16870
- CREATE INDEX IF NOT EXISTS nodes_anchor_hint_path ON nodes(json_extract(attrs_json, '$.anchorHint.path')) WHERE valid_to IS NULL;
18029
+ 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;
16871
18030
  -- Problems by the command / checker / file that opened them. The generalizer
16872
18031
  -- looked these up per shell or tool EVENT by hydrating every Problem: ~12s of
16873
18032
  -- every minute of daemon main-thread time on a 3.4k-Problem store (2026-10-05).
16874
- CREATE INDEX IF NOT EXISTS nodes_command_signature ON nodes(json_extract(attrs_json, '$.commandSignature')) WHERE valid_to IS NULL;
16875
- CREATE INDEX IF NOT EXISTS nodes_diag_command ON nodes(json_extract(attrs_json, '$.diagCommand')) WHERE valid_to IS NULL;
16876
- CREATE INDEX IF NOT EXISTS nodes_lsp_rel_path ON nodes(json_extract(attrs_json, '$.lspRelPath')) WHERE valid_to IS NULL;
18033
+ 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;
18034
+ 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;
18035
+ 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;
16877
18036
  -- Local nodes by the cloud twin their upload receipt recorded. Every burst /
16878
18037
  -- search fusion folds the Network copy of the caller's own knowledge back onto
16879
18038
  -- the local node; unindexed, the reverse lookup was a ~300ms attrs scan per call.
16880
- CREATE INDEX IF NOT EXISTS nodes_cloud_node_id ON nodes(json_extract(attrs_json, '$.cloudNodeId')) WHERE valid_to IS NULL;
18039
+ 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;
16881
18040
  -- Nodes still waiting on a root-cause ask. The MCP server checks this on EVERY
16882
18041
  -- tool response (the enrichment piggyback), so it must not parse every
16883
- -- Solution's and Problem's attrs per call. Not partial: the Problem pull reads
18042
+ -- Solution's and Problem's attrs per call. Any version: the Problem pull reads
16884
18043
  -- closed versions too.
16885
- CREATE INDEX IF NOT EXISTS nodes_enrichment_pending ON nodes(json_extract(attrs_json, '$.enrichmentPending'));
18044
+ 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;
18045
+ -- Revisit flags, any label, any version: listNeedsRevisit's as-of read runs on
18046
+ -- every context render and otherwise json-checks every row (10.3 s on a 324k-row
18047
+ -- store, to return zero flags). The predicate must stay textually identical to
18048
+ -- nodesAsOfWhereTrue's or the planner will not use it.
18049
+ CREATE INDEX IF NOT EXISTS nodes_revisit_true ON nodes(valid_from) WHERE json_type(attrs_json, '$.revisit') = 'true';
16886
18050
  `;
16887
18051
  function encodeEmbedding(emb) {
16888
18052
  if (!emb || emb.length === 0) return null;
@@ -16943,12 +18107,39 @@ function rowToEdge(r) {
16943
18107
  }
16944
18108
  var FINGERPRINT_INDEXED_LABELS = ["Problem", "Vulnerability", "Solution", "RootCause"];
16945
18109
  var NODE_LABELS_CHUNK = 256;
18110
+ var NODE_UPSERT_COLUMNS = [
18111
+ "label",
18112
+ "description",
18113
+ "extraction_confidence",
18114
+ "embedding_json",
18115
+ "embedding_blob",
18116
+ "cumulative_surprise",
18117
+ "peak_surprise",
18118
+ "cumulative_hits",
18119
+ "last_updated_at",
18120
+ "memory_tier",
18121
+ "page_rank",
18122
+ "is_landmark",
18123
+ "community",
18124
+ "stability",
18125
+ "last_reinforced_at_seq",
18126
+ "attrs_json"
18127
+ ];
18128
+ var EDGE_UPSERT_COLUMNS = ["confidence", "last_seen_at", "nav_successes", "nav_failures", "valid_to", "attrs_json"];
18129
+ function sameSqlValue(a, b) {
18130
+ if (a === b || a == null && b == null) return true;
18131
+ if (a instanceof Uint8Array && b instanceof Uint8Array) return Buffer.compare(a, b) === 0;
18132
+ return false;
18133
+ }
18134
+ var PRIOR_TAG_MARKER = '"priorTag":true';
16946
18135
  var SqliteGraphStore = class {
16947
18136
  db;
16948
18137
  // Typed edge lookups build their SQL from the number of types asked for, and
16949
18138
  // used to prepare it on every call: ~600ms of a 2.5s drain build on a live
16950
18139
  // 225k-node store was statement compilation alone. Keyed by the SQL text.
16951
18140
  typedEdgeStmts = /* @__PURE__ */ new Map();
18141
+ /** Narrow merge updates, keyed by table and the columns they set. */
18142
+ changedColumnStmts = /* @__PURE__ */ new Map();
16952
18143
  stmts;
16953
18144
  /** Unified graph-mutation counter (see GraphStore.mutationCount). Bumped on
16954
18145
  * every node/edge upsert — code-index and capture alike — so the consolidation
@@ -16967,6 +18158,8 @@ var SqliteGraphStore = class {
16967
18158
  initialPageRankScale;
16968
18159
  constructor(opts) {
16969
18160
  this.db = openDatabase(opts.path);
18161
+ this.db.exec("PRAGMA cache_size = -32768");
18162
+ this.db.exec("PRAGMA mmap_size = 2147483648");
16970
18163
  this.db.exec(SCHEMA_SQL);
16971
18164
  const versionRow = this.db.prepare("SELECT version FROM schema_version LIMIT 1").get();
16972
18165
  if (!versionRow) {
@@ -16983,12 +18176,15 @@ var SqliteGraphStore = class {
16983
18176
  installEditPlaceholderRetirement(this.db);
16984
18177
  installCloudAttrCompaction(this.db);
16985
18178
  installHistoryAttrSlim(this.db);
18179
+ installTextIndex(this.db);
16986
18180
  if (!this.db.prepare("SELECT value FROM store_meta WHERE key = ?").get(SESSION_REPAIR_KEY)) {
16987
18181
  this.db.transaction(() => {
16988
18182
  this.repairSessionAliases();
16989
18183
  this.db.prepare("INSERT OR REPLACE INTO store_meta (key, value) VALUES (?, 'done')").run(SESSION_REPAIR_KEY);
16990
18184
  });
16991
18185
  }
18186
+ installLineageIntegrityRepair(this.db);
18187
+ installChangeFeed(this.db);
16992
18188
  this.stmts = {
16993
18189
  insertNode: this.db.prepare(`
16994
18190
  INSERT INTO nodes (
@@ -17027,7 +18223,19 @@ var SqliteGraphStore = class {
17027
18223
  last_reinforced_at_seq = excluded.last_reinforced_at_seq,
17028
18224
  attrs_json = excluded.attrs_json
17029
18225
  `),
17030
- getNode: this.db.prepare("SELECT * FROM nodes WHERE id = ?"),
18226
+ nodeUpsertTarget: this.db.prepare(`SELECT rowid AS rid, ${NODE_UPSERT_COLUMNS.join(", ")} FROM nodes WHERE id = ?`),
18227
+ edgeUpsertTarget: this.db.prepare(`SELECT rowid AS rid, ${EDGE_UPSERT_COLUMNS.join(", ")} FROM edges WHERE from_id = ? AND type = ? AND to_id = ?`),
18228
+ 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 = ?"),
18229
+ exposureOf: this.db.prepare("SELECT attrs_json FROM node_exposure WHERE id = ?"),
18230
+ writeExposure: this.db.prepare("INSERT INTO node_exposure(id, attrs_json) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET attrs_json = excluded.attrs_json"),
18231
+ lastUpdatedOf: this.db.prepare("SELECT last_updated_at AS t, attrs_json FROM nodes WHERE id = ?"),
18232
+ labelOf: this.db.prepare("SELECT label FROM nodes WHERE id = ?"),
18233
+ nodeContent: this.db.prepare(
18234
+ "SELECT label, description, extraction_confidence, extraction_source, stability, valid_to, attrs_json FROM nodes WHERE id = ?"
18235
+ ),
18236
+ edgeContent: this.db.prepare(
18237
+ "SELECT confidence, valid_to, attrs_json FROM edges WHERE from_id = ? AND type = ? AND to_id = ?"
18238
+ ),
17031
18239
  // Single-column fast paths for the nightly pass — writing 100k pageRank
17032
18240
  // values through the 18-column COALESCE updateNode took ~30s; these are ~10x
17033
18241
  // cheaper (one bound column, no COALESCE evaluation per row).
@@ -17184,6 +18392,7 @@ var SqliteGraphStore = class {
17184
18392
  "INSERT INTO store_meta (key, value) VALUES ('ingest_seq', :v) ON CONFLICT(key) DO UPDATE SET value = excluded.value"
17185
18393
  )
17186
18394
  };
18395
+ this.moveExposureToSideRows();
17187
18396
  const seqRow = this.db.prepare("SELECT value FROM store_meta WHERE key = 'ingest_seq'").get();
17188
18397
  this.ingestSeq = seqRow ? Number(seqRow.value) : 0;
17189
18398
  }
@@ -17199,6 +18408,93 @@ var SqliteGraphStore = class {
17199
18408
  AND (t.id IS NULL OR t.valid_to IS NOT NULL)
17200
18409
  `).all();
17201
18410
  }
18411
+ strandedSymbolsForFiles(fileIds) {
18412
+ const out = [];
18413
+ for (let i = 0; i < fileIds.length; i += 500) {
18414
+ const slice = fileIds.slice(i, i + 500);
18415
+ if (slice.length === 0) continue;
18416
+ out.push(...this.db.prepare(`
18417
+ SELECT f.id AS fileId, e.to_id AS symbolId
18418
+ FROM nodes f CROSS JOIN edges e ON e.from_id = f.id
18419
+ LEFT JOIN nodes t ON t.id = e.to_id
18420
+ WHERE f.id IN (${slice.map(() => "?").join(",")}) AND f.label = 'File' AND f.valid_to IS NULL AND e.valid_to IS NULL
18421
+ AND e.type IN ('DEFINES', 'CONTAINS')
18422
+ AND (t.id IS NULL OR t.valid_to IS NOT NULL)
18423
+ `).all(...slice));
18424
+ }
18425
+ return out;
18426
+ }
18427
+ firstLiveByNameOrQname(labels, name) {
18428
+ if (labels.length === 0) return null;
18429
+ const inLabels = labels.map(() => "?").join(",");
18430
+ const probe = (index, match) => this.db.prepare(
18431
+ `SELECT rowid AS rid, created_at AS at, label, description, json_extract(attrs_json, '$.qname') AS qname FROM nodes INDEXED BY ${index}
18432
+ WHERE ${match} = ? AND label IN (${inLabels}) AND valid_to IS NULL`
18433
+ ).all(name, ...labels);
18434
+ const rank = (r) => [labels.indexOf(r.label), r.at, r.rid];
18435
+ const before = (a, b) => a[0] < b[0] || a[0] === b[0] && (a[1] < b[1] || a[1] === b[1] && a[2] < b[2]);
18436
+ let best;
18437
+ for (const r of [...probe("nodes_live_description", "description"), ...probe("nodes_live_qname", "json_extract(attrs_json, '$.qname')")]) {
18438
+ if (!best || before(rank(r), rank(best))) best = r;
18439
+ }
18440
+ return best ? { label: best.label, description: best.description, qname: typeof best.qname === "string" ? best.qname : null } : null;
18441
+ }
18442
+ instanceGateRows(label) {
18443
+ return this.db.prepare(
18444
+ `SELECT id, json_extract(attrs_json, '$.source') AS source, json_extract(attrs_json, '$.privateFinding') AS privateFinding,
18445
+ json_extract(attrs_json, '$.resolvedAs') AS resolvedAs
18446
+ FROM nodes INDEXED BY nodes_live_instance_gate
18447
+ WHERE label IN ('Problem', 'Vulnerability', 'Solution', 'RootCause') AND label = ? AND valid_to IS NULL
18448
+ ORDER BY label, created_at, rowid`
18449
+ ).all(label);
18450
+ }
18451
+ wmCalibrationRows(labels) {
18452
+ const counted = this.db.prepare(
18453
+ `SELECT rowid AS rid, logical_id, version, valid_to, label, ${WM_CALIBRATION_KEYS.map((k, i) => `${wmCounterSql(k)} AS c${i}`).join(", ")}
18454
+ FROM nodes INDEXED BY nodes_wm_counted WHERE ${WM_COUNTED_PREDICATE}`
18455
+ ).all();
18456
+ const exposed = this.db.prepare(
18457
+ `SELECT n.rowid AS rid, n.logical_id, n.version, n.valid_to, n.label, x.attrs_json AS exposure
18458
+ FROM node_exposure x JOIN nodes n ON n.id = x.id`
18459
+ ).all();
18460
+ const others = this.db.prepare(
18461
+ `SELECT rowid AS rid, logical_id, version, valid_to, label FROM nodes WHERE rowid IN (
18462
+ SELECT n.rowid FROM nodes AS n INDEXED BY nodes_logical
18463
+ WHERE n.logical_id IN (SELECT logical_id FROM nodes INDEXED BY nodes_wm_counted WHERE ${WM_COUNTED_PREDICATE}
18464
+ UNION SELECT n2.logical_id FROM node_exposure x2 JOIN nodes n2 ON n2.id = x2.id)
18465
+ EXCEPT SELECT rowid FROM nodes INDEXED BY nodes_wm_counted WHERE ${WM_COUNTED_PREDICATE}
18466
+ EXCEPT SELECT n3.rowid FROM node_exposure x3 JOIN nodes n3 ON n3.id = x3.id)`
18467
+ ).all();
18468
+ const byRid = /* @__PURE__ */ new Map();
18469
+ for (const r of [...counted, ...others]) {
18470
+ const attrs = {};
18471
+ WM_CALIBRATION_KEYS.forEach((k, i) => {
18472
+ const v = r[`c${i}`];
18473
+ if (typeof v === "number") attrs[k] = v;
18474
+ });
18475
+ byRid.set(r.rid, { row: r, attrs });
18476
+ }
18477
+ for (const r of exposed) {
18478
+ const side = JSON.parse(r.exposure);
18479
+ const entry = byRid.get(r.rid) ?? { row: r, attrs: {} };
18480
+ for (const k of WM_CALIBRATION_KEYS) if (typeof side[k] === "number" && Number.isFinite(side[k])) entry.attrs[k] = side[k];
18481
+ byRid.set(r.rid, entry);
18482
+ }
18483
+ const order = new Map(labels.map((l, i) => [l, i]));
18484
+ 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 }));
18485
+ }
18486
+ liveCommentsAttachedTo(name) {
18487
+ return this.db.prepare(
18488
+ `SELECT json_extract(attrs_json, '$.kind') AS kind, json_extract(attrs_json, '$.text') AS text, description FROM nodes INDEXED BY nodes_live_attached_to
18489
+ WHERE label = 'Comment' AND valid_to IS NULL AND json_extract(attrs_json, '$.attachedTo') = ? ORDER BY created_at, rowid`
18490
+ ).all(name);
18491
+ }
18492
+ strandedEdges() {
18493
+ return countStrandedEdges(this.db);
18494
+ }
18495
+ liveHeadConflicts() {
18496
+ return countLiveHeadConflicts(this.db);
18497
+ }
17202
18498
  fileIndexStates(ids) {
17203
18499
  if (ids.length === 0) return [];
17204
18500
  if (ids.length > 500) throw new RangeError("fileIndexStates accepts at most 500 ids per read");
@@ -17297,7 +18593,7 @@ var SqliteGraphStore = class {
17297
18593
  }
17298
18594
  mergeNode(node) {
17299
18595
  this.mutations++;
17300
- this.stmts.insertNode.run({
18596
+ const values = {
17301
18597
  id: node.id,
17302
18598
  label: node.label,
17303
18599
  description: node.description,
@@ -17323,8 +18619,37 @@ var SqliteGraphStore = class {
17323
18619
  logical_id: node.logicalId ?? node.id,
17324
18620
  created_at_seq: node.createdAtSeq ?? null,
17325
18621
  last_reinforced_at_seq: node.lastReinforcedAtSeq ?? null,
17326
- attrs_json: JSON.stringify(node.attrs ?? {})
17327
- });
18622
+ attrs_json: this.nodeAttrsJson(node.id, node.attrs ?? {})
18623
+ };
18624
+ const current = this.stmts.nodeUpsertTarget.get(node.id);
18625
+ if (current) this.writeChangedColumns("nodes", NODE_UPSERT_COLUMNS, current, values);
18626
+ else this.stmts.insertNode.run(values);
18627
+ }
18628
+ /** What the upsert's ON CONFLICT would write over `current`, setting only the
18629
+ * columns whose value moves (NODE_UPSERT_COLUMNS says why). */
18630
+ writeChangedColumns(table, columns2, current, next) {
18631
+ const changed = columns2.filter((c) => !sameSqlValue(current[c], next[c]));
18632
+ if (changed.length === 0) return;
18633
+ const key = `${table}:${changed.join(",")}`;
18634
+ let stmt2 = this.changedColumnStmts.get(key);
18635
+ if (!stmt2) {
18636
+ stmt2 = this.db.prepare(`UPDATE ${table} SET ${changed.map((c) => `${c} = :${c}`).join(", ")} WHERE rowid = :rid`);
18637
+ this.changedColumnStmts.set(key, stmt2);
18638
+ }
18639
+ stmt2.run({ rid: current["rid"], ...Object.fromEntries(changed.map((c) => [c, next[c]])) });
18640
+ }
18641
+ /** mergeNode, unless the live row already holds this content. Content is what
18642
+ * the columns a merge rewrites from the producer (label, description, attrs,
18643
+ * confidence, stability);
18644
+ * rank, community, landmark, surprise, hits, tier and the vector belong to
18645
+ * the passes that compute them, and a skipped write leaves them as they are
18646
+ * where a merge would have reset them to the producer's defaults. Returns
18647
+ * whether it wrote. */
18648
+ mergeNodeIfChanged(node) {
18649
+ const row = this.stmts.nodeContent.get(node.id);
18650
+ 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;
18651
+ this.mergeNode(node);
18652
+ return true;
17328
18653
  }
17329
18654
  updateNode(id, patch) {
17330
18655
  const hasEmbedding = patch.embedding !== void 0 && patch.embedding.length > 0;
@@ -17349,7 +18674,37 @@ var SqliteGraphStore = class {
17349
18674
  logical_id: patch.logicalId ?? null,
17350
18675
  created_at_seq: patch.createdAtSeq ?? null,
17351
18676
  last_reinforced_at_seq: patch.lastReinforcedAtSeq ?? null,
17352
- attrs_json: patch.attrs ? JSON.stringify(patch.attrs) : null
18677
+ attrs_json: patch.attrs ? this.nodeAttrsJson(id, patch.attrs) : null
18678
+ });
18679
+ }
18680
+ /**
18681
+ * The node-row attrs of a write, after handing any exposure keys to the side
18682
+ * row (exposure-attrs.ts). `getNode` returns exposure merged into attrs, so a
18683
+ * read-modify-write carries it; kept in the row it would re-bloat the row and
18684
+ * drift from the side row. Unchanged exposure costs no write.
18685
+ */
18686
+ nodeAttrsJson(id, attrs) {
18687
+ if (!EXPOSURE_ATTRS.some((k) => attrs[k] !== void 0)) return JSON.stringify(attrs);
18688
+ const current = this.stmts.exposureOf.get(id);
18689
+ const next = JSON.stringify({ ...current ? JSON.parse(current.attrs_json) : {}, ...pickExposure(attrs) });
18690
+ if (current?.attrs_json !== next) this.stmts.writeExposure.run(id, next);
18691
+ const rest = { ...attrs };
18692
+ for (const k of EXPOSURE_ATTRS) delete rest[k];
18693
+ return JSON.stringify(rest);
18694
+ }
18695
+ /** One-time: exposure keys written to node rows before the side table existed
18696
+ * move to it. Only rows carrying a counter can carry exposure, and those are
18697
+ * what nodes_wm_counted holds, so this reads the index, not the table. */
18698
+ moveExposureToSideRows() {
18699
+ if (this.db.prepare("SELECT value FROM store_meta WHERE key = ?").get(EXPOSURE_SIDE_KEY)) return;
18700
+ const rows = this.db.prepare(`SELECT id, attrs_json FROM nodes INDEXED BY nodes_wm_counted WHERE ${WM_COUNTED_PREDICATE}`).all();
18701
+ const move = this.db.prepare("UPDATE nodes SET attrs_json = ? WHERE id = ?");
18702
+ this.db.transaction(() => {
18703
+ for (const r of rows) {
18704
+ const stripped = this.nodeAttrsJson(r.id, JSON.parse(r.attrs_json));
18705
+ if (stripped !== r.attrs_json) move.run(stripped, r.id);
18706
+ }
18707
+ this.db.prepare("INSERT OR REPLACE INTO store_meta(key, value) VALUES (?, 'done')").run(EXPOSURE_SIDE_KEY);
17353
18708
  });
17354
18709
  }
17355
18710
  /** Fast single-column writes for the nightly pass (avoid the 18-column COALESCE
@@ -17360,26 +18715,30 @@ var SqliteGraphStore = class {
17360
18715
  setLandmark(id, isLandmark) {
17361
18716
  this.stmts.setLandmark.run({ id, v: isLandmark ? 1 : 0 });
17362
18717
  }
17363
- setLocalCommunity(id, community, column) {
17364
- this.stmts.setLocalCommunity.run({ id, v: community, col: column ? 1 : 0 });
18718
+ setLocalCommunity(id, community, column2) {
18719
+ this.stmts.setLocalCommunity.run({ id, v: community, col: column2 ? 1 : 0 });
17365
18720
  }
17366
18721
  fieldStmts = /* @__PURE__ */ new Map();
17367
18722
  findNodeFieldsByLabel(label, attrKeys) {
17368
18723
  for (const k of attrKeys) if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k)) throw new Error(`findNodeFieldsByLabel: bad attr key ${k}`);
17369
18724
  const cacheKey = attrKeys.join(",");
17370
- let stmt = this.fieldStmts.get(cacheKey);
17371
- if (!stmt) {
17372
- const cols = attrKeys.map((k, i) => `, json_extract(attrs_json, '$.${k}') AS a${i}`).join("");
17373
- stmt = this.db.prepare(
17374
- `SELECT id, description, community, extraction_confidence${cols} FROM nodes WHERE label = ? AND valid_to IS NULL`
18725
+ let stmt2 = this.fieldStmts.get(cacheKey);
18726
+ if (!stmt2) {
18727
+ const cols = attrKeys.map((k, i) => `, ${attrValueSql(`'$.${k}'`)} AS a${i}, json_type(attrs_json, '$.${k}') AS t${i}`).join("");
18728
+ stmt2 = this.db.prepare(
18729
+ // Ordered, not left to the planner: readers take "first row wins" (the
18730
+ // symbol lexicon's name winner and doc pick), and the order otherwise
18731
+ // followed whichever partial index the planner chose. nodes_live_created
18732
+ // serves this order without a sort.
18733
+ `SELECT id, description, community, extraction_confidence${cols} FROM nodes WHERE label = ? AND valid_to IS NULL ORDER BY created_at, rowid`
17375
18734
  );
17376
- this.fieldStmts.set(cacheKey, stmt);
18735
+ this.fieldStmts.set(cacheKey, stmt2);
17377
18736
  }
17378
- return stmt.all(label).map((r) => {
18737
+ return stmt2.all(label).map((r) => {
17379
18738
  const attrs = {};
17380
18739
  attrKeys.forEach((k, i) => {
17381
- const v = r[`a${i}`];
17382
- if (v !== null && v !== void 0) attrs[k] = v;
18740
+ const v = decodeAttr(r[`a${i}`], r[`t${i}`]);
18741
+ if (v !== void 0) attrs[k] = v;
17383
18742
  });
17384
18743
  return {
17385
18744
  id: r["id"],
@@ -17390,6 +18749,15 @@ var SqliteGraphStore = class {
17390
18749
  };
17391
18750
  });
17392
18751
  }
18752
+ selectNodes(q) {
18753
+ const { sql, params } = compileNodeSelect(q);
18754
+ const rows = this.db.prepare(sql).all(...params);
18755
+ return rows.map((r) => decodeSelectedRow(r, q.attrs ?? [], q.withEmbedding ? decodeEmbedding : void 0));
18756
+ }
18757
+ countNodes(q) {
18758
+ const { where, params } = compileNodeWhere(q);
18759
+ return Number(this.db.prepare(`SELECT COUNT(*) AS n FROM ${nodeSelectFrom(q)} WHERE ${where}`).get(...params).n);
18760
+ }
17393
18761
  setCommunity(id, community) {
17394
18762
  this.stmts.setCommunity.run({ id, v: community });
17395
18763
  }
@@ -17402,7 +18770,7 @@ var SqliteGraphStore = class {
17402
18770
  return;
17403
18771
  }
17404
18772
  this.mutations++;
17405
- this.stmts.insertEdge.run({
18773
+ const values = {
17406
18774
  id: edge.id,
17407
18775
  from_id: edge.from,
17408
18776
  to_id: edge.to,
@@ -17416,8 +18784,33 @@ var SqliteGraphStore = class {
17416
18784
  valid_from: edge.validFrom ?? edge.createdAt,
17417
18785
  valid_to: edge.validTo ?? null,
17418
18786
  attrs_json: JSON.stringify(edge.attrs ?? {})
18787
+ };
18788
+ const current = this.stmts.edgeUpsertTarget.get(edge.from, edge.type, edge.to);
18789
+ if (!current) {
18790
+ this.stmts.insertEdge.run(values);
18791
+ return;
18792
+ }
18793
+ this.writeChangedColumns("edges", EDGE_UPSERT_COLUMNS, current, {
18794
+ ...values,
18795
+ confidence: Math.max(current["confidence"], edge.confidence),
18796
+ valid_to: null
17419
18797
  });
17420
18798
  }
18799
+ /** mergeEdge, unless the live edge already holds this content: same attrs, and
18800
+ * a confidence the upsert's MAX would keep. Its navigation posteriors and
18801
+ * last-seen stamp stay as they are where a merge would have reset them. The
18802
+ * ontology gate runs first either way, so refusals are recorded as before.
18803
+ * Returns whether it wrote (or refused). */
18804
+ mergeEdgeIfChanged(edge) {
18805
+ if (!this.edgeRuleViolation(edge)) {
18806
+ const row = this.stmts.edgeContent.get(edge.from, edge.type, edge.to);
18807
+ if (row && row.valid_to === null && row.confidence >= edge.confidence && row.attrs_json === JSON.stringify(edge.attrs ?? {})) {
18808
+ return false;
18809
+ }
18810
+ }
18811
+ this.mergeEdge(edge);
18812
+ return true;
18813
+ }
17421
18814
  /** EDGE_RULES check against the stored endpoints' labels, with the LOCAL
17422
18815
  * overlay consulted first. Returns the reason string on a documented-
17423
18816
  * forbidden combination, else null. Point lookups on the id PK — negligible
@@ -17462,11 +18855,13 @@ var SqliteGraphStore = class {
17462
18855
  }
17463
18856
  }
17464
18857
  edgeRuleViolation(edge) {
17465
- return localEdgeViolation(
17466
- this.getNode(edge.from)?.label,
17467
- edge.type,
17468
- this.getNode(edge.to)?.label
17469
- );
18858
+ return localEdgeViolation(this.labelOf(edge.from), edge.type, this.labelOf(edge.to));
18859
+ }
18860
+ /** An endpoint's label, any version, as `getNode(id)?.label` reads it. Every
18861
+ * edge write checks both endpoints, and hydrating them (attrs + vector) to
18862
+ * read one leading column was most of an incremental reindex's reads. */
18863
+ labelOf(id) {
18864
+ return this.stmts.labelOf.get(id)?.label;
17470
18865
  }
17471
18866
  updateEdge(id, patch) {
17472
18867
  this.stmts.updateEdge.run({
@@ -17482,18 +18877,38 @@ var SqliteGraphStore = class {
17482
18877
  }
17483
18878
  getNode(id) {
17484
18879
  const row = this.stmts.getNode.get(id);
17485
- return row ? rowToNode(row) : null;
18880
+ if (!row) return null;
18881
+ const node = rowToNode(row);
18882
+ if (row.exposure_json) Object.assign(node.attrs, JSON.parse(row.exposure_json));
18883
+ return node;
18884
+ }
18885
+ /** A node's render exposure: the side row's keys over the node row's own
18886
+ * (exposure-attrs.ts), plus the node's `lastUpdatedAt` (the "shown since
18887
+ * touch" counter is relative to it) — without reading the node's attrs. */
18888
+ exposureOf(id) {
18889
+ const row = this.stmts.lastUpdatedOf.get(id);
18890
+ if (!row) return null;
18891
+ const side = this.stmts.exposureOf.get(id);
18892
+ return {
18893
+ attrs: { ...pickExposure(JSON.parse(row.attrs_json)), ...side ? JSON.parse(side.attrs_json) : {} },
18894
+ lastUpdatedAt: row.t
18895
+ };
18896
+ }
18897
+ /** Replace a node's exposure (only EXPOSURE_ATTRS keys belong here). Touches
18898
+ * neither the node row nor its indexes, nor the change feed. */
18899
+ writeExposure(id, attrs) {
18900
+ this.stmts.writeExposure.run(id, JSON.stringify(attrs));
17486
18901
  }
17487
18902
  nodeLabels(ids) {
17488
18903
  const width = NODE_LABELS_CHUNK;
17489
- const stmt = this.typedEdgeStmt(
18904
+ const stmt2 = this.typedEdgeStmt(
17490
18905
  `SELECT id, label FROM nodes WHERE id IN (${Array.from({ length: width }, () => "?").join(",")})`
17491
18906
  );
17492
18907
  const labels = /* @__PURE__ */ new Map();
17493
18908
  for (let i = 0; i < ids.length; i += width) {
17494
18909
  const chunk = ids.slice(i, i + width);
17495
18910
  while (chunk.length < width) chunk.push(null);
17496
- for (const row of stmt.all(...chunk)) labels.set(row.id, row.label);
18911
+ for (const row of stmt2.all(...chunk)) labels.set(row.id, row.label);
17497
18912
  }
17498
18913
  return labels;
17499
18914
  }
@@ -17510,7 +18925,21 @@ var SqliteGraphStore = class {
17510
18925
  * per project per request; the materializing form held the daemon's event
17511
18926
  * loop for seconds at scale. */
17512
18927
  countNodesByLabel(label) {
17513
- return Number(this.stmts.countByLabel.get(label).n);
18928
+ return this.countAt(`label:${label}`, [nodeScope(label)], () => Number(this.stmts.countByLabel.get(label).n));
18929
+ }
18930
+ /** Counts asked for on every status poll (/health, `errata status`, the
18931
+ * dashboards): a live-node COUNT(*) reads the whole liveness index, ~3 MB per
18932
+ * call on a 1.1 GB store, to recount a set that moves only on a write. Each is
18933
+ * kept with the change-feed reading of the rows it counts — triggers bump it on
18934
+ * every write from any connection, so a hit is exact, not a TTL guess. */
18935
+ counts = /* @__PURE__ */ new Map();
18936
+ countAt(key, scopes, count) {
18937
+ const seq = readChangeSeq(this.db, scopes);
18938
+ const hit = this.counts.get(key);
18939
+ if (hit && hit.seq === seq) return hit.n;
18940
+ const n = count();
18941
+ this.counts.set(key, { seq, n });
18942
+ return n;
17514
18943
  }
17515
18944
  /** Live rows for a label with attrs and creation stamps only — for scans that
17516
18945
  * read attrs and never the vector. `findNodesByLabel` decodes every embedding
@@ -17519,16 +18948,39 @@ var SqliteGraphStore = class {
17519
18948
  findNodeStampsByLabel(label, attrKey) {
17520
18949
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(attrKey)) throw new Error(`findNodeStampsByLabel: bad attr key ${attrKey}`);
17521
18950
  const rows = this.db.prepare(
17522
- `SELECT id, created_at, created_at_seq, json_type(attrs_json, '$.${attrKey}') IS NOT NULL AS has
18951
+ `SELECT id, created_at, created_at_seq, json_type(${attrDocSql(attrKey)}, '$.${attrKey}') IS NOT NULL AS has,
18952
+ COALESCE(json_extract(attrs_json, '$.source') = 'cloud', 0) AS cloud
17523
18953
  FROM nodes WHERE label = ? AND valid_to IS NULL`
17524
18954
  ).all(label);
17525
18955
  return rows.map((r) => ({
17526
18956
  id: r.id,
17527
18957
  createdAt: r.created_at,
17528
18958
  ...r.created_at_seq != null ? { createdAtSeq: r.created_at_seq } : {},
17529
- has: r.has === 1
18959
+ has: r.has === 1,
18960
+ cloud: r.cloud === 1
17530
18961
  }));
17531
18962
  }
18963
+ findNodeStampsByLabelAttrs(label, attrKeys) {
18964
+ for (const k of attrKeys) if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k)) throw new Error(`findNodeStampsByLabelAttrs: bad attr key ${k}`);
18965
+ const keys = [...new Set(attrKeys)];
18966
+ const rows = this.db.prepare(
18967
+ `SELECT id, created_at, created_at_seq,
18968
+ ${keys.map((k, i) => `json_type(${attrDocSql(k)}, '$.${k}') IS NOT NULL AS h${i}`).join(", ")}${keys.length ? "," : ""}
18969
+ COALESCE(json_extract(attrs_json, '$.source') = 'cloud', 0) AS cloud
18970
+ FROM nodes WHERE label = ? AND valid_to IS NULL`
18971
+ ).all(label);
18972
+ const out = new Map(keys.map((k) => [k, []]));
18973
+ for (const r of rows) {
18974
+ keys.forEach((k, i) => out.get(k).push({
18975
+ id: r.id,
18976
+ createdAt: r.created_at,
18977
+ ...r.created_at_seq != null ? { createdAtSeq: r.created_at_seq } : {},
18978
+ has: r[`h${i}`] === 1,
18979
+ cloud: r.cloud === 1
18980
+ }));
18981
+ }
18982
+ return out;
18983
+ }
17532
18984
  findNodeAttrsByLabel(label) {
17533
18985
  const rows = this.stmts.attrsByLabel.all(label);
17534
18986
  return rows.map((r) => ({
@@ -17556,8 +19008,8 @@ var SqliteGraphStore = class {
17556
19008
  after = rows[rows.length - 1].rid;
17557
19009
  }
17558
19010
  }
17559
- recentNodeIds(label, limit) {
17560
- 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));
19011
+ recentProblemIds(limit) {
19012
+ 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));
17561
19013
  return rows.map((r) => r.id);
17562
19014
  }
17563
19015
  recentOpenProblemCandidates(opts) {
@@ -17637,6 +19089,20 @@ var SqliteGraphStore = class {
17637
19089
  }
17638
19090
  searchTextCandidates(terms) {
17639
19091
  if (terms.length === 0) return [];
19092
+ const indexed = terms.filter((t) => t.length >= TEXT_INDEX_MIN_TERM);
19093
+ if (indexed.length > 0 && textIndexReady(this.db)) {
19094
+ const short = terms.filter((t) => t.length < TEXT_INDEX_MIN_TERM).map((t) => t.toLowerCase());
19095
+ const rows2 = this.db.prepare(`WITH hits AS MATERIALIZED (
19096
+ SELECT t.rowid AS rid, COUNT(*) AS matched FROM json_each(:terms) q, nodes_text t
19097
+ WHERE t.description LIKE '%' || q.value || '%' GROUP BY t.rowid
19098
+ ) SELECT ${TEXT_SEARCH_COLUMNS}, hits.matched FROM hits JOIN nodes ON nodes.rowid = hits.rid
19099
+ WHERE nodes.valid_to IS NULL ORDER BY page_rank DESC, last_updated_at DESC`).all({ terms: JSON.stringify(indexed) });
19100
+ return rows2.map((row) => {
19101
+ const text = row.description.toLowerCase();
19102
+ const extra = short.filter((t) => text.includes(t)).length;
19103
+ return { node: textSearchNode(row), matched: row.matched + extra };
19104
+ });
19105
+ }
17640
19106
  const rows = this.db.prepare(`WITH matches AS MATERIALIZED (
17641
19107
  SELECT n.id, COUNT(*) AS matched FROM nodes n INDEXED BY nodes_live_description, json_each(:terms) t
17642
19108
  WHERE n.valid_to IS NULL AND n.description LIKE '%' || t.value || '%'
@@ -17645,6 +19111,18 @@ var SqliteGraphStore = class {
17645
19111
  JOIN matches USING (id) ORDER BY page_rank DESC, last_updated_at DESC`).all({ terms: JSON.stringify(terms) });
17646
19112
  return rows.map((row) => ({ node: textSearchNode(row), matched: row.matched }));
17647
19113
  }
19114
+ commentAnchors(commentIds) {
19115
+ const out = /* @__PURE__ */ new Map();
19116
+ if (commentIds.length === 0) return out;
19117
+ const rows = this.db.prepare(`WITH a AS MATERIALIZED (
19118
+ SELECT q.value AS cid, COALESCE(
19119
+ (SELECT to_id FROM edges WHERE from_id = q.value AND type IN ('EXPLAINS', 'ANNOTATES') AND valid_to IS NULL LIMIT 1),
19120
+ (SELECT from_id FROM edges WHERE to_id = q.value AND type = 'DOCUMENTED_BY' AND valid_to IS NULL LIMIT 1)
19121
+ ) AS aid FROM json_each(:ids) q
19122
+ ) 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) });
19123
+ for (const row of rows) out.set(row.cid, textSearchNode(row));
19124
+ return out;
19125
+ }
17648
19126
  getTextSearchNode(id) {
17649
19127
  const row = this.db.prepare(`SELECT ${TEXT_SEARCH_COLUMNS} FROM nodes WHERE id = ? AND valid_to IS NULL`).get(id);
17650
19128
  return row ? textSearchNode(row) : null;
@@ -17664,12 +19142,12 @@ var SqliteGraphStore = class {
17664
19142
  return scale;
17665
19143
  }
17666
19144
  typedEdgeStmt(sql) {
17667
- let stmt = this.typedEdgeStmts.get(sql);
17668
- if (!stmt) {
17669
- stmt = this.db.prepare(sql);
17670
- this.typedEdgeStmts.set(sql, stmt);
19145
+ let stmt2 = this.typedEdgeStmts.get(sql);
19146
+ if (!stmt2) {
19147
+ stmt2 = this.db.prepare(sql);
19148
+ this.typedEdgeStmts.set(sql, stmt2);
17671
19149
  }
17672
- return stmt;
19150
+ return stmt2;
17673
19151
  }
17674
19152
  outEdges(nodeId, types) {
17675
19153
  if (!types || types.length === 0) {
@@ -17728,21 +19206,54 @@ var SqliteGraphStore = class {
17728
19206
  return out;
17729
19207
  }
17730
19208
  nodeCount() {
17731
- return Number(this.stmts.nodeCount.get().n);
19209
+ return this.countAt("nodes", [ANY_CHANGE], () => Number(this.stmts.nodeCount.get().n));
17732
19210
  }
17733
19211
  mutationCount() {
17734
19212
  return this.mutations;
17735
19213
  }
19214
+ changeStamp() {
19215
+ const { data_version } = this.db.pragma("data_version");
19216
+ const { n } = this.db.prepare("SELECT total_changes() AS n").get();
19217
+ return `${data_version}:${n}`;
19218
+ }
19219
+ changeSeq(scopes) {
19220
+ return readChangeSeq(this.db, scopes);
19221
+ }
19222
+ lineageChangedFiles(sinceSeq) {
19223
+ return readLineageChangedFiles(this.db, sinceSeq);
19224
+ }
19225
+ changedNodeIds(sinceSeq, labels) {
19226
+ return readChangedNodeIds(this.db, sinceSeq, labels);
19227
+ }
19228
+ changedAmong(sinceSeq, ids) {
19229
+ return readChangedAmong(this.db, sinceSeq, ids);
19230
+ }
19231
+ pruneChangeLog() {
19232
+ return pruneChangeLog(this.db);
19233
+ }
19234
+ fileRowStates(ids) {
19235
+ if (ids.length === 0) return [];
19236
+ if (ids.length > 500) throw new RangeError("fileRowStates accepts at most 500 ids per read");
19237
+ 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);
19238
+ }
19239
+ // Read and advanced in SQL, never from a per-connection copy: pass workers and
19240
+ // MCP processes open their own connections and advance the same row, so a
19241
+ // copy loaded at open went stale, and `copy + 1` wrote a duplicate or a
19242
+ // backwards seq from whichever connection had fallen behind.
17736
19243
  currentIngestSeq() {
19244
+ const row = this.db.prepare("SELECT value FROM store_meta WHERE key = 'ingest_seq'").get();
19245
+ this.ingestSeq = row ? Number(row.value) : 0;
17737
19246
  return this.ingestSeq;
17738
19247
  }
17739
19248
  nextIngestSeq() {
17740
- this.ingestSeq += 1;
17741
- this.stmts.setIngestSeq.run({ v: String(this.ingestSeq) });
19249
+ const row = this.db.prepare(
19250
+ "INSERT INTO store_meta (key, value) VALUES ('ingest_seq', '1') ON CONFLICT(key) DO UPDATE SET value = CAST(value AS INTEGER) + 1 RETURNING value"
19251
+ ).get();
19252
+ this.ingestSeq = Number(row.value);
17742
19253
  return this.ingestSeq;
17743
19254
  }
17744
19255
  edgeCount() {
17745
- return Number(this.stmts.edgeCount.get().n);
19256
+ return this.countAt("edges", [ANY_CHANGE], () => Number(this.stmts.edgeCount.get().n));
17746
19257
  }
17747
19258
  // Lightweight whole-graph scans — select only the columns the caller needs so a
17748
19259
  // 100k-node pass skips the per-row attrs JSON.parse + embedding decode rowToNode
@@ -17870,28 +19381,63 @@ var SqliteGraphStore = class {
17870
19381
  }
17871
19382
  }
17872
19383
  reviveReobserved(relPaths, workspaceId, since) {
17873
- let revived = 0;
19384
+ const revived = [];
17874
19385
  const CHUNK = 400;
17875
19386
  for (let i = 0; i < relPaths.length; i += CHUNK) {
17876
19387
  const slice = relPaths.slice(i, i + CHUNK);
17877
19388
  if (slice.length === 0) continue;
17878
- const r = this.db.prepare(
17879
- `UPDATE nodes SET valid_to = NULL
19389
+ const rows = this.db.prepare(
19390
+ `SELECT id, valid_to AS closedAt, COALESCE(logical_id, id) AS lid FROM nodes
17880
19391
  WHERE valid_to IS NOT NULL
17881
19392
  AND last_updated_at >= ?
17882
19393
  AND json_extract(attrs_json, '$.workspaceId') = ?
17883
- AND json_extract(attrs_json, '$.relPath') IN (${slice.map(() => "?").join(",")})`
17884
- ).run(since, workspaceId, ...slice);
17885
- revived += Number(r.changes);
19394
+ AND json_extract(attrs_json, '$.relPath') IN (${slice.map(() => "?").join(",")})
19395
+ AND json_type(attrs_json, '$.historical') IS NOT 'true'`
19396
+ ).all(since, workspaceId, ...slice);
19397
+ revived.push(...rows);
17886
19398
  }
17887
- if (revived > 0) this.mutations++;
17888
- return revived;
19399
+ if (revived.length === 0) return 0;
19400
+ const reopen = this.db.prepare("UPDATE nodes SET valid_to = NULL WHERE id = ? AND valid_to IS NOT NULL");
19401
+ for (const r of revived) reopen.run(r.id);
19402
+ for (const r of revived) reopenCascadeClosedEdges(this.db, r.id, Number(r.closedAt));
19403
+ collapseLiveHeads(this.db, revived.map((r) => r.lid), since);
19404
+ this.mutations++;
19405
+ return revived.length;
19406
+ }
19407
+ incomingEdgeCoverage(label, edgeTypes, cloudOnly) {
19408
+ if (edgeTypes.length === 0) return { count: 0, total: 0 };
19409
+ const r = this.db.prepare(
19410
+ `SELECT COUNT(*) AS total,
19411
+ COALESCE(SUM(CASE WHEN EXISTS (
19412
+ SELECT 1 FROM edges e WHERE e.to_id = n.id AND e.valid_to IS NULL
19413
+ AND e.type IN (${edgeTypes.map(() => "?").join(",")})
19414
+ ) THEN 1 ELSE 0 END), 0) AS count
19415
+ FROM nodes n
19416
+ WHERE n.label = ? AND n.valid_to IS NULL
19417
+ AND (? = 0 OR json_extract(n.attrs_json, '$.source') = 'cloud')`
19418
+ ).get(...edgeTypes, label, cloudOnly ? 1 : 0);
19419
+ return { count: Number(r.count), total: Number(r.total) };
19420
+ }
19421
+ liveEdgeTargets(edgeTypes) {
19422
+ if (edgeTypes.length === 0) return [];
19423
+ 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);
19424
+ return rows.map((r) => r.to_id);
17889
19425
  }
17890
19426
  recentEdgeAttrCoverage(marker, attr, limit) {
19427
+ if (marker === PRIOR_TAG_MARKER) {
19428
+ const r2 = this.db.prepare(
19429
+ `SELECT COUNT(*) AS total,
19430
+ COALESCE(SUM(CASE WHEN json_extract(attrs_json, '$.' || ?) = 1 THEN 1 ELSE 0 END), 0) AS count
19431
+ FROM (SELECT attrs_json FROM edges INDEXED BY edges_live_prior_tag
19432
+ WHERE valid_to IS NULL AND json_extract(attrs_json, '$.priorTag') = 1
19433
+ ORDER BY created_at DESC LIMIT ?)`
19434
+ ).get(attr, limit);
19435
+ return { count: Number(r2.count), total: Number(r2.total) };
19436
+ }
17891
19437
  const r = this.db.prepare(
17892
19438
  `SELECT COUNT(*) AS total,
17893
19439
  COALESCE(SUM(CASE WHEN json_extract(attrs_json, '$.' || ?) = 1 THEN 1 ELSE 0 END), 0) AS count
17894
- FROM (SELECT attrs_json FROM edges
19440
+ FROM (SELECT attrs_json FROM edges INDEXED BY edges_live_created
17895
19441
  WHERE valid_to IS NULL AND attrs_json LIKE ?
17896
19442
  ORDER BY created_at DESC LIMIT ?)`
17897
19443
  ).get(attr, `%${marker}%`, limit);
@@ -17904,7 +19450,7 @@ var SqliteGraphStore = class {
17904
19450
  const slice = cloudIds.slice(i, i + CHUNK);
17905
19451
  if (slice.length === 0) continue;
17906
19452
  const rows = this.db.prepare(
17907
- `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`
19453
+ `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`
17908
19454
  ).all(...slice);
17909
19455
  for (const r of rows) out.set(r.cloud, [...out.get(r.cloud) ?? [], r.id]);
17910
19456
  }
@@ -18017,8 +19563,9 @@ var SqliteGraphStore = class {
18017
19563
  this.stmts.advanceLive.run({ live_id: liveId, t });
18018
19564
  return frozenId;
18019
19565
  }
18020
- closeNode(id, t) {
18021
- this.stmts.closeNode.run({ id, t });
19566
+ closeNode(id, t, opts = {}) {
19567
+ const r = this.stmts.closeNode.run({ id, t });
19568
+ if (Number(r.changes) > 0 && !opts.retainEdges) closeIncidentEdges(this.db, id, t);
18022
19569
  }
18023
19570
  closeEdge(id, t) {
18024
19571
  this.stmts.closeEdge.run({ id, t });
@@ -18038,9 +19585,11 @@ var SqliteGraphStore = class {
18038
19585
  * so it must be a plain identifier. */
18039
19586
  nodesAsOfWhereTrue(t, attr) {
18040
19587
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(attr)) throw new Error(`nodesAsOfWhereTrue: bad attr ${JSON.stringify(attr)}`);
19588
+ const indexed = attr === "revisit" ? " INDEXED BY nodes_revisit_true" : "";
18041
19589
  const rows = this.db.prepare(
18042
- `SELECT * FROM nodes WHERE valid_from <= :t AND (valid_to IS NULL OR valid_to > :t)
18043
- AND json_type(attrs_json, '$.${attr}') = 'true'`
19590
+ `SELECT * FROM nodes${indexed} WHERE valid_from <= :t AND (valid_to IS NULL OR valid_to > :t)
19591
+ AND json_type(attrs_json, '$.${attr}') = 'true'
19592
+ ORDER BY rowid`
18044
19593
  ).all({ t });
18045
19594
  return rows.map(rowToNode);
18046
19595
  }
@@ -18100,6 +19649,7 @@ init_src();
18100
19649
  // ../../packages/local-graph/src/design-problem.ts
18101
19650
  init_src();
18102
19651
  var RECURRENCE_DEBOUNCE_MS = 60 * 60 * 1e3;
19652
+ var EXPOSURE_LOG_KEYS = ["exposureLog", "exposureLogSince", "exposureLogStart", "exposureTrimmed", "exposureFirst"];
18103
19653
 
18104
19654
  // ../../packages/local-graph/src/intent.ts
18105
19655
  init_src();
@@ -18160,7 +19710,7 @@ function percolate(l1, l2, opts) {
18160
19710
  validFrom: opts.ts,
18161
19711
  validTo: null,
18162
19712
  attrs: {
18163
- ...n.attrs,
19713
+ ...withoutExposureLog(n.attrs),
18164
19714
  abstractionLevel: ABSTRACTION_LEVEL.CONCRETE,
18165
19715
  observedInProjects: [ws]
18166
19716
  }
@@ -18219,6 +19769,11 @@ function percolate(l1, l2, opts) {
18219
19769
  result.touched = [...touched];
18220
19770
  return result;
18221
19771
  }
19772
+ function withoutExposureLog(attrs) {
19773
+ const out = { ...attrs };
19774
+ for (const k of EXPOSURE_LOG_KEYS) delete out[k];
19775
+ return out;
19776
+ }
18222
19777
 
18223
19778
  // ../../packages/local-graph/src/abstraction.ts
18224
19779
  init_src();
@@ -18370,32 +19925,35 @@ function projectsOf(attrs) {
18370
19925
  function induceTriage(l2, opts) {
18371
19926
  const report = { episodes: 0, observations: 0, skippedRefinement: 0 };
18372
19927
  const episodes = [];
18373
- for (const label of ["Problem", "Solution", "Claim"]) {
18374
- for (const n of l2.findNodesByLabel(label)) {
18375
- if (label === "Problem" && n.attrs["resolvedAs"] === PROBLEM_RESOLUTION.FALSE_POSITIVE) {
18376
- continue;
18377
- }
18378
- if (label === "Problem" && n.attrs["resolvedAs"] === PROBLEM_RESOLUTION.INVALID && n.attrs[INDUCTED_MARK] !== true) {
18379
- episodes.push({
18380
- assumedId: n.id,
18381
- actualId: `dcause_dissolve_${n.id}`.slice(0, 56),
18382
- driftKind: DRIFT_KIND.DISSOLUTION,
18383
- projects: projectsOf(n.attrs),
18384
- dissolution: true,
18385
- markNode: n.id
18386
- });
18387
- }
18388
- for (const e of l2.outEdges(n.id)) {
18389
- if (e.attrs["reframe"] !== true || e.attrs[INDUCTED_MARK] === true) continue;
18390
- episodes.push({
18391
- assumedId: String(e.attrs["assumed"] ?? e.from),
18392
- actualId: String(e.attrs["actual"] ?? e.to),
18393
- driftKind: String(e.attrs["driftKind"] ?? ""),
18394
- projects: projectsOf(e.attrs),
18395
- dissolution: false,
18396
- markEdge: e.id
18397
- });
18398
- }
19928
+ const sources = l2.selectNodes({
19929
+ labels: ["Problem", "Solution", "Claim"],
19930
+ attrs: ["resolvedAs", INDUCTED_MARK, "observedInProjects"]
19931
+ });
19932
+ for (const n of sources) {
19933
+ const label = n.label;
19934
+ if (label === "Problem" && n.attrs["resolvedAs"] === PROBLEM_RESOLUTION.FALSE_POSITIVE) {
19935
+ continue;
19936
+ }
19937
+ if (label === "Problem" && n.attrs["resolvedAs"] === PROBLEM_RESOLUTION.INVALID && n.attrs[INDUCTED_MARK] !== true) {
19938
+ episodes.push({
19939
+ assumedId: n.id,
19940
+ actualId: `dcause_dissolve_${n.id}`.slice(0, 56),
19941
+ driftKind: DRIFT_KIND.DISSOLUTION,
19942
+ projects: projectsOf(n.attrs),
19943
+ dissolution: true,
19944
+ markNode: n.id
19945
+ });
19946
+ }
19947
+ for (const e of l2.outEdges(n.id)) {
19948
+ if (e.attrs["reframe"] !== true || e.attrs[INDUCTED_MARK] === true) continue;
19949
+ episodes.push({
19950
+ assumedId: String(e.attrs["assumed"] ?? e.from),
19951
+ actualId: String(e.attrs["actual"] ?? e.to),
19952
+ driftKind: String(e.attrs["driftKind"] ?? ""),
19953
+ projects: projectsOf(e.attrs),
19954
+ dissolution: false,
19955
+ markEdge: e.id
19956
+ });
18399
19957
  }
18400
19958
  }
18401
19959
  for (const ep of episodes) {
@@ -18446,7 +20004,7 @@ function revisitStaleRoutes(l2, ts, opts = {}) {
18446
20004
  const minNavFailures = opts.minNavFailures ?? 3;
18447
20005
  const report = { flagged: 0, demoted: 0 };
18448
20006
  l2.transaction(() => {
18449
- for (const tri of l2.findNodesByLabel("Triage")) {
20007
+ for (const tri of l2.selectNodes({ labels: ["Triage"] })) {
18450
20008
  for (const e of l2.outEdges(tri.id, [...ROUTE_EDGE_TYPES])) {
18451
20009
  if (e.attrs["revisit"] === true) continue;
18452
20010
  let reason = "";
@@ -18552,6 +20110,7 @@ function mergeGeneralizes(store, principleId, memberId, ts) {
18552
20110
  attrs: { provisional: true }
18553
20111
  });
18554
20112
  }
20113
+ var CONCRETE_MEMBER_KEYS = ["retiredReason", "abstractionLevel", "resolvedAs", "rationale"];
18555
20114
  function induceAbstractions(l2, opts) {
18556
20115
  const K = opts.minClusterSize ?? DEFAULT_MIN_CLUSTER_SIZE;
18557
20116
  const minCtx = opts.minDistinctContexts ?? DEFAULT_MIN_DISTINCT_CONTEXTS;
@@ -18564,15 +20123,13 @@ function induceAbstractions(l2, opts) {
18564
20123
  generalizesEdges: 0
18565
20124
  };
18566
20125
  const ids = /* @__PURE__ */ new Set();
18567
- for (const label of PERCOLATING_LABELS) {
18568
- for (const n of l2.findNodesByLabel(label)) {
18569
- if (isEditPlaceholderSolution(n) || n.attrs["retiredReason"] || n.validTo != null) continue;
18570
- if (Number(n.attrs["abstractionLevel"] ?? ABSTRACTION_LEVEL.CONCRETE) >= ABSTRACTION_LEVEL.PRINCIPLE) {
18571
- continue;
18572
- }
18573
- if (n.attrs["resolvedAs"] === PROBLEM_RESOLUTION.FALSE_POSITIVE) continue;
18574
- ids.add(n.id);
20126
+ for (const n of l2.selectNodes({ labels: PERCOLATING_LABELS, attrs: CONCRETE_MEMBER_KEYS })) {
20127
+ if (isEditPlaceholderSolution(n) || n.attrs["retiredReason"] || n.validTo != null) continue;
20128
+ if (Number(n.attrs["abstractionLevel"] ?? ABSTRACTION_LEVEL.CONCRETE) >= ABSTRACTION_LEVEL.PRINCIPLE) {
20129
+ continue;
18575
20130
  }
20131
+ if (n.attrs["resolvedAs"] === PROBLEM_RESOLUTION.FALSE_POSITIVE) continue;
20132
+ ids.add(n.id);
18576
20133
  }
18577
20134
  if (ids.size === 0) return report;
18578
20135
  const adj = /* @__PURE__ */ new Map();
@@ -18640,7 +20197,7 @@ function induceAbstractions(l2, opts) {
18640
20197
  function revisitContradictedPrinciples(l2, ts) {
18641
20198
  const report = { flagged: 0 };
18642
20199
  l2.transaction(() => {
18643
- for (const n of l2.findNodesByLabel("Claim")) {
20200
+ for (const n of l2.selectNodes({ labels: ["Claim"], attrs: ["abstractionLevel", "revisit", "members"] })) {
18644
20201
  if (Number(n.attrs["abstractionLevel"] ?? ABSTRACTION_LEVEL.CONCRETE) < ABSTRACTION_LEVEL.PRINCIPLE) continue;
18645
20202
  if (n.attrs["revisit"] === true) continue;
18646
20203
  const members = Array.isArray(n.attrs["members"]) ? n.attrs["members"] : [];
@@ -18666,9 +20223,11 @@ function revisitContradictedPrinciples(l2, ts) {
18666
20223
  }
18667
20224
  }
18668
20225
  if (!reason) continue;
20226
+ const principle = l2.getNode(n.id);
20227
+ if (!principle) continue;
18669
20228
  l2.updateNode(n.id, {
18670
20229
  attrs: {
18671
- ...n.attrs,
20230
+ ...principle.attrs,
18672
20231
  revisit: true,
18673
20232
  revisitReason: reason,
18674
20233
  revisitSinceTs: ts,
@@ -18701,9 +20260,26 @@ init_src3();
18701
20260
  function findStrandedSymbols(store) {
18702
20261
  return store.strandedSymbols();
18703
20262
  }
20263
+ var FILE_DECLARATION_LABELS = [
20264
+ "Function",
20265
+ "Class",
20266
+ "Method",
20267
+ "Module",
20268
+ "Interface",
20269
+ "Const",
20270
+ "Static",
20271
+ "Property",
20272
+ "TypeAlias",
20273
+ "Namespace",
20274
+ "Enum",
20275
+ "Annotation",
20276
+ "Comment"
20277
+ ];
20278
+ var DECLARATION_LABEL_SET = new Set(FILE_DECLARATION_LABELS);
18704
20279
 
18705
20280
  // src/package-index.ts
18706
20281
  init_src();
20282
+ var CONTEXT_GATE_ATTRS = ["source", "name", "contributedAtSeq"];
18707
20283
  function buildContextIngest(store, profile, daemonVersion, ignorePatterns = [], opts = {}) {
18708
20284
  const labels = opts.includePackages === false ? ["Language"] : ["Package", "Language"];
18709
20285
  const ignored = (s) => ignorePatterns.length > 0 && ignorePatterns.some((p) => s.toLowerCase().includes(p));
@@ -18711,10 +20287,12 @@ function buildContextIngest(store, profile, daemonVersion, ignorePatterns = [],
18711
20287
  const seen = /* @__PURE__ */ new Set();
18712
20288
  const localIdByWireId = {};
18713
20289
  for (const label of labels) {
18714
- for (const n of store.findNodesByLabel(label)) {
18715
- if (n.attrs["source"] === "cloud") continue;
18716
- if (ignored(n.description) || ignored(String(n.attrs["name"] ?? ""))) continue;
18717
- if (typeof n.attrs["contributedAtSeq"] === "number") continue;
20290
+ for (const row of store.selectNodes({ labels: [label], attrs: CONTEXT_GATE_ATTRS })) {
20291
+ if (row.attrs["source"] === "cloud") continue;
20292
+ if (ignored(row.description) || ignored(String(row.attrs["name"] ?? ""))) continue;
20293
+ if (typeof row.attrs["contributedAtSeq"] === "number") continue;
20294
+ const n = store.getNode(row.id);
20295
+ if (!n) continue;
18718
20296
  nodes.push({
18719
20297
  ...n,
18720
20298
  // Language wire id = the shared `languageCanonicalId` (warming-spine
@@ -18743,10 +20321,14 @@ function buildContextIngest(store, profile, daemonVersion, ignorePatterns = [],
18743
20321
  }
18744
20322
  }
18745
20323
  if (nodes.length === 0) return null;
20324
+ const contributed = (id) => {
20325
+ const n = store.getNode(id);
20326
+ return !!n && (n.label === "Package" || n.label === "Language") && n.attrs["source"] !== "cloud" && typeof n.attrs["contributedAtSeq"] === "number" && !ignored(n.description) && !ignored(String(n.attrs["name"] ?? ""));
20327
+ };
18746
20328
  const edges = [];
18747
20329
  for (const id of seen) {
18748
20330
  for (const e of store.outEdges(id, ["DEPENDS_ON"])) {
18749
- if (seen.has(e.to)) edges.push({ ...e, attrs: {} });
20331
+ if (seen.has(e.to) || contributed(e.to)) edges.push({ ...e, attrs: {} });
18750
20332
  }
18751
20333
  }
18752
20334
  const base = {
@@ -19147,7 +20729,7 @@ function publicSymbolIndex(store, workspaceRoot, deps = {}) {
19147
20729
  });
19148
20730
  const internal = internalPackageNames(workspaceRoot);
19149
20731
  const bareModules = /* @__PURE__ */ new Set();
19150
- for (const mod of store.findNodesByLabel("Module")) {
20732
+ for (const mod of store.selectNodes({ labels: ["Module"] })) {
19151
20733
  if (isBareExternal(mod.description, internal)) bareModules.add(mod.id);
19152
20734
  }
19153
20735
  const files = /* @__PURE__ */ new Set();
@@ -19541,7 +21123,14 @@ function summaryFromDoc(doc, kind) {
19541
21123
  }
19542
21124
  if (verb !== null) return null;
19543
21125
  const noun = clipped.replace(/^(?:the|a|an)\s+/i, "");
19544
- return tidy(`${lead(kind)} for ${noun.charAt(0).toLowerCase()}${noun.slice(1)}`);
21126
+ const head = noun.split(/[\s-]/, 1)[0].replace(/[.,;:!?]+$/, "");
21127
+ return tidy(`${lead(kind)} for ${isOrdinaryCapital(head) ? noun.charAt(0).toLowerCase() + noun.slice(1) : noun}`);
21128
+ }
21129
+ var CONTRACTION_BASES = /* @__PURE__ */ new Set(["it", "that", "this", "there", "here", "what", "who", "where", "when", "why", "how", "let", "he", "she"]);
21130
+ function isOrdinaryCapital(word) {
21131
+ const contraction = /^([A-Za-z]+)['’]s$/.exec(word);
21132
+ if (contraction) return CONTRACTION_BASES.has(contraction[1].toLowerCase()) && /^[A-Z][a-z]*$/.test(contraction[1]);
21133
+ return /^[A-Z][a-z]+$/.test(word);
19545
21134
  }
19546
21135
  var VERB_PHRASE_KINDS = /* @__PURE__ */ new Set(["function", "method"]);
19547
21136
  function summaryFromName(name, kind) {
@@ -19651,6 +21240,62 @@ function buildSymbolLexicon(store) {
19651
21240
  }
19652
21241
  return lex;
19653
21242
  }
21243
+ function lazySymbolLexicon(store) {
21244
+ if (!store.firstLiveByNameOrQname || !store.liveCommentsAttachedTo) return null;
21245
+ const labels = SYMBOL_LABELS.map(([label]) => label);
21246
+ const kindOf = new Map(SYMBOL_LABELS.map(([label, kind]) => [label, kind]));
21247
+ const members = /* @__PURE__ */ new Map();
21248
+ const docs = /* @__PURE__ */ new Map();
21249
+ const entries = /* @__PURE__ */ new Map();
21250
+ const member = (name) => {
21251
+ if (!members.has(name)) {
21252
+ const hit = isDistinctiveIdentifier(name) ? store.firstLiveByNameOrQname(labels, name) : null;
21253
+ members.set(name, hit ? { kind: kindOf.get(hit.label), description: hit.description, qname: hit.qname } : null);
21254
+ }
21255
+ return members.get(name);
21256
+ };
21257
+ const docFor = (name) => {
21258
+ if (!docs.has(name)) {
21259
+ let found;
21260
+ for (const c of store.liveCommentsAttachedTo(name)) {
21261
+ if (typeof c.kind !== "string" || !DOC_COMMENT_KINDS.has(c.kind)) continue;
21262
+ const text = typeof c.text === "string" ? c.text : c.description;
21263
+ if (!text?.trim()) continue;
21264
+ found = text.trim().slice(0, MAX_DOC_CHARS);
21265
+ break;
21266
+ }
21267
+ docs.set(name, found);
21268
+ }
21269
+ return docs.get(name);
21270
+ };
21271
+ const isKnown = (tok) => member(tok) !== null;
21272
+ let anySymbol;
21273
+ return {
21274
+ has: isKnown,
21275
+ get(name) {
21276
+ if (!entries.has(name)) {
21277
+ const m = member(name);
21278
+ let entry = null;
21279
+ if (m) {
21280
+ entry = { kind: m.kind };
21281
+ const doc = (m.description ? docFor(m.description) : void 0) ?? (m.qname ? docFor(m.qname) : void 0);
21282
+ for (const phrase of summaryCandidates(name, m.kind, doc)) {
21283
+ if (summaryRejectionReason(phrase, isKnown) === null) {
21284
+ entry.summary = phrase;
21285
+ break;
21286
+ }
21287
+ }
21288
+ }
21289
+ entries.set(name, entry);
21290
+ }
21291
+ return entries.get(name) ?? void 0;
21292
+ },
21293
+ get size() {
21294
+ anySymbol ??= labels.some((label) => store.countNodesByLabel(label) > 0);
21295
+ return anySymbol ? 1 : 0;
21296
+ }
21297
+ };
21298
+ }
19654
21299
  var LEXICON_SYMBOL_LABELS = ["Function", "Method", "Class", "Interface", "Const", "Comment"];
19655
21300
  function codeGraphKey(store) {
19656
21301
  const files = store.labelFingerprint?.(["File"]);
@@ -19662,7 +21307,7 @@ function lexiconsFor(store) {
19662
21307
  const key = codeGraphKey(store);
19663
21308
  const hit = key !== null ? lexiconCache.get(store) : void 0;
19664
21309
  if (hit && hit.key === key) return hit;
19665
- const built = { symbols: buildSymbolLexicon(store), files: buildFileLexicon(store) };
21310
+ const built = { symbols: lazySymbolLexicon(store) ?? buildSymbolLexicon(store), files: buildFileLexicon(store) };
19666
21311
  if (key !== null) lexiconCache.set(store, { key, ...built });
19667
21312
  return built;
19668
21313
  }
@@ -19901,6 +21546,38 @@ function stripCodebaseScope(scope) {
19901
21546
  delete s["codebase"];
19902
21547
  return s;
19903
21548
  }
21549
+ function instanceGateRows(store, label) {
21550
+ return store.instanceGateRows?.(label) ?? store.findNodeFieldsByLabel(label, ["source", "privateFinding", "resolvedAs"]).map((r) => ({
21551
+ id: r.id,
21552
+ source: r.attrs["source"],
21553
+ privateFinding: r.attrs["privateFinding"],
21554
+ resolvedAs: r.attrs["resolvedAs"]
21555
+ }));
21556
+ }
21557
+ var GATE_VIEW_ATTRS = [
21558
+ "privateFinding",
21559
+ "source",
21560
+ "sources",
21561
+ "resolvedAs",
21562
+ "instanceReceipts",
21563
+ "cloudWithdrawn",
21564
+ "withdrawnAtCorrectionSeq",
21565
+ "contributedAtSeq",
21566
+ "cloudNodeId"
21567
+ ];
21568
+ var GATE_VIEW_CHUNK = 500;
21569
+ var gateView = /* @__PURE__ */ new WeakSet();
21570
+ function gateViews(store, label, ids) {
21571
+ const byId = /* @__PURE__ */ new Map();
21572
+ for (let i = 0; i < ids.length; i += GATE_VIEW_CHUNK) {
21573
+ for (const r of store.selectNodes({ labels: [label], attrs: GATE_VIEW_ATTRS, where: [{ idIn: ids.slice(i, i + GATE_VIEW_CHUNK) }] })) {
21574
+ const view = { ...r, embedding: [] };
21575
+ gateView.add(view);
21576
+ byId.set(r.id, view);
21577
+ }
21578
+ }
21579
+ return ids.flatMap((id) => byId.get(id) ?? []);
21580
+ }
19904
21581
  function contributedWireId(n, destinationKey) {
19905
21582
  const cloudId = instanceReceipt(n, destinationKey)["cloudNodeId"];
19906
21583
  return typeof cloudId === "string" && cloudId ? cloudId : n.id;
@@ -20054,10 +21731,12 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
20054
21731
  sidecarNoSummary: 0,
20055
21732
  fileRefsSubstituted: 0,
20056
21733
  fileRefLexiconMisses: 0,
20057
- collapsedEdges: 0
21734
+ collapsedEdges: 0,
21735
+ edgesHeldParked: 0,
21736
+ edgesHeldDropped: 0
20058
21737
  };
20059
21738
  const sharedCauses = /* @__PURE__ */ new Map();
20060
- const causalQueue = store.findNodeFieldsByLabel("Problem", ["source", "resolvedAs"]).filter((r) => r.attrs["source"] !== "cloud" && r.attrs["resolvedAs"] !== PROBLEM_RESOLUTION.FALSE_POSITIVE).map((r) => r.id).filter((id) => ignorePatterns.length === 0 || !ignored(store.getNode(id)?.description ?? ""));
21739
+ const causalQueue = instanceGateRows(store, "Problem").filter((r) => r.source !== "cloud" && r.resolvedAs !== PROBLEM_RESOLUTION.FALSE_POSITIVE).map((r) => r.id).filter((id) => ignorePatterns.length === 0 || !ignored(store.getNode(id)?.description ?? ""));
20061
21740
  const causalSeen = new Set(causalQueue);
20062
21741
  for (let i = 0; i < causalQueue.length; i++) {
20063
21742
  const id = causalQueue[i];
@@ -20073,21 +21752,21 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
20073
21752
  for (const label of INSTANCE_LABELS) {
20074
21753
  const ref = [];
20075
21754
  includedByLabel.set(label, ref);
20076
- const local = [];
20077
- for (const row of store.findNodeFieldsByLabel(label, ["source", "privateFinding"])) {
20078
- if (row.attrs["source"] !== "cloud") {
20079
- const n = store.getNode(row.id);
20080
- if (n) local.push(n);
21755
+ const localIds = [];
21756
+ for (const row of instanceGateRows(store, label)) {
21757
+ if (row.source !== "cloud") {
21758
+ localIds.push(row.id);
20081
21759
  continue;
20082
21760
  }
20083
21761
  if (seen.has(row.id) || walked.has(row.id)) continue;
20084
21762
  walked.add(row.id);
20085
21763
  composition.candidates++;
20086
- if ((row.attrs["privateFinding"] || label === "Vulnerability") && !opts.project) composition.privateTargetMissing++;
21764
+ if ((row.privateFinding || label === "Vulnerability") && !opts.project) composition.privateTargetMissing++;
20087
21765
  else composition.cloudSourced++;
20088
21766
  }
21767
+ const local = gateViews(store, label, localIds);
20089
21768
  const population = label === "RootCause" && opts.sharedStore ? [...local, ...sharedCauses.values()] : local;
20090
- for (const n of population) {
21769
+ for (let n of population) {
20091
21770
  if (seen.has(n.id)) continue;
20092
21771
  if (walked.has(n.id)) continue;
20093
21772
  walked.add(n.id);
@@ -20123,6 +21802,11 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
20123
21802
  semanticEndpoints.set(n.id, contributedWireId(n, opts.destinationKey));
20124
21803
  continue;
20125
21804
  }
21805
+ if (gateView.has(n)) {
21806
+ const full = store.getNode(n.id);
21807
+ if (!full) continue;
21808
+ n = full;
21809
+ }
20126
21810
  if (isEditPlaceholderSolution(n)) {
20127
21811
  composition.autoMint++;
20128
21812
  continue;
@@ -20365,6 +22049,30 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
20365
22049
  if (count > 0) symbolSummaries = sidecar;
20366
22050
  }
20367
22051
  edges.push(...anchorEdges);
22052
+ if (opts.edgeHolds && Object.keys(opts.edgeHolds).length > 0) {
22053
+ const holds = opts.edgeHolds;
22054
+ const shipsHere = (wireId) => (endpointSources[wireId] ?? [wireId]).some((localId) => shippedById.has(localId));
22055
+ const kept = [];
22056
+ for (const e of edges) {
22057
+ const hold = holds[e.id];
22058
+ if (!hold || hold === "held" && (shipsHere(e.from) || shipsHere(e.to))) {
22059
+ kept.push(e);
22060
+ continue;
22061
+ }
22062
+ if (hold === "held") {
22063
+ composition.edgesHeldParked++;
22064
+ for (const localId of endpointSources[e.from] ?? [e.from]) {
22065
+ delete anchorDigests[localId];
22066
+ delete semanticEdgeDigests[localId];
22067
+ delete projectAnchorDigests[localId];
22068
+ }
22069
+ } else {
22070
+ composition.edgesHeldDropped++;
22071
+ }
22072
+ }
22073
+ edges.length = 0;
22074
+ edges.push(...kept);
22075
+ }
20368
22076
  const bindingEndpoints = /* @__PURE__ */ new Set();
20369
22077
  for (const e of edges) {
20370
22078
  if (e.type === "CAUSED_BY" || e.type === "SOLVED_BY") {
@@ -20446,7 +22154,8 @@ function buildDrainInstances(store, sharedStore, input) {
20446
22154
  ...lexicon ? { lexicon } : {},
20447
22155
  ...fileLexicon ? { fileLexicon } : {},
20448
22156
  ...input.project ? { project: input.project } : {},
20449
- ...input.quarantinedSessions?.length ? { quarantinedSessions: input.quarantinedSessions } : {}
22157
+ ...input.quarantinedSessions?.length ? { quarantinedSessions: input.quarantinedSessions } : {},
22158
+ ...input.edgeHolds ? { edgeHolds: input.edgeHolds } : {}
20450
22159
  });
20451
22160
  return {
20452
22161
  batch,
@@ -20457,11 +22166,11 @@ function buildDrainInstances(store, sharedStore, input) {
20457
22166
 
20458
22167
  // src/consolidate-worker.ts
20459
22168
  var stores = /* @__PURE__ */ new Map();
20460
- function storeAt(path) {
20461
- let s = stores.get(path);
22169
+ function storeAt(path2) {
22170
+ let s = stores.get(path2);
20462
22171
  if (!s) {
20463
- s = openGraphStore({ path });
20464
- stores.set(path, s);
22172
+ s = openGraphStore({ path: path2 });
22173
+ stores.set(path2, s);
20465
22174
  }
20466
22175
  return s;
20467
22176
  }
@@ -20501,7 +22210,7 @@ function runPass(msg) {
20501
22210
  return revisitContradictedPrinciples(shared, msg.ts ?? Date.now());
20502
22211
  case "strandedCount": {
20503
22212
  if (!msg.castaliaPath) throw new Error("strandedCount: castaliaPath required");
20504
- return findStrandedSymbols(storeAt(msg.castaliaPath)).length;
22213
+ return findStrandedSymbols(storeAt(msg.castaliaPath));
20505
22214
  }
20506
22215
  case "drainContext": {
20507
22216
  if (!msg.castaliaPath) throw new Error("drainContext: castaliaPath required");