sensemaking 0.7.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/README.md +90 -61
  2. package/dist/cjs/cli/check.js +367 -0
  3. package/dist/cjs/cli/check.js.map +1 -0
  4. package/dist/cjs/{commands → cli}/index.js +2 -2
  5. package/dist/cjs/cli/index.js.map +1 -0
  6. package/dist/cjs/{commands → cli}/init.js +3 -0
  7. package/dist/cjs/cli/init.js.map +1 -0
  8. package/dist/cjs/{commands → cli}/map.js +2 -2
  9. package/dist/cjs/cli/map.js.map +1 -0
  10. package/dist/cjs/{commands → cli}/named.d.cts +1 -1
  11. package/dist/cjs/{commands → cli}/named.d.ts +1 -1
  12. package/dist/cjs/cli/named.js +219 -0
  13. package/dist/cjs/cli/named.js.map +1 -0
  14. package/dist/cjs/{commands → cli}/peek.js +2 -2
  15. package/dist/cjs/cli/peek.js.map +1 -0
  16. package/dist/cjs/cli/query.js.map +1 -0
  17. package/dist/cjs/cli/rebuild.js.map +1 -0
  18. package/dist/cjs/cli/search.d.cts +3 -0
  19. package/dist/cjs/cli/search.d.ts +3 -0
  20. package/dist/cjs/{commands/find.js → cli/search.js} +9 -8
  21. package/dist/cjs/cli/search.js.map +1 -0
  22. package/dist/cjs/{commands → cli}/shared.d.cts +1 -0
  23. package/dist/{esm/commands → cjs/cli}/shared.d.ts +1 -0
  24. package/dist/cjs/{commands → cli}/shared.js +9 -0
  25. package/dist/cjs/cli/shared.js.map +1 -0
  26. package/dist/cjs/{commands → cli}/status.js +36 -13
  27. package/dist/cjs/cli/status.js.map +1 -0
  28. package/dist/cjs/{commands → cli}/types.d.cts +3 -1
  29. package/dist/{esm/commands → cjs/cli}/types.d.ts +3 -1
  30. package/dist/cjs/cli/watch.js.map +1 -0
  31. package/dist/cjs/cli.js +42 -17
  32. package/dist/cjs/cli.js.map +1 -1
  33. package/dist/{esm/verbs.d.ts → cjs/commands.d.cts} +15 -5
  34. package/dist/cjs/{verbs.d.cts → commands.d.ts} +15 -5
  35. package/dist/cjs/{verbs.js → commands.js} +329 -39
  36. package/dist/cjs/commands.js.map +1 -0
  37. package/dist/cjs/config.d.cts +45 -12
  38. package/dist/cjs/config.d.ts +45 -12
  39. package/dist/cjs/config.js +464 -125
  40. package/dist/cjs/config.js.map +1 -1
  41. package/dist/cjs/db.d.cts +1 -1
  42. package/dist/cjs/db.d.ts +1 -1
  43. package/dist/cjs/db.js +293 -76
  44. package/dist/cjs/db.js.map +1 -1
  45. package/dist/cjs/errors.d.cts +1 -1
  46. package/dist/cjs/errors.d.ts +1 -1
  47. package/dist/cjs/errors.js.map +1 -1
  48. package/dist/cjs/features/embed.js +19 -3
  49. package/dist/cjs/features/embed.js.map +1 -1
  50. package/dist/cjs/features/links.js +201 -24
  51. package/dist/cjs/features/links.js.map +1 -1
  52. package/dist/cjs/features/rank.js +8 -1
  53. package/dist/cjs/features/rank.js.map +1 -1
  54. package/dist/cjs/features/types.d.cts +12 -4
  55. package/dist/cjs/features/types.d.ts +12 -4
  56. package/dist/cjs/index.d.cts +3 -3
  57. package/dist/cjs/index.d.ts +3 -3
  58. package/dist/cjs/index.js +9 -6
  59. package/dist/cjs/index.js.map +1 -1
  60. package/dist/cjs/output.d.cts +16 -0
  61. package/dist/cjs/output.d.ts +16 -0
  62. package/dist/cjs/output.js +27 -5
  63. package/dist/cjs/output.js.map +1 -1
  64. package/dist/cjs/progress.d.cts +5 -0
  65. package/dist/cjs/progress.d.ts +5 -0
  66. package/dist/cjs/progress.js +54 -0
  67. package/dist/cjs/progress.js.map +1 -0
  68. package/dist/cjs/scan.d.cts +4 -0
  69. package/dist/cjs/scan.d.ts +4 -0
  70. package/dist/cjs/scan.js +87 -6
  71. package/dist/cjs/scan.js.map +1 -1
  72. package/dist/esm/{commands → cli}/check.js +41 -22
  73. package/dist/esm/cli/check.js.map +1 -0
  74. package/dist/esm/{commands → cli}/index.js +1 -1
  75. package/dist/esm/cli/index.js.map +1 -0
  76. package/dist/esm/{commands → cli}/init.js +3 -0
  77. package/dist/esm/cli/init.js.map +1 -0
  78. package/dist/esm/{commands → cli}/map.js +1 -1
  79. package/dist/esm/cli/map.js.map +1 -0
  80. package/dist/esm/{commands → cli}/named.d.ts +1 -1
  81. package/dist/esm/cli/named.js +41 -0
  82. package/dist/esm/cli/named.js.map +1 -0
  83. package/dist/esm/{commands → cli}/peek.js +1 -1
  84. package/dist/esm/cli/peek.js.map +1 -0
  85. package/dist/esm/cli/query.js.map +1 -0
  86. package/dist/esm/cli/rebuild.js.map +1 -0
  87. package/dist/esm/cli/search.d.ts +3 -0
  88. package/dist/esm/cli/search.js +16 -0
  89. package/dist/esm/cli/search.js.map +1 -0
  90. package/dist/{cjs/commands → esm/cli}/shared.d.ts +1 -0
  91. package/dist/esm/{commands → cli}/shared.js +9 -0
  92. package/dist/esm/cli/shared.js.map +1 -0
  93. package/dist/esm/{commands → cli}/status.js +17 -11
  94. package/dist/esm/cli/status.js.map +1 -0
  95. package/dist/{cjs/commands → esm/cli}/types.d.ts +3 -1
  96. package/dist/esm/cli/types.js.map +1 -0
  97. package/dist/esm/cli/watch.js.map +1 -0
  98. package/dist/esm/cli.js +24 -6
  99. package/dist/esm/cli.js.map +1 -1
  100. package/dist/{cjs/verbs.d.ts → esm/commands.d.ts} +15 -5
  101. package/dist/esm/commands.js +386 -0
  102. package/dist/esm/commands.js.map +1 -0
  103. package/dist/esm/config.d.ts +45 -12
  104. package/dist/esm/config.js +367 -93
  105. package/dist/esm/config.js.map +1 -1
  106. package/dist/esm/db.d.ts +1 -1
  107. package/dist/esm/db.js +126 -17
  108. package/dist/esm/db.js.map +1 -1
  109. package/dist/esm/errors.d.ts +1 -1
  110. package/dist/esm/errors.js.map +1 -1
  111. package/dist/esm/features/embed.js +18 -2
  112. package/dist/esm/features/embed.js.map +1 -1
  113. package/dist/esm/features/links.js +130 -20
  114. package/dist/esm/features/links.js.map +1 -1
  115. package/dist/esm/features/rank.js +8 -1
  116. package/dist/esm/features/rank.js.map +1 -1
  117. package/dist/esm/features/types.d.ts +12 -4
  118. package/dist/esm/features/types.js.map +1 -1
  119. package/dist/esm/index.d.ts +3 -3
  120. package/dist/esm/index.js +1 -1
  121. package/dist/esm/index.js.map +1 -1
  122. package/dist/esm/output.d.ts +16 -0
  123. package/dist/esm/output.js +20 -5
  124. package/dist/esm/output.js.map +1 -1
  125. package/dist/esm/progress.d.ts +5 -0
  126. package/dist/esm/progress.js +44 -0
  127. package/dist/esm/progress.js.map +1 -0
  128. package/dist/esm/scan.d.ts +4 -0
  129. package/dist/esm/scan.js +37 -6
  130. package/dist/esm/scan.js.map +1 -1
  131. package/package.json +12 -2
  132. package/schema.json +85 -50
  133. package/skills/sense/EXAMPLES.md +16 -12
  134. package/skills/sense/SKILL.md +74 -55
  135. package/skills/sense-setup/EXAMPLES.md +121 -0
  136. package/skills/sense-setup/SKILL.md +73 -54
  137. package/dist/cjs/commands/check.js +0 -143
  138. package/dist/cjs/commands/check.js.map +0 -1
  139. package/dist/cjs/commands/find.d.cts +0 -3
  140. package/dist/cjs/commands/find.d.ts +0 -3
  141. package/dist/cjs/commands/find.js.map +0 -1
  142. package/dist/cjs/commands/index.js.map +0 -1
  143. package/dist/cjs/commands/init.js.map +0 -1
  144. package/dist/cjs/commands/map.js.map +0 -1
  145. package/dist/cjs/commands/named.js +0 -23
  146. package/dist/cjs/commands/named.js.map +0 -1
  147. package/dist/cjs/commands/peek.js.map +0 -1
  148. package/dist/cjs/commands/query.js.map +0 -1
  149. package/dist/cjs/commands/rebuild.js.map +0 -1
  150. package/dist/cjs/commands/shared.js.map +0 -1
  151. package/dist/cjs/commands/status.js.map +0 -1
  152. package/dist/cjs/commands/watch.js.map +0 -1
  153. package/dist/cjs/verbs.js.map +0 -1
  154. package/dist/esm/commands/check.js.map +0 -1
  155. package/dist/esm/commands/find.d.ts +0 -3
  156. package/dist/esm/commands/find.js +0 -15
  157. package/dist/esm/commands/find.js.map +0 -1
  158. package/dist/esm/commands/index.js.map +0 -1
  159. package/dist/esm/commands/init.js.map +0 -1
  160. package/dist/esm/commands/map.js.map +0 -1
  161. package/dist/esm/commands/named.js +0 -12
  162. package/dist/esm/commands/named.js.map +0 -1
  163. package/dist/esm/commands/peek.js.map +0 -1
  164. package/dist/esm/commands/query.js.map +0 -1
  165. package/dist/esm/commands/rebuild.js.map +0 -1
  166. package/dist/esm/commands/shared.js.map +0 -1
  167. package/dist/esm/commands/status.js.map +0 -1
  168. package/dist/esm/commands/types.js.map +0 -1
  169. package/dist/esm/commands/watch.js.map +0 -1
  170. package/dist/esm/verbs.js +0 -190
  171. package/dist/esm/verbs.js.map +0 -1
  172. /package/dist/cjs/{commands → cli}/check.d.cts +0 -0
  173. /package/dist/cjs/{commands → cli}/check.d.ts +0 -0
  174. /package/dist/cjs/{commands → cli}/index.d.cts +0 -0
  175. /package/dist/cjs/{commands → cli}/index.d.ts +0 -0
  176. /package/dist/cjs/{commands → cli}/init.d.cts +0 -0
  177. /package/dist/cjs/{commands → cli}/init.d.ts +0 -0
  178. /package/dist/cjs/{commands → cli}/map.d.cts +0 -0
  179. /package/dist/cjs/{commands → cli}/map.d.ts +0 -0
  180. /package/dist/cjs/{commands → cli}/peek.d.cts +0 -0
  181. /package/dist/cjs/{commands → cli}/peek.d.ts +0 -0
  182. /package/dist/cjs/{commands → cli}/query.d.cts +0 -0
  183. /package/dist/cjs/{commands → cli}/query.d.ts +0 -0
  184. /package/dist/cjs/{commands → cli}/query.js +0 -0
  185. /package/dist/cjs/{commands → cli}/rebuild.d.cts +0 -0
  186. /package/dist/cjs/{commands → cli}/rebuild.d.ts +0 -0
  187. /package/dist/cjs/{commands → cli}/rebuild.js +0 -0
  188. /package/dist/cjs/{commands → cli}/status.d.cts +0 -0
  189. /package/dist/cjs/{commands → cli}/status.d.ts +0 -0
  190. /package/dist/cjs/{commands → cli}/types.js +0 -0
  191. /package/dist/cjs/{commands → cli}/types.js.map +0 -0
  192. /package/dist/cjs/{commands → cli}/watch.d.cts +0 -0
  193. /package/dist/cjs/{commands → cli}/watch.d.ts +0 -0
  194. /package/dist/cjs/{commands → cli}/watch.js +0 -0
  195. /package/dist/esm/{commands → cli}/check.d.ts +0 -0
  196. /package/dist/esm/{commands → cli}/index.d.ts +0 -0
  197. /package/dist/esm/{commands → cli}/init.d.ts +0 -0
  198. /package/dist/esm/{commands → cli}/map.d.ts +0 -0
  199. /package/dist/esm/{commands → cli}/peek.d.ts +0 -0
  200. /package/dist/esm/{commands → cli}/query.d.ts +0 -0
  201. /package/dist/esm/{commands → cli}/query.js +0 -0
  202. /package/dist/esm/{commands → cli}/rebuild.d.ts +0 -0
  203. /package/dist/esm/{commands → cli}/rebuild.js +0 -0
  204. /package/dist/esm/{commands → cli}/status.d.ts +0 -0
  205. /package/dist/esm/{commands → cli}/types.js +0 -0
  206. /package/dist/esm/{commands → cli}/watch.d.ts +0 -0
  207. /package/dist/esm/{commands → cli}/watch.js +0 -0
