sensemaking 0.22.0 → 0.22.2

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 (62) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/features/embed.js +9 -1
  3. package/dist/cjs/features/embed.js.map +1 -1
  4. package/dist/cjs/features/links.js +88 -70
  5. package/dist/cjs/features/links.js.map +1 -1
  6. package/dist/cjs/features/sections.js +13 -5
  7. package/dist/cjs/features/sections.js.map +1 -1
  8. package/dist/cjs/features/tags.js +7 -4
  9. package/dist/cjs/features/tags.js.map +1 -1
  10. package/dist/cjs/store/builder.js +2 -3
  11. package/dist/cjs/store/builder.js.map +1 -1
  12. package/dist/cjs/store/duckdb/connection.js +120 -0
  13. package/dist/cjs/store/duckdb/connection.js.map +1 -1
  14. package/dist/cjs/store/duckdb/reconcile.js +11 -113
  15. package/dist/cjs/store/duckdb/reconcile.js.map +1 -1
  16. package/dist/cjs/store/reconcile.js +14 -37
  17. package/dist/cjs/store/reconcile.js.map +1 -1
  18. package/dist/cjs/store/shared.d.cts +1 -0
  19. package/dist/cjs/store/shared.d.ts +1 -0
  20. package/dist/cjs/store/shared.js +41 -0
  21. package/dist/cjs/store/shared.js.map +1 -1
  22. package/dist/cjs/store/turso/connection.d.cts +1 -0
  23. package/dist/cjs/store/turso/connection.d.ts +1 -0
  24. package/dist/cjs/store/turso/connection.js +134 -13
  25. package/dist/cjs/store/turso/connection.js.map +1 -1
  26. package/dist/cjs/store/turso/open.js +7 -1
  27. package/dist/cjs/store/turso/open.js.map +1 -1
  28. package/dist/cjs/store/turso/store.js +8 -1
  29. package/dist/cjs/store/turso/store.js.map +1 -1
  30. package/dist/cjs/store/types.d.cts +1 -1
  31. package/dist/cjs/store/types.d.ts +1 -1
  32. package/dist/cjs/store/types.js.map +1 -1
  33. package/dist/esm/features/embed.js +9 -1
  34. package/dist/esm/features/embed.js.map +1 -1
  35. package/dist/esm/features/links.js +77 -23
  36. package/dist/esm/features/links.js.map +1 -1
  37. package/dist/esm/features/sections.js +13 -3
  38. package/dist/esm/features/sections.js.map +1 -1
  39. package/dist/esm/features/tags.js +7 -2
  40. package/dist/esm/features/tags.js.map +1 -1
  41. package/dist/esm/store/builder.js +2 -3
  42. package/dist/esm/store/builder.js.map +1 -1
  43. package/dist/esm/store/duckdb/connection.js +35 -0
  44. package/dist/esm/store/duckdb/connection.js.map +1 -1
  45. package/dist/esm/store/duckdb/reconcile.js +12 -37
  46. package/dist/esm/store/duckdb/reconcile.js.map +1 -1
  47. package/dist/esm/store/reconcile.js +11 -12
  48. package/dist/esm/store/reconcile.js.map +1 -1
  49. package/dist/esm/store/shared.d.ts +1 -0
  50. package/dist/esm/store/shared.js +8 -0
  51. package/dist/esm/store/shared.js.map +1 -1
  52. package/dist/esm/store/turso/connection.d.ts +1 -0
  53. package/dist/esm/store/turso/connection.js +21 -8
  54. package/dist/esm/store/turso/connection.js.map +1 -1
  55. package/dist/esm/store/turso/open.js +2 -1
  56. package/dist/esm/store/turso/open.js.map +1 -1
  57. package/dist/esm/store/turso/store.js +2 -0
  58. package/dist/esm/store/turso/store.js.map +1 -1
  59. package/dist/esm/store/types.d.ts +1 -1
  60. package/dist/esm/store/types.js.map +1 -1
  61. package/package.json +1 -7
  62. package/skills/sense-setup/SKILL.md +1 -1
package/README.md CHANGED
@@ -93,7 +93,7 @@ ORDER BY bm25(content, 10.0, 5.0, 1.0) LIMIT 10
93
93
  |---|---|
94
94
  | `presets` | named bundles of `include`/`exclude` globs, `k` (result count), `signals` (which engines this scope searches with, `words`, `links`, `vectors`; every signal whose prerequisites hold, unless the preset lists them exhaustively), `where` (a standing SQL filter). A file is indexed if any preset includes it, embedded if a model is named and some covering preset's `signals` include `vectors`; `status` shows each preset's coverage. |
95
95
  | `embed` | the model vectors are built with. Naming one gives the tree vectors; omitting the block means none at all, whatever the presets say. `sense download` fetches it. |
