sensemaking 0.2.1 → 0.3.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 (182) hide show
  1. package/README.md +107 -132
  2. package/dist/cjs/cli.js +172 -182
  3. package/dist/cjs/cli.js.map +1 -1
  4. package/dist/cjs/commands/find.d.cts +3 -0
  5. package/dist/cjs/commands/find.d.ts +3 -0
  6. package/dist/cjs/commands/find.js +73 -0
  7. package/dist/cjs/commands/find.js.map +1 -0
  8. package/dist/cjs/commands/index.d.cts +5 -0
  9. package/dist/cjs/commands/index.d.ts +5 -0
  10. package/dist/cjs/commands/index.js +94 -0
  11. package/dist/cjs/commands/index.js.map +1 -0
  12. package/dist/cjs/commands/init.d.cts +3 -0
  13. package/dist/cjs/commands/init.d.ts +3 -0
  14. package/dist/cjs/commands/init.js +18 -0
  15. package/dist/cjs/commands/init.js.map +1 -0
  16. package/dist/cjs/commands/map.d.cts +3 -0
  17. package/dist/cjs/commands/map.d.ts +3 -0
  18. package/dist/cjs/commands/map.js +21 -0
  19. package/dist/cjs/commands/map.js.map +1 -0
  20. package/dist/cjs/commands/named.d.cts +2 -0
  21. package/dist/cjs/commands/named.d.ts +2 -0
  22. package/dist/cjs/commands/named.js +23 -0
  23. package/dist/cjs/commands/named.js.map +1 -0
  24. package/dist/cjs/commands/peek.d.cts +3 -0
  25. package/dist/cjs/commands/peek.d.ts +3 -0
  26. package/dist/cjs/commands/peek.js +69 -0
  27. package/dist/cjs/commands/peek.js.map +1 -0
  28. package/dist/cjs/commands/query.d.cts +3 -0
  29. package/dist/cjs/commands/query.d.ts +3 -0
  30. package/dist/cjs/commands/query.js +43 -0
  31. package/dist/cjs/commands/query.js.map +1 -0
  32. package/dist/cjs/commands/rebuild.d.cts +3 -0
  33. package/dist/cjs/commands/rebuild.d.ts +3 -0
  34. package/dist/cjs/commands/rebuild.js +20 -0
  35. package/dist/cjs/commands/rebuild.js.map +1 -0
  36. package/dist/cjs/commands/status.d.cts +3 -0
  37. package/dist/cjs/commands/status.d.ts +3 -0
  38. package/dist/cjs/commands/status.js +24 -0
  39. package/dist/cjs/commands/status.js.map +1 -0
  40. package/dist/cjs/commands/types.d.cts +15 -0
  41. package/dist/cjs/commands/types.d.ts +15 -0
  42. package/dist/cjs/commands/types.js +5 -0
  43. package/dist/cjs/commands/types.js.map +1 -0
  44. package/dist/cjs/commands/vault.d.cts +6 -0
  45. package/dist/cjs/commands/vault.d.ts +6 -0
  46. package/dist/cjs/commands/vault.js +94 -0
  47. package/dist/cjs/commands/vault.js.map +1 -0
  48. package/dist/cjs/commands/watch.d.cts +3 -0
  49. package/dist/cjs/commands/watch.d.ts +3 -0
  50. package/dist/cjs/commands/watch.js +173 -0
  51. package/dist/cjs/commands/watch.js.map +1 -0
  52. package/dist/cjs/config.d.cts +11 -1
  53. package/dist/cjs/config.d.ts +11 -1
  54. package/dist/cjs/config.js +82 -9
  55. package/dist/cjs/config.js.map +1 -1
  56. package/dist/cjs/db.d.cts +1 -1
  57. package/dist/cjs/db.d.ts +1 -1
  58. package/dist/cjs/db.js +107 -16
  59. package/dist/cjs/db.js.map +1 -1
  60. package/dist/cjs/errors.d.cts +1 -1
  61. package/dist/cjs/errors.d.ts +1 -1
  62. package/dist/cjs/errors.js.map +1 -1
  63. package/dist/cjs/features/index.d.cts +7 -0
  64. package/dist/cjs/features/index.d.ts +7 -0
  65. package/dist/cjs/features/index.js +36 -0
  66. package/dist/cjs/features/index.js.map +1 -0
  67. package/dist/cjs/features/links.d.cts +4 -0
  68. package/dist/cjs/features/links.d.ts +4 -0
  69. package/dist/cjs/features/links.js +217 -0
  70. package/dist/cjs/features/links.js.map +1 -0
  71. package/dist/cjs/features/rank.d.cts +2 -0
  72. package/dist/cjs/features/rank.d.ts +2 -0
  73. package/dist/cjs/features/rank.js +95 -0
  74. package/dist/cjs/features/rank.js.map +1 -0
  75. package/dist/cjs/features/sections.d.cts +9 -0
  76. package/dist/cjs/features/sections.d.ts +9 -0
  77. package/dist/cjs/features/sections.js +54 -0
  78. package/dist/cjs/features/sections.js.map +1 -0
  79. package/dist/cjs/features/types.d.cts +11 -0
  80. package/dist/cjs/features/types.d.ts +11 -0
  81. package/dist/cjs/features/types.js +5 -0
  82. package/dist/cjs/features/types.js.map +1 -0
  83. package/dist/cjs/graph.d.cts +7 -0
  84. package/dist/cjs/graph.d.ts +7 -0
  85. package/dist/cjs/graph.js +237 -0
  86. package/dist/cjs/graph.js.map +1 -0
  87. package/dist/cjs/index.d.cts +9 -2
  88. package/dist/cjs/index.d.ts +9 -2
  89. package/dist/cjs/index.js +39 -0
  90. package/dist/cjs/index.js.map +1 -1
  91. package/dist/cjs/output.d.cts +21 -0
  92. package/dist/cjs/output.d.ts +21 -0
  93. package/dist/cjs/output.js +112 -3
  94. package/dist/cjs/output.js.map +1 -1
  95. package/dist/cjs/scan.d.cts +3 -1
  96. package/dist/cjs/scan.d.ts +3 -1
  97. package/dist/cjs/scan.js +62 -14
  98. package/dist/cjs/scan.js.map +1 -1
  99. package/dist/cjs/verbs.d.cts +31 -0
  100. package/dist/cjs/verbs.d.ts +31 -0
  101. package/dist/cjs/verbs.js +284 -0
  102. package/dist/cjs/verbs.js.map +1 -0
  103. package/dist/esm/cli.js +63 -107
  104. package/dist/esm/cli.js.map +1 -1
  105. package/dist/esm/commands/find.d.ts +3 -0
  106. package/dist/esm/commands/find.js +14 -0
  107. package/dist/esm/commands/find.js.map +1 -0
  108. package/dist/esm/commands/index.d.ts +5 -0
  109. package/dist/esm/commands/index.js +13 -0
  110. package/dist/esm/commands/index.js.map +1 -0
  111. package/dist/esm/commands/init.d.ts +3 -0
  112. package/dist/esm/commands/init.js +7 -0
  113. package/dist/esm/commands/init.js.map +1 -0
  114. package/dist/esm/commands/map.d.ts +3 -0
  115. package/dist/esm/commands/map.js +10 -0
  116. package/dist/esm/commands/map.js.map +1 -0
  117. package/dist/esm/commands/named.d.ts +2 -0
  118. package/dist/esm/commands/named.js +12 -0
  119. package/dist/esm/commands/named.js.map +1 -0
  120. package/dist/esm/commands/peek.d.ts +3 -0
  121. package/dist/esm/commands/peek.js +12 -0
  122. package/dist/esm/commands/peek.js.map +1 -0
  123. package/dist/esm/commands/query.d.ts +3 -0
  124. package/dist/esm/commands/query.js +7 -0
  125. package/dist/esm/commands/query.js.map +1 -0
  126. package/dist/esm/commands/rebuild.d.ts +3 -0
  127. package/dist/esm/commands/rebuild.js +9 -0
  128. package/dist/esm/commands/rebuild.js.map +1 -0
  129. package/dist/esm/commands/status.d.ts +3 -0
  130. package/dist/esm/commands/status.js +13 -0
  131. package/dist/esm/commands/status.js.map +1 -0
  132. package/dist/esm/commands/types.d.ts +15 -0
  133. package/dist/esm/commands/types.js +1 -0
  134. package/dist/esm/commands/types.js.map +1 -0
  135. package/dist/esm/commands/vault.d.ts +6 -0
  136. package/dist/esm/commands/vault.js +29 -0
  137. package/dist/esm/commands/vault.js.map +1 -0
  138. package/dist/esm/commands/watch.d.ts +3 -0
  139. package/dist/esm/commands/watch.js +19 -0
  140. package/dist/esm/commands/watch.js.map +1 -0
  141. package/dist/esm/config.d.ts +11 -1
  142. package/dist/esm/config.js +68 -12
  143. package/dist/esm/config.js.map +1 -1
  144. package/dist/esm/db.d.ts +1 -1
  145. package/dist/esm/db.js +27 -9
  146. package/dist/esm/db.js.map +1 -1
  147. package/dist/esm/errors.d.ts +1 -1
  148. package/dist/esm/errors.js.map +1 -1
  149. package/dist/esm/features/index.d.ts +7 -0
  150. package/dist/esm/features/index.js +14 -0
  151. package/dist/esm/features/index.js.map +1 -0
  152. package/dist/esm/features/links.d.ts +4 -0
  153. package/dist/esm/features/links.js +81 -0
  154. package/dist/esm/features/links.js.map +1 -0
  155. package/dist/esm/features/rank.d.ts +2 -0
  156. package/dist/esm/features/rank.js +17 -0
  157. package/dist/esm/features/rank.js.map +1 -0
  158. package/dist/esm/features/sections.d.ts +9 -0
  159. package/dist/esm/features/sections.js +41 -0
  160. package/dist/esm/features/sections.js.map +1 -0
  161. package/dist/esm/features/types.d.ts +11 -0
  162. package/dist/esm/features/types.js +3 -0
  163. package/dist/esm/features/types.js.map +1 -0
  164. package/dist/esm/graph.d.ts +7 -0
  165. package/dist/esm/graph.js +79 -0
  166. package/dist/esm/graph.js.map +1 -0
  167. package/dist/esm/index.d.ts +9 -2
  168. package/dist/esm/index.js +4 -1
  169. package/dist/esm/index.js.map +1 -1
  170. package/dist/esm/output.d.ts +21 -0
  171. package/dist/esm/output.js +28 -0
  172. package/dist/esm/output.js.map +1 -1
  173. package/dist/esm/scan.d.ts +3 -1
  174. package/dist/esm/scan.js +56 -7
  175. package/dist/esm/scan.js.map +1 -1
  176. package/dist/esm/verbs.d.ts +31 -0
  177. package/dist/esm/verbs.js +139 -0
  178. package/dist/esm/verbs.js.map +1 -0
  179. package/package.json +4 -4
  180. package/schema.json +23 -4
  181. package/skills/sense/EXAMPLES.md +46 -81
  182. package/skills/sense/SKILL.md +58 -92
