sensemaking 0.18.1 → 0.18.3

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 (213) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/cli/status.js +32 -31
  3. package/dist/cjs/cli/status.js.map +1 -1
  4. package/dist/cjs/commands/map.js +50 -67
  5. package/dist/cjs/commands/map.js.map +1 -1
  6. package/dist/cjs/config/access.d.cts +2 -2
  7. package/dist/cjs/config/access.d.ts +2 -2
  8. package/dist/cjs/config/access.js.map +1 -1
  9. package/dist/cjs/config/index.d.cts +1 -1
  10. package/dist/cjs/config/index.d.ts +1 -1
  11. package/dist/cjs/config/index.js.map +1 -1
  12. package/dist/cjs/config/load.js +3 -2
  13. package/dist/cjs/config/load.js.map +1 -1
  14. package/dist/cjs/config/types.d.cts +2 -1
  15. package/dist/cjs/config/types.d.ts +2 -1
  16. package/dist/cjs/config/types.js.map +1 -1
  17. package/dist/cjs/config/validate.js +3 -0
  18. package/dist/cjs/config/validate.js.map +1 -1
  19. package/dist/cjs/embed/identity.js +3 -2
  20. package/dist/cjs/embed/identity.js.map +1 -1
  21. package/dist/cjs/embed/store.js +3 -3
  22. package/dist/cjs/embed/store.js.map +1 -1
  23. package/dist/cjs/index.d.cts +2 -2
  24. package/dist/cjs/index.d.ts +2 -2
  25. package/dist/cjs/index.js.map +1 -1
  26. package/dist/cjs/lib/atomic-write.d.cts +1 -0
  27. package/dist/cjs/lib/atomic-write.d.ts +1 -0
  28. package/dist/cjs/lib/atomic-write.js +26 -0
  29. package/dist/cjs/lib/atomic-write.js.map +1 -0
  30. package/dist/cjs/scan/reparse.d.cts +6 -1
  31. package/dist/cjs/scan/reparse.d.ts +6 -1
  32. package/dist/cjs/scan/reparse.js +375 -48
  33. package/dist/cjs/scan/reparse.js.map +1 -1
  34. package/dist/cjs/scan/worker-error.d.cts +9 -0
  35. package/dist/cjs/scan/worker-error.d.ts +9 -0
  36. package/dist/cjs/scan/worker-error.js +44 -0
  37. package/dist/cjs/scan/worker-error.js.map +1 -0
  38. package/dist/cjs/store/duckdb/batch.js +3 -2
  39. package/dist/cjs/store/duckdb/batch.js.map +1 -1
  40. package/dist/cjs/store/duckdb/connection.js +12 -0
  41. package/dist/cjs/store/duckdb/connection.js.map +1 -1
  42. package/dist/cjs/store/duckdb/fieldStats.d.cts +2 -0
  43. package/dist/cjs/store/duckdb/fieldStats.d.ts +2 -0
  44. package/dist/cjs/store/duckdb/fieldStats.js +219 -0
  45. package/dist/cjs/store/duckdb/fieldStats.js.map +1 -0
  46. package/dist/cjs/store/duckdb/lexical.js +3 -2
  47. package/dist/cjs/store/duckdb/lexical.js.map +1 -1
  48. package/dist/cjs/store/duckdb/native.d.cts +0 -1
  49. package/dist/cjs/store/duckdb/native.d.ts +0 -1
  50. package/dist/cjs/store/duckdb/native.js +7 -336
  51. package/dist/cjs/store/duckdb/native.js.map +1 -1
  52. package/dist/cjs/store/duckdb/reconcile.js +8 -3
  53. package/dist/cjs/store/duckdb/reconcile.js.map +1 -1
  54. package/dist/cjs/store/duckdb/sql-functions.js +1 -1
  55. package/dist/cjs/store/duckdb/sql-functions.js.map +1 -1
  56. package/dist/cjs/store/duckdb/store.js +16 -1
  57. package/dist/cjs/store/duckdb/store.js.map +1 -1
  58. package/dist/cjs/store/duckdb/vectors.js +5 -17
  59. package/dist/cjs/store/duckdb/vectors.js.map +1 -1
  60. package/dist/cjs/store/index.js +6 -0
  61. package/dist/cjs/store/index.js.map +1 -1
  62. package/dist/cjs/store/native.d.cts +7 -0
  63. package/dist/cjs/store/native.d.ts +7 -0
  64. package/dist/cjs/store/native.js +350 -0
  65. package/dist/cjs/store/native.js.map +1 -0
  66. package/dist/cjs/store/sqlite/fieldStats.d.cts +2 -0
  67. package/dist/cjs/store/sqlite/fieldStats.d.ts +2 -0
  68. package/dist/cjs/store/sqlite/fieldStats.js +201 -0
  69. package/dist/cjs/store/sqlite/fieldStats.js.map +1 -0
  70. package/dist/cjs/store/sqlite/open.js +82 -29
  71. package/dist/cjs/store/sqlite/open.js.map +1 -1
  72. package/dist/cjs/store/sqlite/reconcile.js +13 -8
  73. package/dist/cjs/store/sqlite/reconcile.js.map +1 -1
  74. package/dist/cjs/store/sqlite/store.js +32 -4
  75. package/dist/cjs/store/sqlite/store.js.map +1 -1
  76. package/dist/cjs/store/sqlite/vectors.d.cts +0 -5
  77. package/dist/cjs/store/sqlite/vectors.d.ts +0 -5
  78. package/dist/cjs/store/sqlite/vectors.js +5 -71
  79. package/dist/cjs/store/sqlite/vectors.js.map +1 -1
  80. package/dist/cjs/store/turso/connection.d.cts +3 -0
  81. package/dist/cjs/store/turso/connection.d.ts +3 -0
  82. package/dist/cjs/store/turso/connection.js +529 -0
  83. package/dist/cjs/store/turso/connection.js.map +1 -0
  84. package/dist/cjs/store/turso/fieldStats.d.cts +2 -0
  85. package/dist/cjs/store/turso/fieldStats.d.ts +2 -0
  86. package/dist/cjs/store/turso/fieldStats.js +201 -0
  87. package/dist/cjs/store/turso/fieldStats.js.map +1 -0
  88. package/dist/cjs/store/turso/native.d.cts +2 -0
  89. package/dist/cjs/store/turso/native.d.ts +2 -0
  90. package/dist/cjs/store/turso/native.js +31 -0
  91. package/dist/cjs/store/turso/native.js.map +1 -0
  92. package/dist/cjs/store/turso/open.d.cts +12 -0
  93. package/dist/cjs/store/turso/open.d.ts +12 -0
  94. package/dist/cjs/store/turso/open.js +500 -0
  95. package/dist/cjs/store/turso/open.js.map +1 -0
  96. package/dist/cjs/store/turso/reconcile.d.cts +6 -0
  97. package/dist/cjs/store/turso/reconcile.d.ts +6 -0
  98. package/dist/cjs/store/turso/reconcile.js +793 -0
  99. package/dist/cjs/store/turso/reconcile.js.map +1 -0
  100. package/dist/cjs/store/turso/store.d.cts +5 -0
  101. package/dist/cjs/store/turso/store.d.ts +5 -0
  102. package/dist/cjs/store/turso/store.js +609 -0
  103. package/dist/cjs/store/turso/store.js.map +1 -0
  104. package/dist/cjs/store/types.d.cts +9 -1
  105. package/dist/cjs/store/types.d.ts +9 -1
  106. package/dist/cjs/store/types.js +0 -7
  107. package/dist/cjs/store/types.js.map +1 -1
  108. package/dist/cjs/store/vectors.d.cts +9 -0
  109. package/dist/cjs/store/vectors.d.ts +9 -0
  110. package/dist/cjs/store/vectors.js +215 -0
  111. package/dist/cjs/store/vectors.js.map +1 -0
  112. package/dist/cjs/watch.js +76 -58
  113. package/dist/cjs/watch.js.map +1 -1
  114. package/dist/cjs/workers/parse.d.cts +18 -0
  115. package/dist/cjs/workers/parse.d.ts +18 -0
  116. package/dist/cjs/workers/parse.js +43 -0
  117. package/dist/cjs/workers/parse.js.map +1 -0
  118. package/dist/esm/cli/status.js +5 -7
  119. package/dist/esm/cli/status.js.map +1 -1
  120. package/dist/esm/commands/map.js +7 -19
  121. package/dist/esm/commands/map.js.map +1 -1
  122. package/dist/esm/config/access.d.ts +2 -2
  123. package/dist/esm/config/access.js.map +1 -1
  124. package/dist/esm/config/index.d.ts +1 -1
  125. package/dist/esm/config/index.js.map +1 -1
  126. package/dist/esm/config/load.js +4 -3
  127. package/dist/esm/config/load.js.map +1 -1
  128. package/dist/esm/config/types.d.ts +2 -1
  129. package/dist/esm/config/types.js.map +1 -1
  130. package/dist/esm/config/validate.js +3 -0
  131. package/dist/esm/config/validate.js.map +1 -1
  132. package/dist/esm/embed/identity.js +4 -3
  133. package/dist/esm/embed/identity.js.map +1 -1
  134. package/dist/esm/embed/store.js +3 -3
  135. package/dist/esm/embed/store.js.map +1 -1
  136. package/dist/esm/index.d.ts +2 -2
  137. package/dist/esm/index.js.map +1 -1
  138. package/dist/esm/lib/atomic-write.d.ts +1 -0
  139. package/dist/esm/lib/atomic-write.js +17 -0
  140. package/dist/esm/lib/atomic-write.js.map +1 -0
  141. package/dist/esm/scan/reparse.d.ts +6 -1
  142. package/dist/esm/scan/reparse.js +108 -8
  143. package/dist/esm/scan/reparse.js.map +1 -1
  144. package/dist/esm/scan/worker-error.d.ts +9 -0
  145. package/dist/esm/scan/worker-error.js +19 -0
  146. package/dist/esm/scan/worker-error.js.map +1 -0
  147. package/dist/esm/store/duckdb/batch.js +3 -2
  148. package/dist/esm/store/duckdb/batch.js.map +1 -1
  149. package/dist/esm/store/duckdb/connection.js +12 -0
  150. package/dist/esm/store/duckdb/connection.js.map +1 -1
  151. package/dist/esm/store/duckdb/fieldStats.d.ts +2 -0
  152. package/dist/esm/store/duckdb/fieldStats.js +39 -0
  153. package/dist/esm/store/duckdb/fieldStats.js.map +1 -0
  154. package/dist/esm/store/duckdb/lexical.js +3 -2
  155. package/dist/esm/store/duckdb/lexical.js.map +1 -1
  156. package/dist/esm/store/duckdb/native.d.ts +0 -1
  157. package/dist/esm/store/duckdb/native.js +7 -84
  158. package/dist/esm/store/duckdb/native.js.map +1 -1
  159. package/dist/esm/store/duckdb/reconcile.js +1 -1
  160. package/dist/esm/store/duckdb/reconcile.js.map +1 -1
  161. package/dist/esm/store/duckdb/sql-functions.js +1 -1
  162. package/dist/esm/store/duckdb/sql-functions.js.map +1 -1
  163. package/dist/esm/store/duckdb/store.js +8 -5
  164. package/dist/esm/store/duckdb/store.js.map +1 -1
  165. package/dist/esm/store/duckdb/vectors.js +2 -12
  166. package/dist/esm/store/duckdb/vectors.js.map +1 -1
  167. package/dist/esm/store/index.js +6 -0
  168. package/dist/esm/store/index.js.map +1 -1
  169. package/dist/esm/store/native.d.ts +7 -0
  170. package/dist/esm/store/native.js +83 -0
  171. package/dist/esm/store/native.js.map +1 -0
  172. package/dist/esm/store/sqlite/fieldStats.d.ts +2 -0
  173. package/dist/esm/store/sqlite/fieldStats.js +25 -0
  174. package/dist/esm/store/sqlite/fieldStats.js.map +1 -0
  175. package/dist/esm/store/sqlite/open.js +21 -8
  176. package/dist/esm/store/sqlite/open.js.map +1 -1
  177. package/dist/esm/store/sqlite/reconcile.js +1 -1
  178. package/dist/esm/store/sqlite/reconcile.js.map +1 -1
  179. package/dist/esm/store/sqlite/store.js +14 -1
  180. package/dist/esm/store/sqlite/store.js.map +1 -1
  181. package/dist/esm/store/sqlite/vectors.d.ts +0 -5
  182. package/dist/esm/store/sqlite/vectors.js +2 -22
  183. package/dist/esm/store/sqlite/vectors.js.map +1 -1
  184. package/dist/esm/store/turso/connection.d.ts +3 -0
  185. package/dist/esm/store/turso/connection.js +52 -0
  186. package/dist/esm/store/turso/connection.js.map +1 -0
  187. package/dist/esm/store/turso/fieldStats.d.ts +2 -0
  188. package/dist/esm/store/turso/fieldStats.js +24 -0
  189. package/dist/esm/store/turso/fieldStats.js.map +1 -0
  190. package/dist/esm/store/turso/native.d.ts +2 -0
  191. package/dist/esm/store/turso/native.js +19 -0
  192. package/dist/esm/store/turso/native.js.map +1 -0
  193. package/dist/esm/store/turso/open.d.ts +12 -0
  194. package/dist/esm/store/turso/open.js +113 -0
  195. package/dist/esm/store/turso/open.js.map +1 -0
  196. package/dist/esm/store/turso/reconcile.d.ts +6 -0
  197. package/dist/esm/store/turso/reconcile.js +158 -0
  198. package/dist/esm/store/turso/reconcile.js.map +1 -0
  199. package/dist/esm/store/turso/store.d.ts +5 -0
  200. package/dist/esm/store/turso/store.js +93 -0
  201. package/dist/esm/store/turso/store.js.map +1 -0
  202. package/dist/esm/store/types.d.ts +9 -1
  203. package/dist/esm/store/types.js +0 -7
  204. package/dist/esm/store/types.js.map +1 -1
  205. package/dist/esm/store/vectors.d.ts +9 -0
  206. package/dist/esm/store/vectors.js +26 -0
  207. package/dist/esm/store/vectors.js.map +1 -0
  208. package/dist/esm/watch.js +28 -16
  209. package/dist/esm/watch.js.map +1 -1
  210. package/dist/esm/workers/parse.d.ts +18 -0
  211. package/dist/esm/workers/parse.js +25 -0
  212. package/dist/esm/workers/parse.js.map +1 -0
  213. package/package.json +4 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/config/validate.ts"],"sourcesContent":["import { SenseError } from '../errors.ts';\nimport { SIGNAL_NAMES, SIGNAL_PREREQUISITES, type SignalName } from './signals.ts';\nimport type { Config } from './types.ts';\n\n// Shape check for hand-edited files, so a typo names itself instead of surfacing as a\n// TypeError. Unknown top-level keys warn (forward compat); unknown keys inside a block error.\nconst KNOWN_KEYS = new Set(['$schema', 'version', 'presets', 'features', 'embed', 'content', 'store', 'queries']);\nconst KNOWN_PRESET_KEYS = new Set(['include', 'exclude', 'k', 'signals', 'where']);\nconst KNOWN_FEATURE_KEYS = new Set(['links', 'sections', 'tags', 'rank']);\n// Exported so a docs test can assert every key here has a schema.json property -- one source\n// of truth for what the embed block accepts, checked against the other for drift.\nexport const KNOWN_EMBED_KEYS = new Set(['model', 'provider', 'url', 'key', 'languages', 'chunkTokens']);\nconst KNOWN_CONTENT_KEYS = new Set(['tokenize']);\nconst SAVED_SEARCH_KEYS = new Set(['search', 'preset', 'include', 'exclude', 'where', 'k']);\n\nexport function unknownConfigKeys(cfg: Record<string, unknown>): string[] {\n return Object.keys(cfg).filter((k) => !KNOWN_KEYS.has(k));\n}\n\nfunction isNonEmptyStringArray(value: unknown): value is string[] {\n return Array.isArray(value) && value.length > 0 && value.every((g) => typeof g === 'string' && g.length > 0);\n}\n\n// Pre-v3 shape check, just enough for migrateConfig to run safely (it reads scan.include\n// directly). The full v3 shape is checked by validateConfig once migration has produced it.\nexport function validateLegacyScan(parsed: unknown, configPath: string): void {\n const cfg = typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) ? (parsed as Record<string, unknown>) : null;\n const scan = cfg?.scan as { include?: unknown } | undefined;\n if (!cfg || !scan || !isNonEmptyStringArray(scan.include)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: scan.include must be a non-empty array of glob strings`);\n }\n}\n\n// Only the shape is checked here. Whether the linked SQLite accepts the tokenizer is settled\n// by probing it in store/sqlite/open.ts, so this never has to carry a table of which version added what.\nfunction validateContentBlock(value: unknown, configPath: string): void {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: content must be an object of { tokenize?: string }`);\n }\n const block = value as Record<string, unknown>;\n const unknown = Object.keys(block).filter((k) => !KNOWN_CONTENT_KEYS.has(k));\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: content has unknown key(s) ${unknown.join(', ')}; content takes tokenize`);\n }\n if (block.tokenize !== undefined && (typeof block.tokenize !== 'string' || block.tokenize.trim() === '')) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: content.tokenize must be a non-empty string, e.g. \"trigram\" or \"unicode61 tokenchars '-_'\"`);\n }\n}\n\nfunction validateStoreKey(value: unknown, configPath: string): void {\n if (value !== 'sqlite' && value !== 'duckdb') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: store must be \"sqlite\" or \"duckdb\"`);\n }\n}\n\nfunction validateFeaturesBlock(value: unknown, configPath: string): void {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: features must be an object of name -> boolean`);\n }\n const block = value as Record<string, unknown>;\n const unknown = Object.keys(block).filter((k) => !KNOWN_FEATURE_KEYS.has(k));\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: features has unknown key(s) ${unknown.join(', ')}; embed is not a features key -- see the top-level \"embed\" block`);\n }\n for (const [name, v] of Object.entries(block)) {\n if (typeof v !== 'boolean') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: features.${name} must be a boolean`);\n }\n }\n}\n\nfunction validateEmbedBlock(value: unknown, configPath: string): void {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed must be an object of { model?, provider?: \"static\"|\"openai\"|\"cohere\", url?, key? }`);\n }\n const embed = value as Record<string, unknown>;\n const unknown = Object.keys(embed).filter((k) => !KNOWN_EMBED_KEYS.has(k));\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed has unknown key(s) ${unknown.join(', ')}`);\n }\n if (embed.model !== undefined && typeof embed.model !== 'string') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.model must be a string`);\n }\n // The block exists to name a model; provider settings without one would read as \"vectors\n // configured\" while embedding stays off, which is the ambiguity v4 removed.\n if (embed.model === undefined || embed.model === '') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.model is required when the \"embed\" block is present (it is what turns vectors on); remove the block to index without vectors`);\n }\n if (embed.provider !== undefined && embed.provider !== 'static' && embed.provider !== 'openai' && embed.provider !== 'cohere') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.provider must be \"static\", \"openai\", or \"cohere\"`);\n }\n if (embed.url !== undefined && typeof embed.url !== 'string') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.url must be a string`);\n }\n if (embed.key !== undefined && typeof embed.key !== 'string') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.key must be a string`);\n }\n // openai has no default host, so a missing url would otherwise only surface at the first\n // vector search; a config error belongs at config time.\n if (embed.provider === 'openai' && (embed.url === undefined || embed.url === '')) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.provider \"openai\" requires embed.url (e.g. \"http://localhost:11434/v1\" for Ollama)`);\n }\n if (embed.languages !== undefined && (!Array.isArray(embed.languages) || embed.languages.length === 0 || embed.languages.some((l) => typeof l !== 'string' || l === ''))) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.languages must be a non-empty array of language tags (e.g. [\"en\", \"zh\"])`);\n }\n if (embed.chunkTokens !== undefined && (typeof embed.chunkTokens !== 'number' || !Number.isInteger(embed.chunkTokens) || embed.chunkTokens <= 0)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.chunkTokens must be a positive integer`);\n }\n}\n\nfunction isSignalName(value: unknown): value is SignalName {\n return typeof value === 'string' && (SIGNAL_NAMES as readonly string[]).includes(value);\n}\n\nfunction isSignalWeight(value: unknown): value is number {\n return typeof value === 'number' && Number.isFinite(value) && value > 0;\n}\n\n// Shape (an object of signal name -> weight) plus the one cross-signal prerequisite that\n// doesn't need the top-level embed block: links is seeded by word-match rows, so declaring it\n// without words is always empty.\nfunction validatePresetSignals(name: string, value: unknown, configPath: string): void {\n if (typeof value !== 'object' || value === null || Array.isArray(value) || Object.keys(value).length === 0) {\n throw new SenseError(\n 'CONFIG_INVALID',\n `${configPath}: presets.${name}.signals must be a non-empty object of signal name -> weight (a finite number > 0), naming ${SIGNAL_NAMES.join(', ')} -- a preset that searches nothing is a mistake; name at least \"words\", or omit \"signals\" to use every signal whose prerequisites hold at weight 1`\n );\n }\n const entries = Object.entries(value as Record<string, unknown>);\n const unknown = entries.filter(([k]) => !isSignalName(k)).map(([k]) => k);\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.signals names unknown signal(s) ${unknown.join(', ')}; valid signals are ${SIGNAL_NAMES.join(', ')}`);\n }\n for (const [signal, weight] of entries) {\n if (!isSignalWeight(weight)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.signals.${signal} must be a finite number > 0`);\n }\n }\n const names = new Set(entries.map(([k]) => k));\n for (const [signal, requires] of Object.entries(SIGNAL_PREREQUISITES) as Array<[SignalName, SignalName]>) {\n if (names.has(signal) && !names.has(requires)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.signals has \"${signal}\" without \"${requires}\"; add \"${requires}\" to presets.${name}.signals, or drop \"${signal}\"`);\n }\n }\n}\n\nfunction validatePreset(name: string, value: unknown, configPath: string): void {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name} must be an object`);\n }\n const preset = value as Record<string, unknown>;\n const unknown = Object.keys(preset).filter((k) => !KNOWN_PRESET_KEYS.has(k));\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name} has unknown key(s) ${unknown.join(', ')}; a preset takes include, exclude, k, signals, where`);\n }\n if (!isNonEmptyStringArray(preset.include)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.include must be a non-empty array of glob strings`);\n }\n if (preset.exclude !== undefined && !isNonEmptyStringArray(preset.exclude)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.exclude must be a non-empty array of glob strings`);\n }\n if (preset.signals !== undefined) validatePresetSignals(name, preset.signals, configPath);\n if (preset.k !== undefined && (typeof preset.k !== 'number' || !Number.isInteger(preset.k) || preset.k <= 0)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.k must be a positive integer`);\n }\n if (preset.where !== undefined && typeof preset.where !== 'string') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.where must be a SQL condition string`);\n }\n}\n\n// A queries.<name> entry: { sql } or a saved search { search, preset?, include?, exclude?, where?, k? }.\nfunction validateSavedQuery(name: string, value: unknown, configPath: string): void {\n // A bare string used to mean SQL. It now fails rather than being inferred: an entry says\n // which of the two verbs it runs, the same choice the CLI makes explicit.\n if (typeof value === 'string') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} must say which verb it runs: { \"sql\": ${JSON.stringify(value.length > 40 ? `${value.slice(0, 37)}...` : value)} } to run it as SQL, or { \"search\": \"...\" } for a ranked search`);\n }\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} must be { sql } or { search, preset?, include?, exclude?, where?, k? }`);\n }\n const entry = value as Record<string, unknown>;\n if ('sql' in entry) {\n const unknown = Object.keys(entry).filter((k) => k !== 'sql');\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} has unknown key(s) ${unknown.join(', ')}; a { sql } query takes only sql`);\n }\n if (typeof entry.sql !== 'string' || entry.sql.trim() === '') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.sql must be a non-empty string`);\n }\n return;\n }\n if ('search' in entry) {\n const unknown = Object.keys(entry).filter((k) => !SAVED_SEARCH_KEYS.has(k));\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} has unknown key(s) ${unknown.join(', ')}; a saved search takes search, preset, include, exclude, where, k`);\n }\n // A saved entry saves a question; a scope without a question is just flags.\n if (typeof entry.search !== 'string' || entry.search.trim() === '') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.search must be non-empty text`);\n }\n if (entry.preset !== undefined && (typeof entry.preset !== 'string' || entry.preset.length === 0)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.preset must be a preset name`);\n }\n if (entry.include !== undefined && !isNonEmptyStringArray(entry.include)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.include must be a non-empty array of glob strings`);\n }\n if (entry.exclude !== undefined && !isNonEmptyStringArray(entry.exclude)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.exclude must be a non-empty array of glob strings`);\n }\n if (entry.where !== undefined && typeof entry.where !== 'string') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.where must be a SQL condition string`);\n }\n if (entry.k !== undefined && (typeof entry.k !== 'number' || !Number.isInteger(entry.k) || entry.k <= 0)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.k must be a positive integer`);\n }\n return;\n }\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} must be { sql } or { search, preset?, include?, exclude?, where?, k? }`);\n}\n\nexport function validateConfig(parsed: unknown, configPath: string): Config {\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: config must be a JSON object`);\n }\n const cfg = parsed as Record<string, unknown>;\n\n // `checks` is rejected by name, not warned: silence would hide that assertions are gone.\n if (cfg.checks !== undefined) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: checks was removed in v3 -- sense check no longer asserts on saved queries; a returned row set is the reader's judgment`);\n }\n\n const presets = cfg.presets as Record<string, unknown> | undefined;\n if (!presets || typeof presets !== 'object' || Array.isArray(presets) || Object.keys(presets).length === 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets must be a non-empty object of preset name -> { include, exclude?, k?, signals?, where? }`);\n }\n for (const [name, value] of Object.entries(presets)) {\n validatePreset(name, value, configPath);\n }\n if (presets.default === undefined) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets must include a \"default\" preset`);\n }\n\n if (cfg.queries === undefined) cfg.queries = {};\n if (typeof cfg.queries !== 'object' || cfg.queries === null || Array.isArray(cfg.queries)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries must be an object of name -> { sql } or { search }`);\n }\n const queries = cfg.queries as Record<string, unknown>;\n for (const [name, value] of Object.entries(queries)) {\n validateSavedQuery(name, value, configPath);\n }\n\n if (cfg.features !== undefined) {\n validateFeaturesBlock(cfg.features, configPath);\n }\n if (cfg.embed !== undefined) {\n validateEmbedBlock(cfg.embed, configPath);\n }\n if (cfg.content !== undefined) {\n validateContentBlock(cfg.content, configPath);\n }\n if (cfg.store !== undefined) {\n validateStoreKey(cfg.store, configPath);\n }\n\n // vectors' prerequisite is a named model, checked here once embed is known valid (or absent).\n if (cfg.embed === undefined) {\n for (const [name, value] of Object.entries(presets)) {\n const signals = (value as Record<string, unknown>).signals as Record<string, unknown> | undefined;\n if (signals && 'vectors' in signals) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.signals includes \"vectors\", but no \"embed\" block names a model; add one, or drop \"vectors\" from presets.${name}.signals`);\n }\n }\n }\n\n return cfg as unknown as Config;\n}\n"],"names":["SenseError","SIGNAL_NAMES","SIGNAL_PREREQUISITES","KNOWN_KEYS","Set","KNOWN_PRESET_KEYS","KNOWN_FEATURE_KEYS","KNOWN_EMBED_KEYS","KNOWN_CONTENT_KEYS","SAVED_SEARCH_KEYS","unknownConfigKeys","cfg","Object","keys","filter","k","has","isNonEmptyStringArray","value","Array","isArray","length","every","g","validateLegacyScan","parsed","configPath","scan","include","validateContentBlock","block","unknown","join","tokenize","undefined","trim","validateStoreKey","validateFeaturesBlock","name","v","entries","validateEmbedBlock","embed","model","provider","url","key","languages","some","l","chunkTokens","Number","isInteger","isSignalName","includes","isSignalWeight","isFinite","validatePresetSignals","map","signal","weight","names","requires","validatePreset","preset","exclude","signals","where","validateSavedQuery","JSON","stringify","slice","entry","sql","search","validateConfig","checks","presets","default","queries","features","content","store"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/config/validate.ts"],"sourcesContent":["import { SenseError } from '../errors.ts';\nimport { SIGNAL_NAMES, SIGNAL_PREREQUISITES, type SignalName } from './signals.ts';\nimport type { Config } from './types.ts';\n\n// Shape check for hand-edited files, so a typo names itself instead of surfacing as a\n// TypeError. Unknown top-level keys warn (forward compat); unknown keys inside a block error.\nconst KNOWN_KEYS = new Set(['$schema', 'version', 'presets', 'features', 'embed', 'content', 'store', 'queries']);\nconst KNOWN_PRESET_KEYS = new Set(['include', 'exclude', 'k', 'signals', 'where']);\nconst KNOWN_FEATURE_KEYS = new Set(['links', 'sections', 'tags', 'rank']);\n// Exported so a docs test can assert every key here has a schema.json property -- one source\n// of truth for what the embed block accepts, checked against the other for drift.\nexport const KNOWN_EMBED_KEYS = new Set(['model', 'provider', 'url', 'key', 'languages', 'chunkTokens']);\nconst KNOWN_CONTENT_KEYS = new Set(['tokenize']);\nconst SAVED_SEARCH_KEYS = new Set(['search', 'preset', 'include', 'exclude', 'where', 'k']);\n\nexport function unknownConfigKeys(cfg: Record<string, unknown>): string[] {\n return Object.keys(cfg).filter((k) => !KNOWN_KEYS.has(k));\n}\n\nfunction isNonEmptyStringArray(value: unknown): value is string[] {\n return Array.isArray(value) && value.length > 0 && value.every((g) => typeof g === 'string' && g.length > 0);\n}\n\n// Pre-v3 shape check, just enough for migrateConfig to run safely (it reads scan.include\n// directly). The full v3 shape is checked by validateConfig once migration has produced it.\nexport function validateLegacyScan(parsed: unknown, configPath: string): void {\n const cfg = typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) ? (parsed as Record<string, unknown>) : null;\n const scan = cfg?.scan as { include?: unknown } | undefined;\n if (!cfg || !scan || !isNonEmptyStringArray(scan.include)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: scan.include must be a non-empty array of glob strings`);\n }\n}\n\n// Only the shape is checked here. Whether the linked SQLite accepts the tokenizer is settled\n// by probing it in store/sqlite/open.ts, so this never has to carry a table of which version added what.\nfunction validateContentBlock(value: unknown, configPath: string): void {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: content must be an object of { tokenize?: string }`);\n }\n const block = value as Record<string, unknown>;\n const unknown = Object.keys(block).filter((k) => !KNOWN_CONTENT_KEYS.has(k));\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: content has unknown key(s) ${unknown.join(', ')}; content takes tokenize`);\n }\n if (block.tokenize !== undefined && (typeof block.tokenize !== 'string' || block.tokenize.trim() === '')) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: content.tokenize must be a non-empty string, e.g. \"trigram\" or \"unicode61 tokenchars '-_'\"`);\n }\n}\n\n// \"turso\" is implemented but not offered: it has no lexical or vector search yet, so a tree\n// naming it could not search. Registered in the store registry and exercised by the parity\n// suite; re-add here and in schema.json's enum when those land.\nfunction validateStoreKey(value: unknown, configPath: string): void {\n if (value !== 'sqlite' && value !== 'duckdb') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: store must be \"sqlite\" or \"duckdb\"`);\n }\n}\n\nfunction validateFeaturesBlock(value: unknown, configPath: string): void {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: features must be an object of name -> boolean`);\n }\n const block = value as Record<string, unknown>;\n const unknown = Object.keys(block).filter((k) => !KNOWN_FEATURE_KEYS.has(k));\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: features has unknown key(s) ${unknown.join(', ')}; embed is not a features key -- see the top-level \"embed\" block`);\n }\n for (const [name, v] of Object.entries(block)) {\n if (typeof v !== 'boolean') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: features.${name} must be a boolean`);\n }\n }\n}\n\nfunction validateEmbedBlock(value: unknown, configPath: string): void {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed must be an object of { model?, provider?: \"static\"|\"openai\"|\"cohere\", url?, key? }`);\n }\n const embed = value as Record<string, unknown>;\n const unknown = Object.keys(embed).filter((k) => !KNOWN_EMBED_KEYS.has(k));\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed has unknown key(s) ${unknown.join(', ')}`);\n }\n if (embed.model !== undefined && typeof embed.model !== 'string') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.model must be a string`);\n }\n // The block exists to name a model; provider settings without one would read as \"vectors\n // configured\" while embedding stays off, which is the ambiguity v4 removed.\n if (embed.model === undefined || embed.model === '') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.model is required when the \"embed\" block is present (it is what turns vectors on); remove the block to index without vectors`);\n }\n if (embed.provider !== undefined && embed.provider !== 'static' && embed.provider !== 'openai' && embed.provider !== 'cohere') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.provider must be \"static\", \"openai\", or \"cohere\"`);\n }\n if (embed.url !== undefined && typeof embed.url !== 'string') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.url must be a string`);\n }\n if (embed.key !== undefined && typeof embed.key !== 'string') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.key must be a string`);\n }\n // openai has no default host, so a missing url would otherwise only surface at the first\n // vector search; a config error belongs at config time.\n if (embed.provider === 'openai' && (embed.url === undefined || embed.url === '')) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.provider \"openai\" requires embed.url (e.g. \"http://localhost:11434/v1\" for Ollama)`);\n }\n if (embed.languages !== undefined && (!Array.isArray(embed.languages) || embed.languages.length === 0 || embed.languages.some((l) => typeof l !== 'string' || l === ''))) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.languages must be a non-empty array of language tags (e.g. [\"en\", \"zh\"])`);\n }\n if (embed.chunkTokens !== undefined && (typeof embed.chunkTokens !== 'number' || !Number.isInteger(embed.chunkTokens) || embed.chunkTokens <= 0)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.chunkTokens must be a positive integer`);\n }\n}\n\nfunction isSignalName(value: unknown): value is SignalName {\n return typeof value === 'string' && (SIGNAL_NAMES as readonly string[]).includes(value);\n}\n\nfunction isSignalWeight(value: unknown): value is number {\n return typeof value === 'number' && Number.isFinite(value) && value > 0;\n}\n\n// Shape (an object of signal name -> weight) plus the one cross-signal prerequisite that\n// doesn't need the top-level embed block: links is seeded by word-match rows, so declaring it\n// without words is always empty.\nfunction validatePresetSignals(name: string, value: unknown, configPath: string): void {\n if (typeof value !== 'object' || value === null || Array.isArray(value) || Object.keys(value).length === 0) {\n throw new SenseError(\n 'CONFIG_INVALID',\n `${configPath}: presets.${name}.signals must be a non-empty object of signal name -> weight (a finite number > 0), naming ${SIGNAL_NAMES.join(', ')} -- a preset that searches nothing is a mistake; name at least \"words\", or omit \"signals\" to use every signal whose prerequisites hold at weight 1`\n );\n }\n const entries = Object.entries(value as Record<string, unknown>);\n const unknown = entries.filter(([k]) => !isSignalName(k)).map(([k]) => k);\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.signals names unknown signal(s) ${unknown.join(', ')}; valid signals are ${SIGNAL_NAMES.join(', ')}`);\n }\n for (const [signal, weight] of entries) {\n if (!isSignalWeight(weight)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.signals.${signal} must be a finite number > 0`);\n }\n }\n const names = new Set(entries.map(([k]) => k));\n for (const [signal, requires] of Object.entries(SIGNAL_PREREQUISITES) as Array<[SignalName, SignalName]>) {\n if (names.has(signal) && !names.has(requires)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.signals has \"${signal}\" without \"${requires}\"; add \"${requires}\" to presets.${name}.signals, or drop \"${signal}\"`);\n }\n }\n}\n\nfunction validatePreset(name: string, value: unknown, configPath: string): void {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name} must be an object`);\n }\n const preset = value as Record<string, unknown>;\n const unknown = Object.keys(preset).filter((k) => !KNOWN_PRESET_KEYS.has(k));\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name} has unknown key(s) ${unknown.join(', ')}; a preset takes include, exclude, k, signals, where`);\n }\n if (!isNonEmptyStringArray(preset.include)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.include must be a non-empty array of glob strings`);\n }\n if (preset.exclude !== undefined && !isNonEmptyStringArray(preset.exclude)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.exclude must be a non-empty array of glob strings`);\n }\n if (preset.signals !== undefined) validatePresetSignals(name, preset.signals, configPath);\n if (preset.k !== undefined && (typeof preset.k !== 'number' || !Number.isInteger(preset.k) || preset.k <= 0)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.k must be a positive integer`);\n }\n if (preset.where !== undefined && typeof preset.where !== 'string') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.where must be a SQL condition string`);\n }\n}\n\n// A queries.<name> entry: { sql } or a saved search { search, preset?, include?, exclude?, where?, k? }.\nfunction validateSavedQuery(name: string, value: unknown, configPath: string): void {\n // A bare string used to mean SQL. It now fails rather than being inferred: an entry says\n // which of the two verbs it runs, the same choice the CLI makes explicit.\n if (typeof value === 'string') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} must say which verb it runs: { \"sql\": ${JSON.stringify(value.length > 40 ? `${value.slice(0, 37)}...` : value)} } to run it as SQL, or { \"search\": \"...\" } for a ranked search`);\n }\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} must be { sql } or { search, preset?, include?, exclude?, where?, k? }`);\n }\n const entry = value as Record<string, unknown>;\n if ('sql' in entry) {\n const unknown = Object.keys(entry).filter((k) => k !== 'sql');\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} has unknown key(s) ${unknown.join(', ')}; a { sql } query takes only sql`);\n }\n if (typeof entry.sql !== 'string' || entry.sql.trim() === '') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.sql must be a non-empty string`);\n }\n return;\n }\n if ('search' in entry) {\n const unknown = Object.keys(entry).filter((k) => !SAVED_SEARCH_KEYS.has(k));\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} has unknown key(s) ${unknown.join(', ')}; a saved search takes search, preset, include, exclude, where, k`);\n }\n // A saved entry saves a question; a scope without a question is just flags.\n if (typeof entry.search !== 'string' || entry.search.trim() === '') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.search must be non-empty text`);\n }\n if (entry.preset !== undefined && (typeof entry.preset !== 'string' || entry.preset.length === 0)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.preset must be a preset name`);\n }\n if (entry.include !== undefined && !isNonEmptyStringArray(entry.include)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.include must be a non-empty array of glob strings`);\n }\n if (entry.exclude !== undefined && !isNonEmptyStringArray(entry.exclude)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.exclude must be a non-empty array of glob strings`);\n }\n if (entry.where !== undefined && typeof entry.where !== 'string') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.where must be a SQL condition string`);\n }\n if (entry.k !== undefined && (typeof entry.k !== 'number' || !Number.isInteger(entry.k) || entry.k <= 0)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.k must be a positive integer`);\n }\n return;\n }\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} must be { sql } or { search, preset?, include?, exclude?, where?, k? }`);\n}\n\nexport function validateConfig(parsed: unknown, configPath: string): Config {\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: config must be a JSON object`);\n }\n const cfg = parsed as Record<string, unknown>;\n\n // `checks` is rejected by name, not warned: silence would hide that assertions are gone.\n if (cfg.checks !== undefined) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: checks was removed in v3 -- sense check no longer asserts on saved queries; a returned row set is the reader's judgment`);\n }\n\n const presets = cfg.presets as Record<string, unknown> | undefined;\n if (!presets || typeof presets !== 'object' || Array.isArray(presets) || Object.keys(presets).length === 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets must be a non-empty object of preset name -> { include, exclude?, k?, signals?, where? }`);\n }\n for (const [name, value] of Object.entries(presets)) {\n validatePreset(name, value, configPath);\n }\n if (presets.default === undefined) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets must include a \"default\" preset`);\n }\n\n if (cfg.queries === undefined) cfg.queries = {};\n if (typeof cfg.queries !== 'object' || cfg.queries === null || Array.isArray(cfg.queries)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries must be an object of name -> { sql } or { search }`);\n }\n const queries = cfg.queries as Record<string, unknown>;\n for (const [name, value] of Object.entries(queries)) {\n validateSavedQuery(name, value, configPath);\n }\n\n if (cfg.features !== undefined) {\n validateFeaturesBlock(cfg.features, configPath);\n }\n if (cfg.embed !== undefined) {\n validateEmbedBlock(cfg.embed, configPath);\n }\n if (cfg.content !== undefined) {\n validateContentBlock(cfg.content, configPath);\n }\n if (cfg.store !== undefined) {\n validateStoreKey(cfg.store, configPath);\n }\n\n // vectors' prerequisite is a named model, checked here once embed is known valid (or absent).\n if (cfg.embed === undefined) {\n for (const [name, value] of Object.entries(presets)) {\n const signals = (value as Record<string, unknown>).signals as Record<string, unknown> | undefined;\n if (signals && 'vectors' in signals) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.signals includes \"vectors\", but no \"embed\" block names a model; add one, or drop \"vectors\" from presets.${name}.signals`);\n }\n }\n }\n\n return cfg as unknown as Config;\n}\n"],"names":["SenseError","SIGNAL_NAMES","SIGNAL_PREREQUISITES","KNOWN_KEYS","Set","KNOWN_PRESET_KEYS","KNOWN_FEATURE_KEYS","KNOWN_EMBED_KEYS","KNOWN_CONTENT_KEYS","SAVED_SEARCH_KEYS","unknownConfigKeys","cfg","Object","keys","filter","k","has","isNonEmptyStringArray","value","Array","isArray","length","every","g","validateLegacyScan","parsed","configPath","scan","include","validateContentBlock","block","unknown","join","tokenize","undefined","trim","validateStoreKey","validateFeaturesBlock","name","v","entries","validateEmbedBlock","embed","model","provider","url","key","languages","some","l","chunkTokens","Number","isInteger","isSignalName","includes","isSignalWeight","isFinite","validatePresetSignals","map","signal","weight","names","requires","validatePreset","preset","exclude","signals","where","validateSavedQuery","JSON","stringify","slice","entry","sql","search","validateConfig","checks","presets","default","queries","features","content","store"],"mappings":"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"}