@@ -21,6 +21,9 @@ _export(exports, {
21
21
  get SUPPORTED_CONFIG_VERSION () {
22
22
  return SUPPORTED_CONFIG_VERSION;
23
23
  },
24
+ get anyPresetEmbeds () {
25
+ return anyPresetEmbeds;
26
+ },
24
27
  get embedConfig () {
25
28
  return embedConfig;
26
29
  },
@@ -47,6 +50,18 @@ _export(exports, {
47
50
  },
48
51
  get migrateConfig () {
49
52
  return migrateConfig;
53
+ },
54
+ get presetNames () {
55
+ return presetNames;
56
+ },
57
+ get presetSemanticEnabled () {
58
+ return presetSemanticEnabled;
59
+ },
60
+ get resolvePreset () {
61
+ return resolvePreset;
62
+ },
63
+ get resolveSearch () {
64
+ return resolveSearch;
50
65
  }
51
66
  });
52
67
  var _nodefs = require("node:fs");
@@ -148,6 +163,42 @@ function _object_spread_props(target, source) {
148
163
  }
149
164
  return target;
150
165
  }
166
+ function _object_without_properties(source, excluded) {
167
+ if (source == null) return {};
168
+ var target = {}, sourceKeys, key, i;
169
+ if (typeof Reflect !== "undefined" && Reflect.ownKeys) {
170
+ sourceKeys = Reflect.ownKeys(Object(source));
171
+ for(i = 0; i < sourceKeys.length; i++){
172
+ key = sourceKeys[i];
173
+ if (excluded.indexOf(key) >= 0) continue;
174
+ if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;
175
+ target[key] = source[key];
176
+ }
177
+ return target;
178
+ }
179
+ target = _object_without_properties_loose(source, excluded);
180
+ if (Object.getOwnPropertySymbols) {
181
+ sourceKeys = Object.getOwnPropertySymbols(source);
182
+ for(i = 0; i < sourceKeys.length; i++){
183
+ key = sourceKeys[i];
184
+ if (excluded.indexOf(key) >= 0) continue;
185
+ if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;
186
+ target[key] = source[key];
187
+ }
188
+ }
189
+ return target;
190
+ }
191
+ function _object_without_properties_loose(source, excluded) {
192
+ if (source == null) return {};
193
+ var target = {}, sourceKeys = Object.getOwnPropertyNames(source), key, i;
194
+ for(i = 0; i < sourceKeys.length; i++){
195
+ key = sourceKeys[i];
196
+ if (excluded.indexOf(key) >= 0) continue;
197
+ if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;
198
+ target[key] = source[key];
199
+ }
200
+ return target;
201
+ }
151
202
  function _sliced_to_array(arr, i) {
152
203
  return _array_with_holes(arr) || _iterable_to_array_limit(arr, i) || _unsupported_iterable_to_array(arr, i) || _non_iterable_rest();
153
204
  }