96
- | `store` | backing store engine: `sqlite` (default, zero-dependency, Node's built-in SQLite), or the experimental `duckdb` and `turso`. The first command that opens such a tree installs that engine's package on its own (`@duckdb/node-api`, a one-time native download of ~110 MB; `@tursodatabase/database`, much smaller). The same commands and table names run on all three; what does not port is FTS5 syntax. Under `duckdb` and `turso`, `search` text and raw `MATCH` reject FTS5's prefix (`foo*`), boolean (`AND`/`OR`/`NOT`), `NEAR`, initial-token (`^`), and column-filter (`title:foo`) operators with a named error that says how to rephrase or set `store` to `sqlite`; bare words and quoted phrases work on all three. Raw `sql` is a per-store dialect: the tables are portable, but sqlite's FTS5 `MATCH`/`snippet()`/`bm25()` do not run under `duckdb` or `turso`, so saved queries written in FTS5 syntax are sqlite dialect, and `has`/`basename`/`segment` are registered on `sqlite` and `duckdb` but not `turso`, whose client cannot register SQL functions. `sense watch` runs on all three; on `duckdb` and `turso`, which lock the cache file per connection, a concurrent command waits out the watcher's current cycle instead of failing. Each store keeps its own cache file (`.sense/cache.db`, `.sense/cache.duckdb`, `.sense/cache.turso.db`); switching stores is a rebuild, not a migration. |
96
+ | `store` | backing store engine: `sqlite` (default, zero-dependency, Node's built-in SQLite), or the experimental `duckdb` and `turso`. The first command that opens such a tree installs that engine's package on its own (`@duckdb/node-api`, a one-time native download of ~110 MB; `@tursodatabase/database`, much smaller). The same commands and table names run on all three; what does not port is FTS5 syntax. Under `duckdb` and `turso`, `search` text and raw `MATCH` reject FTS5's prefix (`foo*`), boolean (`AND`/`OR`/`NOT`), `NEAR`, initial-token (`^`), and column-filter (`title:foo`) operators with a named error that says how to rephrase or set `store` to `sqlite`; bare words and quoted phrases work on all three. Raw `sql` is a per-store dialect: the tables are portable, but sqlite's FTS5 `MATCH`/`snippet()`/`bm25()` do not run under `duckdb` or `turso`, so saved queries written in FTS5 syntax are sqlite dialect, and `has`/`basename`/`segment` are registered on `sqlite` and `duckdb` but not `turso`, whose client cannot register SQL functions. `sense watch` runs on all three; on `duckdb` and `turso`, which lock the cache file per connection, a concurrent command waits out the watcher's current cycle instead of failing. Each store keeps its own cache file (`.sense/cache.db`, `.sense/cache.duckdb`, `.sense/cache.turso.db`); switching stores is a rebuild, not a migration. Indexing speed is the other axis, and it does not follow from any of the above: sqlite builds a cold index fastest and turso slowest, by a wide margin on a large tree, and no setting closes that gap, since turso's engine costs more per write and more again to maintain each index. What turso buys instead is concurrent writers, non-blocking I/O and encryption, none of which a one-shot command uses, so reach for it for those rather than for speed. Per-store figures: [BENCHMARKING.md](BENCHMARKING.md). |
97
97
  | `queries` | entries runnable as `sense <name>`, each naming the verb it runs: `{ sql }` for SQL (`?` binds positional args) or `{ search }` for a ranked search with its settings baked in, so `sense hot` needs no flags. Running an entry validates it: a typo'd column errors and exits nonzero, and a parameterised entry validates with any argument, since preparing precedes binding. |
98
98
  | `version` | schema version; older configs auto-migrate on load, noted on stderr. |
99
99
 
@@ -13,6 +13,7 @@ var _indexts1 = require("../config/index.js");
13
13
  var _handoffts = require("../embed/handoff.js");
14
14
  var _identityts = require("../embed/identity.js");
15
15
  var _frontmatterts = require("../scan/frontmatter.js");
16
+ var _sharedts = require("../store/shared.js");
16
17
  function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
17
18
  try {
18
19
  var info = gen[key](arg);
@@ -255,9 +256,16 @@ var embed = {
255
256
  if (rows.length === 0) return [
256
257
  2
257
258
  ];
259
+ // scale and vector are written by the embed pass, not here, so they are not in the column list
260
+ // and take the table's own default on the append path.
258
261
  return [
259
262
  4,
260
- db.runBatch('INSERT INTO embeddings ("path", chunk, start_line, end_line, scale, vector) VALUES (?, ?, ?, ?, NULL, NULL)', rows)
263
+ (0, _sharedts.appendRows)(db, 'embeddings', [
264
+ 'path',
265
+ 'chunk',
266
+ 'start_line',
267
+ 'end_line'
268
+ ], 'INSERT INTO embeddings ("path", chunk, start_line, end_line, scale, vector) VALUES (?, ?, ?, ?, NULL, NULL)', rows)
261
269
  ];
262
270
  case 1:
263
271
  _state.sent();
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/embed.ts"],"sourcesContent":["import type { Block, Chunk } from '../chunk/index.ts';\nimport { CHUNK_VERSION, chunkFromBlocks, parse } from '../chunk/index.ts';\nimport { embedConfig } from '../config/index.ts';\nimport { stashChunkText } from '../embed/handoff.ts';\nimport { modelIdentity } from '../embed/identity.ts';\nimport { countLines } from '../scan/frontmatter.ts';\nimport type { Feature } from './types.ts';\n\n// int8 vectors with a per-vector scale, NULL vector = not yet embedded: reconcile writes dirty\n// rows and embedding tops up on the next search, so staleness costs recall.\n\nexport type { Chunk };\n\n// The title/summary prefix mirrors bm25 field weighting. Chunks the body, not the raw file, so\n// `offset` shifts line numbers back onto raw and a range stays a direct Read range.\nfunction chunksOf(blocks: Block[], body: string, search?: { title: string; summary: string }, offset = 0, chunkTokens?: number): Chunk[] {\n const prefix = [search?.title, search?.summary].filter(Boolean).join('\\n');\n // A hardcoded option change here must bump src/chunk/version.ts's CHUNK_VERSION, the signature\n // for every chunker input not already carried in config.\n return chunkFromBlocks(blocks, body, chunkTokens !== undefined ? { targetTokens: chunkTokens } : undefined).map((c) => ({\n startLine: c.startLine + offset,\n endLine: c.endLine + offset,\n text: prefix ? `${prefix}\\n${c.text}` : c.text,\n }));\n}\n\nexport const embed: Feature = {\n name: 'embed',\n async schema(db) {\n await 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, _data, cfg, blocks) {\n // Lines the frontmatter occupies, so body line 1 maps back to its raw line number.\n // blocks comes from parseFile's shared parse; falls back to parsing body for a direct call.\n return chunksOf(blocks ?? parse(body), body, search, countLines(raw) - countLines(body), cfg?.embed?.chunkTokens);\n },\n async remove(db, paths) {\n if (paths.length === 0) return;\n await db.runBatch(\n 'DELETE FROM embeddings WHERE \"path\" = ?',\n paths.map((p) => [p])\n );\n },\n // A tree with no embedding model never had extract() run for the doc (reconcile.ts's per-file\n // filter skips it), so extracted is undefined here -- those docs contribute no rows.\n async store(db, docs) {\n const rows: unknown[][] = [];\n const texts = new Map<string, string[]>();\n for (const { path, extracted } of docs) {\n if (!extracted) continue;\n const chunks = extracted as Chunk[];\n chunks.forEach((c, idx) => rows.push([path, idx, c.startLine, c.endLine]));\n texts.set(\n path,\n chunks.map((c) => c.text)\n );\n }\n if (rows.length === 0) return;\n await db.runBatch('INSERT INTO embeddings (\"path\", chunk, start_line, end_line, scale, vector) VALUES (?, ?, ?, ?, NULL, NULL)', rows);\n // The text extract() just computed, handed to the embed pass that follows in this same\n // process rather than re-derived from the file there (src/embed/handoff.ts).\n stashChunkText(db, texts);\n },\n enabledForFile(_cfg, file) {\n return file.embed;\n },\n // Keyed so a rebuild notice can name it \"embed settings\". Driven by embedConfig(cfg) directly:\n // a model can be configured with no preset yet using vectors.\n signature(cfg) {\n const e = embedConfig(cfg);\n if (!e) return 'embed:off';\n // Weight identity from identity.ts, no network: a static model's resolved sha or local\n // size+mtime, appended once known so changed weights re-embed.\n const identity = e.provider === 'static' ? modelIdentity(e.model) : undefined;\n // W3b: chunkTokens rides the version token, since it's the one owner lever the chunker\n // itself takes -- changing or clearing it must rebuild exactly like a chunker version bump.\n const chunkVersion = e.chunkTokens !== undefined ? `${CHUNK_VERSION}:${e.chunkTokens}` : CHUNK_VERSION;\n return `embed:${e.provider}:${e.model}:${chunkVersion}${identity !== undefined ? `@${identity}` : ''}`;\n },\n};\n"],"names":["embed","chunksOf","blocks","body","search","offset","chunkTokens","prefix","title","summary","filter","Boolean","join","chunkFromBlocks","undefined","targetTokens","map","c","startLine","endLine","text","name","schema","db","exec","extract","raw","_data","cfg","parse","countLines","remove","paths","length","runBatch","p","store","docs","rows","texts","Map","path","extracted","chunks","forEach","idx","push","set","stashChunkText","enabledForFile","_cfg","file","signature","e","embedConfig","identity","provider","modelIdentity","model","chunkVersion","CHUNK_VERSION"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/embed.ts"],"sourcesContent":["import type { Block, Chunk } from '../chunk/index.ts';\nimport { CHUNK_VERSION, chunkFromBlocks, parse } from '../chunk/index.ts';\nimport { embedConfig } from '../config/index.ts';\nimport { stashChunkText } from '../embed/handoff.ts';\nimport { modelIdentity } from '../embed/identity.ts';\nimport { countLines } from '../scan/frontmatter.ts';\nimport { appendRows } from '../store/shared.ts';\nimport type { Feature } from './types.ts';\n\n// int8 vectors with a per-vector scale, NULL vector = not yet embedded: reconcile writes dirty\n// rows and embedding tops up on the next search, so staleness costs recall.\n\nexport type { Chunk };\n\n// The title/summary prefix mirrors bm25 field weighting. Chunks the body, not the raw file, so\n// `offset` shifts line numbers back onto raw and a range stays a direct Read range.\nfunction chunksOf(blocks: Block[], body: string, search?: { title: string; summary: string }, offset = 0, chunkTokens?: number): Chunk[] {\n const prefix = [search?.title, search?.summary].filter(Boolean).join('\\n');\n // A hardcoded option change here must bump src/chunk/version.ts's CHUNK_VERSION, the signature\n // for every chunker input not already carried in config.\n return chunkFromBlocks(blocks, body, chunkTokens !== undefined ? { targetTokens: chunkTokens } : undefined).map((c) => ({\n startLine: c.startLine + offset,\n endLine: c.endLine + offset,\n text: prefix ? `${prefix}\\n${c.text}` : c.text,\n }));\n}\n\nexport const embed: Feature = {\n name: 'embed',\n async schema(db) {\n await 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, _data, cfg, blocks) {\n // Lines the frontmatter occupies, so body line 1 maps back to its raw line number.\n // blocks comes from parseFile's shared parse; falls back to parsing body for a direct call.\n return chunksOf(blocks ?? parse(body), body, search, countLines(raw) - countLines(body), cfg?.embed?.chunkTokens);\n },\n async remove(db, paths) {\n if (paths.length === 0) return;\n await db.runBatch(\n 'DELETE FROM embeddings WHERE \"path\" = ?',\n paths.map((p) => [p])\n );\n },\n // A tree with no embedding model never had extract() run for the doc (reconcile.ts's per-file\n // filter skips it), so extracted is undefined here -- those docs contribute no rows.\n async store(db, docs) {\n const rows: unknown[][] = [];\n const texts = new Map<string, string[]>();\n for (const { path, extracted } of docs) {\n if (!extracted) continue;\n const chunks = extracted as Chunk[];\n chunks.forEach((c, idx) => rows.push([path, idx, c.startLine, c.endLine]));\n texts.set(\n path,\n chunks.map((c) => c.text)\n );\n }\n if (rows.length === 0) return;\n // scale and vector are written by the embed pass, not here, so they are not in the column list\n // and take the table's own default on the append path.\n await appendRows(db, 'embeddings', ['path', 'chunk', 'start_line', 'end_line'], 'INSERT INTO embeddings (\"path\", chunk, start_line, end_line, scale, vector) VALUES (?, ?, ?, ?, NULL, NULL)', rows);\n // The text extract() just computed, handed to the embed pass that follows in this same\n // process rather than re-derived from the file there (src/embed/handoff.ts).\n stashChunkText(db, texts);\n },\n enabledForFile(_cfg, file) {\n return file.embed;\n },\n // Keyed so a rebuild notice can name it \"embed settings\". Driven by embedConfig(cfg) directly:\n // a model can be configured with no preset yet using vectors.\n signature(cfg) {\n const e = embedConfig(cfg);\n if (!e) return 'embed:off';\n // Weight identity from identity.ts, no network: a static model's resolved sha or local\n // size+mtime, appended once known so changed weights re-embed.\n const identity = e.provider === 'static' ? modelIdentity(e.model) : undefined;\n // W3b: chunkTokens rides the version token, since it's the one owner lever the chunker\n // itself takes -- changing or clearing it must rebuild exactly like a chunker version bump.\n const chunkVersion = e.chunkTokens !== undefined ? `${CHUNK_VERSION}:${e.chunkTokens}` : CHUNK_VERSION;\n return `embed:${e.provider}:${e.model}:${chunkVersion}${identity !== undefined ? `@${identity}` : ''}`;\n },\n};\n"],"names":["embed","chunksOf","blocks","body","search","offset","chunkTokens","prefix","title","summary","filter","Boolean","join","chunkFromBlocks","undefined","targetTokens","map","c","startLine","endLine","text","name","schema","db","exec","extract","raw","_data","cfg","parse","countLines","remove","paths","length","runBatch","p","store","docs","rows","texts","Map","path","extracted","chunks","forEach","idx","push","set","appendRows","stashChunkText","enabledForFile","_cfg","file","signature","e","embedConfig","identity","provider","modelIdentity","model","chunkVersion","CHUNK_VERSION"],"mappings":"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"}
@@ -23,6 +23,7 @@ _export(exports, {
23
23
  }
24
24
  });
25
25
  var _posix = /*#__PURE__*/ _interop_require_default(require("node:path/posix"));
26
+ var _sharedts = require("../store/shared.js");
26
27
  var _fencests = require("./fences.js");
27
28
  function _array_like_to_array(arr, len) {
28
29
  if (len == null || len > arr.length) len = arr.length;
@@ -507,45 +508,6 @@ function writeResolved(db, rows) {
507
508
  });
508
509
  })();
509
510
  }
510
- // A new or deleted file can change any note's resolution, so re-resolve the whole table.
511
- // Fallback for cold builds and large deltas -- see afterReconcile's threshold.
512
- function resolveAll(db, files) {
513
- return _async_to_generator(function() {
514
- var pathSet, byBase, stmt, rows, changed;
515
- return _ts_generator(this, function(_state) {
516
- switch(_state.label){
517
- case 0:
518
- pathSet = new Set(files.map(function(f) {
519
- return f.relPath;
520
- }));
521
- byBase = buildByBase(files);
522
- return [
523
- 4,
524
- db.prepare('SELECT src, target, dst, embed FROM links')
525
- ];
526
- case 1:
527
- stmt = _state.sent();
528
- return [
529
- 4,
530
- stmt.all()
531
- ];
532
- case 2:
533
- rows = _state.sent();
534
- changed = resolveRows(rows, pathSet, byBase);
535
- return [
536
- 4,
537
- writeResolved(db, changed)
538
- ];
539
- case 3:
540
- _state.sent();
541
- return [
542
- 2,
543
- changed.length > 0
544
- ];
545
- }
546
- });
547
- })();
548
- }
549
511
  // Chunked so a large-but-under-threshold delta never exceeds SQLite's bound-variable limit
