sensemaking 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +13 -6
  2. package/dist/cjs/cli.js +8 -1
  3. package/dist/cjs/cli.js.map +1 -1
  4. package/dist/cjs/commands/check.d.cts +3 -0
  5. package/dist/cjs/commands/check.d.ts +3 -0
  6. package/dist/cjs/commands/check.js +130 -0
  7. package/dist/cjs/commands/check.js.map +1 -0
  8. package/dist/cjs/commands/find.js +173 -10
  9. package/dist/cjs/commands/find.js.map +1 -1
  10. package/dist/cjs/commands/index.js +5 -0
  11. package/dist/cjs/commands/index.js.map +1 -1
  12. package/dist/cjs/commands/map.js +1 -1
  13. package/dist/cjs/commands/map.js.map +1 -1
  14. package/dist/cjs/commands/peek.js +1 -1
  15. package/dist/cjs/commands/peek.js.map +1 -1
  16. package/dist/cjs/commands/shared.d.cts +1 -1
  17. package/dist/cjs/commands/shared.d.ts +1 -1
  18. package/dist/cjs/commands/shared.js +166 -8
  19. package/dist/cjs/commands/shared.js.map +1 -1
  20. package/dist/cjs/commands/status.js +72 -5
  21. package/dist/cjs/commands/status.js.map +1 -1
  22. package/dist/cjs/commands/types.d.cts +1 -0
  23. package/dist/cjs/commands/types.d.ts +1 -0
  24. package/dist/cjs/config.d.cts +32 -2
  25. package/dist/cjs/config.d.ts +32 -2
  26. package/dist/cjs/config.js +199 -11
  27. package/dist/cjs/config.js.map +1 -1
  28. package/dist/cjs/db.d.cts +1 -1
  29. package/dist/cjs/db.d.ts +1 -1
  30. package/dist/cjs/db.js +47 -17
  31. package/dist/cjs/db.js.map +1 -1
  32. package/dist/cjs/errors.d.cts +1 -1
  33. package/dist/cjs/errors.d.ts +1 -1
  34. package/dist/cjs/errors.js.map +1 -1
  35. package/dist/cjs/features/embed.d.cts +15 -0
  36. package/dist/cjs/features/embed.d.ts +15 -0
  37. package/dist/cjs/features/embed.js +844 -0
  38. package/dist/cjs/features/embed.js.map +1 -0
  39. package/dist/cjs/features/index.js +3 -1
  40. package/dist/cjs/features/index.js.map +1 -1
  41. package/dist/cjs/features/types.d.cts +4 -1
  42. package/dist/cjs/features/types.d.ts +4 -1
  43. package/dist/cjs/index.d.cts +1 -1
  44. package/dist/cjs/index.d.ts +1 -1
  45. package/dist/cjs/index.js.map +1 -1
  46. package/dist/cjs/output.d.cts +5 -0
  47. package/dist/cjs/output.d.ts +5 -0
  48. package/dist/cjs/output.js +48 -9
  49. package/dist/cjs/output.js.map +1 -1
  50. package/dist/cjs/scan.d.cts +1 -1
  51. package/dist/cjs/scan.d.ts +1 -1
  52. package/dist/cjs/scan.js +17 -10
  53. package/dist/cjs/scan.js.map +1 -1
  54. package/dist/cjs/verbs.d.cts +8 -2
  55. package/dist/cjs/verbs.d.ts +8 -2
  56. package/dist/cjs/verbs.js +308 -81
  57. package/dist/cjs/verbs.js.map +1 -1
  58. package/dist/esm/cli.js +8 -1
  59. package/dist/esm/cli.js.map +1 -1
  60. package/dist/esm/commands/check.d.ts +3 -0
  61. package/dist/esm/commands/check.js +56 -0
  62. package/dist/esm/commands/check.js.map +1 -0
  63. package/dist/esm/commands/find.js +5 -4
  64. package/dist/esm/commands/find.js.map +1 -1
  65. package/dist/esm/commands/index.js +1 -0
  66. package/dist/esm/commands/index.js.map +1 -1
  67. package/dist/esm/commands/map.js +1 -3
  68. package/dist/esm/commands/map.js.map +1 -1
  69. package/dist/esm/commands/peek.js +1 -1
  70. package/dist/esm/commands/peek.js.map +1 -1
  71. package/dist/esm/commands/shared.d.ts +1 -1
  72. package/dist/esm/commands/shared.js +2 -2
  73. package/dist/esm/commands/shared.js.map +1 -1
  74. package/dist/esm/commands/status.js +43 -5
  75. package/dist/esm/commands/status.js.map +1 -1
  76. package/dist/esm/commands/types.d.ts +1 -0
  77. package/dist/esm/commands/types.js.map +1 -1
  78. package/dist/esm/config.d.ts +32 -2
  79. package/dist/esm/config.js +98 -9
  80. package/dist/esm/config.js.map +1 -1
  81. package/dist/esm/db.d.ts +1 -1
  82. package/dist/esm/db.js +20 -11
  83. package/dist/esm/db.js.map +1 -1
  84. package/dist/esm/errors.d.ts +1 -1
  85. package/dist/esm/errors.js.map +1 -1
  86. package/dist/esm/features/embed.d.ts +15 -0
  87. package/dist/esm/features/embed.js +299 -0
  88. package/dist/esm/features/embed.js.map +1 -0
  89. package/dist/esm/features/index.js +3 -1
  90. package/dist/esm/features/index.js.map +1 -1
  91. package/dist/esm/features/types.d.ts +4 -1
  92. package/dist/esm/features/types.js.map +1 -1
  93. package/dist/esm/index.d.ts +1 -1
  94. package/dist/esm/index.js.map +1 -1
  95. package/dist/esm/output.d.ts +5 -0
  96. package/dist/esm/output.js +40 -8
  97. package/dist/esm/output.js.map +1 -1
  98. package/dist/esm/scan.d.ts +1 -1
  99. package/dist/esm/scan.js +17 -10
  100. package/dist/esm/scan.js.map +1 -1
  101. package/dist/esm/verbs.d.ts +8 -2
  102. package/dist/esm/verbs.js +92 -18
  103. package/dist/esm/verbs.js.map +1 -1
  104. package/package.json +2 -1
  105. package/schema.json +35 -1
  106. package/skills/sense/EXAMPLES.md +16 -0
  107. package/skills/sense/SKILL.md +60 -20
  108. package/skills/sense-setup/SKILL.md +91 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/embed.ts"],"sourcesContent":["import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport type { DatabaseSync } from 'node:sqlite';\nimport type { Config, ResolvedConfig } from '../config.ts';\nimport { embedConfig } from '../config.ts';\nimport { SenseError } from '../errors.ts';\nimport { parseFile } from '../scan.ts';\nimport type { Feature } from './types.ts';\n\n// embeddings(path, chunk, start_line, end_line, scale, vector): heading-based chunks,\n// int8 vectors with a per-vector dequantization scale, vector NULL = not yet embedded.\n// Reconcile stores dirty rows inside its transaction; embedding tops up on the next\n// semantic query (embedPending), so staleness costs recall, never correctness.\n\n// Storage lever fixed by the bake-off (BENCHMARKING.md): int8 at 256 dims is\n// quality-free vs f32-512 when fused. Queries stay f32 at the same dims.\nconst STORE_DIMS = 256;\nconst BATCH = 64;\n\nexport interface EmbedProvider {\n id: string; // model identity; participates in the cache key, change -> re-embed\n dims: number;\n embed(texts: string[]): Promise<Float32Array[]>;\n}\n\ninterface Chunk {\n startLine: number;\n endLine: number;\n text: string;\n}\n\n// Deterministic chunker used at reconcile (line ranges stored) and at embed time (text\n// re-derived from the file -- chunk text is never stored). Heading-delimited with the\n// preamble kept; whole file when no headings. The title/summary prefix mirrors the\n// bm25 column weighting.\nfunction chunksOf(raw: string, search?: { title: string; summary: string }): Chunk[] {\n const lines = raw.split('\\n');\n const starts: number[] = [];\n let inFence = false;\n for (let i = 0; i < lines.length; i++) {\n if (/^(```|~~~)/.test(lines[i])) {\n inFence = !inFence;\n continue;\n }\n if (!inFence && /^#{1,6} +/.test(lines[i])) starts.push(i + 1);\n }\n const bounds = starts.length === 0 ? [1] : starts[0] > 1 ? [1, ...starts] : starts;\n const prefix = [search?.title, search?.summary].filter(Boolean).join('\\n');\n const chunks: Chunk[] = [];\n bounds.forEach((start, i) => {\n const end = i + 1 < bounds.length ? bounds[i + 1] - 1 : lines.length;\n const text = lines\n .slice(start - 1, end)\n .join('\\n')\n .trim();\n if (text.length > 0) chunks.push({ startLine: start, endLine: end, text: prefix ? `${prefix}\\n${text}` : text });\n });\n return chunks;\n}\n\nexport const embed: Feature = {\n name: 'embed',\n schema(db) {\n db.exec(`CREATE TABLE IF NOT EXISTS embeddings (\"path\" TEXT, chunk INTEGER, start_line INTEGER, end_line INTEGER, scale REAL, vector BLOB, PRIMARY KEY (\"path\", chunk))`);\n },\n extract(raw, _body, search) {\n return chunksOf(raw, search);\n },\n remove(db, path) {\n db.prepare('DELETE FROM embeddings WHERE \"path\" = ?').run(path);\n },\n store(db, path, extracted) {\n const insert = db.prepare('INSERT INTO embeddings (\"path\", chunk, start_line, end_line, scale, vector) VALUES (?, ?, ?, ?, NULL, NULL)');\n (extracted as Chunk[]).forEach((c, idx) => insert.run(path, idx, c.startLine, c.endLine));\n },\n};\n\n// --- static type: Model2Vec safetensors + pure-JS tokenizer, model cached in ~/.cache ---\n// Encode convention from model2vec/model.py: no special tokens, drop unk ids, mean-pool,\n// L2-normalize.\n\nfunction fetchToFile(url: string, dest: string): Promise<void> {\n return fetch(url).then(async (res) => {\n if (!res.ok) throw new SenseError('EMBED_MODEL', `model download failed: ${url} -> HTTP ${res.status}`);\n writeFileSync(`${dest}.part`, Buffer.from(await res.arrayBuffer()));\n renameSync(`${dest}.part`, dest);\n });\n}\n\nasync function staticProvider(model: string): Promise<EmbedProvider> {\n let dir = model;\n if (!existsSync(join(dir, 'model.safetensors'))) {\n dir = join(homedir(), '.cache', 'sensemaking', 'models', model.replace(/\\//g, '--'));\n mkdirSync(dir, { recursive: true });\n for (const file of ['model.safetensors', 'tokenizer.json']) {\n if (!existsSync(join(dir, file))) {\n console.error(`fetching ${model}/${file} into ${dir} (once; delete to refetch)`);\n await fetchToFile(`https://huggingface.co/${model}/resolve/main/${file}`, join(dir, file));\n }\n }\n }\n\n const raw = readFileSync(join(dir, 'model.safetensors'));\n const headerLen = Number(raw.readBigUInt64LE(0));\n const header = JSON.parse(raw.subarray(8, 8 + headerLen).toString('utf8')) as Record<string, { dtype: string; shape: number[]; data_offsets: number[] }>;\n const entry = Object.entries(header).find(([k]) => k !== '__metadata__');\n if (!entry || entry[1].dtype !== 'F32') throw new SenseError('EMBED_MODEL', `${model}: expected an F32 safetensors matrix`);\n const spec = entry[1];\n const dims = spec.shape[1];\n const dataStart = raw.byteOffset + 8 + headerLen + spec.data_offsets[0];\n const matrix = dataStart % 4 === 0 ? new Float32Array(raw.buffer, dataStart, spec.shape[0] * dims) : new Float32Array(raw.buffer.slice(dataStart, dataStart + spec.shape[0] * dims * 4));\n\n const tokenizerJson = JSON.parse(readFileSync(join(dir, 'tokenizer.json'), 'utf8'));\n // Lazy import: the tokenizer loads only on the semantic path, never at CLI startup.\n const { Tokenizer } = await import('@huggingface/tokenizers');\n const tok = new Tokenizer(tokenizerJson, {});\n const unkId = tokenizerJson.model?.vocab?.[tokenizerJson.model?.unk_token] ?? -1;\n\n function one(text: string): Float32Array {\n // The tokenizer yields undefined (not the unk id) for tokens outside the vocab; an\n // undefined id would index the matrix at NaN and poison the whole mean-pool -- and the\n // int8 conversion then stores the NaN vector as all zeros, silently. Keep integers only.\n const ids = (tok.encode(text, { add_special_tokens: false }).ids as number[]).filter((id) => Number.isInteger(id) && id !== unkId);\n const v = new Float32Array(dims);\n if (ids.length === 0) return v;\n for (const id of ids) {\n const off = id * dims;\n for (let d = 0; d < dims; d++) v[d] += matrix[off + d];\n }\n let norm = 0;\n for (let d = 0; d < dims; d++) {\n v[d] /= ids.length;\n norm += v[d] * v[d];\n }\n norm = Math.sqrt(norm) + 1e-32;\n for (let d = 0; d < dims; d++) v[d] /= norm;\n return v;\n }\n\n return { id: `static:${model}`, dims, embed: async (texts) => texts.map(one) };\n}\n\n// --- api type: one POST against any OpenAI-compatible /embeddings endpoint ---\n\nasync function apiProvider(model: string, url: string | undefined, keyEnv: string | undefined): Promise<EmbedProvider> {\n if (!url) throw new SenseError('EMBED_MODEL', 'features.embed.type \"api\" requires a url');\n const base = url.replace(/\\/+$/, '');\n const headers: Record<string, string> = { 'content-type': 'application/json' };\n const key = keyEnv ? process.env[keyEnv] : undefined;\n if (key) headers.authorization = `Bearer ${key}`;\n\n async function post(texts: string[]): Promise<Float32Array[]> {\n const res = await fetch(`${base}/embeddings`, { method: 'POST', headers, body: JSON.stringify({ model, input: texts }) });\n if (!res.ok) throw new SenseError('EMBED_MODEL', `${base}/embeddings -> HTTP ${res.status}`);\n const body = (await res.json()) as { data: Array<{ embedding: number[] }> };\n return body.data.map((d) => Float32Array.from(d.embedding));\n }\n\n const dims = (await post(['dimension probe']))[0].length;\n return { id: `api:${base}:${model}`, dims, embed: post };\n}\n\nconst providers = new Map<string, Promise<EmbedProvider>>();\n\nfunction getProvider(cfg: Config): Promise<EmbedProvider> {\n const e = embedConfig(cfg);\n if (!e) throw new SenseError('EMBED_DISABLED', 'semantic expansion needs features.embed in sense.config.json (e.g. \"features\": { \"embed\": true })');\n const sig = `${e.type}:${e.model}:${e.url ?? ''}`;\n let p = providers.get(sig);\n if (!p) {\n p = e.type === 'api' ? apiProvider(e.model, e.url, e.key) : staticProvider(e.model);\n providers.set(sig, p);\n }\n return p;\n}\n\n// Slice + re-normalize (Matryoshka); optionally round through int8 storage.\nfunction toStore(full: Float32Array, dims: number, int8: boolean): { v: Float32Array; scale: number } {\n const v = new Float32Array(dims);\n let norm = 0;\n for (let d = 0; d < dims; d++) norm += full[d] * full[d];\n norm = Math.sqrt(norm) + 1e-32;\n for (let d = 0; d < dims; d++) v[d] = full[d] / norm;\n if (!int8) return { v, scale: 1 };\n let max = 0;\n for (let d = 0; d < dims; d++) max = Math.max(max, Math.abs(v[d]));\n return { v, scale: max / 127 || 1 };\n}\n\n// Embed rows whose vector is NULL, re-deriving chunk text from the files through the\n// same parse + chunker that stored the rows.\nexport async function embedPending(db: DatabaseSync, cfg: Config, baseDir: string): Promise<void> {\n const provider = await getProvider(cfg); // throws EMBED_DISABLED before touching the table\n const dirty = db.prepare('SELECT \"path\", chunk FROM embeddings WHERE vector IS NULL ORDER BY \"path\", chunk').all() as Array<{ path: string; chunk: number }>;\n if (dirty.length === 0) return;\n const storeDims = Math.min(STORE_DIMS, provider.dims);\n\n const byPath = new Map<string, number[]>();\n for (const row of dirty) {\n const list = byPath.get(row.path) ?? [];\n list.push(row.chunk);\n byPath.set(row.path, list);\n }\n\n const jobs: Array<{ path: string; chunk: number; text: string }> = [];\n for (const [path, chunkIdxs] of byPath) {\n let chunks: Chunk[];\n try {\n chunks = parseFile({ relPath: path, absPath: join(baseDir, path), mtimeMs: 0, size: 0 }, [embed]).doc.extracted.embed as Chunk[];\n } catch {\n continue; // vanished since reconcile; the next reconcile removes its rows\n }\n for (const idx of chunkIdxs) if (chunks[idx]) jobs.push({ path, chunk: idx, text: chunks[idx].text });\n }\n\n const update = db.prepare('UPDATE embeddings SET scale = ?, vector = ? WHERE \"path\" = ? AND chunk = ?');\n for (let i = 0; i < jobs.length; i += BATCH) {\n const batch = jobs.slice(i, i + BATCH);\n const vectors = await provider.embed(batch.map((j) => j.text));\n db.exec('BEGIN');\n try {\n batch.forEach((job, j) => {\n const { v, scale } = toStore(vectors[j], storeDims, true);\n const q = new Int8Array(storeDims);\n for (let d = 0; d < storeDims; d++) q[d] = Math.round(v[d] / scale);\n update.run(scale, Buffer.from(q.buffer), job.path, job.chunk);\n });\n db.exec('COMMIT');\n } catch (err) {\n db.exec('ROLLBACK');\n throw err;\n }\n }\n}\n\n// Top candidates by cosine for a semantic find: best chunk per file, its line range\n// riding along. FTS5 operators in the terms are lexical syntax, not meaning -- stripped\n// before embedding.\n// Returns the cosine similarity alongside each candidate: stored and query vectors are both\n// L2-normalised before quantisation, so the dot product is cosine in [-1, 1]. `find` surfaces\n// it because the fused rank score cannot express match quality -- nearest-neighbour search\n// always returns a nearest neighbour, so without a magnitude an agent cannot tell a real hit\n// from the best of a bad lot.\nexport async function semanticCandidates(db: DatabaseSync, cfg: Config, terms: string, fetch: number): Promise<Array<{ path: string; lines: string; similarity: number }>> {\n const baseDir = (cfg as Partial<ResolvedConfig>).baseDir;\n if (!baseDir) throw new SenseError('EMBED_MODEL', 'semantic expansion needs a config with baseDir (use loadConfig/open)');\n await embedPending(db, cfg, baseDir);\n\n const provider = await getProvider(cfg);\n const storeDims = Math.min(STORE_DIMS, provider.dims);\n const text = (terms.match(/[\\p{L}\\p{N}]+/gu) ?? []).filter((t) => !['AND', 'OR', 'NOT', 'NEAR'].includes(t)).join(' ');\n const { v: qv } = toStore((await provider.embed([text]))[0], storeDims, false);\n\n const rows = db.prepare('SELECT \"path\", start_line, end_line, scale, vector FROM embeddings WHERE vector IS NOT NULL').all() as Array<{\n path: string;\n start_line: number;\n end_line: number;\n scale: number;\n vector: Uint8Array;\n }>;\n\n const best = new Map<string, { score: number; lines: string }>();\n for (const row of rows) {\n const q = new Int8Array(row.vector.buffer, row.vector.byteOffset, Math.min(storeDims, row.vector.byteLength));\n let dot = 0;\n for (let d = 0; d < q.length; d++) dot += q[d] * qv[d];\n const score = dot * row.scale;\n const existing = best.get(row.path);\n if (!existing || score > existing.score) best.set(row.path, { score, lines: `L${row.start_line}-${row.end_line}` });\n }\n return [...best.entries()]\n .sort((a, b) => b[1].score - a[1].score)\n .slice(0, fetch)\n .map(([path, b]) => ({ path, lines: b.lines, similarity: Math.round(b.score * 1000) / 1000 }));\n}\n"],"names":["existsSync","mkdirSync","readFileSync","renameSync","writeFileSync","homedir","join","embedConfig","SenseError","parseFile","STORE_DIMS","BATCH","chunksOf","raw","search","lines","split","starts","inFence","i","length","test","push","bounds","prefix","title","summary","filter","Boolean","chunks","forEach","start","end","text","slice","trim","startLine","endLine","embed","name","schema","db","exec","extract","_body","remove","path","prepare","run","store","extracted","insert","c","idx","fetchToFile","url","dest","fetch","then","res","ok","status","Buffer","from","arrayBuffer","staticProvider","model","tokenizerJson","dir","replace","recursive","file","console","error","headerLen","Number","readBigUInt64LE","header","JSON","parse","subarray","toString","entry","Object","entries","find","k","dtype","spec","dims","shape","dataStart","byteOffset","data_offsets","matrix","Float32Array","buffer","Tokenizer","tok","unkId","vocab","unk_token","one","ids","encode","add_special_tokens","id","isInteger","v","off","d","norm","Math","sqrt","texts","map","apiProvider","keyEnv","base","headers","key","process","env","undefined","authorization","post","method","body","stringify","input","json","data","embedding","providers","Map","getProvider","cfg","e","sig","type","p","get","set","toStore","full","int8","scale","max","abs","embedPending","baseDir","provider","dirty","all","storeDims","min","byPath","row","list","chunk","jobs","chunkIdxs","relPath","absPath","mtimeMs","size","doc","update","batch","vectors","j","job","q","Int8Array","round","err","semanticCandidates","terms","match","t","includes","qv","rows","best","vector","byteLength","dot","score","existing","start_line","end_line","sort","a","b","similarity"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { featureEnabled } from '../config.js';