@@ -168,25 +219,39 @@ function _unsupported_iterable_to_array(o, minLen) {
168
219
  }
169
220
  var CONFIG_FILENAME = 'sense.config.json';
170
221
  var STATE_DIR = '.sense';
171
- var SUPPORTED_CONFIG_VERSION = 2;
172
- // Each feature owns its tables, parse-time extraction, and reconcile step; verbs degrade when one is off.
173
- // links/sections/rank are opt-out (absent = on); embed is opt-in (absent = off) -- most trees don't need vectors.
222
+ var SUPPORTED_CONFIG_VERSION = 3;
223
+ // Each feature owns its tables, parse-time extraction, and reconcile step; commands degrade when one is off.
224
+ // links/sections/rank are opt-out toggles in the top-level `features` block; embed is not a
225
+ // member of that block -- it is derived from whether any declared preset has semantic on.
174
226
  var FEATURE_NAMES = [
175
227
  'links',
176
228
  'sections',
177
229
  'rank',
178
230
  'embed'
179
231
  ];
180
- var OPT_OUT_NAMES = [
232
+ // v1 -> v2 migration only: the features v2 introduced as opt-out (embed was opt-in then).
233
+ var V2_OPT_OUT_NAMES = [
181
234
  'links',
182
235
  'sections',
183
236
  'rank'
184
237
  ];
185
238
  var DEFAULT_EMBED_MODEL = 'minishlab/potion-retrieval-32M';
239
+ function presetNames(cfg) {
240
+ return Object.keys(cfg.presets);
241
+ }
242
+ function presetSemanticEnabled(cfg, name) {
243
+ var _cfg_presets_name;
244
+ return ((_cfg_presets_name = cfg.presets[name]) === null || _cfg_presets_name === void 0 ? void 0 : _cfg_presets_name.semantic) !== false;
245
+ }
246
+ function anyPresetEmbeds(cfg) {
247
+ return presetNames(cfg).some(function(name) {
248
+ return presetSemanticEnabled(cfg, name);
249
+ });
250
+ }
186
251
  function featureEnabled(cfg, name) {
187
- var _cfg_features, _cfg_features1;
188
- if (name === 'embed') return Boolean((_cfg_features = cfg.features) === null || _cfg_features === void 0 ? void 0 : _cfg_features.embed);
189
- var enabled = ((_cfg_features1 = cfg.features) === null || _cfg_features1 === void 0 ? void 0 : _cfg_features1[name]) !== false;
252
+ var _cfg_features;
253
+ if (name === 'embed') return anyPresetEmbeds(cfg);
254
+ var enabled = ((_cfg_features = cfg.features) === null || _cfg_features === void 0 ? void 0 : _cfg_features[name]) !== false;
190
255
  if (name === 'rank') return enabled && featureEnabled(cfg, 'links');
191
256
  return enabled;
192
257
  }
@@ -206,39 +271,166 @@ function featureStates(cfg) {
206
271
  };
207
272
  }