@@ -3,25 +3,73 @@ import { dirname, join, resolve } from 'node:path';
3
3
  import { SenseError } from './errors.js';
4
4
  export const CONFIG_FILENAME = 'sense.config.json';
5
5
  export const STATE_DIR = '.sense';
6
- // Highest sense.config.json `version` this build understands.
7
- export const SUPPORTED_CONFIG_VERSION = 1;
8
- // Refuses to overwrite an existing config.
9
- export function initConfig(dir) {
10
- const configPath = join(dir, CONFIG_FILENAME);
11
- if (existsSync(configPath)) {
12
- throw new SenseError('CONFIG_EXISTS', `${CONFIG_FILENAME} already exists in ${dir}`);
6
+ // Highest sense.config.json `version` this build understands. Older versions auto-migrate on load.
7
+ export const SUPPORTED_CONFIG_VERSION = 2;
8
+ // Each feature owns its tables, parse-time extraction, and reconcile step; verbs degrade when one is off.
9
+ export const FEATURE_NAMES = [
10
+ 'links',
11
+ 'sections',
12
+ 'rank'
13
+ ];
14
+ // Absent block or key means enabled -- features are opt-out. `rank` additionally requires `links`.
15
+ export function featureEnabled(cfg, name) {
16
+ var _cfg_features;
17
+ const enabled = ((_cfg_features = cfg.features) === null || _cfg_features === void 0 ? void 0 : _cfg_features[name]) !== false;
18
+ if (name === 'rank') return enabled && featureEnabled(cfg, 'links');
19
+ return enabled;
20
+ }
21
+ export function enabledFeatures(cfg) {
22
+ return FEATURE_NAMES.filter((name)=>featureEnabled(cfg, name));
23
+ }
24
+ // Pure per-version steps; loadConfig chains them from the file's version up to SUPPORTED_CONFIG_VERSION.
25
+ const MIGRATIONS = {
26
+ // v1 -> v2: features block introduced, everything enabled (matches the old implicit behavior of `links` etc. not existing).
27
+ 1: (cfg)=>({
28
+ ...cfg,
29
+ version: 2,
30
+ features: Object.fromEntries(FEATURE_NAMES.map((name)=>[
31
+ name,
32
+ true
33
+ ]))
34
+ })
35
+ };
36
+ export function migrateConfig(cfg) {
37
+ var _cfg_version;
38
+ const from = (_cfg_version = cfg.version) !== null && _cfg_version !== void 0 ? _cfg_version : 1;
39
+ let current = cfg;
40
+ for(let v = from; v < SUPPORTED_CONFIG_VERSION; v++){
41
+ const step = MIGRATIONS[v];
42
+ if (!step) throw new SenseError('CONFIG_VERSION_UNSUPPORTED', `no migration from config version ${v}`);
43
+ current = step(current);
13
44
  }
14
- const starter = {
45
+ return {
46
+ cfg: current,
47
+ from
48
+ };
49
+ }
50
+ function starterConfig() {
51
+ return {
15
52
  $schema: 'https://unpkg.com/sensemaking/schema.json',
16
- version: 1,
53
+ version: SUPPORTED_CONFIG_VERSION,
17
54
  scan: {
18
55
  include: [
19
56
  '**/*.md'
20
57
  ]
21
58
  },
59
+ features: Object.fromEntries(FEATURE_NAMES.map((name)=>[
60
+ name,
61
+ true
62
+ ])),
22
63
  queries: {}
23
64
  };
24
- writeFileSync(configPath, `${JSON.stringify(starter, null, 2)}\n`);
65
+ }
66
+ // Refuses to overwrite an existing config.
67
+ export function initConfig(dir) {
68
+ const configPath = join(dir, CONFIG_FILENAME);
69
+ if (existsSync(configPath)) {
70
+ throw new SenseError('CONFIG_EXISTS', `${CONFIG_FILENAME} already exists in ${dir}`);
71
+ }
72
+ writeFileSync(configPath, `${JSON.stringify(starterConfig(), null, 2)}\n`);
25
73
  return configPath;
26
74
  }
27
75
  export function findConfigPath(startDir) {
@@ -48,14 +96,22 @@ export function loadConfig(explicitPath) {
48
96
  configPath = found;
49
97
  }
50
98
  const raw = readFileSync(configPath, 'utf8');
51
- const cfg = JSON.parse(raw);
99
+ let cfg = JSON.parse(raw);
52
100
  const version = (_cfg_version = cfg.version) !== null && _cfg_version !== void 0 ? _cfg_version : 1;
53
101
  if (version > SUPPORTED_CONFIG_VERSION) {
54
102
  throw new SenseError('CONFIG_VERSION_UNSUPPORTED', `config version ${version} requires a newer sense`);
55
103
  }
104
+ let migratedFrom;
105
+ if (version < SUPPORTED_CONFIG_VERSION) {
106
+ const result = migrateConfig(cfg);
107
+ cfg = result.cfg;
108
+ migratedFrom = result.from;
109
+ writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}\n`);
110
+ }
56
111
  return {
57
112
  ...cfg,
58
113
  baseDir: dirname(configPath),
59
- configPath
114
+ configPath,
115
+ migratedFrom
60
116
  };
61
117
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/config.ts"],"sourcesContent":["import { existsSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { SenseError } from './errors.ts';\n\nexport const CONFIG_FILENAME = 'sense.config.json';\nexport const STATE_DIR = '.sense';\n\n// Highest sense.config.json `version` this build understands.\nexport const SUPPORTED_CONFIG_VERSION = 1;\n\nexport interface Config {\n // Editor-only pointer to schema.json; never read by sense.\n $schema?: string;\n version?: number;\n scan: { include: string[] };\n queries: Record<string, string>;\n}\n\nexport interface ResolvedConfig extends Config {\n baseDir: string;\n configPath: string | null;\n}\n\n// Refuses to overwrite an existing config.\nexport function initConfig(dir: string): string {\n const configPath = join(dir, CONFIG_FILENAME);\n if (existsSync(configPath)) {\n throw new SenseError('CONFIG_EXISTS', `${CONFIG_FILENAME} already exists in ${dir}`);\n }\n const starter: Config = {\n $schema: 'https://unpkg.com/sensemaking/schema.json',\n version: 1,\n scan: { include: ['**/*.md'] },\n queries: {},\n };\n writeFileSync(configPath, `${JSON.stringify(starter, null, 2)}\\n`);\n return configPath;\n}\n\nexport function findConfigPath(startDir: string): string | null {\n let dir = resolve(startDir);\n for (;;) {\n const candidate = join(dir, CONFIG_FILENAME);\n if (existsSync(candidate)) return candidate;\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\nexport function loadConfig(explicitPath?: string): ResolvedConfig {\n let configPath: string;\n if (explicitPath) {\n configPath = resolve(process.cwd(), explicitPath);\n if (!existsSync(configPath)) throw new SenseError('CONFIG_NOT_FOUND', `config not found: ${configPath}`);\n } else {\n const found = findConfigPath(process.cwd());\n if (!found) {\n throw new SenseError('CONFIG_NOT_FOUND', `could not find ${CONFIG_FILENAME} in ${process.cwd()} or any parent directory`);\n }\n configPath = found;\n }\n\n const raw = readFileSync(configPath, 'utf8');\n const cfg = JSON.parse(raw) as Config;\n\n const version = cfg.version ?? 1;\n if (version > SUPPORTED_CONFIG_VERSION) {\n throw new SenseError('CONFIG_VERSION_UNSUPPORTED', `config version ${version} requires a newer sense`);\n }\n\n return { ...cfg, baseDir: dirname(configPath), configPath };\n}\n"],"names":["existsSync","readFileSync","writeFileSync","dirname","join","resolve","SenseError","CONFIG_FILENAME","STATE_DIR","SUPPORTED_CONFIG_VERSION","initConfig","dir","configPath","starter","$schema","version","scan","include","queries","JSON","stringify","findConfigPath","startDir","candidate","parent","loadConfig","explicitPath","cfg","process","cwd","found","raw","parse","baseDir"],"mappings":"AAAA,SAASA,UAAU,EAAEC,YAAY,EAAEC,aAAa,QAAQ,UAAU;AAClE,SAASC,OAAO,EAAEC,IAAI,EAAEC,OAAO,QAAQ,YAAY;AACnD,SAASC,UAAU,QAAQ,cAAc;AAEzC,OAAO,MAAMC,kBAAkB,oBAAoB;AACnD,OAAO,MAAMC,YAAY,SAAS;AAElC,8DAA8D;AAC9D,OAAO,MAAMC,2BAA2B,EAAE;AAe1C,2CAA2C;AAC3C,OAAO,SAASC,WAAWC,GAAW;IACpC,MAAMC,aAAaR,KAAKO,KAAKJ;IAC7B,IAAIP,WAAWY,aAAa;QAC1B,MAAM,IAAIN,WAAW,iBAAiB,GAAGC,gBAAgB,mBAAmB,EAAEI,KAAK;IACrF;IACA,MAAME,UAAkB;QACtBC,SAAS;QACTC,SAAS;QACTC,MAAM;YAAEC,SAAS;gBAAC;aAAU;QAAC;QAC7BC,SAAS,CAAC;IACZ;IACAhB,cAAcU,YAAY,GAAGO,KAAKC,SAAS,CAACP,SAAS,MAAM,GAAG,EAAE,CAAC;IACjE,OAAOD;AACT;AAEA,OAAO,SAASS,eAAeC,QAAgB;IAC7C,IAAIX,MAAMN,QAAQiB;IAClB,OAAS;QACP,MAAMC,YAAYnB,KAAKO,KAAKJ;QAC5B,IAAIP,WAAWuB,YAAY,OAAOA;QAClC,MAAMC,SAASrB,QAAQQ;QACvB,IAAIa,WAAWb,KAAK,OAAO;QAC3BA,MAAMa;IACR;AACF;AAEA,OAAO,SAASC,WAAWC,YAAqB;QAgB9BC;IAfhB,IAAIf;IACJ,IAAIc,cAAc;QAChBd,aAAaP,QAAQuB,QAAQC,GAAG,IAAIH;QACpC,IAAI,CAAC1B,WAAWY,aAAa,MAAM,IAAIN,WAAW,oBAAoB,CAAC,kBAAkB,EAAEM,YAAY;IACzG,OAAO;QACL,MAAMkB,QAAQT,eAAeO,QAAQC,GAAG;QACxC,IAAI,CAACC,OAAO;YACV,MAAM,IAAIxB,WAAW,oBAAoB,CAAC,eAAe,EAAEC,gBAAgB,IAAI,EAAEqB,QAAQC,GAAG,GAAG,wBAAwB,CAAC;QAC1H;QACAjB,aAAakB;IACf;IAEA,MAAMC,MAAM9B,aAAaW,YAAY;IACrC,MAAMe,MAAMR,KAAKa,KAAK,CAACD;IAEvB,MAAMhB,WAAUY,eAAAA,IAAIZ,OAAO,cAAXY,0BAAAA,eAAe;IAC/B,IAAIZ,UAAUN,0BAA0B;QACtC,MAAM,IAAIH,WAAW,8BAA8B,CAAC,eAAe,EAAES,QAAQ,uBAAuB,CAAC;IACvG;IAEA,OAAO;QAAE,GAAGY,GAAG;QAAEM,SAAS9B,QAAQS;QAAaA;IAAW;AAC5D"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/config.ts"],"sourcesContent":["import { existsSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { SenseError } from './errors.ts';\n\nexport const CONFIG_FILENAME = 'sense.config.json';\nexport const STATE_DIR = '.sense';\n\n// Highest sense.config.json `version` this build understands. Older versions auto-migrate on load.\nexport const SUPPORTED_CONFIG_VERSION = 2;\n\n// Each feature owns its tables, parse-time extraction, and reconcile step; verbs degrade when one is off.\nexport const FEATURE_NAMES = ['links', 'sections', 'rank'] as const;\nexport type FeatureName = (typeof FEATURE_NAMES)[number];\n\nexport interface Config {\n // Editor-only pointer to schema.json; never read by sense.\n $schema?: string;\n version?: number;\n scan: { include: string[] };\n features?: Partial<Record<FeatureName, boolean>>;\n queries: Record<string, string>;\n}\n\nexport interface ResolvedConfig extends Config {\n baseDir: string;\n configPath: string | null;\n // Set when loadConfig auto-migrated the file on disk; cli reports it.\n migratedFrom?: number;\n}\n\n// Absent block or key means enabled -- features are opt-out. `rank` additionally requires `links`.\nexport function featureEnabled(cfg: Config, name: FeatureName): boolean {\n const enabled = cfg.features?.[name] !== false;\n if (name === 'rank') return enabled && featureEnabled(cfg, 'links');\n return enabled;\n}\n\nexport function enabledFeatures(cfg: Config): FeatureName[] {\n return FEATURE_NAMES.filter((name) => featureEnabled(cfg, name));\n}\n\n// Pure per-version steps; loadConfig chains them from the file's version up to SUPPORTED_CONFIG_VERSION.\nconst MIGRATIONS: Record<number, (cfg: Config) => Config> = {\n // v1 -> v2: features block introduced, everything enabled (matches the old implicit behavior of `links` etc. not existing).\n 1: (cfg) => ({ ...cfg, version: 2, features: Object.fromEntries(FEATURE_NAMES.map((name) => [name, true])) as Config['features'] }),\n};\n\nexport function migrateConfig(cfg: Config): { cfg: Config; from: number } {\n const from = cfg.version ?? 1;\n let current = cfg;\n for (let v = from; v < SUPPORTED_CONFIG_VERSION; v++) {\n const step = MIGRATIONS[v];\n if (!step) throw new SenseError('CONFIG_VERSION_UNSUPPORTED', `no migration from config version ${v}`);\n current = step(current);\n }\n return { cfg: current, from };\n}\n\nfunction starterConfig(): Config {\n return {\n $schema: 'https://unpkg.com/sensemaking/schema.json',\n version: SUPPORTED_CONFIG_VERSION,\n scan: { include: ['**/*.md'] },\n features: Object.fromEntries(FEATURE_NAMES.map((name) => [name, true])) as Config['features'],\n queries: {},\n };\n}\n\n// Refuses to overwrite an existing config.\nexport function initConfig(dir: string): string {\n const configPath = join(dir, CONFIG_FILENAME);\n if (existsSync(configPath)) {\n throw new SenseError('CONFIG_EXISTS', `${CONFIG_FILENAME} already exists in ${dir}`);\n }\n writeFileSync(configPath, `${JSON.stringify(starterConfig(), null, 2)}\\n`);\n return configPath;\n}\n\nexport function findConfigPath(startDir: string): string | null {\n let dir = resolve(startDir);\n for (;;) {\n const candidate = join(dir, CONFIG_FILENAME);\n if (existsSync(candidate)) return candidate;\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\nexport function loadConfig(explicitPath?: string): ResolvedConfig {\n let configPath: string;\n if (explicitPath) {\n configPath = resolve(process.cwd(), explicitPath);\n if (!existsSync(configPath)) throw new SenseError('CONFIG_NOT_FOUND', `config not found: ${configPath}`);\n } else {\n const found = findConfigPath(process.cwd());\n if (!found) {\n throw new SenseError('CONFIG_NOT_FOUND', `could not find ${CONFIG_FILENAME} in ${process.cwd()} or any parent directory`);\n }\n configPath = found;\n }\n\n const raw = readFileSync(configPath, 'utf8');\n let cfg = JSON.parse(raw) as Config;\n\n const version = cfg.version ?? 1;\n if (version > SUPPORTED_CONFIG_VERSION) {\n throw new SenseError('CONFIG_VERSION_UNSUPPORTED', `config version ${version} requires a newer sense`);\n }\n\n let migratedFrom: number | undefined;\n if (version < SUPPORTED_CONFIG_VERSION) {\n const result = migrateConfig(cfg);\n cfg = result.cfg;\n migratedFrom = result.from;\n writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}\\n`);\n }\n\n return { ...cfg, baseDir: dirname(configPath), configPath, migratedFrom };\n}\n"],"names":["existsSync","readFileSync","writeFileSync","dirname","join","resolve","SenseError","CONFIG_FILENAME","STATE_DIR","SUPPORTED_CONFIG_VERSION","FEATURE_NAMES","featureEnabled","cfg","name","enabled","features","enabledFeatures","filter","MIGRATIONS","version","Object","fromEntries","map","migrateConfig","from","current","v","step","starterConfig","$schema","scan","include","queries","initConfig","dir","configPath","JSON","stringify","findConfigPath","startDir","candidate","parent","loadConfig","explicitPath","process","cwd","found","raw","parse","migratedFrom","result","baseDir"],"mappings":"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"}
package/dist/esm/db.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { DatabaseSync } from 'node:sqlite';
2
2
  import type { Config, ResolvedConfig } from './config.js';
3
3
  export declare const DB_FILENAME = "cache.db";
4
- export declare const SCHEMA_VERSION = "2";
4
+ export declare const SCHEMA_VERSION = "3";
5
5
  export interface OpenResult {
6
6
  db: DatabaseSync;
7
7
  cfg: ResolvedConfig;
package/dist/esm/db.js CHANGED
@@ -1,12 +1,14 @@
1
1
  import { mkdirSync, rmSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { DatabaseSync } from 'node:sqlite';
4
- import { STATE_DIR } from './config.js';
4
+ import { enabledFeatures, STATE_DIR } from './config.js';
5
+ import { activeFeatures } from './features/index.js';
5
6
  import { listFiles, parseFile } from './scan.js';
6
- // Rows -> SQLite: schema, reconcile, has(). Filesystem/frontmatter parsing lives in scan.ts.
7
+ // Rows -> SQLite: core schema, reconcile loop, has(). Parsing lives in scan.ts;
8
+ // everything beyond frontmatter + content lives in src/features/.
7
9
  export const DB_FILENAME = 'cache.db';
8
10
  // Cache shape version, independent of the config's own `version`.
9
- export const SCHEMA_VERSION = '2';
11
+ export const SCHEMA_VERSION = '3';
10
12
  function quoteIdent(name) {
11
13
  return `"${name.split('"').join('""')}"`;
12
14
  }
@@ -36,11 +38,14 @@ function getColumns(db) {
36
38
  const rows = db.prepare('PRAGMA table_info(frontmatter)').all();
37
39
  return new Set(rows.map((r)=>r.name));
38
40
  }
39
- // Content is a separate table (not a column on frontmatter) so `SELECT * FROM frontmatter` can't dump file text into context.
40
- function ensureSchema(db) {
41
+ // Content is a separate table (not a column on frontmatter) so `SELECT * FROM frontmatter`
42
+ // can't dump file text into context. Features add their own tables after the core ones.
43
+ function ensureSchema(db, cfg) {
41
44
  db.exec(`CREATE TABLE IF NOT EXISTS frontmatter ("path" TEXT PRIMARY KEY, "_mtime" REAL, "_size" INTEGER)`);
42
45
  db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS content USING fts5(title, summary, text, path UNINDEXED, tokenize = 'porter unicode61')`);
46
+ for (const feature of activeFeatures(cfg))feature.schema(db);
43
47
  if (getMeta(db, 'schema_version') === null) setMeta(db, 'schema_version', SCHEMA_VERSION);
48
+ if (getMeta(db, 'features') === null) setMeta(db, 'features', enabledFeatures(cfg).join(','));
44
49
  }
45
50
  export function getMeta(db, key) {
46
51
  const row = db.prepare('SELECT value FROM meta WHERE key = ?').get(key);
@@ -74,12 +79,13 @@ export function reconcile(db, cfg, baseDir) {
74
79
  parsed: 0,
75
80
  warnings: []
76
81
  };
82
+ const features = activeFeatures(cfg);
77
83
  const seenColumns = getColumns(db);
78
84
  const newColumns = [];
79
85
  const parsedDocs = [];
80
86
  const warnings = [];
81
87
  for (const file of toReparse){
82
- const { doc, warnings: fileWarnings } = parseFile(file);
88
+ const { doc, warnings: fileWarnings } = parseFile(file, features);
83
89
  warnings.push(...fileWarnings);
84
90
  for (const key of Object.keys(doc.data)){
85
91
  if (!seenColumns.has(key)) {
@@ -95,14 +101,17 @@ export function reconcile(db, cfg, baseDir) {
95
101
  const insertSql = `INSERT OR REPLACE INTO frontmatter (${allColumns.map(quoteIdent).join(', ')}) VALUES (${allColumns.map(()=>'?').join(', ')})`;
96
102
  db.exec('BEGIN');
97
103
  try {
104
+ var _feature_afterReconcile;
98
105
  for (const col of newColumns)db.exec(`ALTER TABLE frontmatter ADD COLUMN ${quoteIdent(col)}`);
99
106
  // FTS5 has no upsert, so delete-before-insert into `content`.
100
107
  const delBody = db.prepare(`DELETE FROM content WHERE "path" = ?`);
101
108
  if (vanished.length > 0) {
102
109
  const del = db.prepare(`DELETE FROM frontmatter WHERE "path" = ?`);
103
110
  for (const path of vanished){
111
+ var _feature_remove;
104
112
  del.run(path);
105
113
  delBody.run(path);
114
+ for (const feature of features)(_feature_remove = feature.remove) === null || _feature_remove === void 0 ? void 0 : _feature_remove.call(feature, db, path);
106
115
  }
107
116
  }
108
117
  if (parsedDocs.length > 0) {
@@ -119,8 +128,14 @@ export function reconcile(db, cfg, baseDir) {
119
128
  insert.run(...values);
120
129
  delBody.run(doc.relPath);
121
130
  insertBody.run(doc.search.title, doc.search.summary, doc.search.text, doc.relPath);
131
+ for (const feature of features){
132
+ var _feature_remove1, _feature_store;
133
+ (_feature_remove1 = feature.remove) === null || _feature_remove1 === void 0 ? void 0 : _feature_remove1.call(feature, db, doc.relPath);
134
+ (_feature_store = feature.store) === null || _feature_store === void 0 ? void 0 : _feature_store.call(feature, db, doc.relPath, doc.extracted[feature.name]);
135
+ }
122
136
  }
123
137
  }
138
+ for (const feature of features)(_feature_afterReconcile = feature.afterReconcile) === null || _feature_afterReconcile === void 0 ? void 0 : _feature_afterReconcile.call(feature, db, files);
124
139
  db.exec('COMMIT');
125
140
  } catch (err) {
126
141
  db.exec('ROLLBACK');
@@ -142,9 +157,12 @@ export function open(cfg) {
142
157
  db.exec('PRAGMA busy_timeout = 5000');
143
158
  registerFunctions(db);
144
159
  db.exec('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)');
145
- // Schema version mismatch: reconcile only reparses changed files, so an old cache can't be patched up incrementally -- rebuild instead.
160
+ // Schema-version or feature-set mismatch: reconcile only reparses changed files, so an
161
+ // old cache can't be patched incrementally -- rebuild instead (cheap: nothing expensive lives here).
146
162
  const version = getMeta(db, 'schema_version');
147
- if (version !== null && version !== SCHEMA_VERSION) {
163
+ const features = getMeta(db, 'features');
164
+ const wantFeatures = enabledFeatures(cfg).join(',');
165
+ if (version !== null && version !== SCHEMA_VERSION || features !== null && features !== wantFeatures) {
148
166
  db.close();
149
167
  rmSync(stateDir, {
150
168
  recursive: true,
@@ -152,7 +170,7 @@ export function open(cfg) {
152
170
  });
153
171
  return open(cfg);
154
172
  }
155
- ensureSchema(db);
173
+ ensureSchema(db, cfg);
156
174
  const { parsed, warnings } = reconcile(db, cfg, cfg.baseDir);
157
175
  return {
158
176
  db,
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/db.ts"],"sourcesContent":["import { mkdirSync, rmSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { DatabaseSync } from 'node:sqlite';\nimport type { Config, ResolvedConfig } from './config.ts';\nimport { STATE_DIR } from './config.ts';\nimport type { ParsedDoc } from './scan.ts';\nimport { listFiles, parseFile } from './scan.ts';\n\n// Rows -> SQLite: schema, reconcile, has(). Filesystem/frontmatter parsing lives in scan.ts.\n\nexport const DB_FILENAME = 'cache.db';\n// Cache shape version, independent of the config's own `version`.\nexport const SCHEMA_VERSION = '2';\n\nexport interface OpenResult {\n db: DatabaseSync;\n cfg: ResolvedConfig;\n dbPath: string;\n parsed: number;\n warnings: string[];\n}\n\nfunction quoteIdent(name: string): string {\n return `\"${name.split('\"').join('\"\"')}\"`;\n}\n\n// has(field, value): JSON-array field -> membership, string field -> substring, NULL -> false.\nfunction registerFunctions(db: DatabaseSync): void {\n db.function('has', { deterministic: true, varargs: false }, (field: unknown, value: unknown): number => {\n if (field === null || field === undefined) return 0;\n\n const needle = String(value);\n\n if (typeof field === 'string') {\n if (field.startsWith('[')) {\n try {\n const parsed = JSON.parse(field);\n if (Array.isArray(parsed)) {\n return parsed.some((item) => String(item) === needle) ? 1 : 0;\n }\n } catch {}\n }\n return field.includes(needle) ? 1 : 0;\n }\n\n return String(field).includes(needle) ? 1 : 0;\n });\n}\n\nfunction getColumns(db: DatabaseSync): Set<string> {\n const rows = db.prepare('PRAGMA table_info(frontmatter)').all() as Array<{ name: string }>;\n return new Set(rows.map((r) => r.name));\n}\n\n// Content is a separate table (not a column on frontmatter) so `SELECT * FROM frontmatter` can't dump file text into context.\nfunction ensureSchema(db: DatabaseSync): void {\n db.exec(`CREATE TABLE IF NOT EXISTS frontmatter (\"path\" TEXT PRIMARY KEY, \"_mtime\" REAL, \"_size\" INTEGER)`);\n db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS content USING fts5(title, summary, text, path UNINDEXED, tokenize = 'porter unicode61')`);\n if (getMeta(db, 'schema_version') === null) setMeta(db, 'schema_version', SCHEMA_VERSION);\n}\n\nexport function getMeta(db: DatabaseSync, key: string): string | null {\n const row = db.prepare('SELECT value FROM meta WHERE key = ?').get(key) as { value: string } | undefined;\n return row ? row.value : null;\n}\n\nexport function setMeta(db: DatabaseSync, key: string, value: string | null): void {\n if (value === null) {\n db.prepare('DELETE FROM meta WHERE key = ?').run(key);\n return;\n }\n db.prepare('INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value').run(key, value);\n}\n\nexport function docCount(db: DatabaseSync): number {\n const row = db.prepare('SELECT COUNT(*) AS n FROM frontmatter').get() as { n: number };\n return row.n;\n}\n\nexport function reconcile(db: DatabaseSync, cfg: Config, baseDir: string): { parsed: number; warnings: string[] } {\n const files = listFiles(cfg, baseDir);\n const currentSet = new Set(files.map((f) => f.relPath));\n\n const existingRows = db.prepare(`SELECT \"path\", \"_mtime\", \"_size\" FROM frontmatter`).all() as Array<{\n path: string;\n _mtime: number;\n _size: number;\n }>;\n const existing = new Map(existingRows.map((r) => [r.path, r]));\n const vanished = existingRows.filter((r) => !currentSet.has(r.path)).map((r) => r.path);\n\n const toReparse = files.filter((f) => {\n const row = existing.get(f.relPath);\n return !row || row._mtime !== f.mtimeMs || row._size !== f.size;\n });\n\n if (vanished.length === 0 && toReparse.length === 0) return { parsed: 0, warnings: [] };\n\n const seenColumns = getColumns(db);\n const newColumns: string[] = [];\n const parsedDocs: ParsedDoc[] = [];\n const warnings: string[] = [];\n\n for (const file of toReparse) {\n const { doc, warnings: fileWarnings } = parseFile(file);\n warnings.push(...fileWarnings);\n for (const key of Object.keys(doc.data)) {\n if (!seenColumns.has(key)) {\n seenColumns.add(key);\n newColumns.push(key);\n }\n }\n parsedDocs.push(doc);\n }\n\n const allColumns = [...seenColumns];\n const insertSql = `INSERT OR REPLACE INTO frontmatter (${allColumns.map(quoteIdent).join(', ')}) VALUES (${allColumns.map(() => '?').join(', ')})`;\n\n db.exec('BEGIN');\n try {\n for (const col of newColumns) db.exec(`ALTER TABLE frontmatter ADD COLUMN ${quoteIdent(col)}`);\n // FTS5 has no upsert, so delete-before-insert into `content`.\n const delBody = db.prepare(`DELETE FROM content WHERE \"path\" = ?`);\n if (vanished.length > 0) {\n const del = db.prepare(`DELETE FROM frontmatter WHERE \"path\" = ?`);\n for (const path of vanished) {\n del.run(path);\n delBody.run(path);\n }\n }\n if (parsedDocs.length > 0) {\n const insert = db.prepare(insertSql);\n const insertBody = db.prepare(`INSERT INTO content (title, summary, text, \"path\") VALUES (?, ?, ?, ?)`);\n for (const doc of parsedDocs) {\n const values = allColumns.map((col) => {\n if (col === 'path') return doc.relPath;\n if (col === '_mtime') return doc.mtimeMs;\n if (col === '_size') return doc.size;\n return doc.data[col] ?? null;\n });\n insert.run(...values);\n delBody.run(doc.relPath);\n insertBody.run(doc.search.title, doc.search.summary, doc.search.text, doc.relPath);\n }\n }\n db.exec('COMMIT');\n } catch (err) {\n db.exec('ROLLBACK');\n throw err;\n }\n\n return { parsed: parsedDocs.length, warnings };\n}\n\nexport function open(cfg: ResolvedConfig): OpenResult {\n const stateDir = join(cfg.baseDir, STATE_DIR);\n mkdirSync(stateDir, { recursive: true });\n const dbPath = join(stateDir, DB_FILENAME);\n\n const db = new DatabaseSync(dbPath);\n db.exec('PRAGMA journal_mode = WAL');\n db.exec('PRAGMA busy_timeout = 5000');\n registerFunctions(db);\n\n db.exec('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)');\n\n // Schema version mismatch: reconcile only reparses changed files, so an old cache can't be patched up incrementally -- rebuild instead.\n const version = getMeta(db, 'schema_version');\n if (version !== null && version !== SCHEMA_VERSION) {\n db.close();\n rmSync(stateDir, { recursive: true, force: true });\n return open(cfg);\n }\n\n ensureSchema(db);\n const { parsed, warnings } = reconcile(db, cfg, cfg.baseDir);\n\n return { db, cfg, dbPath, parsed, warnings };\n}\n\n// Manual reset for a doubted cache.\nexport function rebuild(cfg: ResolvedConfig): OpenResult {\n rmSync(join(cfg.baseDir, STATE_DIR), { recursive: true, force: true });\n return open(cfg);\n}\n"],"names":["mkdirSync","rmSync","join","DatabaseSync","STATE_DIR","listFiles","parseFile","DB_FILENAME","SCHEMA_VERSION","quoteIdent","name","split","registerFunctions","db","function","deterministic","varargs","field","value","undefined","needle","String","startsWith","parsed","JSON","parse","Array","isArray","some","item","includes","getColumns","rows","prepare","all","Set","map","r","ensureSchema","exec","getMeta","setMeta","key","row","get","run","docCount","n","reconcile","cfg","baseDir","files","currentSet","f","relPath","existingRows","existing","Map","path","vanished","filter","has","toReparse","_mtime","mtimeMs","_size","size","length","warnings","seenColumns","newColumns","parsedDocs","file","doc","fileWarnings","push","Object","keys","data","add","allColumns","insertSql","col","delBody","del","insert","insertBody","values","search","title","summary","text","err","open","stateDir","recursive","dbPath","version","close","force","rebuild"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/db.ts"],"sourcesContent":["import { mkdirSync, rmSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { DatabaseSync } from 'node:sqlite';\nimport type { Config, ResolvedConfig } from './config.ts';\nimport { enabledFeatures, STATE_DIR } from './config.ts';\nimport { activeFeatures } from './features/index.ts';\nimport type { ParsedDoc } from './scan.ts';\nimport { listFiles, parseFile } from './scan.ts';\n\n// Rows -> SQLite: core schema, reconcile loop, has(). Parsing lives in scan.ts;\n// everything beyond frontmatter + content lives in src/features/.\n\nexport const DB_FILENAME = 'cache.db';\n// Cache shape version, independent of the config's own `version`.\nexport const SCHEMA_VERSION = '3';\n\nexport interface OpenResult {\n db: DatabaseSync;\n cfg: ResolvedConfig;\n dbPath: string;\n parsed: number;\n warnings: string[];\n}\n\nfunction quoteIdent(name: string): string {\n return `\"${name.split('\"').join('\"\"')}\"`;\n}\n\n// has(field, value): JSON-array field -> membership, string field -> substring, NULL -> false.\nfunction registerFunctions(db: DatabaseSync): void {\n db.function('has', { deterministic: true, varargs: false }, (field: unknown, value: unknown): number => {\n if (field === null || field === undefined) return 0;\n\n const needle = String(value);\n\n if (typeof field === 'string') {\n if (field.startsWith('[')) {\n try {\n const parsed = JSON.parse(field);\n if (Array.isArray(parsed)) {\n return parsed.some((item) => String(item) === needle) ? 1 : 0;\n }\n } catch {}\n }\n return field.includes(needle) ? 1 : 0;\n }\n\n return String(field).includes(needle) ? 1 : 0;\n });\n}\n\nfunction getColumns(db: DatabaseSync): Set<string> {\n const rows = db.prepare('PRAGMA table_info(frontmatter)').all() as Array<{ name: string }>;\n return new Set(rows.map((r) => r.name));\n}\n\n// Content is a separate table (not a column on frontmatter) so `SELECT * FROM frontmatter`\n// can't dump file text into context. Features add their own tables after the core ones.\nfunction ensureSchema(db: DatabaseSync, cfg: Config): void {\n db.exec(`CREATE TABLE IF NOT EXISTS frontmatter (\"path\" TEXT PRIMARY KEY, \"_mtime\" REAL, \"_size\" INTEGER)`);\n db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS content USING fts5(title, summary, text, path UNINDEXED, tokenize = 'porter unicode61')`);\n for (const feature of activeFeatures(cfg)) feature.schema(db);\n if (getMeta(db, 'schema_version') === null) setMeta(db, 'schema_version', SCHEMA_VERSION);\n if (getMeta(db, 'features') === null) setMeta(db, 'features', enabledFeatures(cfg).join(','));\n}\n\nexport function getMeta(db: DatabaseSync, key: string): string | null {\n const row = db.prepare('SELECT value FROM meta WHERE key = ?').get(key) as { value: string } | undefined;\n return row ? row.value : null;\n}\n\nexport function setMeta(db: DatabaseSync, key: string, value: string | null): void {\n if (value === null) {\n db.prepare('DELETE FROM meta WHERE key = ?').run(key);\n return;\n }\n db.prepare('INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value').run(key, value);\n}\n\nexport function docCount(db: DatabaseSync): number {\n const row = db.prepare('SELECT COUNT(*) AS n FROM frontmatter').get() as { n: number };\n return row.n;\n}\n\nexport function reconcile(db: DatabaseSync, cfg: Config, baseDir: string): { parsed: number; warnings: string[] } {\n const files = listFiles(cfg, baseDir);\n const currentSet = new Set(files.map((f) => f.relPath));\n\n const existingRows = db.prepare(`SELECT \"path\", \"_mtime\", \"_size\" FROM frontmatter`).all() as Array<{\n path: string;\n _mtime: number;\n _size: number;\n }>;\n const existing = new Map(existingRows.map((r) => [r.path, r]));\n const vanished = existingRows.filter((r) => !currentSet.has(r.path)).map((r) => r.path);\n\n const toReparse = files.filter((f) => {\n const row = existing.get(f.relPath);\n return !row || row._mtime !== f.mtimeMs || row._size !== f.size;\n });\n\n if (vanished.length === 0 && toReparse.length === 0) return { parsed: 0, warnings: [] };\n\n const features = activeFeatures(cfg);\n const seenColumns = getColumns(db);\n const newColumns: string[] = [];\n const parsedDocs: ParsedDoc[] = [];\n const warnings: string[] = [];\n\n for (const file of toReparse) {\n const { doc, warnings: fileWarnings } = parseFile(file, features);\n warnings.push(...fileWarnings);\n for (const key of Object.keys(doc.data)) {\n if (!seenColumns.has(key)) {\n seenColumns.add(key);\n newColumns.push(key);\n }\n }\n parsedDocs.push(doc);\n }\n\n const allColumns = [...seenColumns];\n const insertSql = `INSERT OR REPLACE INTO frontmatter (${allColumns.map(quoteIdent).join(', ')}) VALUES (${allColumns.map(() => '?').join(', ')})`;\n\n db.exec('BEGIN');\n try {\n for (const col of newColumns) db.exec(`ALTER TABLE frontmatter ADD COLUMN ${quoteIdent(col)}`);\n // FTS5 has no upsert, so delete-before-insert into `content`.\n const delBody = db.prepare(`DELETE FROM content WHERE \"path\" = ?`);\n if (vanished.length > 0) {\n const del = db.prepare(`DELETE FROM frontmatter WHERE \"path\" = ?`);\n for (const path of vanished) {\n del.run(path);\n delBody.run(path);\n for (const feature of features) feature.remove?.(db, path);\n }\n }\n if (parsedDocs.length > 0) {\n const insert = db.prepare(insertSql);\n const insertBody = db.prepare(`INSERT INTO content (title, summary, text, \"path\") VALUES (?, ?, ?, ?)`);\n for (const doc of parsedDocs) {\n const values = allColumns.map((col) => {\n if (col === 'path') return doc.relPath;\n if (col === '_mtime') return doc.mtimeMs;\n if (col === '_size') return doc.size;\n return doc.data[col] ?? null;\n });\n insert.run(...values);\n delBody.run(doc.relPath);\n insertBody.run(doc.search.title, doc.search.summary, doc.search.text, doc.relPath);\n for (const feature of features) {\n feature.remove?.(db, doc.relPath);\n feature.store?.(db, doc.relPath, doc.extracted[feature.name]);\n }\n }\n }\n for (const feature of features) feature.afterReconcile?.(db, files);\n db.exec('COMMIT');\n } catch (err) {\n db.exec('ROLLBACK');\n throw err;\n }\n\n return { parsed: parsedDocs.length, warnings };\n}\n\nexport function open(cfg: ResolvedConfig): OpenResult {\n const stateDir = join(cfg.baseDir, STATE_DIR);\n mkdirSync(stateDir, { recursive: true });\n const dbPath = join(stateDir, DB_FILENAME);\n\n const db = new DatabaseSync(dbPath);\n db.exec('PRAGMA journal_mode = WAL');\n db.exec('PRAGMA busy_timeout = 5000');\n registerFunctions(db);\n\n db.exec('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)');\n\n // Schema-version or feature-set mismatch: reconcile only reparses changed files, so an\n // old cache can't be patched incrementally -- rebuild instead (cheap: nothing expensive lives here).\n const version = getMeta(db, 'schema_version');\n const features = getMeta(db, 'features');\n const wantFeatures = enabledFeatures(cfg).join(',');\n if ((version !== null && version !== SCHEMA_VERSION) || (features !== null && features !== wantFeatures)) {\n db.close();\n rmSync(stateDir, { recursive: true, force: true });\n return open(cfg);\n }\n\n ensureSchema(db, cfg);\n const { parsed, warnings } = reconcile(db, cfg, cfg.baseDir);\n\n return { db, cfg, dbPath, parsed, warnings };\n}\n\n// Manual reset for a doubted cache.\nexport function rebuild(cfg: ResolvedConfig): OpenResult {\n rmSync(join(cfg.baseDir, STATE_DIR), { recursive: true, force: true });\n return open(cfg);\n}\n"],"names":["mkdirSync","rmSync","join","DatabaseSync","enabledFeatures","STATE_DIR","activeFeatures","listFiles","parseFile","DB_FILENAME","SCHEMA_VERSION","quoteIdent","name","split","registerFunctions","db","function","deterministic","varargs","field","value","undefined","needle","String","startsWith","parsed","JSON","parse","Array","isArray","some","item","includes","getColumns","rows","prepare","all","Set","map","r","ensureSchema","cfg","exec","feature","schema","getMeta","setMeta","key","row","get","run","docCount","n","reconcile","baseDir","files","currentSet","f","relPath","existingRows","existing","Map","path","vanished","filter","has","toReparse","_mtime","mtimeMs","_size","size","length","warnings","features","seenColumns","newColumns","parsedDocs","file","doc","fileWarnings","push","Object","keys","data","add","allColumns","insertSql","col","delBody","del","remove","insert","insertBody","values","search","title","summary","text","store","extracted","afterReconcile","err","open","stateDir","recursive","dbPath","version","wantFeatures","close","force","rebuild"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- export type SenseErrorCode = 'CONFIG_NOT_FOUND' | 'CONFIG_EXISTS' | 'CONFIG_VERSION_UNSUPPORTED' | 'WATCH_ACTIVE';
1
+ export type SenseErrorCode = 'CONFIG_NOT_FOUND' | 'CONFIG_EXISTS' | 'CONFIG_VERSION_UNSUPPORTED' | 'WATCH_ACTIVE' | 'NOTE_NOT_FOUND' | 'NOTE_AMBIGUOUS';
2
2
  export declare class SenseError extends Error {
3
3
  code: SenseErrorCode;
4
4
  constructor(code: SenseErrorCode, message: string);
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/errors.ts"],"sourcesContent":["// Library code throws; only cli.ts prints and exits.\nexport type SenseErrorCode = 'CONFIG_NOT_FOUND' | 'CONFIG_EXISTS' | 'CONFIG_VERSION_UNSUPPORTED' | 'WATCH_ACTIVE';\n\nexport class SenseError extends Error {\n code: SenseErrorCode;\n\n constructor(code: SenseErrorCode, message: string) {\n super(message);\n this.name = 'SenseError';\n this.code = code;\n }\n}\n"],"names":["SenseError","Error","code","message","name"],"mappings":"AAAA,qDAAqD;AAGrD,OAAO,MAAMA,mBAAmBC;IAG9B,YAAYC,IAAoB,EAAEC,OAAe,CAAE;QACjD,KAAK,CAACA;QACN,IAAI,CAACC,IAAI,GAAG;QACZ,IAAI,CAACF,IAAI,GAAGA;IACd;AACF"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/errors.ts"],"sourcesContent":["// Library code throws; only cli.ts prints and exits.\nexport type SenseErrorCode = 'CONFIG_NOT_FOUND' | 'CONFIG_EXISTS' | 'CONFIG_VERSION_UNSUPPORTED' | 'WATCH_ACTIVE' | 'NOTE_NOT_FOUND' | 'NOTE_AMBIGUOUS';\n\nexport class SenseError extends Error {\n code: SenseErrorCode;\n\n constructor(code: SenseErrorCode, message: string) {\n super(message);\n this.name = 'SenseError';\n this.code = code;\n }\n}\n"],"names":["SenseError","Error","code","message","name"],"mappings":"AAAA,qDAAqD;AAGrD,OAAO,MAAMA,mBAAmBC;IAG9B,YAAYC,IAAoB,EAAEC,OAAe,CAAE;QACjD,KAAK,CAACA;QACN,IAAI,CAACC,IAAI,GAAG;QACZ,IAAI,CAACF,IAAI,GAAGA;IACd;AACF"}
@@ -0,0 +1,7 @@
1
+ import type { Config } from '../config.js';
2
+ import type { Feature } from './types.js';
3
+ export declare const FEATURES: Feature[];
4
+ export declare function activeFeatures(cfg: Config): Feature[];
5
+ export { linkEdges } from './links.js';
6
+ export type { Section } from './sections.js';
7
+ export type { Feature } from './types.js';
@@ -0,0 +1,14 @@
1
+ import { featureEnabled } from '../config.js';
2
+ import { links } from './links.js';
3
+ import { rank } from './rank.js';
4
+ import { sections } from './sections.js';
5
+ // Registry order matters: rank reads the links table in afterReconcile.
6
+ export const FEATURES = [
7
+ links,
8
+ sections,
9
+ rank
10
+ ];
11
+ export function activeFeatures(cfg) {
12
+ return FEATURES.filter((feature)=>featureEnabled(cfg, feature.name));
13
+ }
14
+ export { linkEdges } from './links.js';
@@ -0,0 +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"}
@@ -0,0 +1,4 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import type { Feature } from './types.js';
3
+ export declare function linkEdges(db: DatabaseSync): [string, string][];
4
+ export declare const links: Feature;
@@ -0,0 +1,81 @@
1
+ import posix from 'node:path/posix';
2
+ // links(src, target, dst): target as written, dst the resolved path or NULL (a queryable dead link).
3
+ // Wikilinks ([[target]], [[target#anchor|alias]], embeds) plus relative markdown links to .md files.
4
+ function extract(_raw, body) {
5
+ const targets = new Set();
6
+ for (const m of body.matchAll(/\[\[([^\]|#]+)(?:#[^\]|]*)?(?:\|[^\]]*)?\]\]/g)){
7
+ const target = m[1].trim();
8
+ if (target) targets.add(target);
9
+ }
10
+ for (const m of body.matchAll(/\]\(([^)]+\.md)(?:#[^)]*)?\)/g)){
11
+ const target = m[1].trim();
12
+ if (target && !/^[a-z]+:\/\//i.test(target)) targets.add(target);
13
+ }
14
+ return [
15
+ ...targets
16
+ ];
17
+ }
18
+ function baseKey(path) {
19
+ return posix.basename(path).replace(/\.md$/i, '').toLowerCase();
20
+ }
21
+ // Obsidian-style: exact relative path (with/without .md), path relative to the linking
22
+ // note's directory, then basename match (lexicographically first on ties).
23
+ function resolveTarget(src, target, pathSet, byBase) {
24
+ var _ref;
25
+ var _byBase_get;
26
+ const clean = target.replace(/\\/g, '/').replace(/^\.\//, '');
27
+ const fromSrc = posix.normalize(posix.join(posix.dirname(src), clean));
28
+ for (const candidate of [
29
+ clean,
30
+ `${clean}.md`,
31
+ fromSrc,
32
+ `${fromSrc}.md`
33
+ ]){
34
+ if (pathSet.has(candidate)) return candidate;
35
+ }
36
+ return (_ref = (_byBase_get = byBase.get(baseKey(clean))) === null || _byBase_get === void 0 ? void 0 : _byBase_get[0]) !== null && _ref !== void 0 ? _ref : null;
37
+ }
38
+ // A new or deleted file can change any note's resolution, so re-resolve the whole table
39
+ // whenever the vault changes. An in-memory probe per row; writes only on change.
40
+ function resolveAll(db, files) {
41
+ const pathSet = new Set(files.map((f)=>f.relPath));
42
+ const byBase = new Map();
43
+ for (const path of [
44
+ ...pathSet
45
+ ].sort()){
46
+ const list = byBase.get(baseKey(path));
47
+ if (list) list.push(path);
48
+ else byBase.set(baseKey(path), [
49
+ path
50
+ ]);
51
+ }
52
+ const rows = db.prepare('SELECT src, target, dst FROM links').all();
53
+ const update = db.prepare('UPDATE links SET dst = ? WHERE src = ? AND target = ?');
54
+ for (const row of rows){
55
+ const dst = resolveTarget(row.src, row.target, pathSet, byBase);
56
+ if (dst !== row.dst) update.run(dst, row.src, row.target);
57
+ }
58
+ }
59
+ // Resolved edges, for rank and for find's graph expansion.
60
+ export function linkEdges(db) {
61
+ return db.prepare('SELECT src, dst FROM links WHERE dst IS NOT NULL').all().map((r)=>[
62
+ r.src,
63
+ r.dst
64
+ ]);
65
+ }
66
+ export const links = {
67
+ name: 'links',
68
+ schema (db) {
69
+ db.exec('CREATE TABLE IF NOT EXISTS links (src TEXT, target TEXT, dst TEXT, PRIMARY KEY (src, target))');
70
+ db.exec('CREATE INDEX IF NOT EXISTS links_dst ON links(dst)');
71
+ },
72
+ extract,
73
+ remove (db, path) {
74
+ db.prepare('DELETE FROM links WHERE src = ?').run(path);
75
+ },
76
+ store (db, path, extracted) {
77
+ const insert = db.prepare('INSERT OR REPLACE INTO links (src, target, dst) VALUES (?, ?, NULL)');
78
+ for (const target of extracted)insert.run(path, target);
79
+ },
80
+ afterReconcile: resolveAll
81
+ };
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/links.ts"],"sourcesContent":["import posix from 'node:path/posix';\nimport type { DatabaseSync } from 'node:sqlite';\nimport type { FileStat } from '../scan.ts';\nimport type { Feature } from './types.ts';\n\n// links(src, target, dst): target as written, dst the resolved path or NULL (a queryable dead link).\n\n// Wikilinks ([[target]], [[target#anchor|alias]], embeds) plus relative markdown links to .md files.\nfunction extract(_raw: string, body: string): string[] {\n const targets = new Set<string>();\n for (const m of body.matchAll(/\\[\\[([^\\]|#]+)(?:#[^\\]|]*)?(?:\\|[^\\]]*)?\\]\\]/g)) {\n const target = m[1].trim();\n if (target) targets.add(target);\n }\n for (const m of body.matchAll(/\\]\\(([^)]+\\.md)(?:#[^)]*)?\\)/g)) {\n const target = m[1].trim();\n if (target && !/^[a-z]+:\\/\\//i.test(target)) targets.add(target);\n }\n return [...targets];\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\n// note's directory, then basename match (lexicographically first on ties).\nfunction resolveTarget(src: string, target: string, pathSet: Set<string>, byBase: Map<string, string[]>): string | null {\n const clean = target.replace(/\\\\/g, '/').replace(/^\\.\\//, '');\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 return byBase.get(baseKey(clean))?.[0] ?? null;\n}\n\n// A new or deleted file can change any note's resolution, so re-resolve the whole table\n// whenever the vault changes. An in-memory probe per row; writes only on change.\nfunction resolveAll(db: DatabaseSync, files: FileStat[]): void {\n const pathSet = new Set(files.map((f) => f.relPath));\n const byBase = new Map<string, string[]>();\n for (const path of [...pathSet].sort()) {\n const list = byBase.get(baseKey(path));\n if (list) list.push(path);\n else byBase.set(baseKey(path), [path]);\n }\n const rows = db.prepare('SELECT src, target, dst FROM links').all() as Array<{ src: string; target: string; dst: string | null }>;\n const update = db.prepare('UPDATE links SET dst = ? WHERE src = ? AND target = ?');\n for (const row of rows) {\n const dst = resolveTarget(row.src, row.target, pathSet, byBase);\n if (dst !== row.dst) update.run(dst, row.src, row.target);\n }\n}\n\n// Resolved edges, for rank and for find's graph expansion.\nexport function linkEdges(db: DatabaseSync): [string, string][] {\n return (db.prepare('SELECT src, dst FROM links WHERE dst IS NOT NULL').all() as Array<{ src: string; dst: string }>).map((r) => [r.src, r.dst]);\n}\n\nexport const links: Feature = {\n name: 'links',\n schema(db) {\n db.exec('CREATE TABLE IF NOT EXISTS links (src TEXT, target TEXT, dst TEXT, PRIMARY KEY (src, target))');\n db.exec('CREATE INDEX IF NOT EXISTS links_dst ON links(dst)');\n },\n extract,\n remove(db, path) {\n db.prepare('DELETE FROM links WHERE src = ?').run(path);\n },\n store(db, path, extracted) {\n const insert = db.prepare('INSERT OR REPLACE INTO links (src, target, dst) VALUES (?, ?, NULL)');\n for (const target of extracted as string[]) insert.run(path, target);\n },\n afterReconcile: resolveAll,\n};\n"],"names":["posix","extract","_raw","body","targets","Set","m","matchAll","target","trim","add","test","baseKey","path","basename","replace","toLowerCase","resolveTarget","src","pathSet","byBase","clean","fromSrc","normalize","join","dirname","candidate","has","get","resolveAll","db","files","map","f","relPath","Map","sort","list","push","set","rows","prepare","all","update","row","dst","run","linkEdges","r","links","name","schema","exec","remove","store","extracted","insert","afterReconcile"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { Feature } from './types.js';
2
+ export declare const rank: Feature;
@@ -0,0 +1,17 @@
1
+ import { pagerank } from '../graph.js';
2
+ import { linkEdges } from './links.js';
3
+ // frontmatter._rank: PageRank over resolved links -- a static "how load-bearing is this
4
+ // note" prior. Depends on links (enforced in config.featureEnabled).
5
+ export const rank = {
6
+ name: 'rank',
7
+ schema (db) {
8
+ const columns = db.prepare('PRAGMA table_info(frontmatter)').all();
9
+ if (!columns.some((c)=>c.name === '_rank')) db.exec('ALTER TABLE frontmatter ADD COLUMN "_rank" REAL');
10
+ },
11
+ afterReconcile (db) {
12
+ const nodes = db.prepare('SELECT "path" FROM frontmatter').all().map((r)=>r.path);
13
+ const ranks = pagerank(nodes, linkEdges(db));
14
+ const update = db.prepare('UPDATE frontmatter SET "_rank" = ? WHERE "path" = ?');
15
+ for (const [path, value] of ranks)update.run(value, path);
16
+ }
17
+ };
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/rank.ts"],"sourcesContent":["import { pagerank } from '../graph.ts';\nimport { linkEdges } from './links.ts';\nimport type { Feature } from './types.ts';\n\n// frontmatter._rank: PageRank over resolved links -- a static \"how load-bearing is this\n// note\" prior. Depends on links (enforced in config.featureEnabled).\n\nexport const rank: Feature = {\n name: 'rank',\n schema(db) {\n const columns = db.prepare('PRAGMA table_info(frontmatter)').all() as Array<{ name: string }>;\n if (!columns.some((c) => c.name === '_rank')) db.exec('ALTER TABLE frontmatter ADD COLUMN \"_rank\" REAL');\n },\n afterReconcile(db) {\n const nodes = (db.prepare('SELECT \"path\" FROM frontmatter').all() as Array<{ path: string }>).map((r) => r.path);\n const ranks = pagerank(nodes, linkEdges(db));\n const update = db.prepare('UPDATE frontmatter SET \"_rank\" = ? WHERE \"path\" = ?');\n for (const [path, value] of ranks) update.run(value, path);\n },\n};\n"],"names":["pagerank","linkEdges","rank","name","schema","db","columns","prepare","all","some","c","exec","afterReconcile","nodes","map","r","path","ranks","update","value","run"],"mappings":"AAAA,SAASA,QAAQ,QAAQ,cAAc;AACvC,SAASC,SAAS,QAAQ,aAAa;AAGvC,wFAAwF;AACxF,qEAAqE;AAErE,OAAO,MAAMC,OAAgB;IAC3BC,MAAM;IACNC,QAAOC,EAAE;QACP,MAAMC,UAAUD,GAAGE,OAAO,CAAC,kCAAkCC,GAAG;QAChE,IAAI,CAACF,QAAQG,IAAI,CAAC,CAACC,IAAMA,EAAEP,IAAI,KAAK,UAAUE,GAAGM,IAAI,CAAC;IACxD;IACAC,gBAAeP,EAAE;QACf,MAAMQ,QAAQ,AAACR,GAAGE,OAAO,CAAC,kCAAkCC,GAAG,GAA+BM,GAAG,CAAC,CAACC,IAAMA,EAAEC,IAAI;QAC/G,MAAMC,QAAQjB,SAASa,OAAOZ,UAAUI;QACxC,MAAMa,SAASb,GAAGE,OAAO,CAAC;QAC1B,KAAK,MAAM,CAACS,MAAMG,MAAM,IAAIF,MAAOC,OAAOE,GAAG,CAACD,OAAOH;IACvD;AACF,EAAE"}
@@ -0,0 +1,9 @@
1
+ import type { Feature } from './types.js';
2
+ export interface Section {
3
+ level: number;
4
+ heading: string;
5
+ startLine: number;
6
+ endLine: number;
7
+ tokens: number;
8
+ }
9
+ export declare const sections: Feature;
@@ -0,0 +1,41 @@
1
+ // Headings outside fenced code blocks.
2
+ function extract(raw) {
3
+ const lines = raw.split('\n');
4
+ const found = [];
5
+ let inFence = false;
6
+ for(let i = 0; i < lines.length; i++){
7
+ if (/^(```|~~~)/.test(lines[i])) {
8
+ inFence = !inFence;
9
+ continue;
10
+ }
11
+ if (inFence) continue;
12
+ const m = lines[i].match(/^(#{1,6}) +(.*)/);
13
+ if (m) found.push({
14
+ level: m[1].length,
15
+ heading: m[2].trim(),
16
+ startLine: i + 1,
17
+ endLine: lines.length,
18
+ tokens: 0
19
+ });
20
+ }
21
+ for(let s = 0; s < found.length; s++){
22
+ if (s + 1 < found.length) found[s].endLine = found[s + 1].startLine - 1;
23
+ const chars = lines.slice(found[s].startLine - 1, found[s].endLine).join('\n').length;
24
+ found[s].tokens = Math.ceil(chars / 4);
25
+ }
26
+ return found;
27
+ }
28
+ export const sections = {
29
+ name: 'sections',
30
+ schema (db) {
31
+ 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))`);
32
+ },
33
+ extract,
34
+ remove (db, path) {
35
+ db.prepare('DELETE FROM sections WHERE "path" = ?').run(path);
36
+ },
37
+ store (db, path, extracted) {
38
+ const insert = db.prepare('INSERT INTO sections ("path", idx, level, heading, start_line, end_line, tokens) VALUES (?, ?, ?, ?, ?, ?, ?)');
39
+ extracted.forEach((s, idx)=>insert.run(path, idx, s.level, s.heading, s.startLine, s.endLine, s.tokens));
40
+ }
41
+ };
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/sections.ts"],"sourcesContent":["import 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 a chars/4 estimate.\n\nexport interface Section {\n level: number;\n heading: string;\n startLine: number;\n endLine: number;\n tokens: number;\n}\n\n// Headings outside fenced code blocks.\nfunction extract(raw: string): Section[] {\n const lines = raw.split('\\n');\n const found: Section[] = [];\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) continue;\n const m = lines[i].match(/^(#{1,6}) +(.*)/);\n if (m) found.push({ level: m[1].length, heading: m[2].trim(), startLine: i + 1, endLine: lines.length, tokens: 0 });\n }\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 chars = lines.slice(found[s].startLine - 1, found[s].endLine).join('\\n').length;\n found[s].tokens = Math.ceil(chars / 4);\n }\n return found;\n}\n\nexport const sections: Feature = {\n name: 'sections',\n schema(db) {\n 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,\n remove(db, path) {\n db.prepare('DELETE FROM sections WHERE \"path\" = ?').run(path);\n },\n store(db, path, extracted) {\n const insert = db.prepare('INSERT INTO sections (\"path\", idx, level, heading, start_line, end_line, tokens) VALUES (?, ?, ?, ?, ?, ?, ?)');\n (extracted as Section[]).forEach((s, idx) => insert.run(path, idx, s.level, s.heading, s.startLine, s.endLine, s.tokens));\n },\n};\n"],"names":["extract","raw","lines","split","found","inFence","i","length","test","m","match","push","level","heading","trim","startLine","endLine","tokens","s","chars","slice","join","Math","ceil","sections","name","schema","db","exec","remove","path","prepare","run","store","extracted","insert","forEach","idx"],"mappings":"AAaA,uCAAuC;AACvC,SAASA,QAAQC,GAAW;IAC1B,MAAMC,QAAQD,IAAIE,KAAK,CAAC;IACxB,MAAMC,QAAmB,EAAE;IAC3B,IAAIC,UAAU;IACd,IAAK,IAAIC,IAAI,GAAGA,IAAIJ,MAAMK,MAAM,EAAED,IAAK;QACrC,IAAI,aAAaE,IAAI,CAACN,KAAK,CAACI,EAAE,GAAG;YAC/BD,UAAU,CAACA;YACX;QACF;QACA,IAAIA,SAAS;QACb,MAAMI,IAAIP,KAAK,CAACI,EAAE,CAACI,KAAK,CAAC;QACzB,IAAID,GAAGL,MAAMO,IAAI,CAAC;YAAEC,OAAOH,CAAC,CAAC,EAAE,CAACF,MAAM;YAAEM,SAASJ,CAAC,CAAC,EAAE,CAACK,IAAI;YAAIC,WAAWT,IAAI;YAAGU,SAASd,MAAMK,MAAM;YAAEU,QAAQ;QAAE;IACnH;IACA,IAAK,IAAIC,IAAI,GAAGA,IAAId,MAAMG,MAAM,EAAEW,IAAK;QACrC,IAAIA,IAAI,IAAId,MAAMG,MAAM,EAAEH,KAAK,CAACc,EAAE,CAACF,OAAO,GAAGZ,KAAK,CAACc,IAAI,EAAE,CAACH,SAAS,GAAG;QACtE,MAAMI,QAAQjB,MAAMkB,KAAK,CAAChB,KAAK,CAACc,EAAE,CAACH,SAAS,GAAG,GAAGX,KAAK,CAACc,EAAE,CAACF,OAAO,EAAEK,IAAI,CAAC,MAAMd,MAAM;QACrFH,KAAK,CAACc,EAAE,CAACD,MAAM,GAAGK,KAAKC,IAAI,CAACJ,QAAQ;IACtC;IACA,OAAOf;AACT;AAEA,OAAO,MAAMoB,WAAoB;IAC/BC,MAAM;IACNC,QAAOC,EAAE;QACPA,GAAGC,IAAI,CAAC,CAAC,4KAA4K,CAAC;IACxL;IACA5B;IACA6B,QAAOF,EAAE,EAAEG,IAAI;QACbH,GAAGI,OAAO,CAAC,yCAAyCC,GAAG,CAACF;IAC1D;IACAG,OAAMN,EAAE,EAAEG,IAAI,EAAEI,SAAS;QACvB,MAAMC,SAASR,GAAGI,OAAO,CAAC;QACzBG,UAAwBE,OAAO,CAAC,CAAClB,GAAGmB,MAAQF,OAAOH,GAAG,CAACF,MAAMO,KAAKnB,EAAEN,KAAK,EAAEM,EAAEL,OAAO,EAAEK,EAAEH,SAAS,EAAEG,EAAEF,OAAO,EAAEE,EAAED,MAAM;IACzH;AACF,EAAE"}
@@ -0,0 +1,11 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import type { FeatureName } from '../config.js';
3
+ import type { FileStat } from '../scan.js';
4
+ export interface Feature {
5
+ name: FeatureName;
6
+ schema(db: DatabaseSync): void;
7
+ extract?(raw: string, body: string): unknown;
8
+ remove?(db: DatabaseSync, path: string): void;
9
+ store?(db: DatabaseSync, path: string, extracted: unknown): void;
10
+ afterReconcile?(db: DatabaseSync, files: FileStat[]): void;
11
+ }
@@ -0,0 +1,3 @@
1
+ // A feature owns its schema, per-file extraction/storage, and any whole-vault pass.
2
+ // Adding a feature = one module here + one entry in the registry (index.ts).
3
+ export { };
@@ -0,0 +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-vault 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,oFAAoF;AACpF,6EAA6E;AAC7E,WASC"}
@@ -0,0 +1,7 @@
1
+ export type Edge = [src: string, dst: string];
2
+ export declare function pagerank(nodes: string[], edges: Edge[], opts?: {
3
+ damping?: number;
4
+ iterations?: number;
5
+ personal?: Map<string, number>;
6
+ }): Map<string, number>;
7
+ export declare function personalizedRank(nodes: string[], edges: Edge[], seeds: Map<string, number>): Map<string, number>;