2
+ import { embed } from './embed.js';
2
3
  import { links } from './links.js';
3
4
  import { rank } from './rank.js';
4
5
  import { sections } from './sections.js';
@@ -6,7 +7,8 @@ import { sections } from './sections.js';
6
7
  export const FEATURES = [
7
8
  links,
8
9
  sections,
9
- rank
10
+ rank,
11
+ embed
10
12
  ];
11
13
  export function activeFeatures(cfg) {
12
14
  return FEATURES.filter((feature)=>featureEnabled(cfg, feature.name));
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/index.ts"],"sourcesContent":["import type { Config } from '../config.ts';\nimport { featureEnabled } from '../config.ts';\nimport { links } from './links.ts';\nimport { rank } from './rank.ts';\nimport { sections } from './sections.ts';\nimport type { Feature } from './types.ts';\n\n// Registry order matters: rank reads the links table in afterReconcile.\nexport const FEATURES: Feature[] = [links, sections, rank];\n\nexport function activeFeatures(cfg: Config): Feature[] {\n return FEATURES.filter((feature) => featureEnabled(cfg, feature.name));\n}\n\nexport { linkEdges } from './links.ts';\nexport type { Section } from './sections.ts';\nexport type { Feature } from './types.ts';\n"],"names":["featureEnabled","links","rank","sections","FEATURES","activeFeatures","cfg","filter","feature","name","linkEdges"],"mappings":"AACA,SAASA,cAAc,QAAQ,eAAe;AAC9C,SAASC,KAAK,QAAQ,aAAa;AACnC,SAASC,IAAI,QAAQ,YAAY;AACjC,SAASC,QAAQ,QAAQ,gBAAgB;AAGzC,wEAAwE;AACxE,OAAO,MAAMC,WAAsB;IAACH;IAAOE;IAAUD;CAAK,CAAC;AAE3D,OAAO,SAASG,eAAeC,GAAW;IACxC,OAAOF,SAASG,MAAM,CAAC,CAACC,UAAYR,eAAeM,KAAKE,QAAQC,IAAI;AACtE;AAEA,SAASC,SAAS,QAAQ,aAAa"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/index.ts"],"sourcesContent":["import type { Config } from '../config.ts';\nimport { featureEnabled } from '../config.ts';\nimport { embed } from './embed.ts';\nimport { links } from './links.ts';\nimport { rank } from './rank.ts';\nimport { sections } from './sections.ts';\nimport type { Feature } from './types.ts';\n\n// Registry order matters: rank reads the links table in afterReconcile.\nexport const FEATURES: Feature[] = [links, sections, rank, embed];\n\nexport function activeFeatures(cfg: Config): Feature[] {\n return FEATURES.filter((feature) => featureEnabled(cfg, feature.name));\n}\n\nexport { linkEdges } from './links.ts';\nexport type { Section } from './sections.ts';\nexport type { Feature } from './types.ts';\n"],"names":["featureEnabled","embed","links","rank","sections","FEATURES","activeFeatures","cfg","filter","feature","name","linkEdges"],"mappings":"AACA,SAASA,cAAc,QAAQ,eAAe;AAC9C,SAASC,KAAK,QAAQ,aAAa;AACnC,SAASC,KAAK,QAAQ,aAAa;AACnC,SAASC,IAAI,QAAQ,YAAY;AACjC,SAASC,QAAQ,QAAQ,gBAAgB;AAGzC,wEAAwE;AACxE,OAAO,MAAMC,WAAsB;IAACH;IAAOE;IAAUD;IAAMF;CAAM,CAAC;AAElE,OAAO,SAASK,eAAeC,GAAW;IACxC,OAAOF,SAASG,MAAM,CAAC,CAACC,UAAYT,eAAeO,KAAKE,QAAQC,IAAI;AACtE;AAEA,SAASC,SAAS,QAAQ,aAAa"}
@@ -4,7 +4,10 @@ import type { FileStat } from '../scan.js';
4
4
  export interface Feature {
5
5
  name: FeatureName;
6
6
  schema(db: DatabaseSync): void;
7
- extract?(raw: string, body: string): unknown;
7
+ extract?(raw: string, body: string, search?: {
8
+ title: string;
9
+ summary: string;
10
+ }): unknown;
8
11
  remove?(db: DatabaseSync, path: string): void;
9
12
  store?(db: DatabaseSync, path: string, extracted: unknown): void;
10
13
  afterReconcile?(db: DatabaseSync, files: FileStat[]): void;
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/types.ts"],"sourcesContent":["import type { DatabaseSync } from 'node:sqlite';\nimport type { FeatureName } from '../config.ts';\nimport type { FileStat } from '../scan.ts';\n\n// A feature owns its schema, per-file extraction/storage, and any whole-tree pass.\n// Adding a feature = one module here + one entry in the registry (index.ts).\nexport interface Feature {\n name: FeatureName;\n schema(db: DatabaseSync): void;\n // Pure, per file: raw is the full file, body the prose after frontmatter.\n extract?(raw: string, body: string): unknown;\n remove?(db: DatabaseSync, path: string): void;\n store?(db: DatabaseSync, path: string, extracted: unknown): void;\n // After all rows are current, inside the reconcile transaction (resolution, rank).\n afterReconcile?(db: DatabaseSync, files: FileStat[]): void;\n}\n"],"names":[],"mappings":"AAIA,mFAAmF;AACnF,6EAA6E;AAC7E,WASC"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/types.ts"],"sourcesContent":["import type { DatabaseSync } from 'node:sqlite';\nimport type { FeatureName } from '../config.ts';\nimport type { FileStat } from '../scan.ts';\n\n// A feature owns its schema, per-file extraction/storage, and any whole-tree pass.\n// Adding a feature = one module here + one entry in the registry (index.ts).\nexport interface Feature {\n name: FeatureName;\n schema(db: DatabaseSync): void;\n // Pure, per file: raw is the full file, body the prose after frontmatter,\n // search the normalized title/summary strings.\n extract?(raw: string, body: string, search?: { title: string; summary: string }): unknown;\n remove?(db: DatabaseSync, path: string): void;\n store?(db: DatabaseSync, path: string, extracted: unknown): void;\n // After all rows are current, inside the reconcile transaction (resolution, rank).\n afterReconcile?(db: DatabaseSync, files: FileStat[]): void;\n}\n"],"names":[],"mappings":"AAIA,mFAAmF;AACnF,6EAA6E;AAC7E,WAUC"}
@@ -1,4 +1,4 @@
1
- export type { Config, FeatureName, ResolvedConfig } from './config.js';
1
+ export type { Config, EmbedConfig, FeatureName, ResolvedConfig } from './config.js';
2
2
  export { CONFIG_FILENAME, initConfig, loadConfig, migrateConfig, STATE_DIR, SUPPORTED_CONFIG_VERSION } from './config.js';
3
3
  export type { OpenResult } from './db.js';
4
4
  export { open, rebuild } from './db.js';
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/index.ts"],"sourcesContent":["// Public library API. Deliberately small: every export is a stability promise;\n// internals (feature registry, graph, scan, meta) stay module-private.\n\nexport type { Config, FeatureName, ResolvedConfig } from './config.ts';\nexport { CONFIG_FILENAME, initConfig, loadConfig, migrateConfig, STATE_DIR, SUPPORTED_CONFIG_VERSION } from './config.ts';\n\nexport type { OpenResult } from './db.ts';\nexport { open, rebuild } from './db.ts';\n\nexport type { SenseErrorCode } from './errors.ts';\nexport { SenseError } from './errors.ts';\n\nexport type { Row } from './output.ts';\nexport { printRows } from './output.ts';\n\nexport type { FindOptions, Peek, TreeMap } from './verbs.ts';\nexport { find, mapTree, peek } from './verbs.ts';\n\nexport type { WatchEvent, WatchOptions } from './watch.ts';\nexport { runWatch } from './watch.ts';\n"],"names":["CONFIG_FILENAME","initConfig","loadConfig","migrateConfig","STATE_DIR","SUPPORTED_CONFIG_VERSION","open","rebuild","SenseError","printRows","find","mapTree","peek","runWatch"],"mappings":"AAAA,+EAA+E;AAC/E,uEAAuE;AAGvE,SAASA,eAAe,EAAEC,UAAU,EAAEC,UAAU,EAAEC,aAAa,EAAEC,SAAS,EAAEC,wBAAwB,QAAQ,cAAc;AAG1H,SAASC,IAAI,EAAEC,OAAO,QAAQ,UAAU;AAGxC,SAASC,UAAU,QAAQ,cAAc;AAGzC,SAASC,SAAS,QAAQ,cAAc;AAGxC,SAASC,IAAI,EAAEC,OAAO,EAAEC,IAAI,QAAQ,aAAa;AAGjD,SAASC,QAAQ,QAAQ,aAAa"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/index.ts"],"sourcesContent":["// Public library API. Deliberately small: every export is a stability promise;\n// internals (feature registry, graph, scan, meta) stay module-private.\n\nexport type { Config, EmbedConfig, FeatureName, ResolvedConfig } from './config.ts';\nexport { CONFIG_FILENAME, initConfig, loadConfig, migrateConfig, STATE_DIR, SUPPORTED_CONFIG_VERSION } from './config.ts';\n\nexport type { OpenResult } from './db.ts';\nexport { open, rebuild } from './db.ts';\n\nexport type { SenseErrorCode } from './errors.ts';\nexport { SenseError } from './errors.ts';\n\nexport type { Row } from './output.ts';\nexport { printRows } from './output.ts';\n\nexport type { FindOptions, Peek, TreeMap } from './verbs.ts';\nexport { find, mapTree, peek } from './verbs.ts';\n\nexport type { WatchEvent, WatchOptions } from './watch.ts';\nexport { runWatch } from './watch.ts';\n"],"names":["CONFIG_FILENAME","initConfig","loadConfig","migrateConfig","STATE_DIR","SUPPORTED_CONFIG_VERSION","open","rebuild","SenseError","printRows","find","mapTree","peek","runWatch"],"mappings":"AAAA,+EAA+E;AAC/E,uEAAuE;AAGvE,SAASA,eAAe,EAAEC,UAAU,EAAEC,UAAU,EAAEC,aAAa,EAAEC,SAAS,EAAEC,wBAAwB,QAAQ,cAAc;AAG1H,SAASC,IAAI,EAAEC,OAAO,QAAQ,UAAU;AAGxC,SAASC,UAAU,QAAQ,cAAc;AAGzC,SAASC,SAAS,QAAQ,cAAc;AAGxC,SAASC,IAAI,EAAEC,OAAO,EAAEC,IAAI,QAAQ,aAAa;AAGjD,SAASC,QAAQ,QAAQ,aAAa"}
@@ -7,6 +7,10 @@ export declare function renderMap(result: {
7
7
  };
8
8
  fields: Row[];
9
9
  fieldsTotal: number;
10
+ features: {
11
+ on: string[];
12
+ off: string[];
13
+ };
10
14
  hubs: Row[];
11
15
  recent: Row[];
12
16
  }): string;
@@ -21,4 +25,5 @@ export declare function renderPeek(result: {
21
25
  outboundTotal: number;
22
26
  backlinksTotal: number;
23
27
  unresolvedTotal: number;
28
+ off: string[];
24
29
  }): string;
@@ -6,19 +6,41 @@ function cell(value) {
6
6
  return String(value !== null && value !== void 0 ? value : '').replace(/\s*\n\s*/g, ' ');
7
7
  }
8
8
  export function printRows(rows, format) {
9
- const rendered = render(rows, format);
9
+ const rendered = renderRows(rows, format);
10
10
  console.log(rendered);
11
11
  if (rendered.length > OVERSIZE_BYTES) {
12
12
  console.warn(`warning: result is ${Math.round(rendered.length / 1000)} KB across ${rows.length} row(s); consider a LIMIT, fewer columns, or snippet() instead of whole values`);
13
13
  }
14
14
  }
15
- function render(rows, format) {
15
+ // Table output is for reading; a row wider than the terminal wraps into an unreadable
16
+ // block (a `find` header measured 330 chars once `summary` is present, which drove one
17
+ // user to pipe --format json through a script for every interactive query). Widest
18
+ // columns give up space first, down to a floor, and only as far as the terminal needs.
19
+ // --format json is never truncated: it is the machine-readable surface.
20
+ const MIN_COLUMN = 12;
21
+ const TRUNCATED = '…';
22
+ function fitWidths(widths, limit) {
23
+ const gaps = (widths.length - 1) * 2;
24
+ const fitted = [
25
+ ...widths
26
+ ];
27
+ for(;;){
28
+ const total = fitted.reduce((a, b)=>a + b, 0) + gaps;
29
+ if (total <= limit) return fitted;
30
+ const widest = fitted.indexOf(Math.max(...fitted));
31
+ if (fitted[widest] <= MIN_COLUMN) return fitted;
32
+ fitted[widest] = Math.max(MIN_COLUMN, fitted[widest] - Math.max(1, total - limit));
33
+ }
34
+ }
35
+ function renderRows(rows, format, width = process.stdout.columns) {
16
36
  if (format === 'json') return JSON.stringify(rows, null, 2);
17
37
  if (rows.length === 0) return '(0 rows)';
18
38
  const columns = Object.keys(rows[0]);
19
39
  const cells = rows.map((row)=>columns.map((col)=>cell(row[col])));
20
- const widths = columns.map((col, i)=>Math.max(col.length, ...cells.map((row)=>row[i].length)));
21
- const formatRow = (values)=>values.map((value, i)=>value.padEnd(widths[i])).join(' ');
40
+ const natural = columns.map((col, i)=>Math.max(col.length, ...cells.map((row)=>row[i].length)));
41
+ const widths = width && width > MIN_COLUMN ? fitWidths(natural, width) : natural;
42
+ const clip = (value, w)=>value.length <= w ? value.padEnd(w) : `${value.slice(0, w - 1)}${TRUNCATED}`;
43
+ const formatRow = (values)=>values.map((value, i)=>clip(value, widths[i])).join(' ').trimEnd();
22
44
  return [
23
45
  formatRow(columns),
24
46
  widths.map((w)=>'-'.repeat(w)).join(' '),
@@ -26,14 +48,20 @@ function render(rows, format) {
26
48
  ].join('\n');
27
49
  }
28
50
  // Text renderers for the layer verbs; cli.ts prints what these return.
51
+ function featuresLine(features) {
52
+ const off = features.off.length > 0 ? ` · off: ${features.off.map((name)=>`${name} (features.${name})`).join(', ')}` : '';
53
+ return `features: ${features.on.join(', ')}${off}`;
54
+ }
29
55
  export function renderMap(result) {
30
56
  const parts = [
31
- `docs: ${result.docs.count} (${Math.round(result.docs.bytes / 1024)} KB)\n`,
32
- render(result.fields, 'table')
57
+ `docs: ${result.docs.count} (${Math.round(result.docs.bytes / 1024)} KB)`,
58
+ `${featuresLine(result.features)}\n`,
59
+ renderRows(result.fields, 'table')
33
60
  ];
34
61
  if (result.fieldsTotal > result.fields.length) parts.push(`(+${result.fieldsTotal - result.fields.length} more fields)`);
35
- if (result.hubs.length > 0) parts.push('\nhubs (by link rank):', render(result.hubs, 'table'));
36
- parts.push('\nrecent:', render(result.recent, 'table'));
62
+ if (result.hubs.length > 0) parts.push('\nhubs (by link rank):', renderRows(result.hubs, 'table'));
63
+ else if (result.features.off.includes('rank')) parts.push('\nhubs: off (features.rank)');
64
+ parts.push('\nrecent:', renderRows(result.recent, 'table'));
37
65
  return parts.join('\n');
38
66
  }
39
67
  export function renderPeek(result) {
@@ -44,6 +72,10 @@ export function renderPeek(result) {
44
72
  if (result.sections.length > 0) {
45
73
  lines.push('', 'sections:');
46
74
  for (const s of result.sections)lines.push(` ${'#'.repeat(s.level)} ${s.heading} [L${s.start_line}-${s.end_line}, ~${s.tokens}t]`);
75
+ } else if (result.off.includes('sections')) lines.push('', 'sections: off (features.sections)');
76
+ if (result.off.includes('links')) {
77
+ lines.push('', 'links: off (features.links)');
78
+ return lines.join('\n');
47
79
  }
48
80
  const linkLine = (label, shown, total)=>{
49
81
  if (total > 0) lines.push(`${label} (${total}): ${shown.join(', ')}${total > shown.length ? `, +${total - shown.length} more` : ''}`);
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/output.ts"],"sourcesContent":["// rows -> table (default) | json\n\nexport type Row = Record<string, unknown>;\n\n// Warned about on stderr (keeps --format json stdout machine-readable), never truncated.\nconst OVERSIZE_BYTES = 50_000;\n\n// Flatten embedded newlines so they can't break table column alignment; JSON output is left alone.\nfunction cell(value: unknown): string {\n return String(value ?? '').replace(/\\s*\\n\\s*/g, ' ');\n}\n\nexport function printRows(rows: Row[], format: 'table' | 'json'): void {\n const rendered = render(rows, format);\n console.log(rendered);\n if (rendered.length > OVERSIZE_BYTES) {\n console.warn(`warning: result is ${Math.round(rendered.length / 1000)} KB across ${rows.length} row(s); consider a LIMIT, fewer columns, or snippet() instead of whole values`);\n }\n}\n\nfunction render(rows: Row[], format: 'table' | 'json'): string {\n if (format === 'json') return JSON.stringify(rows, null, 2);\n if (rows.length === 0) return '(0 rows)';\n\n const columns = Object.keys(rows[0]);\n const cells = rows.map((row) => columns.map((col) => cell(row[col])));\n const widths = columns.map((col, i) => Math.max(col.length, ...cells.map((row) => row[i].length)));\n\n const formatRow = (values: string[]) => values.map((value, i) => value.padEnd(widths[i])).join(' ');\n\n return [formatRow(columns), widths.map((w) => '-'.repeat(w)).join(' '), ...cells.map(formatRow)].join('\\n');\n}\n\n// Text renderers for the layer verbs; cli.ts prints what these return.\n\nexport function renderMap(result: { docs: { count: number; bytes: number }; fields: Row[]; fieldsTotal: number; hubs: Row[]; recent: Row[] }): string {\n const parts = [`docs: ${result.docs.count} (${Math.round(result.docs.bytes / 1024)} KB)\\n`, render(result.fields, 'table')];\n if (result.fieldsTotal > result.fields.length) parts.push(`(+${result.fieldsTotal - result.fields.length} more fields)`);\n if (result.hubs.length > 0) parts.push('\\nhubs (by link rank):', render(result.hubs, 'table'));\n parts.push('\\nrecent:', render(result.recent, 'table'));\n return parts.join('\\n');\n}\n\nexport function renderPeek(result: { path: string; tokens: number; frontmatter: Row; sections: Row[]; outbound: string[]; backlinks: string[]; unresolved: string[]; outboundTotal: number; backlinksTotal: number; unresolvedTotal: number }): string {\n const lines = [`${result.path} (~${result.tokens} tokens)`];\n for (const [key, value] of Object.entries(result.frontmatter)) lines.push(` ${key}: ${value}`);\n if (result.sections.length > 0) {\n lines.push('', 'sections:');\n for (const s of result.sections) lines.push(` ${'#'.repeat(s.level as number)} ${s.heading} [L${s.start_line}-${s.end_line}, ~${s.tokens}t]`);\n }\n const linkLine = (label: string, shown: string[], total: number) => {\n if (total > 0) lines.push(`${label} (${total}): ${shown.join(', ')}${total > shown.length ? `, +${total - shown.length} more` : ''}`);\n };\n if (result.outboundTotal + result.unresolvedTotal + result.backlinksTotal > 0) lines.push('');\n linkLine('links out', result.outbound, result.outboundTotal);\n linkLine('unresolved', result.unresolved, result.unresolvedTotal);\n linkLine('backlinks', result.backlinks, result.backlinksTotal);\n return lines.join('\\n');\n}\n"],"names":["OVERSIZE_BYTES","cell","value","String","replace","printRows","rows","format","rendered","render","console","log","length","warn","Math","round","JSON","stringify","columns","Object","keys","cells","map","row","col","widths","i","max","formatRow","values","padEnd","join","w","repeat","renderMap","result","parts","docs","count","bytes","fields","fieldsTotal","push","hubs","recent","renderPeek","lines","path","tokens","key","entries","frontmatter","sections","s","level","heading","start_line","end_line","linkLine","label","shown","total","outboundTotal","unresolvedTotal","backlinksTotal","outbound","unresolved","backlinks"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/output.ts"],"sourcesContent":["// rows -> table (default) | json\n\nexport type Row = Record<string, unknown>;\n\n// Warned about on stderr (keeps --format json stdout machine-readable), never truncated.\nconst OVERSIZE_BYTES = 50_000;\n\n// Flatten embedded newlines so they can't break table column alignment; JSON output is left alone.\nfunction cell(value: unknown): string {\n return String(value ?? '').replace(/\\s*\\n\\s*/g, ' ');\n}\n\nexport function printRows(rows: Row[], format: 'table' | 'json'): void {\n const rendered = renderRows(rows, format);\n console.log(rendered);\n if (rendered.length > OVERSIZE_BYTES) {\n console.warn(`warning: result is ${Math.round(rendered.length / 1000)} KB across ${rows.length} row(s); consider a LIMIT, fewer columns, or snippet() instead of whole values`);\n }\n}\n\n// Table output is for reading; a row wider than the terminal wraps into an unreadable\n// block (a `find` header measured 330 chars once `summary` is present, which drove one\n// user to pipe --format json through a script for every interactive query). Widest\n// columns give up space first, down to a floor, and only as far as the terminal needs.\n// --format json is never truncated: it is the machine-readable surface.\nconst MIN_COLUMN = 12;\nconst TRUNCATED = '…';\n\nfunction fitWidths(widths: number[], limit: number): number[] {\n const gaps = (widths.length - 1) * 2;\n const fitted = [...widths];\n for (;;) {\n const total = fitted.reduce((a, b) => a + b, 0) + gaps;\n if (total <= limit) return fitted;\n const widest = fitted.indexOf(Math.max(...fitted));\n if (fitted[widest] <= MIN_COLUMN) return fitted;\n fitted[widest] = Math.max(MIN_COLUMN, fitted[widest] - Math.max(1, total - limit));\n }\n}\n\nfunction renderRows(rows: Row[], format: 'table' | 'json', width = process.stdout.columns): string {\n if (format === 'json') return JSON.stringify(rows, null, 2);\n if (rows.length === 0) return '(0 rows)';\n\n const columns = Object.keys(rows[0]);\n const cells = rows.map((row) => columns.map((col) => cell(row[col])));\n const natural = columns.map((col, i) => Math.max(col.length, ...cells.map((row) => row[i].length)));\n const widths = width && width > MIN_COLUMN ? fitWidths(natural, width) : natural;\n\n const clip = (value: string, w: number) => (value.length <= w ? value.padEnd(w) : `${value.slice(0, w - 1)}${TRUNCATED}`);\n const formatRow = (values: string[]) =>\n values\n .map((value, i) => clip(value, widths[i]))\n .join(' ')\n .trimEnd();\n\n return [formatRow(columns), widths.map((w) => '-'.repeat(w)).join(' '), ...cells.map(formatRow)].join('\\n');\n}\n\n// Text renderers for the layer verbs; cli.ts prints what these return.\n\nfunction featuresLine(features: { on: string[]; off: string[] }): string {\n const off = features.off.length > 0 ? ` · off: ${features.off.map((name) => `${name} (features.${name})`).join(', ')}` : '';\n return `features: ${features.on.join(', ')}${off}`;\n}\n\nexport function renderMap(result: { docs: { count: number; bytes: number }; fields: Row[]; fieldsTotal: number; features: { on: string[]; off: string[] }; hubs: Row[]; recent: Row[] }): string {\n const parts = [`docs: ${result.docs.count} (${Math.round(result.docs.bytes / 1024)} KB)`, `${featuresLine(result.features)}\\n`, renderRows(result.fields, 'table')];\n if (result.fieldsTotal > result.fields.length) parts.push(`(+${result.fieldsTotal - result.fields.length} more fields)`);\n if (result.hubs.length > 0) parts.push('\\nhubs (by link rank):', renderRows(result.hubs, 'table'));\n else if (result.features.off.includes('rank')) parts.push('\\nhubs: off (features.rank)');\n parts.push('\\nrecent:', renderRows(result.recent, 'table'));\n return parts.join('\\n');\n}\n\nexport function renderPeek(result: { path: string; tokens: number; frontmatter: Row; sections: Row[]; outbound: string[]; backlinks: string[]; unresolved: string[]; outboundTotal: number; backlinksTotal: number; unresolvedTotal: number; off: string[] }): string {\n const lines = [`${result.path} (~${result.tokens} tokens)`];\n for (const [key, value] of Object.entries(result.frontmatter)) lines.push(` ${key}: ${value}`);\n if (result.sections.length > 0) {\n lines.push('', 'sections:');\n for (const s of result.sections) lines.push(` ${'#'.repeat(s.level as number)} ${s.heading} [L${s.start_line}-${s.end_line}, ~${s.tokens}t]`);\n } else if (result.off.includes('sections')) lines.push('', 'sections: off (features.sections)');\n if (result.off.includes('links')) {\n lines.push('', 'links: off (features.links)');\n return lines.join('\\n');\n }\n const linkLine = (label: string, shown: string[], total: number) => {\n if (total > 0) lines.push(`${label} (${total}): ${shown.join(', ')}${total > shown.length ? `, +${total - shown.length} more` : ''}`);\n };\n if (result.outboundTotal + result.unresolvedTotal + result.backlinksTotal > 0) lines.push('');\n linkLine('links out', result.outbound, result.outboundTotal);\n linkLine('unresolved', result.unresolved, result.unresolvedTotal);\n linkLine('backlinks', result.backlinks, result.backlinksTotal);\n return lines.join('\\n');\n}\n"],"names":["OVERSIZE_BYTES","cell","value","String","replace","printRows","rows","format","rendered","renderRows","console","log","length","warn","Math","round","MIN_COLUMN","TRUNCATED","fitWidths","widths","limit","gaps","fitted","total","reduce","a","b","widest","indexOf","max","width","process","stdout","columns","JSON","stringify","Object","keys","cells","map","row","col","natural","i","clip","w","padEnd","slice","formatRow","values","join","trimEnd","repeat","featuresLine","features","off","name","on","renderMap","result","parts","docs","count","bytes","fields","fieldsTotal","push","hubs","includes","recent","renderPeek","lines","path","tokens","key","entries","frontmatter","sections","s","level","heading","start_line","end_line","linkLine","label","shown","outboundTotal","unresolvedTotal","backlinksTotal","outbound","unresolved","backlinks"],"mappings":"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"}
@@ -11,7 +11,7 @@ export interface ParsedDoc {
11
11
  relPath: string;
12
12
  mtimeMs: number;
13
13
  size: number;
14
- data: Record<string, string | number | null>;
14
+ data: Record<string, string | number | bigint | null>;
15
15
  search: {
16
16
  title: string;
17
17
  summary: string;
package/dist/esm/scan.js CHANGED
@@ -41,10 +41,16 @@ export function listFiles(cfg, baseDir) {
41
41
  };
42
42
  });
43
43
  }
44
+ // SQLite storage classes follow the YAML scalar: strings -> TEXT, booleans and whole
45
+ // numbers -> INTEGER, fractions -> REAL, everything else (lists, maps) -> JSON TEXT.
46
+ // Booleans have no SQLite type of their own; 1/0 is the convention, so `WHERE flag = 1`
47
+ // matches and `WHERE flag = 'true'` cannot -- `map` prints the observed type per field so
48
+ // that mismatch is visible rather than a silent zero-row answer.
44
49
  function mapValue(value) {
45
50
  if (value === null || value === undefined) return null;
46
- if (typeof value === 'boolean') return value ? 1 : 0;
47
- if (typeof value === 'string' || typeof value === 'number') return value;
51
+ if (typeof value === 'boolean') return BigInt(value ? 1 : 0);
52
+ if (typeof value === 'number') return Number.isSafeInteger(value) ? BigInt(value) : value;
53
+ if (typeof value === 'string') return value;
48
54
  return JSON.stringify(value);
49
55
  }
50
56
  // The delimiter split is all this package used gray-matter for.
@@ -99,24 +105,25 @@ export function parseFile(file, extractors = []) {
99
105
  }
100
106
  mapped[key] = mapValue(data[key]);
101
107
  }
108
+ // title/summary are plain YAML strings -- whitespace-collapse only;
109
+ // the prose gets the full markdown strip.
110
+ const search = {
111
+ title: normalizeText(data.title),
112
+ summary: normalizeText(data.summary),
113
+ text: stripText(content)
114
+ };
102
115
  return {
103
116
  doc: {
104
117
  relPath: file.relPath,
105
118
  mtimeMs: file.mtimeMs,
106
119
  size: file.size,
107
120
  data: mapped,
108
- search: {
109
- // title/summary are plain YAML strings -- whitespace-collapse only;
110
- // the prose gets the full markdown strip.
111
- title: normalizeText(data.title),
112
- summary: normalizeText(data.summary),
113
- text: stripText(content)
114
- },
121
+ search,
115
122
  extracted: Object.fromEntries(extractors.filter((f)=>f.extract).map((f)=>{
116
123
  var _f_extract;
117
124
  return [
118
125
  f.name,
119
- (_f_extract = f.extract) === null || _f_extract === void 0 ? void 0 : _f_extract.call(f, raw, content)
126
+ (_f_extract = f.extract) === null || _f_extract === void 0 ? void 0 : _f_extract.call(f, raw, content, search)
120
127
  ];
121
128
  }))
122
129
  },
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/scan.ts"],"sourcesContent":["import { readFileSync, statSync } from 'node:fs';\nimport { join } from 'node:path';\nimport fastGlob from 'fast-glob';\nimport removeMarkdown from 'remove-markdown';\nimport { parseDocument } from 'yaml';\nimport type { Config } from './config.ts';\nimport type { Feature } from './features/types.ts';\n\n// Filesystem -> rows. Pure data in, data + warnings out; db.ts does the SQL.\n\n// Reserved: colliding with these would clash with the `frontmatter` table's own columns or the other tables (`content`, `links`, `sections`).\nconst RESERVED_COLUMNS = new Set(['path', '_mtime', '_size', '_rank', 'content', 'links', 'sections']);\n\nfunction normalizeText(value: unknown): string {\n if (value === null || value === undefined) return '';\n return String(value).replace(/\\s+/g, ' ').trim();\n}\n\n// Markdown is stripped at index time so snippets read as clean prose (also improves matching, e.g. `**bold**` indexes as `bold`).\n// remove-markdown doesn't cover Obsidian wikilinks or table scaffolding, so those are handled first/after.\nfunction stripText(value: string): string {\n const withoutWikilinks = value.replace(/\\[\\[([^\\]|]+)\\|([^\\]]+)\\]\\]/g, '$2').replace(/\\[\\[([^\\]]+)\\]\\]/g, '$1');\n const withoutMarkdown = removeMarkdown(withoutWikilinks);\n const withoutTables = withoutMarkdown.replace(/^\\s*\\|?[-\\s|:]+\\|\\s*$/gm, '').replace(/\\|/g, ' ');\n return normalizeText(withoutTables);\n}\n\nexport interface FileStat {\n relPath: string;\n absPath: string;\n mtimeMs: number;\n size: number;\n}\n\nexport function listFiles(cfg: Config, baseDir: string): FileStat[] {\n const relPaths = fastGlob.sync(cfg.scan.include, { cwd: baseDir }).sort();\n return relPaths.map((relPath) => {\n const absPath = join(baseDir, relPath);\n const st = statSync(absPath);\n return { relPath, absPath, mtimeMs: st.mtimeMs, size: st.size };\n });\n}\n\nexport interface ParsedDoc {\n relPath: string;\n mtimeMs: number;\n size: number;\n data: Record<string, string | number | null>;\n // title/summary are duplicated from frontmatter so bm25() can weight them above the body text.\n search: { title: string; summary: string; text: string };\n // Per-feature extraction results, keyed by feature name; features store them at reconcile.\n extracted: Record<string, unknown>;\n}\n\nfunction mapValue(value: unknown): string | number | null {\n if (value === null || value === undefined) return null;\n if (typeof value === 'boolean') return value ? 1 : 0;\n if (typeof value === 'string' || typeof value === 'number') return value;\n return JSON.stringify(value);\n}\n\n// The delimiter split is all this package used gray-matter for.\nfunction splitFrontmatter(raw: string): { fm: string | null; body: string } {\n const open = raw.match(/^---\\r?\\n/);\n if (!open) return { fm: null, body: raw };\n const rest = raw.slice(open[0].length);\n const close = rest.match(/^---\\r?(\\n|$)/m);\n if (!close || close.index === undefined) return { fm: null, body: raw };\n return { fm: rest.slice(0, close.index), body: rest.slice(close.index + close[0].length) };\n}\n\n// Lenient by design: parseDocument collects syntax errors as data and still yields values,\n// so Obsidian-style frontmatter (e.g. an alias starting with @) survives with a warning.\nfunction parseFrontmatter(relPath: string, fm: string, warnings: string[]): Record<string, unknown> {\n const doc = parseDocument(fm);\n if (doc.errors.length > 0) {\n warnings.push(`warning: ${relPath} frontmatter has ${doc.errors.length} syntax error(s) (${doc.errors[0].message.split('\\n')[0]}); parsed leniently`);\n }\n let data: unknown;\n try {\n data = doc.toJS();\n } catch (err) {\n warnings.push(`warning: ${relPath} has unparseable frontmatter (${(err as Error).message.split('\\n')[0]}); indexing without it`);\n return {};\n }\n if (data === null || data === undefined) return {};\n if (typeof data !== 'object' || Array.isArray(data)) {\n warnings.push(`warning: ${relPath} frontmatter is not a key-value mapping; ignoring it`);\n return {};\n }\n return data as Record<string, unknown>;\n}\n\nexport function parseFile(file: FileStat, extractors: Feature[] = []): { doc: ParsedDoc; warnings: string[] } {\n const raw = readFileSync(file.absPath, 'utf8');\n const warnings: string[] = [];\n\n const { fm, body: content } = splitFrontmatter(raw);\n const data = fm === null ? {} : parseFrontmatter(file.relPath, fm, warnings);\n const mapped: Record<string, string | number | null> = {};\n\n for (const key of Object.keys(data)) {\n if (RESERVED_COLUMNS.has(key)) {\n warnings.push(`warning: ${file.relPath} has a frontmatter key named \"${key}\", which is reserved; ignoring it`);\n continue;\n }\n mapped[key] = mapValue(data[key]);\n }\n\n return {\n doc: {\n relPath: file.relPath,\n mtimeMs: file.mtimeMs,\n size: file.size,\n data: mapped,\n search: {\n // title/summary are plain YAML strings -- whitespace-collapse only;\n // the prose gets the full markdown strip.\n title: normalizeText(data.title),\n summary: normalizeText(data.summary),\n text: stripText(content),\n },\n extracted: Object.fromEntries(extractors.filter((f) => f.extract).map((f) => [f.name, f.extract?.(raw, content)])),\n },\n warnings,\n };\n}\n"],"names":["readFileSync","statSync","join","fastGlob","removeMarkdown","parseDocument","RESERVED_COLUMNS","Set","normalizeText","value","undefined","String","replace","trim","stripText","withoutWikilinks","withoutMarkdown","withoutTables","listFiles","cfg","baseDir","relPaths","sync","scan","include","cwd","sort","map","relPath","absPath","st","mtimeMs","size","mapValue","JSON","stringify","splitFrontmatter","raw","open","match","fm","body","rest","slice","length","close","index","parseFrontmatter","warnings","doc","errors","push","message","split","data","toJS","err","Array","isArray","parseFile","file","extractors","content","mapped","key","Object","keys","has","search","title","summary","text","extracted","fromEntries","filter","f","extract","name"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/scan.ts"],"sourcesContent":["import { readFileSync, statSync } from 'node:fs';\nimport { join } from 'node:path';\nimport fastGlob from 'fast-glob';\nimport removeMarkdown from 'remove-markdown';\nimport { parseDocument } from 'yaml';\nimport type { Config } from './config.ts';\nimport type { Feature } from './features/types.ts';\n\n// Filesystem -> rows. Pure data in, data + warnings out; db.ts does the SQL.\n\n// Reserved: colliding with these would clash with the `frontmatter` table's own columns or the other tables (`content`, `links`, `sections`).\nconst RESERVED_COLUMNS = new Set(['path', '_mtime', '_size', '_rank', 'content', 'links', 'sections']);\n\nfunction normalizeText(value: unknown): string {\n if (value === null || value === undefined) return '';\n return String(value).replace(/\\s+/g, ' ').trim();\n}\n\n// Markdown is stripped at index time so snippets read as clean prose (also improves matching, e.g. `**bold**` indexes as `bold`).\n// remove-markdown doesn't cover Obsidian wikilinks or table scaffolding, so those are handled first/after.\nfunction stripText(value: string): string {\n const withoutWikilinks = value.replace(/\\[\\[([^\\]|]+)\\|([^\\]]+)\\]\\]/g, '$2').replace(/\\[\\[([^\\]]+)\\]\\]/g, '$1');\n const withoutMarkdown = removeMarkdown(withoutWikilinks);\n const withoutTables = withoutMarkdown.replace(/^\\s*\\|?[-\\s|:]+\\|\\s*$/gm, '').replace(/\\|/g, ' ');\n return normalizeText(withoutTables);\n}\n\nexport interface FileStat {\n relPath: string;\n absPath: string;\n mtimeMs: number;\n size: number;\n}\n\nexport function listFiles(cfg: Config, baseDir: string): FileStat[] {\n const relPaths = fastGlob.sync(cfg.scan.include, { cwd: baseDir }).sort();\n return relPaths.map((relPath) => {\n const absPath = join(baseDir, relPath);\n const st = statSync(absPath);\n return { relPath, absPath, mtimeMs: st.mtimeMs, size: st.size };\n });\n}\n\nexport interface ParsedDoc {\n relPath: string;\n mtimeMs: number;\n size: number;\n data: Record<string, string | number | bigint | null>;\n // title/summary are duplicated from frontmatter so bm25() can weight them above the body text.\n search: { title: string; summary: string; text: string };\n // Per-feature extraction results, keyed by feature name; features store them at reconcile.\n extracted: Record<string, unknown>;\n}\n\n// SQLite storage classes follow the YAML scalar: strings -> TEXT, booleans and whole\n// numbers -> INTEGER, fractions -> REAL, everything else (lists, maps) -> JSON TEXT.\n// Booleans have no SQLite type of their own; 1/0 is the convention, so `WHERE flag = 1`\n// matches and `WHERE flag = 'true'` cannot -- `map` prints the observed type per field so\n// that mismatch is visible rather than a silent zero-row answer.\nfunction mapValue(value: unknown): string | number | bigint | null {\n if (value === null || value === undefined) return null;\n if (typeof value === 'boolean') return BigInt(value ? 1 : 0);\n if (typeof value === 'number') return Number.isSafeInteger(value) ? BigInt(value) : value;\n if (typeof value === 'string') return value;\n return JSON.stringify(value);\n}\n\n// The delimiter split is all this package used gray-matter for.\nfunction splitFrontmatter(raw: string): { fm: string | null; body: string } {\n const open = raw.match(/^---\\r?\\n/);\n if (!open) return { fm: null, body: raw };\n const rest = raw.slice(open[0].length);\n const close = rest.match(/^---\\r?(\\n|$)/m);\n if (!close || close.index === undefined) return { fm: null, body: raw };\n return { fm: rest.slice(0, close.index), body: rest.slice(close.index + close[0].length) };\n}\n\n// Lenient by design: parseDocument collects syntax errors as data and still yields values,\n// so Obsidian-style frontmatter (e.g. an alias starting with @) survives with a warning.\nfunction parseFrontmatter(relPath: string, fm: string, warnings: string[]): Record<string, unknown> {\n const doc = parseDocument(fm);\n if (doc.errors.length > 0) {\n warnings.push(`warning: ${relPath} frontmatter has ${doc.errors.length} syntax error(s) (${doc.errors[0].message.split('\\n')[0]}); parsed leniently`);\n }\n let data: unknown;\n try {\n data = doc.toJS();\n } catch (err) {\n warnings.push(`warning: ${relPath} has unparseable frontmatter (${(err as Error).message.split('\\n')[0]}); indexing without it`);\n return {};\n }\n if (data === null || data === undefined) return {};\n if (typeof data !== 'object' || Array.isArray(data)) {\n warnings.push(`warning: ${relPath} frontmatter is not a key-value mapping; ignoring it`);\n return {};\n }\n return data as Record<string, unknown>;\n}\n\nexport function parseFile(file: FileStat, extractors: Feature[] = []): { doc: ParsedDoc; warnings: string[] } {\n const raw = readFileSync(file.absPath, 'utf8');\n const warnings: string[] = [];\n\n const { fm, body: content } = splitFrontmatter(raw);\n const data = fm === null ? {} : parseFrontmatter(file.relPath, fm, warnings);\n const mapped: Record<string, string | number | bigint | null> = {};\n\n for (const key of Object.keys(data)) {\n if (RESERVED_COLUMNS.has(key)) {\n warnings.push(`warning: ${file.relPath} has a frontmatter key named \"${key}\", which is reserved; ignoring it`);\n continue;\n }\n mapped[key] = mapValue(data[key]);\n }\n\n // title/summary are plain YAML strings -- whitespace-collapse only;\n // the prose gets the full markdown strip.\n const search = { title: normalizeText(data.title), summary: normalizeText(data.summary), text: stripText(content) };\n\n return {\n doc: {\n relPath: file.relPath,\n mtimeMs: file.mtimeMs,\n size: file.size,\n data: mapped,\n search,\n extracted: Object.fromEntries(extractors.filter((f) => f.extract).map((f) => [f.name, f.extract?.(raw, content, search)])),\n },\n warnings,\n };\n}\n"],"names":["readFileSync","statSync","join","fastGlob","removeMarkdown","parseDocument","RESERVED_COLUMNS","Set","normalizeText","value","undefined","String","replace","trim","stripText","withoutWikilinks","withoutMarkdown","withoutTables","listFiles","cfg","baseDir","relPaths","sync","scan","include","cwd","sort","map","relPath","absPath","st","mtimeMs","size","mapValue","BigInt","Number","isSafeInteger","JSON","stringify","splitFrontmatter","raw","open","match","fm","body","rest","slice","length","close","index","parseFrontmatter","warnings","doc","errors","push","message","split","data","toJS","err","Array","isArray","parseFile","file","extractors","content","mapped","key","Object","keys","has","search","title","summary","text","extracted","fromEntries","filter","f","extract","name"],"mappings":"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"}
@@ -1,11 +1,12 @@
1
1
  import type { DatabaseSync } from 'node:sqlite';
2
- import type { Config } from './config.js';
2
+ import type { Config, FeatureName } from './config.js';
3
3
  import type { Row } from './output.js';
4
4
  export interface FindOptions {
5
5
  k?: number;
6
6
  where?: string;
7
+ semantic?: boolean;
7
8
  }
8
- export declare function find(db: DatabaseSync, cfg: Config, terms: string, opts?: FindOptions): Row[];
9
+ export declare function find(db: DatabaseSync, cfg: Config, terms: string, opts?: FindOptions): Promise<Row[]>;
9
10
  export interface TreeMap {
10
11
  docs: {
11
12
  count: number;
@@ -13,6 +14,10 @@ export interface TreeMap {
13
14
  };
14
15
  fields: Row[];
15
16
  fieldsTotal: number;
17
+ features: {
18
+ on: FeatureName[];
19
+ off: FeatureName[];
20
+ };
16
21
  hubs: Row[];
17
22
  recent: Row[];
18
23
  }
@@ -28,5 +33,6 @@ export interface Peek {
28
33
  outboundTotal: number;
29
34
  backlinksTotal: number;
30
35
  unresolvedTotal: number;
36
+ off: FeatureName[];
31
37
  }
32
38
  export declare function peek(db: DatabaseSync, cfg: Config, pathArg: string): Peek;
package/dist/esm/verbs.js CHANGED
@@ -1,26 +1,62 @@
1
1
  import posix from 'node:path/posix';
2
- import { featureEnabled } from './config.js';
2
+ import { featureEnabled, featureStates } from './config.js';
3
3
  import { SenseError } from './errors.js';
4
+ import { semanticCandidates } from './features/embed.js';
4
5
  import { linkEdges } from './features/index.js';
5
6
  import { personalizedRank } from './graph.js';
6
7
  // The three layer verbs: mapTree (orient), find (locate), peek (structure).
7
8
  // Each returns data; cli.ts renders. All of them degrade when a feature is off.
8
9
  const WEIGHTED_BM25 = 'bm25(content, 10.0, 5.0, 1.0)';
9
10
  const RRF_K = 60;
11
+ // FTS5 reads these as operators, so a bare term containing one is a syntax error or --
12
+ // worse -- a column filter: `end-to-end` parses as a filter on column `to`, and SQLite
13
+ // reports `no such column: to`, which is a true statement about the parse and a false
14
+ // one about the input. Both field reports on 0.6.0 misdiagnosed that message.
15
+ const FTS5_OPERATORS = /[-'"/.:^*()]/;
16
+ function searchError(err, terms, scope) {
17
+ var _terms_match;
18
+ var _exec;
19
+ const message = err.message;
20
+ if (!/no such column|fts5: syntax error|malformed MATCH/.test(message)) return err;
21
+ const suspects = ((_terms_match = terms.match(/\S+/g)) !== null && _terms_match !== void 0 ? _terms_match : []).filter((t)=>!t.startsWith('"') && FTS5_OPERATORS.test(t));
22
+ // Blame the terms only when the failing token actually came from one -- a typo'd column
23
+ // in --where (or the tree's default scope) raises "no such column" through this same
24
+ // statement, and naming a term for it would state a false fact about the input.
25
+ const col = (_exec = /no such column: (\S+)/.exec(message)) === null || _exec === void 0 ? void 0 : _exec[1];
26
+ const fromTerms = col === undefined ? suspects.length > 0 : suspects.some((t)=>t.split(/[^\p{L}\p{N}]+/u).includes(col));
27
+ if (fromTerms && suspects.length > 0) {
28
+ return new SenseError('SEARCH_SYNTAX', `${message} -- the punctuation in ${suspects.map((t)=>`\`${t}\``).join(', ')} is FTS5 syntax, not literal text; search for it literally by double-quoting: '"${suspects[0]}"'. Searchable columns are title, summary, text.`);
29
+ }
30
+ if (col !== undefined && scope !== undefined) {
31
+ return new SenseError('SEARCH_SYNTAX', `${message} -- the where condition (${scope}) references it; frontmatter columns are listed by sense query "SELECT name FROM pragma_table_info('frontmatter')".`);
32
+ }
33
+ return new SenseError('SEARCH_SYNTAX', `${message} -- searchable columns are title, summary, text; frontmatter fields are queried with --where or sense query (list them with pragma_table_info('frontmatter')).`);
34
+ }
10
35
  // Layer 1: BM25 + link-graph expansion, fused by reciprocal rank. `via` says which
11
36
  // signal produced each row so the agent knows what evidence it is trusting.
12
- export function find(db, cfg, terms, opts = {}) {
13
- var _opts_k, _hits_get;
37
+ // Semantic expansion is per-query opt-in: without opts.semantic the result is
38
+ // byte-for-byte independent of the embed feature.
39
+ export async function find(db, cfg, terms, opts = {}) {
40
+ var _opts_k, _opts_where, _hits_get, _chunkLines_get, _chunkSimilarity_get;
41
+ var _cfg_defaults_find, _cfg_defaults;
14
42
  const k = (_opts_k = opts.k) !== null && _opts_k !== void 0 ? _opts_k : 10;
15
43
  const fetch = Math.max(k * 3, 30);
16
44
  // Terms pass verbatim to FTS5 MATCH: bare words AND-join, operators are the caller's.
17
45
  // Invalid syntax propagates as an error, zero matches return zero -- no silent rewrites.
18
46
  // --where applies inside the candidate query (a post-filter over the top-N would drop
19
47
  // matches ranked past the pool) and again on the final select for link-derived rows.
20
- const whereJoin = opts.where ? `JOIN frontmatter f ON f."path" = content.path` : '';
21
- const whereCond = opts.where ? `AND (${opts.where})` : '';
48
+ // An explicit --where replaces the tree's declared default rather than ANDing with it,
49
+ // so a caller can always widen back to the whole tree.
50
+ const scope = (_opts_where = opts.where) !== null && _opts_where !== void 0 ? _opts_where : (_cfg_defaults = cfg.defaults) === null || _cfg_defaults === void 0 ? void 0 : (_cfg_defaults_find = _cfg_defaults.find) === null || _cfg_defaults_find === void 0 ? void 0 : _cfg_defaults_find.where;
51
+ const whereJoin = scope ? `JOIN frontmatter f ON f."path" = content.path` : '';
52
+ const whereCond = scope ? `AND (${scope})` : '';
22
53
  const matchSql = `SELECT content.path AS path, snippet(content, -1, '«', '»', '…', 10) AS hit FROM content ${whereJoin} WHERE content MATCH ? ${whereCond} ORDER BY ${WEIGHTED_BM25} LIMIT ${fetch}`;
23
- const matchRows = db.prepare(matchSql).all(terms);
54
+ let matchRows;
55
+ try {
56
+ matchRows = db.prepare(matchSql).all(terms);
57
+ } catch (err) {
58
+ throw searchError(err, terms, scope);
59
+ }
24
60
  const hits = new Map(matchRows.map((r)=>[
25
61
  r.path,
26
62
  r.hit
@@ -32,20 +68,26 @@ export function find(db, cfg, terms, opts = {}) {
32
68
  via: 'match'
33
69
  });
34
70
  });
35
- if (featureEnabled(cfg, 'links') && matchRows.length > 0) {
71
+ const edges = featureEnabled(cfg, 'links') && matchRows.length > 0 ? linkEdges(db) : [];
72
+ if (edges.length > 0) {
73
+ // `linked` gates the label only, not the score: PPR restart mass gives every seed a
74
+ // nonzero rank even without an incident edge, which is not link evidence — but dropping
75
+ // that mass from the score list reweights fusion toward connectivity and measurably
76
+ // wrecks ranking on link-dense corpora (FEVER hit@10 0.997 -> 0.907; fusion-tuning.md).
77
+ const linked = new Set(edges.flat());
36
78
  const nodes = db.prepare('SELECT "path" FROM frontmatter').all().map((r)=>r.path);
37
79
  const seeds = new Map(matchRows.map((r, i)=>[
38
80
  r.path,
39
81
  1 / (i + 1)
40
82
  ]));
41
83
  const ranked = [
42
- ...personalizedRank(nodes, linkEdges(db), seeds)
84
+ ...personalizedRank(nodes, edges, seeds)
43
85
  ].filter(([, score])=>score > 1e-9).sort((a, b)=>b[1] - a[1]).slice(0, fetch);
44
86
  ranked.forEach(([path], i)=>{
45
87
  const existing = candidates.get(path);
46
88
  if (existing) {
47
89
  existing.score += 1 / (RRF_K + i);
48
- existing.via = 'match+link';
90
+ if (linked.has(path)) existing.via = 'match+link';
49
91
  } else {
50
92
  candidates.set(path, {
51
93
  score: 1 / (RRF_K + i),
@@ -54,12 +96,34 @@ export function find(db, cfg, terms, opts = {}) {
54
96
  }
55
97
  });
56
98
  }
57
- db.exec('CREATE TEMP TABLE IF NOT EXISTS _find ("path" TEXT PRIMARY KEY, score REAL, via TEXT, hit TEXT)');
58
- db.exec('DELETE FROM _find');
59
- const insert = db.prepare('INSERT INTO _find ("path", score, via, hit) VALUES (?, ?, ?, ?)');
60
- for (const [path, c] of candidates)insert.run(path, c.score, c.via, (_hits_get = hits.get(path)) !== null && _hits_get !== void 0 ? _hits_get : null);
61
- const where = opts.where ? `WHERE ${opts.where}` : '';
62
- return db.prepare(`SELECT f."path" AS path, content.title, content.summary, _find.hit, _find.via, round(_find.score, 4) AS score
99
+ // Vector expansion, invoked only: a third RRF list at the swept flat-region constants
100
+ // (weight 1, pool = fetch). Each row carries its best chunk's line range.
101
+ const chunkLines = new Map();
102
+ const chunkSimilarity = new Map();
103
+ if (opts.semantic) {
104
+ const vec = await semanticCandidates(db, cfg, terms, fetch);
105
+ vec.forEach(({ path, lines, similarity }, i)=>{
106
+ chunkLines.set(path, lines);
107
+ chunkSimilarity.set(path, similarity);
108
+ const existing = candidates.get(path);
109
+ if (existing) {
110
+ existing.score += 1 / (RRF_K + i);
111
+ existing.via = `${existing.via}+vector`;
112
+ } else {
113
+ candidates.set(path, {
114
+ score: 1 / (RRF_K + i),
115
+ via: 'vector'
116
+ });
117
+ }
118
+ });
119
+ }
120
+ db.exec('DROP TABLE IF EXISTS _find');
121
+ db.exec('CREATE TEMP TABLE _find ("path" TEXT PRIMARY KEY, score REAL, via TEXT, hit TEXT, lines TEXT, similarity REAL)');
122
+ const insert = db.prepare('INSERT INTO _find ("path", score, via, hit, lines, similarity) VALUES (?, ?, ?, ?, ?, ?)');
123
+ for (const [path, c] of candidates)insert.run(path, c.score, c.via, (_hits_get = hits.get(path)) !== null && _hits_get !== void 0 ? _hits_get : null, (_chunkLines_get = chunkLines.get(path)) !== null && _chunkLines_get !== void 0 ? _chunkLines_get : null, (_chunkSimilarity_get = chunkSimilarity.get(path)) !== null && _chunkSimilarity_get !== void 0 ? _chunkSimilarity_get : null);
124
+ const where = scope ? `WHERE ${scope}` : '';
125
+ const linesCol = opts.semantic ? ', _find.lines, _find.similarity' : '';
126
+ return db.prepare(`SELECT f."path" AS path, content.title, content.summary, _find.hit, _find.via, round(_find.score, 4) AS score${linesCol}
63
127
  FROM _find JOIN frontmatter f ON f."path" = _find."path" JOIN content ON content.path = _find."path"
64
128
  ${where} ORDER BY _find.score DESC LIMIT ?`).all(k);
65
129
  }
@@ -74,10 +138,15 @@ export function mapTree(db, cfg) {
74
138
  const docs = db.prepare('SELECT COUNT(*) AS count, COALESCE(SUM("_size"), 0) AS bytes FROM frontmatter').get();
75
139
  const columns = db.prepare('PRAGMA table_info(frontmatter)').all().map((r)=>r.name).filter((name)=>!INTERNAL_COLUMNS.has(name));
76
140
  const allFields = columns.map((name)=>{
77
- const { n } = db.prepare(`SELECT COUNT("${name.split('"').join('""')}") AS n FROM frontmatter`).get();
141
+ const quoted = `"${name.split('"').join('""')}"`;
142
+ // Observed storage class, not a declared one: these columns are added dynamically and
143
+ // SQLite types per value, so a field can be text in most notes and numeric in a few.
144
+ // Listing every distinct type makes both the type and any drift visible.
145
+ const { n, types } = db.prepare(`SELECT COUNT(${quoted}) AS n, GROUP_CONCAT(DISTINCT typeof(${quoted})) AS types FROM frontmatter WHERE ${quoted} IS NOT NULL`).get();
78
146
  return {
79
147
  field: name,
80
- coverage: n
148
+ coverage: n,
149
+ type: types !== null && types !== void 0 ? types : ''
81
150
  };
82
151
  }).sort((a, b)=>b.coverage - a.coverage);
83
152
  const fields = allFields.slice(0, 20);
@@ -87,6 +156,7 @@ export function mapTree(db, cfg) {
87
156
  docs,
88
157
  fields,
89
158
  fieldsTotal: allFields.length,
159
+ features: featureStates(cfg),
90
160
  hubs,
91
161
  recent
92
162
  };
@@ -134,6 +204,10 @@ export function peek(db, cfg, pathArg) {
134
204
  unresolved: unresolved.slice(0, PEEK_LINK_LIMIT),
135
205
  outboundTotal: outbound.length,
136
206
  backlinksTotal,
137
- unresolvedTotal: unresolved.length
207
+ unresolvedTotal: unresolved.length,
208
+ off: [
209
+ 'sections',
210
+ 'links'
211
+ ].filter((name)=>!featureEnabled(cfg, name))
138
212
  };
139
213
  }