208
273
  function embedConfig(cfg) {
209
- var _o_model, _o_type;
210
- var _cfg_features;
211
- var e = (_cfg_features = cfg.features) === null || _cfg_features === void 0 ? void 0 : _cfg_features.embed;
212
- if (!e) return null;
213
- var o = e === true ? {} : e;
274
+ var _cfg_embed, _e_model, _e_type;
275
+ if (!anyPresetEmbeds(cfg)) return null;
276
+ var e = (_cfg_embed = cfg.embed) !== null && _cfg_embed !== void 0 ? _cfg_embed : {};
214
277
  return {
215
- model: (_o_model = o.model) !== null && _o_model !== void 0 ? _o_model : DEFAULT_EMBED_MODEL,
216
- type: (_o_type = o.type) !== null && _o_type !== void 0 ? _o_type : 'static',
217
- url: o.url,
218
- key: o.key
278
+ model: (_e_model = e.model) !== null && _e_model !== void 0 ? _e_model : DEFAULT_EMBED_MODEL,
279
+ type: (_e_type = e.type) !== null && _e_type !== void 0 ? _e_type : 'static',
280
+ url: e.url,
281
+ key: e.key
219
282
  };
220
283
  }
221
284
  function featureSignature(cfg) {
222
- return enabledFeatures(cfg).map(function(name) {
223
- if (name !== 'embed') return name;
224
- var e = embedConfig(cfg);
225
- return "embed:".concat(e === null || e === void 0 ? void 0 : e.type, ":").concat(e === null || e === void 0 ? void 0 : e.model);
285
+ var globalPart = enabledFeatures(cfg).filter(function(name) {
286
+ return name !== 'embed';
226
287
  }).join(',');
288
+ var e = embedConfig(cfg);
289
+ var embedPart = e ? "embed:".concat(e.type, ":").concat(e.model) : 'embed:off';
290
+ // One keyed segment per preset so a rebuild notice can name exactly which preset moved.
291
+ var presetsPart = _to_consumable_array(presetNames(cfg)).sort().map(function(name) {
292
+ var _p_exclude;
293
+ var p = cfg.presets[name];
294
+ var include = _to_consumable_array(p.include).sort().join('+');
295
+ var exclude = _to_consumable_array((_p_exclude = p.exclude) !== null && _p_exclude !== void 0 ? _p_exclude : []).sort().join('+');
296
+ return "preset:".concat(name, ":").concat(include, ":").concat(exclude, ":").concat(p.semantic === false ? 'off' : 'on');
297
+ }).join('|');
298
+ return [
299
+ "features:".concat(globalPart),
300
+ embedPart,
301
+ presetsPart
302
+ ].join('|');
303
+ }
304
+ function resolvePreset(cfg, name) {
305
+ var presetName = name !== null && name !== void 0 ? name : 'default';
306
+ var preset = cfg.presets[presetName];
307
+ if (!preset) throw new _errorsts.SenseError('PRESET_UNKNOWN', 'unknown preset "'.concat(presetName, '"; declared presets: ').concat(presetNames(cfg).join(', ')));
308
+ return {
309
+ name: presetName,
310
+ preset: preset
311
+ };
312
+ }
313
+ function resolveSearch(cfg) {
314
+ var opts = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : {};
315
+ var _ref, _opts_k, _opts_where, _opts_include;
316
+ var _resolvePreset = resolvePreset(cfg, opts.preset), presetName = _resolvePreset.name, preset = _resolvePreset.preset;
317
+ var k = (_ref = (_opts_k = opts.k) !== null && _opts_k !== void 0 ? _opts_k : preset.k) !== null && _ref !== void 0 ? _ref : 10;
318
+ var where = (_opts_where = opts.where) !== null && _opts_where !== void 0 ? _opts_where : preset.where;
319
+ // An explicit include (CLI --include, or a saved search's own `include`) is a full ad hoc
320
+ // scope override -- it replaces the preset's include/exclude pair rather than layering on
321
+ // top of it, the same "replaces, doesn't AND" rule --where already uses.
322
+ var include = (_opts_include = opts.include) !== null && _opts_include !== void 0 ? _opts_include : preset.include;
323
+ var exclude = opts.include ? undefined : preset.exclude;
324
+ var semantic = opts.semantic !== undefined ? opts.semantic : preset.semantic !== false;
325
+ return {
326
+ presetName: presetName,
327
+ k: k,
328
+ where: where,
329
+ include: include,
330
+ exclude: exclude,
331
+ semantic: semantic
332
+ };
227
333
  }
228
334
  // Pure per-version steps; loadConfig chains them from the file's version up to SUPPORTED_CONFIG_VERSION.
335
+ // Intermediate shapes predate the current Config type (v1 has no features block, v2 has
336
+ // scan/find not presets/search), so steps work loosely-typed and only the final result is cast
337
+ // back to Config.
229
338
  var MIGRATIONS = {
230
339
  // v1 -> v2: features block introduced, opt-out features enabled (matches the old implicit
231
340
  // behavior of `links` etc. not existing). embed stays absent -- opt-in.
232
341
  1: function(cfg) {
233
342
  return _object_spread_props(_object_spread({}, cfg), {
234
343
  version: 2,
235
- features: Object.fromEntries(OPT_OUT_NAMES.map(function(name) {
344
+ features: Object.fromEntries(V2_OPT_OUT_NAMES.map(function(name) {
236
345
  return [
237
346
  name,
238
347
  true
239
348
  ];
240
349
  }))
241
350
  });
351
+ },
352
+ // v2 -> v3: mechanical-minimal, by decision -- only what keeps an existing config loading.
353
+ // No restructuring, no preset inference, no query rewriting; real trees get hand-migrated
354
+ // separately to actually use presets.
355
+ 2: function(cfg) {
356
+ var _prevDefaults_find;
357
+ var scan = cfg.scan, defaults = cfg.defaults, features = cfg.features, checks = cfg.checks, queries = cfg.queries, rest = _object_without_properties(cfg, [
358
+ "scan",
359
+ "defaults",
360
+ "features",
361
+ "checks",
362
+ "queries"
363
+ ]);
364
+ var scanInclude = scan.include;
365
+ var nextQueries = {};
366
+ var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
367
+ try {
368
+ for(var _iterator = Object.entries(queries !== null && queries !== void 0 ? queries : {})[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
369
+ var _step_value = _sliced_to_array(_step.value, 2), name = _step_value[0], value = _step_value[1];
370
+ if (typeof value === 'string') {
371
+ nextQueries[name] = value;
372
+ continue;
373
+ }
374
+ var entry = value;
375
+ var search = {
376
+ search: entry.find
377
+ };
378
+ if (entry.k !== undefined) search.k = entry.k;
379
+ if (entry.where !== undefined) search.where = entry.where;
380
+ // semantic: true was opt-in; it is now the default, so it drops. semantic: false is kept.
381
+ if (entry.semantic === false) search.semantic = false;
382
+ nextQueries[name] = search;
383
+ }
384
+ } catch (err) {
385
+ _didIteratorError = true;
386
+ _iteratorError = err;
387
+ } finally{
388
+ try {
389
+ if (!_iteratorNormalCompletion && _iterator.return != null) {
390
+ _iterator.return();
391
+ }
392
+ } finally{
393
+ if (_didIteratorError) {
394
+ throw _iteratorError;
395
+ }
396
+ }
397
+ }
398
+ var prevDefaults = defaults;
399
+ var defaultWhere = prevDefaults === null || prevDefaults === void 0 ? void 0 : (_prevDefaults_find = prevDefaults.find) === null || _prevDefaults_find === void 0 ? void 0 : _prevDefaults_find.where;
400
+ // features.embed: true/absent -> simply removed (v3 default is on). An explicit `false`
401
+ // becomes semantic: false on every preset this migration produces -- there is only ever
402
+ // one (`default`), since v2 has no notion of multiple scopes, but the rule is written to
403
+ // apply to "every migrated preset" rather than hardcoded to `default` in case a later
404
+ // migration step ever produces more than one.
405
+ var prevFeatures = features !== null && features !== void 0 ? features : {};
406
+ var embedWasOff = prevFeatures.embed === false;
407
+ // Object form carried provider settings (model/type/url/key), not a toggle -- they move
408
+ // to the v3 top-level `embed` block verbatim; dropping them would silently switch an
409
+ // api tree back to the built-in static model.
410
+ var embedProvider = _type_of(prevFeatures.embed) === 'object' && prevFeatures.embed !== null ? prevFeatures.embed : undefined;
411
+ var _embed = prevFeatures.embed, restFeatures = _object_without_properties(prevFeatures, [
412
+ "embed"
413
+ ]);
414
+ var defaultPreset = {
415
+ include: scanInclude
416
+ };
417
+ if (defaultWhere !== undefined) defaultPreset.where = defaultWhere;
418
+ if (embedWasOff) defaultPreset.semantic = false;
419
+ // checks (assertions over saved queries) was removed in v3; its queries still run under
420
+ // `queries`, just without the pass/fail assertion -- reachable now only as a saved query.
421
+ if (checks !== undefined) {
422
+ console.error('sense: v2 "checks" was removed in v3 (sense check no longer asserts on saved queries); its queries are carried over under "queries" -- a returned row set is now the reader\'s judgment');
423
+ }
424
+ var result = _object_spread_props(_object_spread({}, rest), {
425
+ version: 3,
426
+ presets: {
427
+ default: defaultPreset
428
+ },
429
+ queries: nextQueries
430
+ });
431
+ if (embedProvider !== undefined) result.embed = embedProvider;
432
+ if (Object.keys(restFeatures).length > 0) result.features = restFeatures;
433
+ return result;
242
434
  }
243
435
  };
244
436
  function migrateConfig(cfg) {
@@ -259,17 +451,20 @@ function starterConfig() {
259
451
  return {
260
452
  $schema: 'https://unpkg.com/sensemaking/schema.json',
261
453
  version: SUPPORTED_CONFIG_VERSION,
262
- scan: {
263
- include: [
264
- '**/*.md'
265
- ]
454
+ presets: {
455
+ default: {
456
+ include: [
457
+ '**/*.md'
458
+ ],
459
+ k: 10
460
+ },
461
+ large: {
462
+ include: [
463
+ '**/*.md'
464
+ ],
465
+ k: 20
466
+ }
266
467
  },
267
- features: Object.fromEntries(OPT_OUT_NAMES.map(function(name) {
268
- return [
269
- name,
270
- true
271
- ];
272
- })),
273
468
  queries: {}
274
469
  };
275
470
  }
@@ -293,124 +488,248 @@ function findConfigPath(startDir) {
293
488
  }
294
489
  // Shape check for hand-edited files: a typo'd config fails with a named error, not a
295
490
  // TypeError from whatever code touched the missing field first. `queries` is optional
296
- // on disk (absent = none); `scan.include` has no usable default.
297
- // Keys each block actually reads. A key outside these sets is accepted and ignored, which
298
- // looks identical to working -- one field report set `scan.exclude`, saw no change in doc
299
- // count and no warning, and reasonably concluded the filter had applied. Reported as a
300
- // warning rather than an error so a config carrying an unknown key still runs.
491
+ // on disk (absent = none); `presets` has no usable default.
492
+ // Unknown top-level keys are reported as a soft warning (cli.ts prints it) rather than an
493
+ // error, so a config carrying a stray field still runs -- but unknown keys inside a preset,
494
+ // features, embed, or saved-query block are hard errors: those blocks are small and fully
495
+ // owned, so a typo there is far more likely a mistake than a forward-compat field.
301
496
  var KNOWN_KEYS = new Set([
302
497
  '$schema',
303
498
  'version',
304
- 'scan',
499
+ 'presets',
305
500
  'features',
306
- 'defaults',
307
- 'checks',
501
+ 'embed',
308
502
  'queries'
309
503
  ]);
310
- var KNOWN_SCAN_KEYS = new Set([
311
- 'include'
504
+ var KNOWN_PRESET_KEYS = new Set([
505
+ 'include',
506
+ 'exclude',
507
+ 'k',
508
+ 'semantic',
509
+ 'where'
510
+ ]);
511
+ var KNOWN_FEATURE_KEYS = new Set([
512
+ 'links',
513
+ 'sections',
514
+ 'rank'
515
+ ]);
516
+ var KNOWN_EMBED_KEYS = new Set([
517
+ 'model',
518
+ 'type',
519
+ 'url',
520
+ 'key'
521
+ ]);
522
+ var SAVED_SEARCH_KEYS = new Set([
523
+ 'search',
524
+ 'preset',
525
+ 'include',
526
+ 'where',
527
+ 'k',
528
+ 'semantic'
312
529
  ]);
313
530
  function unknownConfigKeys(cfg) {
314
- var unknown = Object.keys(cfg).filter(function(k) {
531
+ return Object.keys(cfg).filter(function(k) {
315
532
  return !KNOWN_KEYS.has(k);
316
- }).map(function(k) {
317
- return k;
318
533
  });
319
- var scan = cfg.scan;
320
- if (scan && (typeof scan === "undefined" ? "undefined" : _type_of(scan)) === 'object' && !Array.isArray(scan)) {
321
- var _unknown;
322
- (_unknown = unknown).push.apply(_unknown, _to_consumable_array(Object.keys(scan).filter(function(k) {
323
- return !KNOWN_SCAN_KEYS.has(k);
324
- }).map(function(k) {
325
- return "scan.".concat(k);
326
- })));
327
- }
328
- var features = cfg.features;
329
- if (features && (typeof features === "undefined" ? "undefined" : _type_of(features)) === 'object' && !Array.isArray(features)) {
330
- var _unknown1;
331
- (_unknown1 = unknown).push.apply(_unknown1, _to_consumable_array(Object.keys(features).filter(function(k) {
332
- return !FEATURE_NAMES.includes(k);
333
- }).map(function(k) {
334
- return "features.".concat(k);
335
- })));
336
- }
337
- return unknown;
534
+ }
535
+ function isNonEmptyStringArray(value) {
536
+ return Array.isArray(value) && value.length > 0 && value.every(function(g) {
537
+ return typeof g === 'string' && g.length > 0;
538
+ });
539
+ }
540
+ // Pre-v3 shape check, just enough for migrateConfig to run safely (it reads scan.include
541
+ // directly). The full v3 shape is checked by validateConfig once migration has produced it.
542
+ function validateLegacyScan(parsed, configPath) {
543
+ var cfg = (typeof parsed === "undefined" ? "undefined" : _type_of(parsed)) === 'object' && parsed !== null && !Array.isArray(parsed) ? parsed : null;
544
+ var scan = cfg === null || cfg === void 0 ? void 0 : cfg.scan;
545
+ if (!cfg || !scan || !isNonEmptyStringArray(scan.include)) {
546
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": scan.include must be a non-empty array of glob strings"));
547
+ }
548
+ }
549
+ function validateFeaturesBlock(value, configPath) {
550
+ if ((typeof value === "undefined" ? "undefined" : _type_of(value)) !== 'object' || value === null || Array.isArray(value)) {
551
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": features must be an object of name -> boolean"));
552
+ }
553
+ var block = value;
554
+ var unknown = Object.keys(block).filter(function(k) {
555
+ return !KNOWN_FEATURE_KEYS.has(k);
556
+ });
557
+ if (unknown.length > 0) {
558
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": features has unknown key(s) ").concat(unknown.join(', '), '; embed is not a features key -- see the top-level "embed" block and each preset\'s "semantic"'));
559
+ }
560
+ var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
561
+ try {
562
+ for(var _iterator = Object.entries(block)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
563
+ var _step_value = _sliced_to_array(_step.value, 2), name = _step_value[0], v = _step_value[1];
564
+ if (typeof v !== 'boolean') {
565
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": features.").concat(name, " must be a boolean"));
566
+ }
567
+ }
568
+ } catch (err) {
569
+ _didIteratorError = true;
570
+ _iteratorError = err;
571
+ } finally{
572
+ try {
573
+ if (!_iteratorNormalCompletion && _iterator.return != null) {
574
+ _iterator.return();
575
+ }
576
+ } finally{
577
+ if (_didIteratorError) {
578
+ throw _iteratorError;
579
+ }
580
+ }
581
+ }
582
+ }
583
+ function validateEmbedBlock(value, configPath) {
584
+ if ((typeof value === "undefined" ? "undefined" : _type_of(value)) !== 'object' || value === null || Array.isArray(value)) {
585
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ': embed must be an object of { model?, type?: "static"|"api", url?, key? }'));
586
+ }
587
+ var embed = value;
588
+ var unknown = Object.keys(embed).filter(function(k) {
589
+ return !KNOWN_EMBED_KEYS.has(k);
590
+ });
591
+ if (unknown.length > 0) {
592
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": embed has unknown key(s) ").concat(unknown.join(', ')));
593
+ }
594
+ if (embed.model !== undefined && typeof embed.model !== 'string') {
595
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": embed.model must be a string"));
596
+ }
597
+ if (embed.type !== undefined && embed.type !== 'static' && embed.type !== 'api') {
598
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ': embed.type must be "static" or "api"'));
599
+ }
600
+ if (embed.url !== undefined && typeof embed.url !== 'string') {
601
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": embed.url must be a string"));
602
+ }
603
+ if (embed.key !== undefined && typeof embed.key !== 'string') {
604
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": embed.key must be a string"));
605
+ }
606
+ }
607
+ function validatePreset(name, value, configPath) {
608
+ if ((typeof value === "undefined" ? "undefined" : _type_of(value)) !== 'object' || value === null || Array.isArray(value)) {
609
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": presets.").concat(name, " must be an object"));
610
+ }
611
+ var preset = value;
612
+ var unknown = Object.keys(preset).filter(function(k) {
613
+ return !KNOWN_PRESET_KEYS.has(k);
614
+ });
615
+ if (unknown.length > 0) {
616
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": presets.").concat(name, " has unknown key(s) ").concat(unknown.join(', '), "; a preset takes include, exclude, k, semantic, where"));
617
+ }
618
+ if (!isNonEmptyStringArray(preset.include)) {
619
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": presets.").concat(name, ".include must be a non-empty array of glob strings"));
620
+ }
621
+ if (preset.exclude !== undefined && !isNonEmptyStringArray(preset.exclude)) {
622
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": presets.").concat(name, ".exclude must be a non-empty array of glob strings"));
623
+ }
624
+ if (preset.k !== undefined && (typeof preset.k !== 'number' || !Number.isInteger(preset.k) || preset.k <= 0)) {
625
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": presets.").concat(name, ".k must be a positive integer"));
626
+ }
627
+ if (preset.semantic !== undefined && typeof preset.semantic !== 'boolean') {
628
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": presets.").concat(name, ".semantic must be a boolean"));
629
+ }
630
+ if (preset.where !== undefined && typeof preset.where !== 'string') {
631
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": presets.").concat(name, ".where must be a SQL condition string"));
632
+ }
633
+ }
634
+ // A queries.<name> entry: a SQL string, { sql }, or a saved search { search, preset?, include?, where?, k?, semantic? }.
635
+ function validateSavedQuery(name, value, configPath) {
636
+ if (typeof value === 'string') return;
637
+ if ((typeof value === "undefined" ? "undefined" : _type_of(value)) !== 'object' || value === null || Array.isArray(value)) {
638
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": queries.").concat(name, " must be a SQL string, { sql }, or { search, preset?, include?, where?, k?, semantic? }"));
639
+ }
640
+ var entry = value;
641
+ if ('sql' in entry) {
642
+ var unknown = Object.keys(entry).filter(function(k) {
643
+ return k !== 'sql';
644
+ });
645
+ if (unknown.length > 0) {
646
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": queries.").concat(name, " has unknown key(s) ").concat(unknown.join(', '), "; a { sql } query takes only sql"));
647
+ }
648
+ if (typeof entry.sql !== 'string' || entry.sql.trim() === '') {
649
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": queries.").concat(name, ".sql must be a non-empty string"));
650
+ }
651
+ return;
652
+ }
653
+ if ('search' in entry) {
654
+ var unknown1 = Object.keys(entry).filter(function(k) {
655
+ return !SAVED_SEARCH_KEYS.has(k);
656
+ });
657
+ if (unknown1.length > 0) {
658
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": queries.").concat(name, " has unknown key(s) ").concat(unknown1.join(', '), "; a saved search takes search, preset, include, where, k, semantic"));
659
+ }
660
+ // A saved query saves a question; a scope without a question is just flags.
661
+ if (typeof entry.search !== 'string' || entry.search.trim() === '') {
662
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": queries.").concat(name, ".search must be non-empty text"));
663
+ }
664
+ if (entry.preset !== undefined && (typeof entry.preset !== 'string' || entry.preset.length === 0)) {
665
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": queries.").concat(name, ".preset must be a preset name"));
666
+ }
667
+ if (entry.include !== undefined && !isNonEmptyStringArray(entry.include)) {
668
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": queries.").concat(name, ".include must be a non-empty array of glob strings"));
669
+ }
670
+ if (entry.where !== undefined && typeof entry.where !== 'string') {
671
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": queries.").concat(name, ".where must be a SQL condition string"));
672
+ }
673
+ if (entry.k !== undefined && (typeof entry.k !== 'number' || !Number.isInteger(entry.k) || entry.k <= 0)) {
674
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": queries.").concat(name, ".k must be a positive integer"));
675
+ }
676
+ if (entry.semantic !== undefined && typeof entry.semantic !== 'boolean') {
677
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": queries.").concat(name, ".semantic must be a boolean"));
678
+ }
679
+ return;
680
+ }
681
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": queries.").concat(name, " must be a SQL string, { sql }, or { search, preset?, include?, where?, k?, semantic? }"));
338
682
  }