550
512
  // (SQLITE_MAX_VARIABLE_NUMBER, 32766); harmless headroom on DuckDB, which has no such cap.
551
513
  function selectIn(db, column, keys) {
@@ -756,6 +718,26 @@ function linkEdges(db) {
756
718
  // Records whether remove()/store() deleted any link row, since afterReconcile runs after both
757
719
  // and dstChanged alone misses the reparsed-to-zero-links case. Keyed on delta so state dies with it.
758
720
  var removedWithLinks = new WeakMap();
721
+ // Cold build only: whether store() resolved at least one row to a non-null dst, so afterReconcile
722
+ // can report linksChanged without re-reading the table it just wrote.
723
+ var coldResolved = new WeakMap();
724
+ // dst stays NULL here on an incremental store (resolution runs afterward); a cold build resolves
725
+ // it inline in store() instead, through the *_RESOLVED column list and SQL below.
726
+ var LINK_COLUMNS = [
727
+ 'src',
728
+ 'target',
729
+ 'target_base',
730
+ 'embed'
731
+ ];
732
+ var UPSERT_LINK_SQL = 'INSERT INTO links (src, target, target_base, dst, embed) VALUES (?, ?, ?, NULL, ?) ON CONFLICT(src, target, embed) DO UPDATE SET target_base = excluded.target_base';
733
+ var LINK_COLUMNS_RESOLVED = [
734
+ 'src',
735
+ 'target',
736
+ 'target_base',
737
+ 'dst',
738
+ 'embed'
739
+ ];
740
+ var UPSERT_LINK_SQL_RESOLVED = 'INSERT INTO links (src, target, target_base, dst, embed) VALUES (?, ?, ?, ?, ?) ON CONFLICT(src, target, embed) DO UPDATE SET target_base = excluded.target_base';
759
741
  var links = {
760
742
  name: 'links',
761
743
  schema: function schema(db) {
@@ -830,7 +812,7 @@ var links = {
830
812
  },
831
813
  store: function store(db, docs, delta) {
832
814
  return _async_to_generator(function() {
833
- var byPath, addedSet, reparsedExisting, existingRows, staleRows, upsertRows, _iteratorNormalCompletion, _didIteratorError, _iteratorError, _iterator, _step, _step_value, path, targets, _iteratorNormalCompletion1, _didIteratorError1, _iteratorError1, _iterator1, _step1, _step_value1, target, embed;
815
+ var byPath, addedSet, reparsedExisting, existingRows, staleRows, cold, resolveNew, pathSet, byBase, resolvedLink, appendable, upsertRows, _iteratorNormalCompletion, _didIteratorError, _iteratorError, _iterator, _step, _step_value, path, targets, _iteratorNormalCompletion1, _didIteratorError1, _iteratorError1, _iterator1, _step1, _step_value1, target, embed, targetBase, dst;
834
816
  return _ts_generator(this, function(_state) {
835
817
  switch(_state.label){
836
818
  case 0:
@@ -878,8 +860,25 @@ var links = {
878
860
  removedWithLinks.set(delta, true);
879
861
  _state.label = 3;
880
862
  case 3:
881
- // Upsert preserves dst on surviving rows; only new rows start at NULL. A same-note anchor
882
- // gets no target_base, so SQL's `IN` excludes it from resolveIncremental's lookup.
863
+ // Cold build: every path is in `added`, so pathSet/byBase cover the same complete tree
864
+ // afterReconcile's large branch would otherwise re-derive; resolve dst here instead.
865
+ cold = delta.files.length === 0 || delta.added.length === delta.files.length;
866
+ if (cold) {
867
+ pathSet = new Set(delta.files.map(function(f) {
868
+ return f.relPath;
869
+ }));
870
+ byBase = buildByBase(delta.files);
871
+ resolveNew = function resolveNew(src, target) {
872
+ return resolveTarget(src, target, pathSet, byBase);
873
+ };
874
+ }
875
+ resolvedLink = false;
876
+ // Upsert preserves dst on surviving rows; a cold build resolves new rows, else they start NULL.
877
+ // A same-note anchor gets no target_base, so SQL's `IN` excludes it from resolveIncremental's lookup.
878
+ // Split, unlike the other features: this hook has no remove() pass behind it (see above), so
879
+ // only a path in `added` is guaranteed to have no row yet. The rest genuinely conflict and the
880
+ // upsert is what preserves their `dst`.
881
+ appendable = [];
883
882
  upsertRows = [];
884
883
  _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
885
884
  try {
@@ -889,10 +888,32 @@ var links = {
889
888
  try {
890
889
  for(_iterator1 = targets[Symbol.iterator](); !(_iteratorNormalCompletion1 = (_step1 = _iterator1.next()).done); _iteratorNormalCompletion1 = true){
891
890
  _step_value1 = _step1.value, target = _step_value1.target, embed = _step_value1.embed;
892
- upsertRows.push([
891
+ targetBase = target.startsWith('#') ? null : baseKey(cleanTarget(target));
892
+ if (!addedSet.has(path)) {
893
+ upsertRows.push([
894
+ path,
895
+ target,
896
+ targetBase,
897
+ embed ? 1 : 0
898
+ ]);
899
+ continue;
900
+ }
901
+ if (!resolveNew) {
902
+ appendable.push([
903
+ path,
904
+ target,
905
+ targetBase,
906
+ embed ? 1 : 0
907
+ ]);
908
+ continue;
909
+ }
910
+ dst = resolveNew(path, target);
911
+ if (dst !== null) resolvedLink = true;
912
+ appendable.push([
893
913
  path,
894
914
  target,
895
- target.startsWith('#') ? null : baseKey(cleanTarget(target)),
915
+ targetBase,
916
+ dst,
896
917
  embed ? 1 : 0
897
918
  ]);
898
919
  }
@@ -925,18 +946,25 @@ var links = {
925
946
  }
926
947
  }
927
948
  }
949
+ return [
950
+ 4,
951
+ (0, _sharedts.appendRows)(db, 'links', resolveNew ? LINK_COLUMNS_RESOLVED : LINK_COLUMNS, resolveNew ? UPSERT_LINK_SQL_RESOLVED : UPSERT_LINK_SQL, appendable)
952
+ ];
953
+ case 4:
954
+ _state.sent();
928
955
  if (!(upsertRows.length > 0)) return [
929
956
  3,
930
- 5
957
+ 6
931
958
  ];
932
959
  return [
933
960
  4,
934
- db.runBatch('INSERT INTO links (src, target, target_base, dst, embed) VALUES (?, ?, ?, NULL, ?) ON CONFLICT(src, target, embed) DO UPDATE SET target_base = excluded.target_base', upsertRows)
961
+ db.runBatch(UPSERT_LINK_SQL, upsertRows)
935
962
  ];
936
- case 4:
937
- _state.sent();
938
- _state.label = 5;
939
963
  case 5:
964
+ _state.sent();
965
+ _state.label = 6;
966
+ case 6:
967
+ if (cold) coldResolved.set(delta, resolvedLink);
940
968
  return [
941
969
  2
942
970
  ];
@@ -946,7 +974,7 @@ var links = {
946
974
  },
947
975
  afterReconcile: function afterReconcile(db, delta) {
948
976
  return _async_to_generator(function() {
949
- var _removedWithLinks_get, removeHadLinks, large, dstChanged, _tmp;
977
+ var _removedWithLinks_get, removeHadLinks, large, _coldResolved_get, dstChanged;
950
978
  return _ts_generator(this, function(_state) {
951
979
  switch(_state.label){
952
980
  case 0:
@@ -956,30 +984,20 @@ var links = {
956
984
  // A cold build is the only case a full pass wins. Measured 2026-09-01 on 6,566 notes, resolve
957
985
  // time alone: full is flat near 90ms at any churn, incremental 7ms at 10 files and 30ms at 2,000.
958
986
  large = delta.files.length === 0 || delta.added.length === delta.files.length;
959
- if (!large) return [
960
- 3,
961
- 2
962
- ];
963
- return [
964
- 4,
965
- resolveAll(db, delta.files)
966
- ];
967
- case 1:
968
- _tmp = _state.sent();
969
- return [
970
- 3,
971
- 4
972
- ];
973
- case 2:
987
+ // store() already resolved dst inline on this cold build (same test), so nothing is left to update.
988
+ if (large) {
989
+ ;
990
+ delta.linksChanged = ((_coldResolved_get = coldResolved.get(delta)) !== null && _coldResolved_get !== void 0 ? _coldResolved_get : false) || removeHadLinks;
991
+ return [
992
+ 2
993
+ ];
994
+ }
974
995
  return [
975
996
  4,
976
997
  resolveIncremental(db, delta)
977
998
  ];
978
- case 3:
979
- _tmp = _state.sent();
980
- _state.label = 4;
981
- case 4:
982
- dstChanged = _tmp;
999
+ case 1:
1000
+ dstChanged = _state.sent();
983
1001
  delta.linksChanged = dstChanged || removeHadLinks;
984
1002
  return [
985
1003
  2
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/links.ts"],"sourcesContent":["import posix from 'node:path/posix';\nimport type { FileStat } from '../scan/index.ts';\nimport type { Connection } from '../store/types.ts';\nimport { maskRegions } from './fences.ts';\nimport type { ExtractedDoc, Feature, ReconcileDelta } from './types.ts';\n\n// links(src, target, target_base, dst, embed): target as written, target_base its indexed baseKey, dst the resolved path or NULL (a queryable dead link).\n// embed distinguishes `![[x]]`/`![](x.md)` from `[[x]]`/`[](x.md)` -- Obsidian's grain: a target both linked and embedded in one note is two rows.\n\n// Alias stripped after |; a leading # keeps the anchor as the target (self-link needs a\n// heading name), else the anchor splits off. Null = not a link.\nfunction parseWikilinkInner(inner: string): string | null {\n const beforeAlias = inner.split('|')[0].trim();\n if (beforeAlias.startsWith('#')) return beforeAlias.length > 1 ? beforeAlias : null;\n const target = beforeAlias.split('#')[0].trim();\n return target || null;\n}\n\n// A frontmatter value counts only when it is exactly a wikilink (\"[[X]]\") -- mid-string and\n// ![[...]] forms don't count there, per Obsidian's rule (probe-verified).\nfunction extract(_raw: string, rawBody: string, _search?: { title: string; summary: string }, data?: Record<string, unknown>): Array<{ target: string; embed: boolean }> {\n const body = /\\[\\[|\\]\\(/.test(rawBody) ? maskRegions(rawBody) : rawBody;\n const seen = new Set<string>();\n const results: Array<{ target: string; embed: boolean }> = [];\n const add = (target: string, embed: boolean) => {\n const key = `${target}\\0${embed ? '1' : '0'}`;\n if (seen.has(key)) return;\n seen.add(key);\n results.push({ target, embed });\n };\n // To the first ]], as Obsidian parses it, so a heading or alias holding a lone ] still\n // matches; anchor and alias split off in parseWikilinkInner.\n for (const m of body.matchAll(/\\[\\[(.*?)\\]\\]/g)) {\n const target = parseWikilinkInner(m[1]);\n if (target) add(target, body[(m.index ?? 0) - 1] === '!');\n }\n // Any internal destination, not only .md-suffixed: \\](Zektor) resolves like [[Zektor]] and\n // \\](#anchor) is a self-edge. External URLs and titled links (dest stops at whitespace) skip.\n for (const m of body.matchAll(/(!)?\\[[^\\]]*\\]\\(((?:[^()\\s]|\\([^()\\s]*\\))+)(?:\\s+(?:\"[^)]*\"|'[^)]*'))?\\)/g)) {\n const dest = m[2].trim();\n // External: a URI scheme prefix (mailto:, tel:, data:, https:...), protocol-relative\n // //host, a www. shorthand, or a scheme anywhere (the malformed double-paren case).\n if (!dest || dest.includes('://')) continue;\n if (/^([a-z][a-z0-9+.-]*:|\\/\\/|www\\.)/i.test(dest)) continue;\n const target = dest.startsWith('#') ? dest : dest.split('#')[0];\n if (target) add(target, m[1] === '!');\n }\n for (const value of Object.values(data ?? {})) {\n for (const item of Array.isArray(value) ? value : [value]) {\n if (typeof item !== 'string') continue;\n const m = /^\\[\\[(.*)\\]\\]$/.exec(item.trim());\n if (!m) continue;\n const target = parseWikilinkInner(m[1]);\n if (target) add(target, false);\n }\n }\n return results;\n}\n\nfunction cleanTarget(target: string): string {\n return target.replace(/\\\\/g, '/').replace(/^\\.\\//, '');\n}\n\nfunction baseKey(path: string): string {\n return posix.basename(path).replace(/\\.md$/i, '').toLowerCase();\n}\n\n// Obsidian-style: exact relative path (with/without .md), path relative to the linking note's\n// directory, then basename match (shortest path wins ties, verified against a cold-loaded vault).\nfunction resolveTarget(src: string, target: string, pathSet: Set<string>, byBase: Map<string, string[]>): string | null {\n // [[#Heading]]: a same-note anchor, always the note itself -- never depends on other files.\n if (target.startsWith('#')) return src;\n const clean = cleanTarget(target);\n const fromSrc = posix.normalize(posix.join(posix.dirname(src), clean));\n for (const candidate of [clean, `${clean}.md`, fromSrc, `${fromSrc}.md`]) {\n if (pathSet.has(candidate)) return candidate;\n }\n const candidates = byBase.get(baseKey(clean));\n if (!candidates) return null;\n // The linking note itself wins a basename collision (Obsidian resolves to self before the\n // shortest-path rule -- cold-load verified on the hub corpus).\n return candidates.includes(src) ? src : candidates[0];\n}\n\n// Shortest path wins a basename collision; equal lengths fall back to lexicographic, our own\n// deterministic tiebreak for a case Obsidian itself leaves registration-order-dependent.\nfunction buildByBase(files: FileStat[]): Map<string, string[]> {\n const byBase = new Map<string, string[]>();\n const paths = files.map((f) => f.relPath).sort((a, b) => a.length - b.length || (a < b ? -1 : a > b ? 1 : 0));\n for (const path of paths) {\n const key = baseKey(path);\n const list = byBase.get(key);\n if (list) list.push(path);\n else byBase.set(key, [path]);\n }\n return byBase;\n}\n\ninterface LinkRow {\n src: string;\n target: string;\n dst: string | null;\n embed: number;\n}\n\n// resolveTarget applied to `rows`, returning only the rows whose dst changed, so the caller\n// can write them in one runBatch call.\nfunction resolveRows(rows: LinkRow[], pathSet: Set<string>, byBase: Map<string, string[]>): LinkRow[] {\n const changed: LinkRow[] = [];\n for (const row of rows) {\n const dst = resolveTarget(row.src, row.target, pathSet, byBase);\n if (dst !== row.dst) changed.push({ ...row, dst });\n }\n return changed;\n}\n\nasync function writeResolved(db: Connection, rows: LinkRow[]): Promise<void> {\n if (rows.length === 0) return;\n await db.runBatch(\n 'UPDATE links SET dst = ? WHERE src = ? AND target = ?',\n rows.map((r) => [r.dst, r.src, r.target])\n );\n}\n\n// A new or deleted file can change any note's resolution, so re-resolve the whole table.\n// Fallback for cold builds and large deltas -- see afterReconcile's threshold.\nasync function resolveAll(db: Connection, files: FileStat[]): Promise<boolean> {\n const pathSet = new Set(files.map((f) => f.relPath));\n const byBase = buildByBase(files);\n const stmt = await db.prepare('SELECT src, target, dst, embed FROM links');\n const rows = (await stmt.all()) as unknown as LinkRow[];\n const changed = resolveRows(rows, pathSet, byBase);\n await writeResolved(db, changed);\n return changed.length > 0;\n}\n\n// Chunked so a large-but-under-threshold delta never exceeds SQLite's bound-variable limit\n// (SQLITE_MAX_VARIABLE_NUMBER, 32766); harmless headroom on DuckDB, which has no such cap.\nasync function selectIn(db: Connection, column: string, keys: string[]): Promise<LinkRow[]> {\n const rows: LinkRow[] = [];\n for (let i = 0; i < keys.length; i += 500) {\n const chunk = keys.slice(i, i + 500);\n const stmt = await db.prepare(`SELECT src, target, dst, embed FROM links WHERE ${column} IN (${chunk.map(() => '?').join(', ')})`);\n rows.push(...((await stmt.all(...chunk)) as unknown as LinkRow[]));\n }\n return rows;\n}\n\n// Re-resolves only what this reconcile could have touched: re-stored sources, links whose\n// target basename matches an added/vanished file, and links whose dst vanished -- all indexed.\nasync function resolveIncremental(db: Connection, delta: ReconcileDelta): Promise<boolean> {\n const pathSet = new Set(delta.files.map((f) => f.relPath));\n const byBase = buildByBase(delta.files);\n\n const changedBasenames = new Set<string>();\n for (const p of delta.added) changedBasenames.add(baseKey(p));\n for (const p of delta.vanished) changedBasenames.add(baseKey(p));\n\n // \\0 can't appear in a path or target, so the key never collides; a space can.\n const candidates = new Map<string, LinkRow>();\n const collect = (rows: LinkRow[]) => {\n for (const row of rows) candidates.set(`${row.src}\\0${row.target}\\0${row.embed}`, row);\n };\n collect(await selectIn(db, 'src', delta.reparsed));\n collect(await selectIn(db, 'target_base', [...changedBasenames]));\n collect(await selectIn(db, 'dst', delta.vanished));\n\n const changed = resolveRows([...candidates.values()], pathSet, byBase);\n await writeResolved(db, changed);\n return changed.length > 0;\n}\n\n// One edge per (src, target); an embed row is excluded only when the same pair also exists as\n// a link. Self-edges are dropped so PageRank mass is not self-recycled.\nexport const LINK_EDGES_SQL = `SELECT src, dst FROM links WHERE dst IS NOT NULL AND embed = 0 AND src != dst\n UNION ALL\n SELECT src, dst FROM links l WHERE dst IS NOT NULL AND embed = 1 AND src != dst\n AND NOT EXISTS (SELECT 1 FROM links l0 WHERE l0.src = l.src AND l0.target = l.target AND l0.embed = 0)`;\n\nexport function toEdges(rows: Array<{ src: string; dst: string }>): [string, string][] {\n return rows.map((r) => [r.src, r.dst]);\n}\n\n// Resolved edges, for rank (inside reconcile's afterReconcile pass).\nexport async function linkEdges(db: Connection): Promise<[string, string][]> {\n const stmt = await db.prepare(LINK_EDGES_SQL);\n return toEdges((await stmt.all()) as Array<{ src: string; dst: string }>);\n}\n\n// Records whether remove()/store() deleted any link row, since afterReconcile runs after both\n// and dstChanged alone misses the reparsed-to-zero-links case. Keyed on delta so state dies with it.\nconst removedWithLinks = new WeakMap<ReconcileDelta, boolean>();\n\nexport const links: Feature = {\n name: 'links',\n async schema(db) {\n await db.exec('CREATE TABLE IF NOT EXISTS links (src TEXT, target TEXT, target_base TEXT, dst TEXT, embed INTEGER, PRIMARY KEY (src, target, embed))');\n await db.exec('CREATE INDEX IF NOT EXISTS links_dst ON links(dst)');\n await db.exec('CREATE INDEX IF NOT EXISTS links_target_base ON links(target_base)');\n },\n extract,\n // Reparsed docs are diffed in store() instead of wiped here: delete-and-reinsert resets dst\n // to NULL, making every touch-only reparse look like an edge change and recompute PageRank.\n async remove(db, paths, delta) {\n const vanishedSet = new Set(delta.vanished);\n const vanished = paths.filter((p) => vanishedSet.has(p));\n if (vanished.length === 0) return;\n const existingRows = await selectIn(db, 'src', vanished);\n await db.runBatch(\n 'DELETE FROM links WHERE src = ?',\n vanished.map((p) => [p])\n );\n if (existingRows.length > 0) removedWithLinks.set(delta, true);\n },\n async store(db, docs: ExtractedDoc[], delta) {\n const byPath = new Map(docs.map((d) => [d.path, d.extracted as Array<{ target: string; embed: boolean }>]));\n const addedSet = new Set(delta.added);\n const reparsedExisting = docs.map((d) => d.path).filter((p) => !addedSet.has(p));\n\n // Stale rows (target/embed pairs missing from the new parse) are diffed in JS from one\n // bulk SELECT, so this pass costs one query plus one batched DELETE regardless of file count.\n const existingRows = await selectIn(db, 'src', reparsedExisting);\n const staleRows = existingRows.filter((row) => {\n const targets = byPath.get(row.src) ?? [];\n return !targets.some((t) => t.target === row.target && (t.embed ? 1 : 0) === row.embed);\n });\n if (staleRows.length > 0) {\n await db.runBatch(\n 'DELETE FROM links WHERE src = ? AND target = ? AND embed = ?',\n staleRows.map((r) => [r.src, r.target, r.embed])\n );\n removedWithLinks.set(delta, true);\n }\n\n // Upsert preserves dst on surviving rows; only new rows start at NULL. A same-note anchor\n // gets no target_base, so SQL's `IN` excludes it from resolveIncremental's lookup.\n const upsertRows: unknown[][] = [];\n for (const [path, targets] of byPath) {\n for (const { target, embed } of targets) upsertRows.push([path, target, target.startsWith('#') ? null : baseKey(cleanTarget(target)), embed ? 1 : 0]);\n }\n if (upsertRows.length > 0) {\n await db.runBatch('INSERT INTO links (src, target, target_base, dst, embed) VALUES (?, ?, ?, NULL, ?) ON CONFLICT(src, target, embed) DO UPDATE SET target_base = excluded.target_base', upsertRows);\n }\n },\n async afterReconcile(db, delta) {\n // Any deleted rows mean the edge set shrank -- whether the file vanished or was reparsed\n // down to fewer/no links.\n const removeHadLinks = removedWithLinks.get(delta) ?? false;\n\n // A cold build is the only case a full pass wins. Measured 2026-09-01 on 6,566 notes, resolve\n // time alone: full is flat near 90ms at any churn, incremental 7ms at 10 files and 30ms at 2,000.\n const large = delta.files.length === 0 || delta.added.length === delta.files.length;\n\n const dstChanged = large ? await resolveAll(db, delta.files) : await resolveIncremental(db, delta);\n delta.linksChanged = dstChanged || removeHadLinks;\n },\n};\n"],"names":["LINK_EDGES_SQL","linkEdges","links","toEdges","parseWikilinkInner","inner","beforeAlias","split","trim","startsWith","length","target","extract","_raw","rawBody","_search","data","body","test","maskRegions","seen","Set","results","add","embed","key","has","push","matchAll","m","index","dest","includes","Object","values","value","Array","isArray","item","exec","cleanTarget","replace","baseKey","path","posix","basename","toLowerCase","resolveTarget","src","pathSet","byBase","clean","fromSrc","normalize","join","dirname","candidate","candidates","get","buildByBase","files","Map","paths","map","f","relPath","sort","a","b","list","set","resolveRows","rows","changed","row","dst","writeResolved","db","runBatch","r","resolveAll","stmt","prepare","all","selectIn","column","keys","i","chunk","slice","resolveIncremental","delta","changedBasenames","p","collect","added","vanished","reparsed","removedWithLinks","WeakMap","name","schema","remove","vanishedSet","existingRows","filter","store","docs","byPath","addedSet","reparsedExisting","staleRows","upsertRows","targets","d","extracted","some","t","afterReconcile","removeHadLinks","large","dstChanged","linksChanged"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/links.ts"],"sourcesContent":["import posix from 'node:path/posix';\nimport type { FileStat } from '../scan/index.ts';\nimport { appendRows } from '../store/shared.ts';\nimport type { Connection } from '../store/types.ts';\nimport { maskRegions } from './fences.ts';\nimport type { ExtractedDoc, Feature, ReconcileDelta } from './types.ts';\n\n// links(src, target, target_base, dst, embed): target as written, target_base its indexed baseKey, dst the resolved path or NULL (a queryable dead link).\n// embed distinguishes `![[x]]`/`![](x.md)` from `[[x]]`/`[](x.md)` -- Obsidian's grain: a target both linked and embedded in one note is two rows.\n\n// Alias stripped after |; a leading # keeps the anchor as the target (self-link needs a\n// heading name), else the anchor splits off. Null = not a link.\nfunction parseWikilinkInner(inner: string): string | null {\n const beforeAlias = inner.split('|')[0].trim();\n if (beforeAlias.startsWith('#')) return beforeAlias.length > 1 ? beforeAlias : null;\n const target = beforeAlias.split('#')[0].trim();\n return target || null;\n}\n\n// A frontmatter value counts only when it is exactly a wikilink (\"[[X]]\") -- mid-string and\n// ![[...]] forms don't count there, per Obsidian's rule (probe-verified).\nfunction extract(_raw: string, rawBody: string, _search?: { title: string; summary: string }, data?: Record<string, unknown>): Array<{ target: string; embed: boolean }> {\n const body = /\\[\\[|\\]\\(/.test(rawBody) ? maskRegions(rawBody) : rawBody;\n const seen = new Set<string>();\n const results: Array<{ target: string; embed: boolean }> = [];\n const add = (target: string, embed: boolean) => {\n const key = `${target}\\0${embed ? '1' : '0'}`;\n if (seen.has(key)) return;\n seen.add(key);\n results.push({ target, embed });\n };\n // To the first ]], as Obsidian parses it, so a heading or alias holding a lone ] still\n // matches; anchor and alias split off in parseWikilinkInner.\n for (const m of body.matchAll(/\\[\\[(.*?)\\]\\]/g)) {\n const target = parseWikilinkInner(m[1]);\n if (target) add(target, body[(m.index ?? 0) - 1] === '!');\n }\n // Any internal destination, not only .md-suffixed: \\](Zektor) resolves like [[Zektor]] and\n // \\](#anchor) is a self-edge. External URLs and titled links (dest stops at whitespace) skip.\n for (const m of body.matchAll(/(!)?\\[[^\\]]*\\]\\(((?:[^()\\s]|\\([^()\\s]*\\))+)(?:\\s+(?:\"[^)]*\"|'[^)]*'))?\\)/g)) {\n const dest = m[2].trim();\n // External: a URI scheme prefix (mailto:, tel:, data:, https:...), protocol-relative\n // //host, a www. shorthand, or a scheme anywhere (the malformed double-paren case).\n if (!dest || dest.includes('://')) continue;\n if (/^([a-z][a-z0-9+.-]*:|\\/\\/|www\\.)/i.test(dest)) continue;\n const target = dest.startsWith('#') ? dest : dest.split('#')[0];\n if (target) add(target, m[1] === '!');\n }\n for (const value of Object.values(data ?? {})) {\n for (const item of Array.isArray(value) ? value : [value]) {\n if (typeof item !== 'string') continue;\n const m = /^\\[\\[(.*)\\]\\]$/.exec(item.trim());\n if (!m) continue;\n const target = parseWikilinkInner(m[1]);\n if (target) add(target, false);\n }\n }\n return results;\n}\n\nfunction cleanTarget(target: string): string {\n return target.replace(/\\\\/g, '/').replace(/^\\.\\//, '');\n}\n\nfunction baseKey(path: string): string {\n return posix.basename(path).replace(/\\.md$/i, '').toLowerCase();\n}\n\n// Obsidian-style: exact relative path (with/without .md), path relative to the linking note's\n// directory, then basename match (shortest path wins ties, verified against a cold-loaded vault).\nfunction resolveTarget(src: string, target: string, pathSet: Set<string>, byBase: Map<string, string[]>): string | null {\n // [[#Heading]]: a same-note anchor, always the note itself -- never depends on other files.\n if (target.startsWith('#')) return src;\n const clean = cleanTarget(target);\n const fromSrc = posix.normalize(posix.join(posix.dirname(src), clean));\n for (const candidate of [clean, `${clean}.md`, fromSrc, `${fromSrc}.md`]) {\n if (pathSet.has(candidate)) return candidate;\n }\n const candidates = byBase.get(baseKey(clean));\n if (!candidates) return null;\n // The linking note itself wins a basename collision (Obsidian resolves to self before the\n // shortest-path rule -- cold-load verified on the hub corpus).\n return candidates.includes(src) ? src : candidates[0];\n}\n\n// Shortest path wins a basename collision; equal lengths fall back to lexicographic, our own\n// deterministic tiebreak for a case Obsidian itself leaves registration-order-dependent.\nfunction buildByBase(files: FileStat[]): Map<string, string[]> {\n const byBase = new Map<string, string[]>();\n const paths = files.map((f) => f.relPath).sort((a, b) => a.length - b.length || (a < b ? -1 : a > b ? 1 : 0));\n for (const path of paths) {\n const key = baseKey(path);\n const list = byBase.get(key);\n if (list) list.push(path);\n else byBase.set(key, [path]);\n }\n return byBase;\n}\n\ninterface LinkRow {\n src: string;\n target: string;\n dst: string | null;\n embed: number;\n}\n\n// resolveTarget applied to `rows`, returning only the rows whose dst changed, so the caller\n// can write them in one runBatch call.\nfunction resolveRows(rows: LinkRow[], pathSet: Set<string>, byBase: Map<string, string[]>): LinkRow[] {\n const changed: LinkRow[] = [];\n for (const row of rows) {\n const dst = resolveTarget(row.src, row.target, pathSet, byBase);\n if (dst !== row.dst) changed.push({ ...row, dst });\n }\n return changed;\n}\n\nasync function writeResolved(db: Connection, rows: LinkRow[]): Promise<void> {\n if (rows.length === 0) return;\n await db.runBatch(\n 'UPDATE links SET dst = ? WHERE src = ? AND target = ?',\n rows.map((r) => [r.dst, r.src, r.target])\n );\n}\n\n// Chunked so a large-but-under-threshold delta never exceeds SQLite's bound-variable limit\n// (SQLITE_MAX_VARIABLE_NUMBER, 32766); harmless headroom on DuckDB, which has no such cap.\nasync function selectIn(db: Connection, column: string, keys: string[]): Promise<LinkRow[]> {\n const rows: LinkRow[] = [];\n for (let i = 0; i < keys.length; i += 500) {\n const chunk = keys.slice(i, i + 500);\n const stmt = await db.prepare(`SELECT src, target, dst, embed FROM links WHERE ${column} IN (${chunk.map(() => '?').join(', ')})`);\n rows.push(...((await stmt.all(...chunk)) as unknown as LinkRow[]));\n }\n return rows;\n}\n\n// Re-resolves only what this reconcile could have touched: re-stored sources, links whose\n// target basename matches an added/vanished file, and links whose dst vanished -- all indexed.\nasync function resolveIncremental(db: Connection, delta: ReconcileDelta): Promise<boolean> {\n const pathSet = new Set(delta.files.map((f) => f.relPath));\n const byBase = buildByBase(delta.files);\n\n const changedBasenames = new Set<string>();\n for (const p of delta.added) changedBasenames.add(baseKey(p));\n for (const p of delta.vanished) changedBasenames.add(baseKey(p));\n\n // \\0 can't appear in a path or target, so the key never collides; a space can.\n const candidates = new Map<string, LinkRow>();\n const collect = (rows: LinkRow[]) => {\n for (const row of rows) candidates.set(`${row.src}\\0${row.target}\\0${row.embed}`, row);\n };\n collect(await selectIn(db, 'src', delta.reparsed));\n collect(await selectIn(db, 'target_base', [...changedBasenames]));\n collect(await selectIn(db, 'dst', delta.vanished));\n\n const changed = resolveRows([...candidates.values()], pathSet, byBase);\n await writeResolved(db, changed);\n return changed.length > 0;\n}\n\n// One edge per (src, target); an embed row is excluded only when the same pair also exists as\n// a link. Self-edges are dropped so PageRank mass is not self-recycled.\nexport const LINK_EDGES_SQL = `SELECT src, dst FROM links WHERE dst IS NOT NULL AND embed = 0 AND src != dst\n UNION ALL\n SELECT src, dst FROM links l WHERE dst IS NOT NULL AND embed = 1 AND src != dst\n AND NOT EXISTS (SELECT 1 FROM links l0 WHERE l0.src = l.src AND l0.target = l.target AND l0.embed = 0)`;\n\nexport function toEdges(rows: Array<{ src: string; dst: string }>): [string, string][] {\n return rows.map((r) => [r.src, r.dst]);\n}\n\n// Resolved edges, for rank (inside reconcile's afterReconcile pass).\nexport async function linkEdges(db: Connection): Promise<[string, string][]> {\n const stmt = await db.prepare(LINK_EDGES_SQL);\n return toEdges((await stmt.all()) as Array<{ src: string; dst: string }>);\n}\n\n// Records whether remove()/store() deleted any link row, since afterReconcile runs after both\n// and dstChanged alone misses the reparsed-to-zero-links case. Keyed on delta so state dies with it.\nconst removedWithLinks = new WeakMap<ReconcileDelta, boolean>();\n\n// Cold build only: whether store() resolved at least one row to a non-null dst, so afterReconcile\n// can report linksChanged without re-reading the table it just wrote.\nconst coldResolved = new WeakMap<ReconcileDelta, boolean>();\n\n// dst stays NULL here on an incremental store (resolution runs afterward); a cold build resolves\n// it inline in store() instead, through the *_RESOLVED column list and SQL below.\nconst LINK_COLUMNS = ['src', 'target', 'target_base', 'embed'];\nconst UPSERT_LINK_SQL = 'INSERT INTO links (src, target, target_base, dst, embed) VALUES (?, ?, ?, NULL, ?) ON CONFLICT(src, target, embed) DO UPDATE SET target_base = excluded.target_base';\nconst LINK_COLUMNS_RESOLVED = ['src', 'target', 'target_base', 'dst', 'embed'];\nconst UPSERT_LINK_SQL_RESOLVED = 'INSERT INTO links (src, target, target_base, dst, embed) VALUES (?, ?, ?, ?, ?) ON CONFLICT(src, target, embed) DO UPDATE SET target_base = excluded.target_base';\n\nexport const links: Feature = {\n name: 'links',\n async schema(db) {\n await db.exec('CREATE TABLE IF NOT EXISTS links (src TEXT, target TEXT, target_base TEXT, dst TEXT, embed INTEGER, PRIMARY KEY (src, target, embed))');\n await db.exec('CREATE INDEX IF NOT EXISTS links_dst ON links(dst)');\n await db.exec('CREATE INDEX IF NOT EXISTS links_target_base ON links(target_base)');\n },\n extract,\n // Reparsed docs are diffed in store() instead of wiped here: delete-and-reinsert resets dst\n // to NULL, making every touch-only reparse look like an edge change and recompute PageRank.\n async remove(db, paths, delta) {\n const vanishedSet = new Set(delta.vanished);\n const vanished = paths.filter((p) => vanishedSet.has(p));\n if (vanished.length === 0) return;\n const existingRows = await selectIn(db, 'src', vanished);\n await db.runBatch(\n 'DELETE FROM links WHERE src = ?',\n vanished.map((p) => [p])\n );\n if (existingRows.length > 0) removedWithLinks.set(delta, true);\n },\n async store(db, docs: ExtractedDoc[], delta) {\n const byPath = new Map(docs.map((d) => [d.path, d.extracted as Array<{ target: string; embed: boolean }>]));\n const addedSet = new Set(delta.added);\n const reparsedExisting = docs.map((d) => d.path).filter((p) => !addedSet.has(p));\n\n // Stale rows (target/embed pairs missing from the new parse) are diffed in JS from one\n // bulk SELECT, so this pass costs one query plus one batched DELETE regardless of file count.\n const existingRows = await selectIn(db, 'src', reparsedExisting);\n const staleRows = existingRows.filter((row) => {\n const targets = byPath.get(row.src) ?? [];\n return !targets.some((t) => t.target === row.target && (t.embed ? 1 : 0) === row.embed);\n });\n if (staleRows.length > 0) {\n await db.runBatch(\n 'DELETE FROM links WHERE src = ? AND target = ? AND embed = ?',\n staleRows.map((r) => [r.src, r.target, r.embed])\n );\n removedWithLinks.set(delta, true);\n }\n\n // Cold build: every path is in `added`, so pathSet/byBase cover the same complete tree\n // afterReconcile's large branch would otherwise re-derive; resolve dst here instead.\n const cold = delta.files.length === 0 || delta.added.length === delta.files.length;\n let resolveNew: ((src: string, target: string) => string | null) | undefined;\n if (cold) {\n const pathSet = new Set(delta.files.map((f) => f.relPath));\n const byBase = buildByBase(delta.files);\n resolveNew = (src, target) => resolveTarget(src, target, pathSet, byBase);\n }\n let resolvedLink = false;\n\n // Upsert preserves dst on surviving rows; a cold build resolves new rows, else they start NULL.\n // A same-note anchor gets no target_base, so SQL's `IN` excludes it from resolveIncremental's lookup.\n // Split, unlike the other features: this hook has no remove() pass behind it (see above), so\n // only a path in `added` is guaranteed to have no row yet. The rest genuinely conflict and the\n // upsert is what preserves their `dst`.\n const appendable: unknown[][] = [];\n const upsertRows: unknown[][] = [];\n for (const [path, targets] of byPath) {\n for (const { target, embed } of targets) {\n const targetBase = target.startsWith('#') ? null : baseKey(cleanTarget(target));\n if (!addedSet.has(path)) {\n upsertRows.push([path, target, targetBase, embed ? 1 : 0]);\n continue;\n }\n if (!resolveNew) {\n appendable.push([path, target, targetBase, embed ? 1 : 0]);\n continue;\n }\n const dst = resolveNew(path, target);\n if (dst !== null) resolvedLink = true;\n appendable.push([path, target, targetBase, dst, embed ? 1 : 0]);\n }\n }\n await appendRows(db, 'links', resolveNew ? LINK_COLUMNS_RESOLVED : LINK_COLUMNS, resolveNew ? UPSERT_LINK_SQL_RESOLVED : UPSERT_LINK_SQL, appendable);\n if (upsertRows.length > 0) await db.runBatch(UPSERT_LINK_SQL, upsertRows);\n if (cold) coldResolved.set(delta, resolvedLink);\n },\n async afterReconcile(db, delta) {\n // Any deleted rows mean the edge set shrank -- whether the file vanished or was reparsed\n // down to fewer/no links.\n const removeHadLinks = removedWithLinks.get(delta) ?? false;\n\n // A cold build is the only case a full pass wins. Measured 2026-09-01 on 6,566 notes, resolve\n // time alone: full is flat near 90ms at any churn, incremental 7ms at 10 files and 30ms at 2,000.\n const large = delta.files.length === 0 || delta.added.length === delta.files.length;\n // store() already resolved dst inline on this cold build (same test), so nothing is left to update.\n if (large) {\n delta.linksChanged = (coldResolved.get(delta) ?? false) || removeHadLinks;\n return;\n }\n\n const dstChanged = await resolveIncremental(db, delta);\n delta.linksChanged = dstChanged || removeHadLinks;\n },\n};\n"],"names":["LINK_EDGES_SQL","linkEdges","links","toEdges","parseWikilinkInner","inner","beforeAlias","split","trim","startsWith","length","target","extract","_raw","rawBody","_search","data","body","test","maskRegions","seen","Set","results","add","embed","key","has","push","matchAll","m","index","dest","includes","Object","values","value","Array","isArray","item","exec","cleanTarget","replace","baseKey","path","posix","basename","toLowerCase","resolveTarget","src","pathSet","byBase","clean","fromSrc","normalize","join","dirname","candidate","candidates","get","buildByBase","files","Map","paths","map","f","relPath","sort","a","b","list","set","resolveRows","rows","changed","row","dst","writeResolved","db","runBatch","r","selectIn","column","keys","i","stmt","chunk","slice","prepare","all","resolveIncremental","delta","changedBasenames","p","collect","added","vanished","reparsed","removedWithLinks","WeakMap","coldResolved","LINK_COLUMNS","UPSERT_LINK_SQL","LINK_COLUMNS_RESOLVED","UPSERT_LINK_SQL_RESOLVED","name","schema","remove","vanishedSet","existingRows","filter","store","docs","byPath","addedSet","reparsedExisting","staleRows","cold","resolveNew","resolvedLink","appendable","upsertRows","targets","targetBase","d","extracted","some","t","appendRows","afterReconcile","removeHadLinks","large","dstChanged","linksChanged"],"mappings":"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"}
@@ -10,6 +10,7 @@ Object.defineProperty(exports, "sections", {
10
10
  });
11
11
  var _indexts = require("../chunk/index.js");
12
12
  var _frontmatterts = require("../scan/frontmatter.js");
13
+ var _sharedts = require("../store/shared.js");
13
14
  function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
14
15
  try {
15
16
  var info = gen[key](arg);
@@ -159,6 +160,15 @@ function sectionsFromBlocks(blocks, raw, body) {
159
160
  }
160
161
  return found;
161
162
  }
163
+ var SECTION_COLUMNS = [
164
+ 'path',
165
+ 'idx',
166
+ 'level',
167
+ 'heading',
168
+ 'start_line',
169
+ 'end_line',
170
+ 'tokens'
171
+ ];
162
172
  var sections = {
163
173
  name: 'sections',
164
174
  schema: function schema(db) {
@@ -245,15 +255,13 @@ var sections = {
245
255
  }
246
256
  }
247
257
  }
248
- if (rows.length === 0) return [
249
- 2
250
- ];
251
258
  // DO NOTHING, not a bare INSERT: reconcile's added/touched split is decided before this
252
259
  // write's lock, so a path called "added" here can already have this row from a concurrent
253
- // reconcile that committed first -- same file, same parse, same row.
260
+ // reconcile that committed first -- same file, same parse, same row. A store with an append
261
+ // path has no second writer, and remove() cleared every touched path above, so it skips the guard.
254
262
  return [
255
263
  4,
256
- db.runBatch('INSERT INTO sections ("path", idx, level, heading, start_line, end_line, tokens) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT("path", idx) DO NOTHING', rows)
264
+ (0, _sharedts.appendRows)(db, 'sections', SECTION_COLUMNS, 'INSERT INTO sections ("path", idx, level, heading, start_line, end_line, tokens) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT("path", idx) DO NOTHING', rows)
257
265
  ];
258
266
  case 1:
259
267
  _state.sent();
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/sections.ts"],"sourcesContent":["import type { Block } from '../chunk/index.ts';\nimport { estimateTokens, parse } from '../chunk/index.ts';\nimport { countLines } from '../scan/frontmatter.ts';\nimport type { Feature } from './types.ts';\n\n// sections(path, idx, level, heading, start_line, end_line, tokens): the heading outline,\n// 1-indexed over the raw file so a row is a direct Read range; tokens is estimateTokens (D5), CJK-aware.\n\nexport interface Section {\n level: number;\n heading: string;\n startLine: number;\n endLine: number;\n tokens: number;\n}\n\n// Heading blocks parse() already found (mdast/CommonMark fences), offset back onto the raw file;\n// a section runs to just before the next heading, or EOF.\nfunction sectionsFromBlocks(blocks: Block[], raw: string, body: string): Section[] {\n const rawLines = raw.split('\\n');\n const offset = rawLines.length - countLines(body);\n const found: Section[] = blocks.filter((b) => b.type === 'heading').map((b) => ({ level: b.depth ?? 1, heading: (b.text ?? '').trim(), startLine: b.startLine + offset, endLine: rawLines.length, tokens: 0 }));\n for (let s = 0; s < found.length; s++) {\n if (s + 1 < found.length) found[s].endLine = found[s + 1].startLine - 1;\n const text = rawLines.slice(found[s].startLine - 1, found[s].endLine).join('\\n');\n found[s].tokens = Math.ceil(estimateTokens(text));\n }\n return found;\n}\n\nexport const sections: Feature = {\n name: 'sections',\n async schema(db) {\n await db.exec(`CREATE TABLE IF NOT EXISTS sections (\"path\" TEXT, idx INTEGER, level INTEGER, heading TEXT, start_line INTEGER, end_line INTEGER, tokens INTEGER, PRIMARY KEY (\"path\", idx))`);\n },\n extract(raw, body, _search, _data, _cfg, blocks) {\n return sectionsFromBlocks(blocks ?? parse(body), raw, body);\n },\n async remove(db, paths) {\n if (paths.length === 0) return;\n await db.runBatch(\n 'DELETE FROM sections WHERE \"path\" = ?',\n paths.map((p) => [p])\n );\n },\n async store(db, docs) {\n const rows: unknown[][] = [];\n for (const { path, extracted } of docs) (extracted as Section[]).forEach((s, idx) => rows.push([path, idx, s.level, s.heading, s.startLine, s.endLine, s.tokens]));\n if (rows.length === 0) return;\n // DO NOTHING, not a bare INSERT: reconcile's added/touched split is decided before this\n // write's lock, so a path called \"added\" here can already have this row from a concurrent\n // reconcile that committed first -- same file, same parse, same row.\n await db.runBatch('INSERT INTO sections (\"path\", idx, level, heading, start_line, end_line, tokens) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(\"path\", idx) DO NOTHING', rows);\n },\n};\n"],"names":["sections","sectionsFromBlocks","blocks","raw","body","rawLines","split","offset","length","countLines","found","filter","b","type","map","level","depth","heading","text","trim","startLine","endLine","tokens","s","slice","join","Math","ceil","estimateTokens","name","schema","db","exec","extract","_search","_data","_cfg","parse","remove","paths","runBatch","p","store","docs","rows","path","extracted","forEach","idx","push"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/sections.ts"],"sourcesContent":["import type { Block } from '../chunk/index.ts';\nimport { estimateTokens, parse } from '../chunk/index.ts';\nimport { countLines } from '../scan/frontmatter.ts';\nimport { appendRows } from '../store/shared.ts';\nimport type { Feature } from './types.ts';\n\n// sections(path, idx, level, heading, start_line, end_line, tokens): the heading outline,\n// 1-indexed over the raw file so a row is a direct Read range; tokens is estimateTokens (D5), CJK-aware.\n\nexport interface Section {\n level: number;\n heading: string;\n startLine: number;\n endLine: number;\n tokens: number;\n}\n\n// Heading blocks parse() already found (mdast/CommonMark fences), offset back onto the raw file;\n// a section runs to just before the next heading, or EOF.\nfunction sectionsFromBlocks(blocks: Block[], raw: string, body: string): Section[] {\n const rawLines = raw.split('\\n');\n const offset = rawLines.length - countLines(body);\n const found: Section[] = blocks.filter((b) => b.type === 'heading').map((b) => ({ level: b.depth ?? 1, heading: (b.text ?? '').trim(), startLine: b.startLine + offset, endLine: rawLines.length, tokens: 0 }));\n for (let s = 0; s < found.length; s++) {\n if (s + 1 < found.length) found[s].endLine = found[s + 1].startLine - 1;\n const text = rawLines.slice(found[s].startLine - 1, found[s].endLine).join('\\n');\n found[s].tokens = Math.ceil(estimateTokens(text));\n }\n return found;\n}\n\nconst SECTION_COLUMNS = ['path', 'idx', 'level', 'heading', 'start_line', 'end_line', 'tokens'];\n\nexport const sections: Feature = {\n name: 'sections',\n async schema(db) {\n await db.exec(`CREATE TABLE IF NOT EXISTS sections (\"path\" TEXT, idx INTEGER, level INTEGER, heading TEXT, start_line INTEGER, end_line INTEGER, tokens INTEGER, PRIMARY KEY (\"path\", idx))`);\n },\n extract(raw, body, _search, _data, _cfg, blocks) {\n return sectionsFromBlocks(blocks ?? parse(body), raw, body);\n },\n async remove(db, paths) {\n if (paths.length === 0) return;\n await db.runBatch(\n 'DELETE FROM sections WHERE \"path\" = ?',\n paths.map((p) => [p])\n );\n },\n async store(db, docs) {\n const rows: unknown[][] = [];\n for (const { path, extracted } of docs) (extracted as Section[]).forEach((s, idx) => rows.push([path, idx, s.level, s.heading, s.startLine, s.endLine, s.tokens]));\n // DO NOTHING, not a bare INSERT: reconcile's added/touched split is decided before this\n // write's lock, so a path called \"added\" here can already have this row from a concurrent\n // reconcile that committed first -- same file, same parse, same row. A store with an append\n // path has no second writer, and remove() cleared every touched path above, so it skips the guard.\n await appendRows(db, 'sections', SECTION_COLUMNS, 'INSERT INTO sections (\"path\", idx, level, heading, start_line, end_line, tokens) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(\"path\", idx) DO NOTHING', rows);\n },\n};\n"],"names":["sections","sectionsFromBlocks","blocks","raw","body","rawLines","split","offset","length","countLines","found","filter","b","type","map","level","depth","heading","text","trim","startLine","endLine","tokens","s","slice","join","Math","ceil","estimateTokens","SECTION_COLUMNS","name","schema","db","exec","extract","_search","_data","_cfg","parse","remove","paths","runBatch","p","store","docs","rows","path","extracted","forEach","idx","push","appendRows"],"mappings":"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"}
@@ -8,6 +8,7 @@ Object.defineProperty(exports, "tags", {
8
8
  return tags;
9
9
  }
10
10
  });
11
+ var _sharedts = require("../store/shared.js");
11
12
  var _fencests = require("./fences.js");
12
13
  function _array_like_to_array(arr, len) {
13
14
  if (len == null || len > arr.length) len = arr.length;
@@ -372,12 +373,14 @@ var tags = {
372
373
  }
373
374
  }
374
375
  }
375
- if (rows.length === 0) return [
376
- 2
377
- ];
376
+ // OR IGNORE guards the same concurrent-reconcile race sections.ts documents; remove() cleared
377
+ // every touched path above, so a store with an append path needs no guard.
378
378
  return [
379
379
  4,
380
- db.runBatch('INSERT OR IGNORE INTO tags ("path", tag) VALUES (?, ?)', rows)
380
+ (0, _sharedts.appendRows)(db, 'tags', [
381
+ 'path',
382
+ 'tag'
383
+ ], 'INSERT OR IGNORE INTO tags ("path", tag) VALUES (?, ?)', rows)
381
384
  ];
382
385
  case 1:
383
386
  _state.sent();
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/tags.ts"],"sourcesContent":["import { maskRegions } from './fences.ts';\nimport type { Feature } from './types.ts';\n\n// tags(path, tag): Obsidian's file.tags grain -- frontmatter list/string tags plus inline\n// #tags, deduplicated, source not distinguished. Nested tags store full (book/scifi); `tag = 'book' OR tag LIKE 'book/%'` matches the parent too.\n\n// Obsidian treats [[#Heading]] as a same-note link, not a tag.\nconst WIKILINK_RE = /\\[\\[.*?\\]\\]/g; // to the first ]], so a heading holding a lone ] still masks\n// Obsidian doesn't read tags inside HTML markup.\nconst HTML_TAG_RE = /<\\/?[a-zA-Z][^>]*>/g; // tag-shaped only: a comparison's `< 5` must not open a span\n// Anchors on start-of-line or a preceding whitespace/(/[ so `a#b` and URL fragments don't count.\nconst INLINE_TAG_RE = /(?:^|[\\s([])#([\\p{L}\\p{N}_/-]+)/gu;\n// A markdown link destination `](...)` -- `[text](#anchor)` is a same-page link, not a tag.\nconst LINK_DEST_RE = /\\]\\((?:[^()]|\\([^()]*\\))*\\)/g; // one paren-nesting level, as CommonMark destinations allow: (https://x/a_(b)#frag)\n\n// Strips a leading # (frontmatter entries may carry one) and a trailing /; rejects an\n// all-digit result -- a tag needs at least one non-digit character.\nfunction normalizeTag(raw: string): string | null {\n const stripped = raw.replace(/^#/, '').replace(/\\/+$/, '');\n if (!stripped || /^\\d+$/.test(stripped)) return null;\n return stripped;\n}\n\n// data.tags: a YAML list (Obsidian also accepts a bare string). Null members and non-string\n// members are skipped rather than throwing -- `tags:\\n -` parses to [null].\nfunction frontmatterTags(data?: Record<string, unknown>): string[] {\n const raw = data?.tags;\n const list = Array.isArray(raw) ? raw : typeof raw === 'string' ? [raw] : [];\n const found: string[] = [];\n for (const item of list) {\n if (typeof item !== 'string') continue;\n const tag = normalizeTag(item);\n if (tag) found.push(tag);\n }\n return found;\n}\n\n// #tag tokens outside the regions maskRegions() masks (fences, code spans, wikilinks, HTML,\n// comments, link destinations); this loop only does per-line tag extraction.\nfunction inlineTags(body: string): string[] {\n const found: string[] = [];\n for (const line of maskRegions(body).split('\\n')) {\n if (!line.includes('#')) continue; // most lines; skip the regex work\n let cleaned = line;\n if (cleaned.includes('[[')) cleaned = cleaned.replace(WIKILINK_RE, (m) => ' '.repeat(m.length));\n if (cleaned.includes('](')) cleaned = cleaned.replace(LINK_DEST_RE, (m) => ' '.repeat(m.length));\n if (cleaned.includes('<')) cleaned = cleaned.replace(HTML_TAG_RE, (m) => ' '.repeat(m.length));\n for (const m of cleaned.matchAll(INLINE_TAG_RE)) {\n const tag = normalizeTag(m[1]);\n if (tag) found.push(tag);\n }\n }\n return found;\n}\n\nfunction extract(_raw: string, body: string, _search?: { title: string; summary: string }, data?: Record<string, unknown>): string[] {\n return [...new Set([...frontmatterTags(data), ...inlineTags(body)])].sort();\n}\n\nexport const tags: Feature = {\n name: 'tags',\n async schema(db) {\n await db.exec('CREATE TABLE IF NOT EXISTS tags (\"path\" TEXT, tag TEXT, PRIMARY KEY (\"path\", tag))');\n await db.exec('CREATE INDEX IF NOT EXISTS tags_tag ON tags(tag)');\n },\n extract,\n // Blanket clear-then-reinsert (store() always writes the full found list back), so remove()\n // covers vanished and reparsed files without per-file NOT IN diffing, which cannot be one statement shared across files.\n async remove(db, paths) {\n if (paths.length === 0) return;\n await db.runBatch(\n 'DELETE FROM tags WHERE \"path\" = ?',\n paths.map((p) => [p])\n );\n },\n async store(db, docs) {\n const rows: unknown[][] = [];\n for (const { path, extracted } of docs) for (const tag of extracted as string[]) rows.push([path, tag]);\n if (rows.length === 0) return;\n await db.runBatch('INSERT OR IGNORE INTO tags (\"path\", tag) VALUES (?, ?)', rows);\n },\n};\n"],"names":["tags","WIKILINK_RE","HTML_TAG_RE","INLINE_TAG_RE","LINK_DEST_RE","normalizeTag","raw","stripped","replace","test","frontmatterTags","data","list","Array","isArray","found","item","tag","push","inlineTags","body","maskRegions","split","line","includes","cleaned","m","repeat","length","matchAll","extract","_raw","_search","Set","sort","name","schema","db","exec","remove","paths","runBatch","map","p","store","docs","rows","path","extracted"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/tags.ts"],"sourcesContent":["import { appendRows } from '../store/shared.ts';\nimport { maskRegions } from './fences.ts';\nimport type { Feature } from './types.ts';\n\n// tags(path, tag): Obsidian's file.tags grain -- frontmatter list/string tags plus inline\n// #tags, deduplicated, source not distinguished. Nested tags store full (book/scifi); `tag = 'book' OR tag LIKE 'book/%'` matches the parent too.\n\n// Obsidian treats [[#Heading]] as a same-note link, not a tag.\nconst WIKILINK_RE = /\\[\\[.*?\\]\\]/g; // to the first ]], so a heading holding a lone ] still masks\n// Obsidian doesn't read tags inside HTML markup.\nconst HTML_TAG_RE = /<\\/?[a-zA-Z][^>]*>/g; // tag-shaped only: a comparison's `< 5` must not open a span\n// Anchors on start-of-line or a preceding whitespace/(/[ so `a#b` and URL fragments don't count.\nconst INLINE_TAG_RE = /(?:^|[\\s([])#([\\p{L}\\p{N}_/-]+)/gu;\n// A markdown link destination `](...)` -- `[text](#anchor)` is a same-page link, not a tag.\nconst LINK_DEST_RE = /\\]\\((?:[^()]|\\([^()]*\\))*\\)/g; // one paren-nesting level, as CommonMark destinations allow: (https://x/a_(b)#frag)\n\n// Strips a leading # (frontmatter entries may carry one) and a trailing /; rejects an\n// all-digit result -- a tag needs at least one non-digit character.\nfunction normalizeTag(raw: string): string | null {\n const stripped = raw.replace(/^#/, '').replace(/\\/+$/, '');\n if (!stripped || /^\\d+$/.test(stripped)) return null;\n return stripped;\n}\n\n// data.tags: a YAML list (Obsidian also accepts a bare string). Null members and non-string\n// members are skipped rather than throwing -- `tags:\\n -` parses to [null].\nfunction frontmatterTags(data?: Record<string, unknown>): string[] {\n const raw = data?.tags;\n const list = Array.isArray(raw) ? raw : typeof raw === 'string' ? [raw] : [];\n const found: string[] = [];\n for (const item of list) {\n if (typeof item !== 'string') continue;\n const tag = normalizeTag(item);\n if (tag) found.push(tag);\n }\n return found;\n}\n\n// #tag tokens outside the regions maskRegions() masks (fences, code spans, wikilinks, HTML,\n// comments, link destinations); this loop only does per-line tag extraction.\nfunction inlineTags(body: string): string[] {\n const found: string[] = [];\n for (const line of maskRegions(body).split('\\n')) {\n if (!line.includes('#')) continue; // most lines; skip the regex work\n let cleaned = line;\n if (cleaned.includes('[[')) cleaned = cleaned.replace(WIKILINK_RE, (m) => ' '.repeat(m.length));\n if (cleaned.includes('](')) cleaned = cleaned.replace(LINK_DEST_RE, (m) => ' '.repeat(m.length));\n if (cleaned.includes('<')) cleaned = cleaned.replace(HTML_TAG_RE, (m) => ' '.repeat(m.length));\n for (const m of cleaned.matchAll(INLINE_TAG_RE)) {\n const tag = normalizeTag(m[1]);\n if (tag) found.push(tag);\n }\n }\n return found;\n}\n\nfunction extract(_raw: string, body: string, _search?: { title: string; summary: string }, data?: Record<string, unknown>): string[] {\n return [...new Set([...frontmatterTags(data), ...inlineTags(body)])].sort();\n}\n\nexport const tags: Feature = {\n name: 'tags',\n async schema(db) {\n await db.exec('CREATE TABLE IF NOT EXISTS tags (\"path\" TEXT, tag TEXT, PRIMARY KEY (\"path\", tag))');\n await db.exec('CREATE INDEX IF NOT EXISTS tags_tag ON tags(tag)');\n },\n extract,\n // Blanket clear-then-reinsert (store() always writes the full found list back), so remove()\n // covers vanished and reparsed files without per-file NOT IN diffing, which cannot be one statement shared across files.\n async remove(db, paths) {\n if (paths.length === 0) return;\n await db.runBatch(\n 'DELETE FROM tags WHERE \"path\" = ?',\n paths.map((p) => [p])\n );\n },\n async store(db, docs) {\n const rows: unknown[][] = [];\n for (const { path, extracted } of docs) for (const tag of extracted as string[]) rows.push([path, tag]);\n // OR IGNORE guards the same concurrent-reconcile race sections.ts documents; remove() cleared\n // every touched path above, so a store with an append path needs no guard.\n await appendRows(db, 'tags', ['path', 'tag'], 'INSERT OR IGNORE INTO tags (\"path\", tag) VALUES (?, ?)', rows);\n },\n};\n"],"names":["tags","WIKILINK_RE","HTML_TAG_RE","INLINE_TAG_RE","LINK_DEST_RE","normalizeTag","raw","stripped","replace","test","frontmatterTags","data","list","Array","isArray","found","item","tag","push","inlineTags","body","maskRegions","split","line","includes","cleaned","m","repeat","length","matchAll","extract","_raw","_search","Set","sort","name","schema","db","exec","remove","paths","runBatch","map","p","store","docs","rows","path","extracted","appendRows"],"mappings":"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"}
@@ -323,9 +323,8 @@ function invalidateFeatureTable(conn, cfg, baseDir, dialect, pool, toggle) {
323
323
  paths = docs.map(function(d) {
324
324
  return d.relPath;
325
325
  });
326
- // Every path counts as added, not just touched: the table was just dropped, so there is nothing
327
- // stale to diff against, and links' own afterReconcile takes its cold-build (resolveAll) path
328
- // only when added.length equals files.length.
326
+ // Every path counts as added, not just touched: nothing stale to diff against. Links resolves
327
+ // dst inside store() itself when added.length equals files.length (a cold build).
329
328
  delta = {
330
329
  files: files,
331
330
  reparsed: paths,