eminiar 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/hook.js CHANGED
@@ -26434,7 +26434,7 @@ function chunkMarkdown(text, baseTitle, opts = DEFAULT_CHUNKER) {
26434
26434
  if (/^\s*(```|~~~)/.test(line2)) inFence = !inFence;
26435
26435
  const m = !inFence && /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(line2);
26436
26436
  if (m) {
26437
- if (current.lines.length) sections.push(current);
26437
+ sections.push(current);
26438
26438
  const level = m[1].length;
26439
26439
  while (stack.length && stack[stack.length - 1].level >= level) stack.pop();
26440
26440
  stack.push({ level, text: m[2] });
@@ -26443,14 +26443,23 @@ function chunkMarkdown(text, baseTitle, opts = DEFAULT_CHUNKER) {
26443
26443
  }
26444
26444
  current.lines.push(line2);
26445
26445
  });
26446
- if (current.lines.length) sections.push(current);
26446
+ sections.push(current);
26447
26447
  const out = [];
26448
- for (const s of sections) {
26448
+ sections.forEach((s, k) => {
26449
26449
  const title = s.path.length ? `${baseTitle} > ${s.path.join(" > ")}` : baseTitle;
26450
26450
  const body = s.lines.join("\n");
26451
+ if (!body.trim()) {
26452
+ const next = sections[k + 1];
26453
+ const isParent = next !== void 0 && next.path.length > s.path.length && s.path.every((p, j) => next.path[j] === p);
26454
+ if (s.path.length && !isParent) {
26455
+ const heading = s.path[s.path.length - 1];
26456
+ out.push({ title, body: heading, startLine: s.startLine, endLine: s.startLine });
26457
+ }
26458
+ return;
26459
+ }
26451
26460
  if (body.length <= opts.maxChars) {
26452
26461
  out.push({ title, body, startLine: s.startLine, endLine: s.startLine + s.lines.length - 1 });
26453
- continue;
26462
+ return;
26454
26463
  }
26455
26464
  let fence = false;
26456
26465
  const fenceState = s.lines.map((l) => {
@@ -26466,7 +26475,7 @@ function chunkMarkdown(text, baseTitle, opts = DEFAULT_CHUNKER) {
26466
26475
  s.startLine - 1
26467
26476
  )
26468
26477
  );
26469
- }
26478
+ });
26470
26479
  return out;
26471
26480
  }
26472
26481
 
@@ -26549,7 +26558,20 @@ function chunk(content, baseTitle, kind, uri, opts = DEFAULT_CHUNKER) {
26549
26558
  // src/index/indexer.ts
26550
26559
  function indexContent(db, input2) {
26551
26560
  const kind = input2.kind ?? (input2.contentType === "code" ? "code" : input2.contentType === "json" ? "json" : void 0);
26552
- const { chunks, kind: k } = input2.chunks ? { chunks: finalize2(input2.chunks, input2.contentType ?? "code"), kind: kind ?? "code" } : chunk(input2.content, input2.title, kind, input2.uri);
26561
+ const cut = input2.chunks ? { chunks: finalize2(input2.chunks, input2.contentType ?? "code"), kind: kind ?? "code" } : chunk(input2.content, input2.title, kind, input2.uri);
26562
+ const k = cut.kind;
26563
+ const fallback = cut.chunks.length === 0 && input2.content.trim().length > 0;
26564
+ const chunks = fallback ? finalize2(
26565
+ [
26566
+ {
26567
+ title: input2.title,
26568
+ body: input2.content,
26569
+ startLine: 1,
26570
+ endLine: input2.content.split("\n").length
26571
+ }
26572
+ ],
26573
+ kindToContentType(k)
26574
+ ) : cut.chunks;
26553
26575
  const contentHash = hashText(input2.content);
26554
26576
  const existing = input2.uri ? db.findDocumentByUri(input2.uri) : void 0;
26555
26577
  if (existing && existing.content_hash === contentHash) {
@@ -26559,7 +26581,8 @@ function indexContent(db, input2) {
26559
26581
  created: false,
26560
26582
  chunks: chunks.length,
26561
26583
  chars: input2.content.length,
26562
- kind: k
26584
+ kind: k,
26585
+ fallback
26563
26586
  };
26564
26587
  }
26565
26588
  db.deleteDocument(existing.id);
@@ -26576,7 +26599,14 @@ function indexContent(db, input2) {
26576
26599
  },
26577
26600
  chunks
26578
26601
  );
26579
- return { documentId: id, created, chunks: chunks.length, chars: input2.content.length, kind: k };
26602
+ return {
26603
+ documentId: id,
26604
+ created,
26605
+ chunks: chunks.length,
26606
+ chars: input2.content.length,
26607
+ kind: k,
26608
+ fallback
26609
+ };
26580
26610
  }
26581
26611
  var MAX_FILE_BYTES = 2 * 1024 * 1024;
26582
26612
 
@@ -26597,6 +26627,23 @@ function fileOf(id) {
26597
26627
  return void 0;
26598
26628
  }
26599
26629
 
26630
+ // src/graph/metrics.ts
26631
+ init_esm_shims();
26632
+
26633
+ // src/graph/clusters.ts
26634
+ init_esm_shims();
26635
+
26636
+ // src/graph/metrics.ts
26637
+ var DEPENDENCY_KINDS = [
26638
+ "calls",
26639
+ "references",
26640
+ "extends",
26641
+ "implements",
26642
+ "imports",
26643
+ "reexports",
26644
+ "handles"
26645
+ ];
26646
+
26600
26647
  // src/history/model.ts
26601
26648
  init_esm_shims();
26602
26649
 
@@ -27298,6 +27345,9 @@ function memoriesForFile(db, rel, opts = {}) {
27298
27345
  }
27299
27346
  var RISKY = 0.3;
27300
27347
  var FAN_IN = 5;
27348
+ function dependentFiles(db, paths2) {
27349
+ return db.graphDependentFileCounts(paths2, DEPENDENCY_KINDS);
27350
+ }
27301
27351
  function riskLine(path2, f) {
27302
27352
  if (f.risk < RISKY && !f.bugOrigins) return void 0;
27303
27353
  const parts = [`${f.fixes}/${f.commits} commits were fixes`];
@@ -27325,10 +27375,10 @@ function fileContext(db, rel, opts = {}) {
27325
27375
  }
27326
27376
  const fanKey = `fanin:${rel}`;
27327
27377
  if (opts.edit && !seen.has(fanKey)) {
27328
- const fanIn = db.metricsGet([fileId(rel)])[0]?.fan_in ?? 0;
27329
- if (fanIn >= FAN_IN) {
27378
+ const dependents = dependentFiles(db, [rel]).get(rel) ?? 0;
27379
+ if (dependents >= FAN_IN) {
27330
27380
  lines.push(
27331
- `eminiar graph: ${rel} is imported by ${fanIn} files; em_graph_impact {node:"${fileId(rel)}"} lists them before you change an exported signature.`
27381
+ `eminiar graph: ${rel} is imported by ${dependents} files; em_graph_impact {node:"${fileId(rel)}"} lists them before you change an exported signature.`
27332
27382
  );
27333
27383
  keys.push(fanKey);
27334
27384
  }
@@ -27340,7 +27390,7 @@ function commitContext(db, files) {
27340
27390
  if (!files.length) return "";
27341
27391
  const table = readFileRisk(db);
27342
27392
  const risky = files.map((p) => table[p] ? riskLine(p, table[p]) : void 0).filter((l) => !!l).slice(0, 4);
27343
- const wide = db.metricsGet(files.map(fileId)).filter((m) => m.fan_in >= FAN_IN).sort((a, b) => b.fan_in - a.fan_in).slice(0, 4).map((m) => `${m.path} (${m.fan_in} importers)`);
27393
+ const wide = [...dependentFiles(db, files)].filter(([, n]) => n >= FAN_IN).sort((a, b) => b[1] - a[1]).slice(0, 4).map(([p, n]) => `${p} (${n} importers)`);
27344
27394
  if (!risky.length && !wide.length) return "";
27345
27395
  const lines = ["eminiar history: this commit includes files that deserve a second look:"];
27346
27396
  for (const r of risky) lines.push(`- ${r}`);
@@ -27624,6 +27674,10 @@ function topTermsOf(bodies, limit) {
27624
27674
 
27625
27675
  // src/storage/sqlite/schema.ts
27626
27676
  init_esm_shims();
27677
+ function hasColumn(db, table, column) {
27678
+ const cols = db.pragma(`table_info(${table})`);
27679
+ return cols.some((c) => c.name === column);
27680
+ }
27627
27681
  var migrations = [
27628
27682
  {
27629
27683
  version: 1,
@@ -27958,7 +28012,6 @@ CREATE TABLE IF NOT EXISTS transcript_files (
27958
28012
  offset INTEGER NOT NULL DEFAULT 0,
27959
28013
  size INTEGER NOT NULL DEFAULT 0,
27960
28014
  mtime INTEGER NOT NULL DEFAULT 0,
27961
- head_hash TEXT,
27962
28015
  ingested_at INTEGER NOT NULL
27963
28016
  );
27964
28017
 
@@ -27986,6 +28039,16 @@ CREATE TABLE IF NOT EXISTS statusline_snapshots (
27986
28039
  CREATE INDEX IF NOT EXISTS statusline_snapshots_session_ts ON statusline_snapshots(session_id, ts);
27987
28040
  CREATE INDEX IF NOT EXISTS statusline_snapshots_ts ON statusline_snapshots(ts);
27988
28041
  `
28042
+ },
28043
+ {
28044
+ // transcript_files.head_hash (fingerprint of the file head, so a rewrite with the same
28045
+ // size is noticed) was first added to v5 after v5 had already run on real databases, so
28046
+ // version-5 databases exist both with and without the column. Add it only when missing.
28047
+ version: 6,
28048
+ up(db) {
28049
+ if (!hasColumn(db, "transcript_files", "head_hash"))
28050
+ db.exec("ALTER TABLE transcript_files ADD COLUMN head_hash TEXT");
28051
+ }
27989
28052
  }
27990
28053
  ];
27991
28054
  function vecTableSql(dims) {
@@ -28097,7 +28160,8 @@ var SqliteAdapter = class _SqliteAdapter {
28097
28160
  if (pending.length) {
28098
28161
  this.transaction(() => {
28099
28162
  for (const m of pending) {
28100
- this.db.exec(m.sql);
28163
+ if (m.sql) this.db.exec(m.sql);
28164
+ m.up?.(this.db);
28101
28165
  this.metaSet("schema_version", String(m.version));
28102
28166
  }
28103
28167
  });
@@ -28316,18 +28380,42 @@ var SqliteAdapter = class _SqliteAdapter {
28316
28380
  }
28317
28381
  vecSearch(embedding, k, filters) {
28318
28382
  if (!this.vectors) return [];
28319
- const raw = this.db.prepare(
28320
- "SELECT chunk_id id, distance FROM chunk_vec WHERE embedding MATCH ? AND k = ? ORDER BY distance"
28321
- ).all(embedding, BigInt(filters ? k * 4 : k));
28322
- if (!filters || !raw.length) return raw.map((r) => ({ chunkId: r.id, distance: r.distance }));
28323
28383
  const { where, params } = filterSql(filters);
28324
- const allowed = new Set(
28325
- this.db.prepare(
28326
- `SELECT c.id FROM chunks c JOIN documents d ON d.id = c.document_id
28327
- WHERE c.id IN (${raw.map(() => "?").join(",")})${where}`
28328
- ).all(...raw.map((r) => r.id), ...params).map((r) => r.id)
28329
- );
28330
- return raw.filter((r) => allowed.has(r.id)).slice(0, k).map((r) => ({ chunkId: r.id, distance: r.distance }));
28384
+ if (!where) return this.knn(embedding, k);
28385
+ try {
28386
+ return this.knn(
28387
+ embedding,
28388
+ k,
28389
+ `AND chunk_id IN (SELECT c.id FROM chunks c JOIN documents d ON d.id = c.document_id WHERE 1=1${where})`,
28390
+ params
28391
+ );
28392
+ } catch {
28393
+ return this.vecSearchWidening(embedding, k, where, params);
28394
+ }
28395
+ }
28396
+ knn(embedding, k, extra = "", params = []) {
28397
+ return this.db.prepare(
28398
+ `SELECT chunk_id id, distance FROM chunk_vec WHERE embedding MATCH ? AND k = ? ${extra} ORDER BY distance`
28399
+ ).all(embedding, BigInt(k), ...params).map((r) => ({ chunkId: r.id, distance: r.distance }));
28400
+ }
28401
+ /** Fallback filtered KNN: fetch k*4, k*16, ... nearest until k pass the filter or all were seen. */
28402
+ vecSearchWidening(embedding, k, where, params) {
28403
+ const total = this.vecCount();
28404
+ for (let fetch = k * 4; ; fetch *= 4) {
28405
+ const raw = this.knn(embedding, Math.min(fetch, total));
28406
+ if (!raw.length) return [];
28407
+ const allowed = /* @__PURE__ */ new Set();
28408
+ for (let i = 0; i < raw.length; i += 400) {
28409
+ const ids = raw.slice(i, i + 400).map((r) => r.chunkId);
28410
+ for (const r of this.db.prepare(
28411
+ `SELECT c.id FROM chunks c JOIN documents d ON d.id = c.document_id
28412
+ WHERE c.id IN (${ids.map(() => "?").join(",")})${where}`
28413
+ ).all(...ids, ...params))
28414
+ allowed.add(r.id);
28415
+ }
28416
+ const hits = raw.filter((r) => allowed.has(r.chunkId)).slice(0, k);
28417
+ if (hits.length >= k || fetch >= total) return hits;
28418
+ }
28331
28419
  }
28332
28420
  unembeddedChunkIds(limit) {
28333
28421
  return this.db.prepare("SELECT id FROM chunks WHERE embedded = 0 ORDER BY id LIMIT ?").all(limit).map((r) => r.id);
@@ -28606,6 +28694,26 @@ var SqliteAdapter = class _SqliteAdapter {
28606
28694
  }
28607
28695
  return out;
28608
28696
  }
28697
+ graphDependentFileCounts(paths2, kinds) {
28698
+ const out = /* @__PURE__ */ new Map();
28699
+ if (!paths2.length || !kinds.length) return out;
28700
+ const kph = kinds.map(() => "?").join(",");
28701
+ for (let i = 0; i < paths2.length; i += 400) {
28702
+ const batch = paths2.slice(i, i + 400);
28703
+ const pph = batch.map(() => "?").join(",");
28704
+ for (const r of this.db.prepare(
28705
+ `SELECT d.path p, COUNT(DISTINCT s.path) n
28706
+ FROM graph_nodes d
28707
+ JOIN graph_edges e ON e.dst = d.id AND e.kind IN (${kph})
28708
+ JOIN graph_nodes s ON s.id = e.src
28709
+ WHERE d.path IN (${pph}) AND s.path IS NOT NULL AND s.path <> d.path
28710
+ GROUP BY d.path`
28711
+ ).all(...kinds, ...batch)) {
28712
+ out.set(r.p, r.n);
28713
+ }
28714
+ }
28715
+ return out;
28716
+ }
28609
28717
  edgesWhere(col, ids, kinds) {
28610
28718
  if (!ids.length) return [];
28611
28719
  const out = [];
@@ -30755,6 +30863,18 @@ function ingestSession(db, root, sessionId, transcriptPath, opts = {}) {
30755
30863
  return total;
30756
30864
  }
30757
30865
 
30866
+ // src/hooks/log.ts
30867
+ init_esm_shims();
30868
+ import { appendFileSync } from "fs";
30869
+ function hookLog(level, msg) {
30870
+ try {
30871
+ const p = ensureDirs();
30872
+ appendFileSync(p.hookLog, `${(/* @__PURE__ */ new Date()).toISOString()} ${level} ${msg}
30873
+ `);
30874
+ } catch {
30875
+ }
30876
+ }
30877
+
30758
30878
  // src/hooks/capture.ts
30759
30879
  var INDEXED = /* @__PURE__ */ new Set([
30760
30880
  "decision",
@@ -30797,6 +30917,7 @@ function open3(input2) {
30797
30917
  });
30798
30918
  return { db, root: ctx.root, sessionId };
30799
30919
  }
30920
+ var usageFailureLogged = false;
30800
30921
  function ingestUsage(db, input2, sessionId) {
30801
30922
  try {
30802
30923
  const ctx = getContext(input2.cwd);
@@ -30805,7 +30926,13 @@ function ingestUsage(db, input2, sessionId) {
30805
30926
  ingestSession(db, ctx.root, sessionId, input2.transcript_path, { overrides });
30806
30927
  if (input2.agent_transcript_path)
30807
30928
  ingestTranscript(db, input2.agent_transcript_path, { sessionId, overrides });
30808
- } catch {
30929
+ } catch (err) {
30930
+ if (usageFailureLogged) return;
30931
+ usageFailureLogged = true;
30932
+ hookLog(
30933
+ "error",
30934
+ `usage ingest failed (session ${sessionId}): ${err.message}; run \`eminiar usage ingest\` to see the error`
30935
+ );
30809
30936
  }
30810
30937
  }
30811
30938
  var userPromptSubmit = async (input2) => {
@@ -31072,6 +31199,7 @@ import { extname as extname2, join as join12, relative as relative4, resolve as
31072
31199
  init_esm_shims();
31073
31200
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync } from "fs";
31074
31201
  import { dirname as dirname3, join as join5 } from "path";
31202
+ var DOCS_PREFIX = "docs:";
31075
31203
  var NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
31076
31204
  function isValidName(name) {
31077
31205
  return NAME_RE.test(name);
@@ -31451,18 +31579,6 @@ function effectiveRoutingMode(config2) {
31451
31579
  return contextModeInstalled() ? "annotate" : "enforce";
31452
31580
  }
31453
31581
 
31454
- // src/hooks/log.ts
31455
- init_esm_shims();
31456
- import { appendFileSync } from "fs";
31457
- function hookLog(level, msg) {
31458
- try {
31459
- const p = ensureDirs();
31460
- appendFileSync(p.hookLog, `${(/* @__PURE__ */ new Date()).toISOString()} ${level} ${msg}
31461
- `);
31462
- } catch {
31463
- }
31464
- }
31465
-
31466
31582
  // src/hooks/routing.ts
31467
31583
  var SYMBOL_KINDS2 = [
31468
31584
  "function",
@@ -31573,6 +31689,7 @@ var BIG_TEXT_EXT = /* @__PURE__ */ new Set([
31573
31689
  ".sql",
31574
31690
  ".har"
31575
31691
  ]);
31692
+ var VISUAL_EXT = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".pdf"]);
31576
31693
  function withoutHeredocs(cmd) {
31577
31694
  return cmd.replace(/<<-?\s*(['"]?)(\w+)\1[^\n]*\n[\s\S]*?\n\s*\2\s*(?=\n|$)/g, "<<$2");
31578
31695
  }
@@ -31774,10 +31891,11 @@ function suggest(toolName, input2, root, extra = {}) {
31774
31891
  case "Read": {
31775
31892
  const fp = String(input2.file_path ?? "");
31776
31893
  if (!fp) return void 0;
31894
+ const ext = extname2(fp).toLowerCase();
31895
+ if (VISUAL_EXT.has(ext)) return void 0;
31777
31896
  try {
31778
31897
  const abs = resolve4(root, fp);
31779
31898
  const st = statSync5(abs);
31780
- const ext = extname2(fp).toLowerCase();
31781
31899
  if (st.size > 200 * 1024 || BIG_TEXT_EXT.has(ext) && st.size > 20 * 1024) {
31782
31900
  if (!checkReadPath(loadPermissionRules(root), abs).allowed) return void 0;
31783
31901
  return {
@@ -32193,10 +32311,12 @@ ${guide}`);
32193
32311
  parts.push(
32194
32312
  "No onboarding memories yet: run /eminiar:em-onboard once so future sessions start with the project's structure, build and test commands."
32195
32313
  );
32196
- else if (!readManifest(ctx).sources.length)
32314
+ else if (!readManifest(ctx).sources.length) {
32315
+ const orphaned = db.documentSourceStats(DOCS_PREFIX).length;
32197
32316
  parts.push(
32198
- `No documentation sources registered yet: em_docs {action:"discover"} maps this project's libraries, services and APIs to their documentation, and {action:"add", pick:"recommended"} indexes it for em_docs lookup.`
32317
+ orphaned ? `No documentation sources registered (${orphaned} orphaned docs:* source(s) still in the index; em_docs {action:"list"} shows them, {action:"remove"} drops them, {action:"add", pick:"recommended"} re-registers).` : `No documentation sources registered yet: em_docs {action:"discover"} maps this project's libraries, services and APIs to their documentation, and {action:"add", pick:"recommended"} indexes it for em_docs lookup.`
32199
32318
  );
32319
+ }
32200
32320
  } catch {
32201
32321
  }
32202
32322
  try {