339
683
  function validateConfig(parsed, configPath) {
340
- var _cfg_features;
341
- var _defaults_find;
342
684
  if ((typeof parsed === "undefined" ? "undefined" : _type_of(parsed)) !== 'object' || parsed === null || Array.isArray(parsed)) {
343
685
  throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": config must be a JSON object"));
344
686
  }
345
687
  var cfg = parsed;
346
- var scan = cfg.scan;
347
- if (!scan || !Array.isArray(scan.include) || scan.include.length === 0 || !scan.include.every(function(g) {
348
- return typeof g === 'string';
349
- })) {
350
- throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": scan.include must be a non-empty array of glob strings"));
688
+ // `checks` (assertions over saved queries) was removed in v3, not merely deprecated: a
689
+ // hand-written v3 config still carrying it gets a named rejection rather than a silent
690
+ // "unknown key" warning, since silence here would hide that the assertion behavior it
691
+ // implies no longer exists.
692
+ if (cfg.checks !== undefined) {
693
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": checks was removed in v3 -- sense check no longer asserts on saved queries; a returned row set is the reader's judgment"));
351
694
  }
352
- if (cfg.queries === undefined) cfg.queries = {};
353
- if (_type_of(cfg.queries) !== 'object' || cfg.queries === null || Array.isArray(cfg.queries) || !Object.values(cfg.queries).every(function(q) {
354
- return typeof q === 'string';
355
- })) {
356
- throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": queries must be an object of name -> SQL string"));
357
- }
358
- var defaults = cfg.defaults;
359
- if (defaults !== undefined && ((typeof defaults === "undefined" ? "undefined" : _type_of(defaults)) !== 'object' || defaults === null || Array.isArray(defaults) || defaults.find !== undefined && typeof ((_defaults_find = defaults.find) === null || _defaults_find === void 0 ? void 0 : _defaults_find.where) !== 'string')) {
360
- throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": defaults.find.where must be a SQL condition string"));
361
- }
362
- var checks = cfg.checks;
363
- if (checks !== undefined) {
364
- var _cfg_queries;
365
- if ((typeof checks === "undefined" ? "undefined" : _type_of(checks)) !== 'object' || checks === null || Array.isArray(checks) || !Object.values(checks).every(function(v) {
366
- return v === 'empty';
367
- })) {
368
- throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ': checks must be an object of query name -> "empty"'));
695
+ var presets = cfg.presets;
696
+ if (!presets || (typeof presets === "undefined" ? "undefined" : _type_of(presets)) !== 'object' || Array.isArray(presets) || Object.keys(presets).length === 0) {
697
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": presets must be a non-empty object of preset name -> { include, exclude?, k?, semantic?, where? }"));
698
+ }
699
+ var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
700
+ try {
701
+ for(var _iterator = Object.entries(presets)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
702
+ var _step_value = _sliced_to_array(_step.value, 2), name = _step_value[0], value = _step_value[1];
703
+ validatePreset(name, value, configPath);
369
704
  }
370
- var queries = (_cfg_queries = cfg.queries) !== null && _cfg_queries !== void 0 ? _cfg_queries : {};
371
- var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
705
+ } catch (err) {
706
+ _didIteratorError = true;
707
+ _iteratorError = err;
708
+ } finally{
372
709
  try {
373
- for(var _iterator = Object.keys(checks)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
374
- var name = _step.value;
375
- if (queries[name] === undefined) throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ': checks names "').concat(name, '", which is not a saved query'));
710
+ if (!_iteratorNormalCompletion && _iterator.return != null) {
711
+ _iterator.return();
376
712
  }
377
- } catch (err) {
378
- _didIteratorError = true;
379
- _iteratorError = err;
380
713
  } finally{
381
- try {
382
- if (!_iteratorNormalCompletion && _iterator.return != null) {
383
- _iterator.return();
384
- }
385
- } finally{
386
- if (_didIteratorError) {
387
- throw _iteratorError;
388
- }
714
+ if (_didIteratorError) {
715
+ throw _iteratorError;
389
716
  }
390
717
  }
391
718
  }