@@ -1,6 +1,7 @@
1
- import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, readFileSync, statSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { join } from 'node:path';
4
+ import { writeFileAtomic } from '../lib/atomic-write.js';
4
5
  // HF cache layout, no network: ~/.sense/models/models--<org>--<name>/{refs/main,
5
6
  // snapshots/<sha>/<files>}, files stored directly (hf_hub's blobs/symlinks layer skipped).
6
7
  export const MODEL_FILES = [
@@ -35,7 +36,7 @@ export function writeRef(model, sha) {
35
36
  mkdirSync(join(repoDir(model), 'refs'), {
36
37
  recursive: true
37
38
  });
38
- writeFileSync(refsMainPath(model), sha);
39
+ writeFileAtomic(refsMainPath(model), sha);
39
40
  }
40
41
  export function snapshotDir(model, sha) {
41
42
  return join(repoDir(model), 'snapshots', sha);
@@ -60,7 +61,7 @@ export function writeLanguages(model, languages) {
60
61
  mkdirSync(repoDir(model), {
61
62
  recursive: true
62
63
  });
63
- writeFileSync(languagesPath(model), JSON.stringify(languages));
64
+ writeFileAtomic(languagesPath(model), JSON.stringify(languages));
64
65
  }
65
66
  // A local directory the caller manages, or the resolved snapshot once `main` is pinned; an HF
66
67
  // id with no recorded ref yet has no known snapshot, so this names the (fileless) repo dir.
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/embed/identity.ts"],"sourcesContent":["import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\n// HF cache layout, no network: ~/.sense/models/models--<org>--<name>/{refs/main,\n// snapshots/<sha>/<files>}, files stored directly (hf_hub's blobs/symlinks layer skipped).\n\nexport const MODEL_FILES = ['model.safetensors', 'tokenizer.json'];\n// The pair, for messages that name what a local model directory must contain.\nexport const MODEL_FILENAMES = MODEL_FILES.join(' and ');\n\n// A Hugging Face repo id: one slash, HF's charset. Anything else is a path, used as one rather\n// than resolved against the HF cache.\nconst HF_MODEL_ID = /^[A-Za-z0-9][\\w.-]*\\/[A-Za-z0-9][\\w.-]*$/;\n\nexport function isDownloadable(model: string): boolean {\n return HF_MODEL_ID.test(model);\n}\n\nfunction repoDir(model: string): string {\n const [org, name] = model.split('/');\n return join(homedir(), '.sense', 'models', `models--${org}--${name}`);\n}\n\nexport function refsMainPath(model: string): string {\n return join(repoDir(model), 'refs', 'main');\n}\n\n// What `main` last resolved to, read from disk -- no network. Undefined until some resolution\n// (a download or an explicit prefetch) has written it once.\nexport function readRef(model: string): string | undefined {\n const p = refsMainPath(model);\n if (!existsSync(p)) return undefined;\n const sha = readFileSync(p, 'utf8').trim();\n return sha.length > 0 ? sha : undefined;\n}\n\n// A recorded ref pins until its snapshot directory is removed; nothing here re-resolves it.\nexport function writeRef(model: string, sha: string): void {\n mkdirSync(join(repoDir(model), 'refs'), { recursive: true });\n writeFileSync(refsMainPath(model), sha);\n}\n\nexport function snapshotDir(model: string, sha: string): string {\n return join(repoDir(model), 'snapshots', sha);\n}\n\n// Languages describe the repo, not one weight snapshot, so this lives beside refs/, not\n// inside a snapshot dir.\nexport function languagesPath(model: string): string {\n return join(repoDir(model), 'languages.json');\n}\n\n// undefined means \"never resolved yet\"; an empty array is a resolved fact (checked, none\n// declared) and is cached the same way, so a languageless model is not re-fetched every run.\nexport function readLanguages(model: string): string[] | undefined {\n const p = languagesPath(model);\n if (!existsSync(p)) return undefined;\n try {\n return JSON.parse(readFileSync(p, 'utf8'));\n } catch {\n return undefined;\n }\n}\n\nexport function writeLanguages(model: string, languages: string[]): void {\n mkdirSync(repoDir(model), { recursive: true });\n writeFileSync(languagesPath(model), JSON.stringify(languages));\n}\n\n// A local directory the caller manages, or the resolved snapshot once `main` is pinned; an HF\n// id with no recorded ref yet has no known snapshot, so this names the (fileless) repo dir.\nexport function modelDir(model: string): string {\n if (!isDownloadable(model)) return model;\n const sha = readRef(model);\n return sha ? snapshotDir(model, sha) : repoDir(model);\n}\n\n// Both files, so an interrupted download reads as absent and the next one resumes it.\nexport function hasModelFiles(model: string): boolean {\n const dir = modelDir(model);\n return MODEL_FILES.every((file) => existsSync(join(dir, file)));\n}\n\n// Weight identity for the signature, no network: a HF id's resolved sha, or the two\n// files' size+mtime for a local path. Undefined when nothing is known yet.\nexport function modelIdentity(model: string): string | undefined {\n if (isDownloadable(model)) return readRef(model);\n const dir = modelDir(model);\n const parts: string[] = [];\n for (const file of MODEL_FILES) {\n try {\n const st = statSync(join(dir, file));\n parts.push(`${st.size}:${Math.trunc(st.mtimeMs)}`);\n } catch {\n return undefined;\n }\n }\n return parts.join(',');\n}\n"],"names":["existsSync","mkdirSync","readFileSync","statSync","writeFileSync","homedir","join","MODEL_FILES","MODEL_FILENAMES","HF_MODEL_ID","isDownloadable","model","test","repoDir","org","name","split","refsMainPath","readRef","p","undefined","sha","trim","length","writeRef","recursive","snapshotDir","languagesPath","readLanguages","JSON","parse","writeLanguages","languages","stringify","modelDir","hasModelFiles","dir","every","file","modelIdentity","parts","st","push","size","Math","trunc","mtimeMs"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/embed/identity.ts"],"sourcesContent":["import { existsSync, mkdirSync, readFileSync, statSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { writeFileAtomic } from '../lib/atomic-write.ts';\n\n// HF cache layout, no network: ~/.sense/models/models--<org>--<name>/{refs/main,\n// snapshots/<sha>/<files>}, files stored directly (hf_hub's blobs/symlinks layer skipped).\n\nexport const MODEL_FILES = ['model.safetensors', 'tokenizer.json'];\n// The pair, for messages that name what a local model directory must contain.\nexport const MODEL_FILENAMES = MODEL_FILES.join(' and ');\n\n// A Hugging Face repo id: one slash, HF's charset. Anything else is a path, used as one rather\n// than resolved against the HF cache.\nconst HF_MODEL_ID = /^[A-Za-z0-9][\\w.-]*\\/[A-Za-z0-9][\\w.-]*$/;\n\nexport function isDownloadable(model: string): boolean {\n return HF_MODEL_ID.test(model);\n}\n\nfunction repoDir(model: string): string {\n const [org, name] = model.split('/');\n return join(homedir(), '.sense', 'models', `models--${org}--${name}`);\n}\n\nexport function refsMainPath(model: string): string {\n return join(repoDir(model), 'refs', 'main');\n}\n\n// What `main` last resolved to, read from disk -- no network. Undefined until some resolution\n// (a download or an explicit prefetch) has written it once.\nexport function readRef(model: string): string | undefined {\n const p = refsMainPath(model);\n if (!existsSync(p)) return undefined;\n const sha = readFileSync(p, 'utf8').trim();\n return sha.length > 0 ? sha : undefined;\n}\n\n// A recorded ref pins until its snapshot directory is removed; nothing here re-resolves it.\nexport function writeRef(model: string, sha: string): void {\n mkdirSync(join(repoDir(model), 'refs'), { recursive: true });\n writeFileAtomic(refsMainPath(model), sha);\n}\n\nexport function snapshotDir(model: string, sha: string): string {\n return join(repoDir(model), 'snapshots', sha);\n}\n\n// Languages describe the repo, not one weight snapshot, so this lives beside refs/, not\n// inside a snapshot dir.\nexport function languagesPath(model: string): string {\n return join(repoDir(model), 'languages.json');\n}\n\n// undefined means \"never resolved yet\"; an empty array is a resolved fact (checked, none\n// declared) and is cached the same way, so a languageless model is not re-fetched every run.\nexport function readLanguages(model: string): string[] | undefined {\n const p = languagesPath(model);\n if (!existsSync(p)) return undefined;\n try {\n return JSON.parse(readFileSync(p, 'utf8'));\n } catch {\n return undefined;\n }\n}\n\nexport function writeLanguages(model: string, languages: string[]): void {\n mkdirSync(repoDir(model), { recursive: true });\n writeFileAtomic(languagesPath(model), JSON.stringify(languages));\n}\n\n// A local directory the caller manages, or the resolved snapshot once `main` is pinned; an HF\n// id with no recorded ref yet has no known snapshot, so this names the (fileless) repo dir.\nexport function modelDir(model: string): string {\n if (!isDownloadable(model)) return model;\n const sha = readRef(model);\n return sha ? snapshotDir(model, sha) : repoDir(model);\n}\n\n// Both files, so an interrupted download reads as absent and the next one resumes it.\nexport function hasModelFiles(model: string): boolean {\n const dir = modelDir(model);\n return MODEL_FILES.every((file) => existsSync(join(dir, file)));\n}\n\n// Weight identity for the signature, no network: a HF id's resolved sha, or the two\n// files' size+mtime for a local path. Undefined when nothing is known yet.\nexport function modelIdentity(model: string): string | undefined {\n if (isDownloadable(model)) return readRef(model);\n const dir = modelDir(model);\n const parts: string[] = [];\n for (const file of MODEL_FILES) {\n try {\n const st = statSync(join(dir, file));\n parts.push(`${st.size}:${Math.trunc(st.mtimeMs)}`);\n } catch {\n return undefined;\n }\n }\n return parts.join(',');\n}\n"],"names":["existsSync","mkdirSync","readFileSync","statSync","homedir","join","writeFileAtomic","MODEL_FILES","MODEL_FILENAMES","HF_MODEL_ID","isDownloadable","model","test","repoDir","org","name","split","refsMainPath","readRef","p","undefined","sha","trim","length","writeRef","recursive","snapshotDir","languagesPath","readLanguages","JSON","parse","writeLanguages","languages","stringify","modelDir","hasModelFiles","dir","every","file","modelIdentity","parts","st","push","size","Math","trunc","mtimeMs"],"mappings":"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"}
@@ -1,7 +1,8 @@
1
- import { existsSync, mkdirSync, renameSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { embedConfig } from '../config/index.js';
4
4
  import { SenseError } from '../errors.js';
5
+ import { writeFileAtomic } from '../lib/atomic-write.js';
5
6
  import { fetchWithRetry } from './http.js';
6
7
  import { hasModelFiles, isDownloadable, languagesPath, MODEL_FILES, readRef, snapshotDir, writeLanguages, writeRef } from './identity.js';
7
8
  import { isKnownLanguageTag, MEASURED_MODEL_LANGUAGES } from './languages.js';
@@ -92,8 +93,7 @@ async function fetchToFile(url, dest) {
92
93
  throw new SenseError('EMBED_MODEL', `could not reach ${url}: ${err.message}`);
93
94
  }
94
95
  if (!res.ok) throw new SenseError('EMBED_MODEL', `${url} -> HTTP ${res.status}`);
95
- writeFileSync(`${dest}.part`, Buffer.from(await res.arrayBuffer()));
96
- renameSync(`${dest}.part`, dest);
96
+ writeFileAtomic(dest, Buffer.from(await res.arrayBuffer()));
97
97
  }
98
98
  // Fetches an already-known sha straight into its snapshot dir, without touching refs/main --
99
99
  // for downloadModel below, and for callers pinning an exact revision (the parity suite).
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/embed/store.ts"],"sourcesContent":["import { existsSync, mkdirSync, renameSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { Config } from '../config/index.ts';\nimport { embedConfig } from '../config/index.ts';\nimport { SenseError } from '../errors.ts';\nimport { fetchWithRetry } from './http.ts';\nimport { hasModelFiles, isDownloadable, languagesPath, MODEL_FILES, readRef, snapshotDir, writeLanguages, writeRef } from './identity.ts';\nimport { isKnownLanguageTag, MEASURED_MODEL_LANGUAGES } from './languages.ts';\n\n// identity.ts holds paths and cache identity, no network; store.ts holds the network fetches\n// (downloads, sha resolution, language metadata). Re-exported here for callers that otherwise\n// only need store.ts's network surface.\nexport { hasModelFiles, isDownloadable, MODEL_FILENAMES, MODEL_FILES, modelDir, readLanguages, refsMainPath, snapshotDir, writeLanguages, writeRef } from './identity.ts';\n\n// A non-static provider has nothing to download here; an unreachable endpoint surfaces as an\n// EMBED_MODEL error at call time instead.\nexport function modelPresent(cfg: Config): boolean {\n const e = embedConfig(cfg);\n if (!e) return false;\n return e.provider !== 'static' || hasModelFiles(e.model);\n}\n\ntype ResolvedEmbedConfig = NonNullable<ReturnType<typeof embedConfig>>;\n\n// True when embed.model names a local path (not a Hugging Face id) that is missing its model\n// files -- nothing will ever fetch it for itself, unlike a downloadable id.\nexport function localModelMissing(e: ResolvedEmbedConfig | null): e is ResolvedEmbedConfig {\n return e !== null && e.provider === 'static' && !isDownloadable(e.model) && !hasModelFiles(e.model);\n}\n\n// `main` resolves to a sha once per model: a recorded ref pins until its snapshot is\n// removed, so only the first need for an id ever costs this HEAD request.\nasync function resolveSha(model: string): Promise<string> {\n const existing = readRef(model);\n if (existing) return existing;\n const url = `https://huggingface.co/${model}/resolve/main/model.safetensors`;\n // redirect: manual -- the header rides the Hub's own response; following the LFS/CDN\n // redirect would read the CDN's headers instead, which do not carry it.\n let res: Response;\n try {\n res = await fetchWithRetry(url, { method: 'HEAD', redirect: 'manual' });\n } catch (err) {\n throw new SenseError('EMBED_MODEL', `could not reach ${url}: ${(err as Error).message}`);\n }\n const sha = res.headers.get('x-repo-commit');\n if (!sha) throw new SenseError('EMBED_MODEL', `${url}: response carried no x-repo-commit header`);\n writeRef(model, sha);\n return sha;\n}\n\ninterface HfModelInfo {\n cardData?: { language?: string | string[] };\n tags?: string[];\n}\n\n// cardData.language is the structured field (verified live: minishlab/potion-retrieval-32M has\n// neither this nor any language tag, so it stays silent; potion-multilingual-128M carries an\n// array here). Tags are a fallback for cards that only tag languages, filtered to recognized\n// codes so an unrelated short tag (format markers etc.) is never mistaken for one.\nfunction languagesFromCard(info: HfModelInfo): string[] | undefined {\n const lang = info.cardData?.language;\n if (typeof lang === 'string') return [lang];\n if (Array.isArray(lang) && lang.length > 0) return lang;\n const tagLangs = (info.tags ?? []).filter(isKnownLanguageTag);\n return tagLangs.length > 0 ? tagLangs : undefined;\n}\n\n// Best-effort, cached forever once resolved (even to \"none\"): unlike model weights, missing or\n// unreachable metadata is not an error -- it just means the language-fit check stays off.\nasync function ensureLanguages(model: string): Promise<void> {\n if (existsSync(languagesPath(model))) return;\n // A measured entry needs no card and no network, and stays deterministic offline.\n const measured = MEASURED_MODEL_LANGUAGES[model];\n if (measured) {\n writeLanguages(model, measured);\n return;\n }\n let res: Response;\n try {\n res = await fetchWithRetry(`https://huggingface.co/api/models/${model}`, {});\n } catch {\n return;\n }\n if (!res.ok) return;\n let info: HfModelInfo;\n try {\n info = (await res.json()) as HfModelInfo;\n } catch {\n return;\n }\n writeLanguages(model, languagesFromCard(info) ?? []);\n}\n\nasync function fetchToFile(url: string, dest: string): Promise<void> {\n let res: Response;\n try {\n res = await fetchWithRetry(url, {});\n } catch (err) {\n throw new SenseError('EMBED_MODEL', `could not reach ${url}: ${(err as Error).message}`);\n }\n if (!res.ok) throw new SenseError('EMBED_MODEL', `${url} -> HTTP ${res.status}`);\n writeFileSync(`${dest}.part`, Buffer.from(await res.arrayBuffer()));\n renameSync(`${dest}.part`, dest);\n}\n\n// Fetches an already-known sha straight into its snapshot dir, without touching refs/main --\n// for downloadModel below, and for callers pinning an exact revision (the parity suite).\nexport async function downloadModelRevision(model: string, sha: string, onFile?: (file: string, dir: string) => void): Promise<string> {\n const dir = snapshotDir(model, sha);\n mkdirSync(dir, { recursive: true });\n for (const file of MODEL_FILES) {\n if (existsSync(join(dir, file))) continue;\n onFile?.(file, dir);\n await fetchToFile(`https://huggingface.co/${model}/resolve/${sha}/${file}`, join(dir, file));\n }\n return dir;\n}\n\n// The only code path that resolves `main`: a static provider's first construction calls it\n// lazily, `sense download` as an explicit prefetch. Idempotent either way.\nexport async function downloadModel(model: string, onFile?: (file: string, dir: string) => void): Promise<string> {\n if (!isDownloadable(model)) {\n throw new SenseError('EMBED_MODEL', `embed.model \"${model}\" is a local path, not a Hugging Face model id, so there is nothing to download; put model.safetensors and tokenizer.json in that directory, or name a model id like \"minishlab/potion-retrieval-32M\"`);\n }\n const sha = await resolveSha(model);\n // Independent of the weights above: a fresh HEAD/GET pair either way, so a lazy first\n // construction and an explicit prefetch both end up with a cached language verdict.\n await ensureLanguages(model);\n return downloadModelRevision(model, sha, onFile);\n}\n"],"names":["existsSync","mkdirSync","renameSync","writeFileSync","join","embedConfig","SenseError","fetchWithRetry","hasModelFiles","isDownloadable","languagesPath","MODEL_FILES","readRef","snapshotDir","writeLanguages","writeRef","isKnownLanguageTag","MEASURED_MODEL_LANGUAGES","MODEL_FILENAMES","modelDir","readLanguages","refsMainPath","modelPresent","cfg","e","provider","model","localModelMissing","resolveSha","existing","url","res","method","redirect","err","message","sha","headers","get","languagesFromCard","info","lang","cardData","language","Array","isArray","length","tagLangs","tags","filter","undefined","ensureLanguages","measured","ok","json","fetchToFile","dest","status","Buffer","from","arrayBuffer","downloadModelRevision","onFile","dir","recursive","file","downloadModel"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/embed/store.ts"],"sourcesContent":["import { existsSync, mkdirSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { Config } from '../config/index.ts';\nimport { embedConfig } from '../config/index.ts';\nimport { SenseError } from '../errors.ts';\nimport { writeFileAtomic } from '../lib/atomic-write.ts';\nimport { fetchWithRetry } from './http.ts';\nimport { hasModelFiles, isDownloadable, languagesPath, MODEL_FILES, readRef, snapshotDir, writeLanguages, writeRef } from './identity.ts';\nimport { isKnownLanguageTag, MEASURED_MODEL_LANGUAGES } from './languages.ts';\n\n// identity.ts holds paths and cache identity, no network; store.ts holds the network fetches\n// (downloads, sha resolution, language metadata). Re-exported here for callers that otherwise\n// only need store.ts's network surface.\nexport { hasModelFiles, isDownloadable, MODEL_FILENAMES, MODEL_FILES, modelDir, readLanguages, refsMainPath, snapshotDir, writeLanguages, writeRef } from './identity.ts';\n\n// A non-static provider has nothing to download here; an unreachable endpoint surfaces as an\n// EMBED_MODEL error at call time instead.\nexport function modelPresent(cfg: Config): boolean {\n const e = embedConfig(cfg);\n if (!e) return false;\n return e.provider !== 'static' || hasModelFiles(e.model);\n}\n\ntype ResolvedEmbedConfig = NonNullable<ReturnType<typeof embedConfig>>;\n\n// True when embed.model names a local path (not a Hugging Face id) that is missing its model\n// files -- nothing will ever fetch it for itself, unlike a downloadable id.\nexport function localModelMissing(e: ResolvedEmbedConfig | null): e is ResolvedEmbedConfig {\n return e !== null && e.provider === 'static' && !isDownloadable(e.model) && !hasModelFiles(e.model);\n}\n\n// `main` resolves to a sha once per model: a recorded ref pins until its snapshot is\n// removed, so only the first need for an id ever costs this HEAD request.\nasync function resolveSha(model: string): Promise<string> {\n const existing = readRef(model);\n if (existing) return existing;\n const url = `https://huggingface.co/${model}/resolve/main/model.safetensors`;\n // redirect: manual -- the header rides the Hub's own response; following the LFS/CDN\n // redirect would read the CDN's headers instead, which do not carry it.\n let res: Response;\n try {\n res = await fetchWithRetry(url, { method: 'HEAD', redirect: 'manual' });\n } catch (err) {\n throw new SenseError('EMBED_MODEL', `could not reach ${url}: ${(err as Error).message}`);\n }\n const sha = res.headers.get('x-repo-commit');\n if (!sha) throw new SenseError('EMBED_MODEL', `${url}: response carried no x-repo-commit header`);\n writeRef(model, sha);\n return sha;\n}\n\ninterface HfModelInfo {\n cardData?: { language?: string | string[] };\n tags?: string[];\n}\n\n// cardData.language is the structured field (verified live: minishlab/potion-retrieval-32M has\n// neither this nor any language tag, so it stays silent; potion-multilingual-128M carries an\n// array here). Tags are a fallback for cards that only tag languages, filtered to recognized\n// codes so an unrelated short tag (format markers etc.) is never mistaken for one.\nfunction languagesFromCard(info: HfModelInfo): string[] | undefined {\n const lang = info.cardData?.language;\n if (typeof lang === 'string') return [lang];\n if (Array.isArray(lang) && lang.length > 0) return lang;\n const tagLangs = (info.tags ?? []).filter(isKnownLanguageTag);\n return tagLangs.length > 0 ? tagLangs : undefined;\n}\n\n// Best-effort, cached forever once resolved (even to \"none\"): unlike model weights, missing or\n// unreachable metadata is not an error -- it just means the language-fit check stays off.\nasync function ensureLanguages(model: string): Promise<void> {\n if (existsSync(languagesPath(model))) return;\n // A measured entry needs no card and no network, and stays deterministic offline.\n const measured = MEASURED_MODEL_LANGUAGES[model];\n if (measured) {\n writeLanguages(model, measured);\n return;\n }\n let res: Response;\n try {\n res = await fetchWithRetry(`https://huggingface.co/api/models/${model}`, {});\n } catch {\n return;\n }\n if (!res.ok) return;\n let info: HfModelInfo;\n try {\n info = (await res.json()) as HfModelInfo;\n } catch {\n return;\n }\n writeLanguages(model, languagesFromCard(info) ?? []);\n}\n\nasync function fetchToFile(url: string, dest: string): Promise<void> {\n let res: Response;\n try {\n res = await fetchWithRetry(url, {});\n } catch (err) {\n throw new SenseError('EMBED_MODEL', `could not reach ${url}: ${(err as Error).message}`);\n }\n if (!res.ok) throw new SenseError('EMBED_MODEL', `${url} -> HTTP ${res.status}`);\n writeFileAtomic(dest, Buffer.from(await res.arrayBuffer()));\n}\n\n// Fetches an already-known sha straight into its snapshot dir, without touching refs/main --\n// for downloadModel below, and for callers pinning an exact revision (the parity suite).\nexport async function downloadModelRevision(model: string, sha: string, onFile?: (file: string, dir: string) => void): Promise<string> {\n const dir = snapshotDir(model, sha);\n mkdirSync(dir, { recursive: true });\n for (const file of MODEL_FILES) {\n if (existsSync(join(dir, file))) continue;\n onFile?.(file, dir);\n await fetchToFile(`https://huggingface.co/${model}/resolve/${sha}/${file}`, join(dir, file));\n }\n return dir;\n}\n\n// The only code path that resolves `main`: a static provider's first construction calls it\n// lazily, `sense download` as an explicit prefetch. Idempotent either way.\nexport async function downloadModel(model: string, onFile?: (file: string, dir: string) => void): Promise<string> {\n if (!isDownloadable(model)) {\n throw new SenseError('EMBED_MODEL', `embed.model \"${model}\" is a local path, not a Hugging Face model id, so there is nothing to download; put model.safetensors and tokenizer.json in that directory, or name a model id like \"minishlab/potion-retrieval-32M\"`);\n }\n const sha = await resolveSha(model);\n // Independent of the weights above: a fresh HEAD/GET pair either way, so a lazy first\n // construction and an explicit prefetch both end up with a cached language verdict.\n await ensureLanguages(model);\n return downloadModelRevision(model, sha, onFile);\n}\n"],"names":["existsSync","mkdirSync","join","embedConfig","SenseError","writeFileAtomic","fetchWithRetry","hasModelFiles","isDownloadable","languagesPath","MODEL_FILES","readRef","snapshotDir","writeLanguages","writeRef","isKnownLanguageTag","MEASURED_MODEL_LANGUAGES","MODEL_FILENAMES","modelDir","readLanguages","refsMainPath","modelPresent","cfg","e","provider","model","localModelMissing","resolveSha","existing","url","res","method","redirect","err","message","sha","headers","get","languagesFromCard","info","lang","cardData","language","Array","isArray","length","tagLangs","tags","filter","undefined","ensureLanguages","measured","ok","json","fetchToFile","dest","status","Buffer","from","arrayBuffer","downloadModelRevision","onFile","dir","recursive","file","downloadModel"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  export type { Peek, PresetCoverage, SearchOptions, TreeMap } from './commands/index.js';
2
2
  export { mapTree, peek, presetCoverage, search } from './commands/index.js';
3
- export type { Config, EmbedConfig, FeatureName, InitOverrides, Preset, ResolvedConfig, SavedQuery, SavedSearch, SearchOverrides, SignalName, SignalWeights } from './config/index.js';
3
+ export type { Config, EmbedConfig, FeatureName, InitOverrides, Preset, ResolvedConfig, SavedQuery, SavedSearch, SearchOverrides, SignalName, SignalWeights, StoreName } from './config/index.js';
4
4
  export { CONFIG_FILENAME, initConfig, loadConfig, migrateConfig, SIGNAL_NAMES, STATE_DIR, SUPPORTED_CONFIG_VERSION } from './config/index.js';
5
5
  export type { SenseErrorCode } from './errors.js';
6
6
  export { SenseError } from './errors.js';
@@ -8,6 +8,6 @@ export type { Format, Row, RowFormat } from './output/output.js';
8
8
  export { printRows } from './output/output.js';
9
9
  export type { OpenResult } from './store/index.js';
10
10
  export { clearCache, openStore as open } from './store/index.js';
11
- export type { Capability, DocumentStore, LexicalHit, LexicalIndex, LexicalQueryOptions, RawStatement, RunResult, SqlSession, Statement, Store, VectorCandidate, VectorSimilar, VectorStore, VectorWriteRow } from './store/types.js';
11
+ export type { Capability, DocumentStore, FieldStat, LexicalHit, LexicalIndex, LexicalQueryOptions, RawStatement, RunResult, SqlSession, Statement, Store, VectorCandidate, VectorSimilar, VectorStore, VectorWriteRow } from './store/types.js';
12
12
  export type { WatchEvent, WatchOptions } from './watch.js';
13
13
  export { runWatch } from './watch.js';
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/index.ts"],"sourcesContent":["// Public library API. Deliberately small: every export is a stability promise;\n// internals (feature registry, graph, scan, meta) stay module-private.\n\nexport type { Peek, PresetCoverage, SearchOptions, TreeMap } from './commands/index.ts';\nexport { mapTree, peek, presetCoverage, search } from './commands/index.ts';\nexport type { Config, EmbedConfig, FeatureName, InitOverrides, Preset, ResolvedConfig, SavedQuery, SavedSearch, SearchOverrides, SignalName, SignalWeights } from './config/index.ts';\nexport { CONFIG_FILENAME, initConfig, loadConfig, migrateConfig, SIGNAL_NAMES, STATE_DIR, SUPPORTED_CONFIG_VERSION } from './config/index.ts';\nexport type { SenseErrorCode } from './errors.ts';\nexport { SenseError } from './errors.ts';\nexport type { Format, Row, RowFormat } from './output/output.ts';\nexport { printRows } from './output/output.ts';\nexport type { OpenResult } from './store/index.ts';\nexport { clearCache, openStore as open } from './store/index.ts';\nexport type { Capability, DocumentStore, LexicalHit, LexicalIndex, LexicalQueryOptions, RawStatement, RunResult, SqlSession, Statement, Store, VectorCandidate, VectorSimilar, VectorStore, VectorWriteRow } from './store/types.ts';\n\nexport type { WatchEvent, WatchOptions } from './watch.ts';\nexport { runWatch } from './watch.ts';\n"],"names":["mapTree","peek","presetCoverage","search","CONFIG_FILENAME","initConfig","loadConfig","migrateConfig","SIGNAL_NAMES","STATE_DIR","SUPPORTED_CONFIG_VERSION","SenseError","printRows","clearCache","openStore","open","runWatch"],"mappings":"AAAA,+EAA+E;AAC/E,uEAAuE;AAGvE,SAASA,OAAO,EAAEC,IAAI,EAAEC,cAAc,EAAEC,MAAM,QAAQ,sBAAsB;AAE5E,SAASC,eAAe,EAAEC,UAAU,EAAEC,UAAU,EAAEC,aAAa,EAAEC,YAAY,EAAEC,SAAS,EAAEC,wBAAwB,QAAQ,oBAAoB;AAE9I,SAASC,UAAU,QAAQ,cAAc;AAEzC,SAASC,SAAS,QAAQ,qBAAqB;AAE/C,SAASC,UAAU,EAAEC,aAAaC,IAAI,QAAQ,mBAAmB;AAIjE,SAASC,QAAQ,QAAQ,aAAa"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/index.ts"],"sourcesContent":["// Public library API. Deliberately small: every export is a stability promise;\n// internals (feature registry, graph, scan, meta) stay module-private.\n\nexport type { Peek, PresetCoverage, SearchOptions, TreeMap } from './commands/index.ts';\nexport { mapTree, peek, presetCoverage, search } from './commands/index.ts';\nexport type { Config, EmbedConfig, FeatureName, InitOverrides, Preset, ResolvedConfig, SavedQuery, SavedSearch, SearchOverrides, SignalName, SignalWeights, StoreName } from './config/index.ts';\nexport { CONFIG_FILENAME, initConfig, loadConfig, migrateConfig, SIGNAL_NAMES, STATE_DIR, SUPPORTED_CONFIG_VERSION } from './config/index.ts';\nexport type { SenseErrorCode } from './errors.ts';\nexport { SenseError } from './errors.ts';\nexport type { Format, Row, RowFormat } from './output/output.ts';\nexport { printRows } from './output/output.ts';\nexport type { OpenResult } from './store/index.ts';\nexport { clearCache, openStore as open } from './store/index.ts';\nexport type { Capability, DocumentStore, FieldStat, LexicalHit, LexicalIndex, LexicalQueryOptions, RawStatement, RunResult, SqlSession, Statement, Store, VectorCandidate, VectorSimilar, VectorStore, VectorWriteRow } from './store/types.ts';\n\nexport type { WatchEvent, WatchOptions } from './watch.ts';\nexport { runWatch } from './watch.ts';\n"],"names":["mapTree","peek","presetCoverage","search","CONFIG_FILENAME","initConfig","loadConfig","migrateConfig","SIGNAL_NAMES","STATE_DIR","SUPPORTED_CONFIG_VERSION","SenseError","printRows","clearCache","openStore","open","runWatch"],"mappings":"AAAA,+EAA+E;AAC/E,uEAAuE;AAGvE,SAASA,OAAO,EAAEC,IAAI,EAAEC,cAAc,EAAEC,MAAM,QAAQ,sBAAsB;AAE5E,SAASC,eAAe,EAAEC,UAAU,EAAEC,UAAU,EAAEC,aAAa,EAAEC,YAAY,EAAEC,SAAS,EAAEC,wBAAwB,QAAQ,oBAAoB;AAE9I,SAASC,UAAU,QAAQ,cAAc;AAEzC,SAASC,SAAS,QAAQ,qBAAqB;AAE/C,SAASC,UAAU,EAAEC,aAAaC,IAAI,QAAQ,mBAAmB;AAIjE,SAASC,QAAQ,QAAQ,aAAa"}
@@ -0,0 +1 @@
1
+ export declare function writeFileAtomic(path: string, data: string | Buffer): void;
@@ -0,0 +1,17 @@
1
+ import { renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
+ // Stage-then-swap: write beside the target, then rename over it. A crash or full disk mid-write
3
+ // leaves the target untouched instead of truncated. Matches embed/store.ts's model-download idiom.
4
+ export function writeFileAtomic(path, data) {
5
+ const tmp = `${path}.part`;
6
+ writeFileSync(tmp, data);
7
+ try {
8
+ renameSync(tmp, path);
9
+ } catch (err) {
10
+ try {
11
+ unlinkSync(tmp);
12
+ } catch {
13
+ // best-effort cleanup; the rename error below is what matters
14
+ }
15
+ throw err;
16
+ }
17
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/lib/atomic-write.ts"],"sourcesContent":["import { renameSync, unlinkSync, writeFileSync } from 'node:fs';\n\n// Stage-then-swap: write beside the target, then rename over it. A crash or full disk mid-write\n// leaves the target untouched instead of truncated. Matches embed/store.ts's model-download idiom.\nexport function writeFileAtomic(path: string, data: string | Buffer): void {\n const tmp = `${path}.part`;\n writeFileSync(tmp, data);\n try {\n renameSync(tmp, path);\n } catch (err) {\n try {\n unlinkSync(tmp);\n } catch {\n // best-effort cleanup; the rename error below is what matters\n }\n throw err;\n }\n}\n"],"names":["renameSync","unlinkSync","writeFileSync","writeFileAtomic","path","data","tmp","err"],"mappings":"AAAA,SAASA,UAAU,EAAEC,UAAU,EAAEC,aAAa,QAAQ,UAAU;AAEhE,gGAAgG;AAChG,mGAAmG;AACnG,OAAO,SAASC,gBAAgBC,IAAY,EAAEC,IAAqB;IACjE,MAAMC,MAAM,GAAGF,KAAK,KAAK,CAAC;IAC1BF,cAAcI,KAAKD;IACnB,IAAI;QACFL,WAAWM,KAAKF;IAClB,EAAE,OAAOG,KAAK;QACZ,IAAI;YACFN,WAAWK;QACb,EAAE,OAAM;QACN,8DAA8D;QAChE;QACA,MAAMC;IACR;AACF"}
@@ -7,4 +7,9 @@ export interface ReparseResult {
7
7
  warnings: string[];
8
8
  newColumns: string[];
9
9
  }
10
- export declare function reparseFiles(files: FileStat[], features: Feature[], cfg: Config, knownColumns: ReadonlySet<string>, onParsed?: (done: number) => void): ReparseResult;
10
+ export interface ReparseOptions {
11
+ threshold?: number;
12
+ maxWorkers?: number;
13
+ }
14
+ export declare function featuresForFile(features: Feature[], cfg: Config, file: FileStat): Feature[];
15
+ export declare function reparseFiles(files: FileStat[], features: Feature[], cfg: Config, knownColumns: ReadonlySet<string>, onParsed?: (done: number) => void, options?: ReparseOptions): Promise<ReparseResult>;
@@ -1,18 +1,118 @@
1
+ import { createRequire } from 'node:module';
2
+ import { availableParallelism } from 'node:os';
3
+ import { dirname, join } from 'node:path';
1
4
  import { parseFile } from './index.js';
5
+ import { reviveError } from './worker-error.js';
6
+ // Tinypool is ESM-only; our floor (>=22.20) has native require(esm), so the tier-2 house
7
+ // deferral reaches it. Deferred (not a top-level import) because most reconciles stay below
8
+ // DEFAULT_THRESHOLD and should not pay for constructing pool machinery they never use.
9
+ const _require = typeof require === 'undefined' ? createRequire(import.meta.url) : require;
10
+ // Measured 2026-08-29 on a 14-logical-core Apple M4 Pro: serial and pooled cross between 160
11
+ // and 180 files (pool fixed cost here is ~85-100ms, not the ~300ms a heavier module graph
12
+ // costs elsewhere), so 200 keeps a safety margin without giving up real wins. 8 workers matches
13
+ // an interleaved worker-count sweep at 6,566 files -- 8 and 10 tie within noise, 12+ trends
14
+ // worse (main-thread + OS contention on the 6 non-parse-worker cores).
15
+ const DEFAULT_THRESHOLD = 200;
16
+ const DEFAULT_MAX_WORKERS = 8;
17
+ // Worker MUST always load from dist/cjs/ (house pattern: install-module-linked,
18
+ // install-optional, node-version-install, node-exec-path all carry this same rule). A
19
+ // worker_threads thread is a fresh realm and does not inherit the main thread's TS loader
20
+ // hook, so a source path cannot work on any platform; dist/cjs loads from the esm and cjs
21
+ // builds alike. Root is found the way cli.ts finds it, so this also resolves when the suite
22
+ // runs against src/ (which already requires a build for the exports specs).
23
+ // Resolved on first pooled dispatch, not at import: a tree under the threshold never pools and
24
+ // must not pay for this, let alone fail on it.
25
+ let workerFile;
26
+ function resolveWorkerFile() {
27
+ if (workerFile) return workerFile;
28
+ const load = createRequire(import.meta.url);
29
+ for (const rel of [
30
+ '..',
31
+ '../..',
32
+ '../../..'
33
+ ]){
34
+ try {
35
+ if (load(`${rel}/package.json`).name === 'sensemaking') {
36
+ workerFile = join(dirname(load.resolve(`${rel}/package.json`)), 'dist', 'cjs', 'workers', 'parse.js');
37
+ return workerFile;
38
+ }
39
+ } catch {}
40
+ }
41
+ throw new Error('cannot locate the sensemaking package root, so the parse worker cannot be found; run npm run build');
42
+ }
43
+ // Per-file feature filtering, shared by the serial loop (which reads `features` straight from
44
+ // the caller, real registry or a test double) and the worker path (which can only pass `cfg`
45
+ // across the thread boundary, so it derives `features` from activeFeatures(cfg) itself).
46
+ export function featuresForFile(features, cfg, file) {
47
+ return features.filter((feature)=>!feature.enabledForFile || feature.enabledForFile(cfg, file));
48
+ }
49
+ function reparseSerial(files, features, cfg, onParsed) {
50
+ const results = [];
51
+ let done = 0;
52
+ for (const file of files){
53
+ const fileFeatures = featuresForFile(features, cfg, file);
54
+ results.push(parseFile(file, fileFeatures, cfg));
55
+ onParsed === null || onParsed === void 0 ? void 0 : onParsed(++done);
56
+ }
57
+ return results;
58
+ }
59
+ // Never the tree: a worker task carries one FileStat and returns only what parseFile already
60
+ // returns -- extracted text and feature values, never an mdast tree. The worker reads the file
61
+ // itself. Pool created once here and destroyed once dispatch finishes; never per task.
62
+ async function reparsePooled(files, features, cfg, onParsed, maxWorkers) {
63
+ const { Tinypool } = _require('tinypool');
64
+ // cfg and the caller's feature selection are constant for the whole dispatch, so they cross
65
+ // once per worker as workerData rather than once per file in the task payload. Features carry
66
+ // closures and cannot cross at all; their names can, and the worker resolves them back
67
+ // against the same registry, so a caller passing a subset gets that subset in both modes.
68
+ const workerData = {
69
+ cfg,
70
+ featureNames: features.map((feature)=>feature.name)
71
+ };
72
+ const pool = new Tinypool({
73
+ filename: resolveWorkerFile(),
74
+ minThreads: maxWorkers,
75
+ maxThreads: maxWorkers,
76
+ workerData
77
+ });
78
+ let done = 0;
79
+ try {
80
+ // Promise.all over a mapped array is load-bearing: the resolved array keeps `files`
81
+ // order regardless of which task finishes first, which is what first-seen column order
82
+ // and warning order depend on downstream. Progress ticks fire in completion order instead
83
+ // (only the count matters for a progress bar) -- free, since it rides the same per-task
84
+ // response tinypool already sends, no extra ping.
85
+ const results = await Promise.all(files.map(async (file)=>{
86
+ const result = await pool.run(file);
87
+ if (!result.ok) throw reviveError(result.error);
88
+ onParsed === null || onParsed === void 0 ? void 0 : onParsed(++done);
89
+ return {
90
+ doc: result.doc,
91
+ warnings: result.warnings
92
+ };
93
+ }));
94
+ await pool.destroy();
95
+ return results;
96
+ } catch (err) {
97
+ // Tear the pool down before rethrowing, but never let a destroy failure replace the parse
98
+ // failure that caused it: that one names the file, this one names nothing. Not a `finally`
99
+ // for exactly that reason.
100
+ await pool.destroy().catch(()=>{});
101
+ throw err;
102
+ }
103
+ }
2
104
  // `onParsed` receives the running count (1-based) after each file, mirroring a Progress.tick
3
105
  // call; pass one in to keep progress reporting working without this module owning a reporter.
4
- export function reparseFiles(files, features, cfg, knownColumns, onParsed) {
106
+ export async function reparseFiles(files, features, cfg, knownColumns, onParsed, options = {}) {
107
+ var _options_threshold, _options_maxWorkers;
108
+ const threshold = (_options_threshold = options.threshold) !== null && _options_threshold !== void 0 ? _options_threshold : DEFAULT_THRESHOLD;
109
+ const maxWorkers = (_options_maxWorkers = options.maxWorkers) !== null && _options_maxWorkers !== void 0 ? _options_maxWorkers : Math.min(DEFAULT_MAX_WORKERS, availableParallelism());
110
+ const results = files.length >= threshold ? await reparsePooled(files, features, cfg, onParsed, maxWorkers) : reparseSerial(files, features, cfg, onParsed);
5
111
  const docs = [];
6
112
  const warnings = [];
7
113
  const newColumns = [];
8
114
  const seen = new Set(knownColumns);
9
- let done = 0;
10
- for (const file of files){
11
- // A doc only gets extract/store from features that apply to it (currently: embed, via
12
- // FileStat.embed -- true iff the config names an embedding model).
13
- const fileFeatures = features.filter((feature)=>!feature.enabledForFile || feature.enabledForFile(cfg, file));
14
- const { doc, warnings: fileWarnings } = parseFile(file, fileFeatures, cfg);
15
- onParsed === null || onParsed === void 0 ? void 0 : onParsed(++done);
115
+ for (const { doc, warnings: fileWarnings } of results){
16
116
  warnings.push(...fileWarnings);
17
117
  for (const key of Object.keys(doc.data)){
18
118
  if (!seen.has(key)) {
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/scan/reparse.ts"],"sourcesContent":["import type { Config } from '../config/index.ts';\nimport type { Feature } from '../features/types.ts';\nimport type { ParsedDoc } from './index.ts';\nimport { parseFile } from './index.ts';\nimport type { FileStat } from './list.ts';\n\n// Store-agnostic per-file parse pass shared by every store's reconcile(). Index-preserving by\n// construction (one pass over `files`, pushed in order) so a future concurrent dispatch can\n// replace the loop body without changing this contract or either call site.\nexport interface ReparseResult {\n docs: ParsedDoc[];\n warnings: string[];\n // Frontmatter keys not in `knownColumns`, first-seen order across `files` -- the order\n // callers ALTER TABLE ADD COLUMN in.\n newColumns: string[];\n}\n\n// `onParsed` receives the running count (1-based) after each file, mirroring a Progress.tick\n// call; pass one in to keep progress reporting working without this module owning a reporter.\nexport function reparseFiles(files: FileStat[], features: Feature[], cfg: Config, knownColumns: ReadonlySet<string>, onParsed?: (done: number) => void): ReparseResult {\n const docs: ParsedDoc[] = [];\n const warnings: string[] = [];\n const newColumns: string[] = [];\n const seen = new Set(knownColumns);\n let done = 0;\n\n for (const file of files) {\n // A doc only gets extract/store from features that apply to it (currently: embed, via\n // FileStat.embed -- true iff the config names an embedding model).\n const fileFeatures = features.filter((feature) => !feature.enabledForFile || feature.enabledForFile(cfg, file));\n const { doc, warnings: fileWarnings } = parseFile(file, fileFeatures, cfg);\n onParsed?.(++done);\n warnings.push(...fileWarnings);\n for (const key of Object.keys(doc.data)) {\n if (!seen.has(key)) {\n seen.add(key);\n newColumns.push(key);\n }\n }\n docs.push(doc);\n }\n\n return { docs, warnings, newColumns };\n}\n"],"names":["parseFile","reparseFiles","files","features","cfg","knownColumns","onParsed","docs","warnings","newColumns","seen","Set","done","file","fileFeatures","filter","feature","enabledForFile","doc","fileWarnings","push","key","Object","keys","data","has","add"],"mappings":"AAGA,SAASA,SAAS,QAAQ,aAAa;AAcvC,6FAA6F;AAC7F,8FAA8F;AAC9F,OAAO,SAASC,aAAaC,KAAiB,EAAEC,QAAmB,EAAEC,GAAW,EAAEC,YAAiC,EAAEC,QAAiC;IACpJ,MAAMC,OAAoB,EAAE;IAC5B,MAAMC,WAAqB,EAAE;IAC7B,MAAMC,aAAuB,EAAE;IAC/B,MAAMC,OAAO,IAAIC,IAAIN;IACrB,IAAIO,OAAO;IAEX,KAAK,MAAMC,QAAQX,MAAO;QACxB,sFAAsF;QACtF,mEAAmE;QACnE,MAAMY,eAAeX,SAASY,MAAM,CAAC,CAACC,UAAY,CAACA,QAAQC,cAAc,IAAID,QAAQC,cAAc,CAACb,KAAKS;QACzG,MAAM,EAAEK,GAAG,EAAEV,UAAUW,YAAY,EAAE,GAAGnB,UAAUa,MAAMC,cAAcV;QACtEE,qBAAAA,+BAAAA,SAAW,EAAEM;QACbJ,SAASY,IAAI,IAAID;QACjB,KAAK,MAAME,OAAOC,OAAOC,IAAI,CAACL,IAAIM,IAAI,EAAG;YACvC,IAAI,CAACd,KAAKe,GAAG,CAACJ,MAAM;gBAClBX,KAAKgB,GAAG,CAACL;gBACTZ,WAAWW,IAAI,CAACC;YAClB;QACF;QACAd,KAAKa,IAAI,CAACF;IACZ;IAEA,OAAO;QAAEX;QAAMC;QAAUC;IAAW;AACtC"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/scan/reparse.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport { availableParallelism } from 'node:os';\nimport { dirname, join } from 'node:path';\n// Type-only: erased at build, but keeps depcheck's usage check satisfied for the tier-2\n// `_require` below (see coding-standards' deferral tiers).\nimport type * as TinypoolNS from 'tinypool';\nimport type { Config } from '../config/index.ts';\nimport type { Feature } from '../features/types.ts';\nimport type { ParseTask, ParseTaskResult, ParseWorkerData } from '../workers/parse.ts';\nimport type { ParsedDoc } from './index.ts';\nimport { parseFile } from './index.ts';\nimport type { FileStat } from './list.ts';\nimport { reviveError } from './worker-error.ts';\n\n// Tinypool is ESM-only; our floor (>=22.20) has native require(esm), so the tier-2 house\n// deferral reaches it. Deferred (not a top-level import) because most reconciles stay below\n// DEFAULT_THRESHOLD and should not pay for constructing pool machinery they never use.\nconst _require = typeof require === 'undefined' ? createRequire(import.meta.url) : require;\n\n// Store-agnostic per-file parse pass shared by every store's reconcile(). Index-preserving by\n// construction (one pass over `files`, pushed in order) so a future concurrent dispatch can\n// replace the loop body without changing this contract or either call site.\nexport interface ReparseResult {\n docs: ParsedDoc[];\n warnings: string[];\n // Frontmatter keys not in `knownColumns`, first-seen order across `files` -- the order\n // callers ALTER TABLE ADD COLUMN in.\n newColumns: string[];\n}\n\nexport interface ReparseOptions {\n // Overrides DEFAULT_THRESHOLD below, so tests can force either dispatch mode without\n // needing thousands of fixture files. Internal: no caller in src/ passes it.\n threshold?: number;\n // Overrides DEFAULT_MAX_WORKERS below.\n maxWorkers?: number;\n}\n\n// Measured 2026-08-29 on a 14-logical-core Apple M4 Pro: serial and pooled cross between 160\n// and 180 files (pool fixed cost here is ~85-100ms, not the ~300ms a heavier module graph\n// costs elsewhere), so 200 keeps a safety margin without giving up real wins. 8 workers matches\n// an interleaved worker-count sweep at 6,566 files -- 8 and 10 tie within noise, 12+ trends\n// worse (main-thread + OS contention on the 6 non-parse-worker cores).\nconst DEFAULT_THRESHOLD = 200;\nconst DEFAULT_MAX_WORKERS = 8;\n\n// Worker MUST always load from dist/cjs/ (house pattern: install-module-linked,\n// install-optional, node-version-install, node-exec-path all carry this same rule). A\n// worker_threads thread is a fresh realm and does not inherit the main thread's TS loader\n// hook, so a source path cannot work on any platform; dist/cjs loads from the esm and cjs\n// builds alike. Root is found the way cli.ts finds it, so this also resolves when the suite\n// runs against src/ (which already requires a build for the exports specs).\n// Resolved on first pooled dispatch, not at import: a tree under the threshold never pools and\n// must not pay for this, let alone fail on it.\nlet workerFile: string | undefined;\nfunction resolveWorkerFile(): string {\n if (workerFile) return workerFile;\n const load = createRequire(import.meta.url);\n for (const rel of ['..', '../..', '../../..']) {\n try {\n if ((load(`${rel}/package.json`) as { name?: string }).name === 'sensemaking') {\n workerFile = join(dirname(load.resolve(`${rel}/package.json`)), 'dist', 'cjs', 'workers', 'parse.js');\n return workerFile;\n }\n } catch {}\n }\n throw new Error('cannot locate the sensemaking package root, so the parse worker cannot be found; run npm run build');\n}\n\ntype FileResult = { doc: ParsedDoc; warnings: string[] };\n\n// Per-file feature filtering, shared by the serial loop (which reads `features` straight from\n// the caller, real registry or a test double) and the worker path (which can only pass `cfg`\n// across the thread boundary, so it derives `features` from activeFeatures(cfg) itself).\nexport function featuresForFile(features: Feature[], cfg: Config, file: FileStat): Feature[] {\n return features.filter((feature) => !feature.enabledForFile || feature.enabledForFile(cfg, file));\n}\n\nfunction reparseSerial(files: FileStat[], features: Feature[], cfg: Config, onParsed?: (done: number) => void): FileResult[] {\n const results: FileResult[] = [];\n let done = 0;\n for (const file of files) {\n const fileFeatures = featuresForFile(features, cfg, file);\n results.push(parseFile(file, fileFeatures, cfg));\n onParsed?.(++done);\n }\n return results;\n}\n\n// Never the tree: a worker task carries one FileStat and returns only what parseFile already\n// returns -- extracted text and feature values, never an mdast tree. The worker reads the file\n// itself. Pool created once here and destroyed once dispatch finishes; never per task.\nasync function reparsePooled(files: FileStat[], features: Feature[], cfg: Config, onParsed: ((done: number) => void) | undefined, maxWorkers: number): Promise<FileResult[]> {\n const { Tinypool } = _require('tinypool') as typeof TinypoolNS;\n // cfg and the caller's feature selection are constant for the whole dispatch, so they cross\n // once per worker as workerData rather than once per file in the task payload. Features carry\n // closures and cannot cross at all; their names can, and the worker resolves them back\n // against the same registry, so a caller passing a subset gets that subset in both modes.\n const workerData: ParseWorkerData = { cfg, featureNames: features.map((feature) => feature.name) };\n const pool = new Tinypool({ filename: resolveWorkerFile(), minThreads: maxWorkers, maxThreads: maxWorkers, workerData });\n let done = 0;\n try {\n // Promise.all over a mapped array is load-bearing: the resolved array keeps `files`\n // order regardless of which task finishes first, which is what first-seen column order\n // and warning order depend on downstream. Progress ticks fire in completion order instead\n // (only the count matters for a progress bar) -- free, since it rides the same per-task\n // response tinypool already sends, no extra ping.\n const results = await Promise.all(\n files.map(async (file): Promise<FileResult> => {\n const result = (await pool.run(file as ParseTask)) as ParseTaskResult;\n if (!result.ok) throw reviveError(result.error);\n onParsed?.(++done);\n return { doc: result.doc, warnings: result.warnings };\n })\n );\n await pool.destroy();\n return results;\n } catch (err) {\n // Tear the pool down before rethrowing, but never let a destroy failure replace the parse\n // failure that caused it: that one names the file, this one names nothing. Not a `finally`\n // for exactly that reason.\n await pool.destroy().catch(() => {});\n throw err;\n }\n}\n\n// `onParsed` receives the running count (1-based) after each file, mirroring a Progress.tick\n// call; pass one in to keep progress reporting working without this module owning a reporter.\nexport async function reparseFiles(files: FileStat[], features: Feature[], cfg: Config, knownColumns: ReadonlySet<string>, onParsed?: (done: number) => void, options: ReparseOptions = {}): Promise<ReparseResult> {\n const threshold = options.threshold ?? DEFAULT_THRESHOLD;\n const maxWorkers = options.maxWorkers ?? Math.min(DEFAULT_MAX_WORKERS, availableParallelism());\n const results = files.length >= threshold ? await reparsePooled(files, features, cfg, onParsed, maxWorkers) : reparseSerial(files, features, cfg, onParsed);\n\n const docs: ParsedDoc[] = [];\n const warnings: string[] = [];\n const newColumns: string[] = [];\n const seen = new Set(knownColumns);\n for (const { doc, warnings: fileWarnings } of results) {\n warnings.push(...fileWarnings);\n for (const key of Object.keys(doc.data)) {\n if (!seen.has(key)) {\n seen.add(key);\n newColumns.push(key);\n }\n }\n docs.push(doc);\n }\n\n return { docs, warnings, newColumns };\n}\n"],"names":["createRequire","availableParallelism","dirname","join","parseFile","reviveError","_require","require","url","DEFAULT_THRESHOLD","DEFAULT_MAX_WORKERS","workerFile","resolveWorkerFile","load","rel","name","resolve","Error","featuresForFile","features","cfg","file","filter","feature","enabledForFile","reparseSerial","files","onParsed","results","done","fileFeatures","push","reparsePooled","maxWorkers","Tinypool","workerData","featureNames","map","pool","filename","minThreads","maxThreads","Promise","all","result","run","ok","error","doc","warnings","destroy","err","catch","reparseFiles","knownColumns","options","threshold","Math","min","length","docs","newColumns","seen","Set","fileWarnings","key","Object","keys","data","has","add"],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import { type SenseErrorCode } from '../errors.js';
2
+ export interface WorkerErrorPayload {
3
+ name: string;
4
+ code?: SenseErrorCode;
5
+ message: string;
6
+ stack?: string;
7
+ }
8
+ export declare function serializeError(err: unknown): WorkerErrorPayload;
9
+ export declare function reviveError(payload: WorkerErrorPayload): Error;
@@ -0,0 +1,19 @@
1
+ import { SenseError } from '../errors.js';
2
+ export function serializeError(err) {
3
+ if (err instanceof Error) return {
4
+ name: err.name,
5
+ code: err instanceof SenseError ? err.code : undefined,
6
+ message: err.message,
7
+ stack: err.stack
8
+ };
9
+ return {
10
+ name: 'Error',
11
+ message: String(err)
12
+ };
13
+ }
14
+ export function reviveError(payload) {
15
+ const err = payload.code ? new SenseError(payload.code, payload.message) : new Error(payload.message);
16
+ err.name = payload.name;
17
+ if (payload.stack) err.stack = payload.stack;
18
+ return err;
19
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/scan/worker-error.ts"],"sourcesContent":["import { SenseError, type SenseErrorCode } from '../errors.ts';\n\n// Structured clone across a worker_threads boundary drops a subclass and its custom\n// properties, so a SenseError arrives as a plain Error with `code` undefined -- and `code`\n// drives caller behavior. The worker serializes explicitly (computed on its side, where\n// `instanceof SenseError` is still meaningful) and the main thread rebuilds by hand.\nexport interface WorkerErrorPayload {\n name: string;\n code?: SenseErrorCode;\n message: string;\n stack?: string;\n}\n\nexport function serializeError(err: unknown): WorkerErrorPayload {\n if (err instanceof Error) return { name: err.name, code: err instanceof SenseError ? err.code : undefined, message: err.message, stack: err.stack };\n return { name: 'Error', message: String(err) };\n}\n\nexport function reviveError(payload: WorkerErrorPayload): Error {\n const err = payload.code ? new SenseError(payload.code, payload.message) : new Error(payload.message);\n err.name = payload.name;\n if (payload.stack) err.stack = payload.stack;\n return err;\n}\n"],"names":["SenseError","serializeError","err","Error","name","code","undefined","message","stack","String","reviveError","payload"],"mappings":"AAAA,SAASA,UAAU,QAA6B,eAAe;AAa/D,OAAO,SAASC,eAAeC,GAAY;IACzC,IAAIA,eAAeC,OAAO,OAAO;QAAEC,MAAMF,IAAIE,IAAI;QAAEC,MAAMH,eAAeF,aAAaE,IAAIG,IAAI,GAAGC;QAAWC,SAASL,IAAIK,OAAO;QAAEC,OAAON,IAAIM,KAAK;IAAC;IAClJ,OAAO;QAAEJ,MAAM;QAASG,SAASE,OAAOP;IAAK;AAC/C;AAEA,OAAO,SAASQ,YAAYC,OAA2B;IACrD,MAAMT,MAAMS,QAAQN,IAAI,GAAG,IAAIL,WAAWW,QAAQN,IAAI,EAAEM,QAAQJ,OAAO,IAAI,IAAIJ,MAAMQ,QAAQJ,OAAO;IACpGL,IAAIE,IAAI,GAAGO,QAAQP,IAAI;IACvB,IAAIO,QAAQH,KAAK,EAAEN,IAAIM,KAAK,GAAGG,QAAQH,KAAK;IAC5C,OAAON;AACT"}
@@ -4,8 +4,9 @@
4
4
  // not just the async crossing to reach them, so this recognizes the three statement shapes the
5
5
  // store issues (INSERT ... VALUES (...), UPDATE ... SET ... WHERE ..., DELETE ... WHERE ...) and
6
6
  // rewrites each into a multi-row form, verified against a live DuckDB connection at
7
- // implementation time (principle 8). Unrecognized SQL returns null; connection.ts's runBatch
8
- // falls back to a prepare-once, bind-and-run loop -- still one call from outside the store.
7
+ // implementation time (PRINCIPLES: documented-means-tested). Unrecognized SQL returns null;
8
+ // connection.ts's runBatch falls back to a prepare-once, bind-and-run loop -- still one call
9
+ // from outside the store.
9
10
  //
10
11
  // This only ever runs against sense's own SQL literals (reconcile, the feature hooks), never
11
12
  // arbitrary user SQL (`sense sql` is a separate, unrewritten passthrough), so the shapes below