392
- if (cfg.features !== undefined && (_type_of(cfg.features) !== 'object' || cfg.features === null || Array.isArray(cfg.features))) {
393
- throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": features must be an object of name -> boolean"));
719
+ if (presets.default === undefined) {
720
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ': presets must include a "default" preset'));
721
+ }
722
+ if (cfg.queries === undefined) cfg.queries = {};
723
+ if (_type_of(cfg.queries) !== 'object' || cfg.queries === null || Array.isArray(cfg.queries)) {
724
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": queries must be an object of name -> SQL string, { sql }, or saved search"));
394
725
  }
726
+ var queries = cfg.queries;
395
727
  var _iteratorNormalCompletion1 = true, _didIteratorError1 = false, _iteratorError1 = undefined;
396
728
  try {
397
- var _loop = function() {
398
- var _step_value = _sliced_to_array(_step1.value, 2), name = _step_value[0], value = _step_value[1];
399
- if (typeof value === 'boolean') return "continue";
400
- // embed alone takes an object form: { model?, type?: static|api, url?, key? }.
401
- var embed = value;
402
- var shapeOk = name === 'embed' && (typeof value === "undefined" ? "undefined" : _type_of(value)) === 'object' && value !== null && !Array.isArray(value) && [
403
- 'model',
404
- 'url',
405
- 'key'
406
- ].every(function(f) {
407
- return embed[f] === undefined || typeof embed[f] === 'string';
408
- }) && (embed.type === undefined || embed.type === 'static' || embed.type === 'api');
409
- if (!shapeOk) {
410
- throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": features.").concat(name, " must be a boolean").concat(name === 'embed' ? ' or { model?, type?: "static"|"api", url?, key? }' : ''));
411
- }
412
- };
413
- for(var _iterator1 = Object.entries((_cfg_features = cfg.features) !== null && _cfg_features !== void 0 ? _cfg_features : {})[Symbol.iterator](), _step1; !(_iteratorNormalCompletion1 = (_step1 = _iterator1.next()).done); _iteratorNormalCompletion1 = true)_loop();
729
+ for(var _iterator1 = Object.entries(queries)[Symbol.iterator](), _step1; !(_iteratorNormalCompletion1 = (_step1 = _iterator1.next()).done); _iteratorNormalCompletion1 = true){
730
+ var _step_value1 = _sliced_to_array(_step1.value, 2), name1 = _step_value1[0], value1 = _step_value1[1];
731
+ validateSavedQuery(name1, value1, configPath);
732
+ }
414
733
  } catch (err) {
415
734
  _didIteratorError1 = true;
416
735
  _iteratorError1 = err;
@@ -425,6 +744,12 @@ function validateConfig(parsed, configPath) {
425
744
  }
426
745
  }
427
746
  }
747
+ if (cfg.features !== undefined) {
748
+ validateFeaturesBlock(cfg.features, configPath);
749
+ }
750
+ if (cfg.embed !== undefined) {
751
+ validateEmbedBlock(cfg.embed, configPath);
752
+ }
428
753
  return cfg;
429
754
  }
430
755
  function loadConfig(explicitPath) {
@@ -441,19 +766,33 @@ function loadConfig(explicitPath) {
441
766
  }
442
767
  var raw = (0, _nodefs.readFileSync)(configPath, 'utf8');
443
768
  var parsed = JSON.parse(raw);
769
+ // Ahead of both the version gate and legacy/current shape checks: a non-object file (or
770
+ // an array -- typeof [] is also 'object') is malformed at every version, so it gets one
771
+ // error regardless of which branch would otherwise run.
772
+ if ((typeof parsed === "undefined" ? "undefined" : _type_of(parsed)) !== 'object' || parsed === null || Array.isArray(parsed)) {
773
+ throw new _errorsts.SenseError('CONFIG_INVALID', "".concat(configPath, ": config must be a JSON object"));
774
+ }
444
775
  // Version gate before shape validation: a config written by a newer sense should fail
445
776
  // with "requires a newer sense", not with shape errors its own version may not have.
446
777
  var version = (typeof parsed === "undefined" ? "undefined" : _type_of(parsed)) === 'object' && parsed !== null && typeof parsed.version === 'number' ? parsed.version : 1;
447
778
  if (version > SUPPORTED_CONFIG_VERSION) {
448
779
  throw new _errorsts.SenseError('CONFIG_VERSION_UNSUPPORTED', "config version ".concat(version, " requires a newer sense"));
449
780
  }
450
- var cfg = validateConfig(parsed, configPath);
781
+ // validateConfig only understands the current v3 shape (presets, saved-query object
782
+ // shapes); a pre-v3 file on disk is scan/find-shaped and must migrate before it can pass
783
+ // that check. Validate just enough of the old shape to migrate safely, migrate, then run
784
+ // the full v3 validateConfig on the migrated result -- the shape that actually lands on
785
+ // disk and in ResolvedConfig.
786
+ var cfg;
451
787
  var migratedFrom;
452
788
  if (version < SUPPORTED_CONFIG_VERSION) {
453
- var result = migrateConfig(cfg);
454
- cfg = result.cfg;
789
+ validateLegacyScan(parsed, configPath);
790
+ var result = migrateConfig(parsed);
791
+ cfg = validateConfig(result.cfg, configPath);
455
792
  migratedFrom = result.from;
456
793
  (0, _nodefs.writeFileSync)(configPath, "".concat(JSON.stringify(cfg, null, 2), "\n"));
794
+ } else {
795
+ cfg = validateConfig(parsed, configPath);
457
796
  }
458
797
  var unknownKeys = unknownConfigKeys(cfg);
459
798
  return _object_spread_props(_object_spread({}, cfg), {