sensemaking 0.8.0 → 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.
- package/README.md +48 -39
- package/dist/cjs/cli/check.js +24 -43
- package/dist/cjs/cli/check.js.map +1 -1
- package/dist/cjs/cli/index.js +2 -2
- package/dist/cjs/cli/index.js.map +1 -1
- package/dist/cjs/cli/named.js +18 -8
- package/dist/cjs/cli/named.js.map +1 -1
- package/dist/cjs/cli/search.d.cts +3 -0
- package/dist/cjs/cli/search.d.ts +3 -0
- package/dist/cjs/cli/{find.js → search.js} +7 -5
- package/dist/cjs/cli/search.js.map +1 -0
- package/dist/cjs/cli/shared.js.map +1 -1
- package/dist/cjs/cli/status.js +29 -12
- package/dist/cjs/cli/status.js.map +1 -1
- package/dist/cjs/cli/types.d.cts +3 -1
- package/dist/cjs/cli/types.d.ts +3 -1
- package/dist/cjs/cli.js +25 -5
- package/dist/cjs/cli.js.map +1 -1
- package/dist/cjs/commands.d.cts +11 -2
- package/dist/cjs/commands.d.ts +11 -2
- package/dist/cjs/commands.js +86 -21
- package/dist/cjs/commands.js.map +1 -1
- package/dist/cjs/config.d.cts +41 -14
- package/dist/cjs/config.d.ts +41 -14
- package/dist/cjs/config.js +448 -160
- package/dist/cjs/config.js.map +1 -1
- package/dist/cjs/db.d.cts +1 -1
- package/dist/cjs/db.d.ts +1 -1
- package/dist/cjs/db.js +208 -62
- package/dist/cjs/db.js.map +1 -1
- package/dist/cjs/errors.d.cts +1 -1
- package/dist/cjs/errors.d.ts +1 -1
- package/dist/cjs/errors.js.map +1 -1
- package/dist/cjs/features/embed.js +12 -2
- package/dist/cjs/features/embed.js.map +1 -1
- package/dist/cjs/features/types.d.cts +2 -1
- package/dist/cjs/features/types.d.ts +2 -1
- package/dist/cjs/index.d.cts +3 -3
- package/dist/cjs/index.d.ts +3 -3
- package/dist/cjs/index.js +6 -3
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/output.d.cts +15 -0
- package/dist/cjs/output.d.ts +15 -0
- package/dist/cjs/output.js +26 -5
- package/dist/cjs/output.js.map +1 -1
- package/dist/cjs/scan.d.cts +3 -0
- package/dist/cjs/scan.d.ts +3 -0
- package/dist/cjs/scan.js +84 -4
- package/dist/cjs/scan.js.map +1 -1
- package/dist/esm/cli/check.js +20 -41
- package/dist/esm/cli/check.js.map +1 -1
- package/dist/esm/cli/index.js +1 -1
- package/dist/esm/cli/index.js.map +1 -1
- package/dist/esm/cli/named.js +17 -9
- package/dist/esm/cli/named.js.map +1 -1
- package/dist/esm/cli/search.d.ts +3 -0
- package/dist/esm/cli/search.js +16 -0
- package/dist/esm/cli/search.js.map +1 -0
- package/dist/esm/cli/shared.js +1 -1
- package/dist/esm/cli/shared.js.map +1 -1
- package/dist/esm/cli/status.js +10 -10
- package/dist/esm/cli/status.js.map +1 -1
- package/dist/esm/cli/types.d.ts +3 -1
- package/dist/esm/cli/types.js.map +1 -1
- package/dist/esm/cli.js +20 -4
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/commands.d.ts +11 -2
- package/dist/esm/commands.js +76 -24
- package/dist/esm/commands.js.map +1 -1
- package/dist/esm/config.d.ts +41 -14
- package/dist/esm/config.js +354 -114
- package/dist/esm/config.js.map +1 -1
- package/dist/esm/db.d.ts +1 -1
- package/dist/esm/db.js +53 -6
- package/dist/esm/db.js.map +1 -1
- package/dist/esm/errors.d.ts +1 -1
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/features/embed.js +12 -2
- package/dist/esm/features/embed.js.map +1 -1
- package/dist/esm/features/types.d.ts +2 -1
- package/dist/esm/features/types.js.map +1 -1
- package/dist/esm/index.d.ts +3 -3
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/output.d.ts +15 -0
- package/dist/esm/output.js +19 -5
- package/dist/esm/output.js.map +1 -1
- package/dist/esm/scan.d.ts +3 -0
- package/dist/esm/scan.js +32 -4
- package/dist/esm/scan.js.map +1 -1
- package/package.json +9 -3
- package/schema.json +74 -54
- package/skills/sense/EXAMPLES.md +8 -6
- package/skills/sense/SKILL.md +51 -48
- package/skills/sense-setup/EXAMPLES.md +121 -0
- package/skills/sense-setup/SKILL.md +71 -66
- package/dist/cjs/cli/find.d.cts +0 -3
- package/dist/cjs/cli/find.d.ts +0 -3
- package/dist/cjs/cli/find.js.map +0 -1
- package/dist/esm/cli/find.d.ts +0 -3
- package/dist/esm/cli/find.js +0 -14
- package/dist/esm/cli/find.js.map +0 -1
package/dist/esm/config.js
CHANGED
|
@@ -4,27 +4,42 @@ import { SenseError } from './errors.js';
|
|
|
4
4
|
export const CONFIG_FILENAME = 'sense.config.json';
|
|
5
5
|
export const STATE_DIR = '.sense';
|
|
6
6
|
// Highest sense.config.json `version` this build understands. Older versions auto-migrate on load.
|
|
7
|
-
export const SUPPORTED_CONFIG_VERSION =
|
|
7
|
+
export const SUPPORTED_CONFIG_VERSION = 3;
|
|
8
8
|
// Each feature owns its tables, parse-time extraction, and reconcile step; commands degrade when one is off.
|
|
9
|
-
// links/sections/rank are opt-out
|
|
9
|
+
// links/sections/rank are opt-out toggles in the top-level `features` block; embed is not a
|
|
10
|
+
// member of that block -- it is derived from whether any declared preset has semantic on.
|
|
10
11
|
const FEATURE_NAMES = [
|
|
11
12
|
'links',
|
|
12
13
|
'sections',
|
|
13
14
|
'rank',
|
|
14
15
|
'embed'
|
|
15
16
|
];
|
|
16
|
-
|
|
17
|
+
// v1 -> v2 migration only: the features v2 introduced as opt-out (embed was opt-in then).
|
|
18
|
+
const V2_OPT_OUT_NAMES = [
|
|
17
19
|
'links',
|
|
18
20
|
'sections',
|
|
19
21
|
'rank'
|
|
20
22
|
];
|
|
21
23
|
export const DEFAULT_EMBED_MODEL = 'minishlab/potion-retrieval-32M';
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
export function presetNames(cfg) {
|
|
25
|
+
return Object.keys(cfg.presets);
|
|
26
|
+
}
|
|
27
|
+
// A preset's own resolved vector participation: absent or true means on, false means off.
|
|
28
|
+
export function presetSemanticEnabled(cfg, name) {
|
|
29
|
+
var _cfg_presets_name;
|
|
30
|
+
return ((_cfg_presets_name = cfg.presets[name]) === null || _cfg_presets_name === void 0 ? void 0 : _cfg_presets_name.semantic) !== false;
|
|
31
|
+
}
|
|
32
|
+
// Whether embedding needs to run at all: true when at least one declared preset wants vectors.
|
|
33
|
+
export function anyPresetEmbeds(cfg) {
|
|
34
|
+
return presetNames(cfg).some((name)=>presetSemanticEnabled(cfg, name));
|
|
35
|
+
}
|
|
36
|
+
// Opt-out features (default on): absent block or key means enabled. `rank` additionally
|
|
37
|
+
// requires `links`. `embed` is derived, not a features-block member: on iff any declared
|
|
38
|
+
// preset has semantic on.
|
|
24
39
|
export function featureEnabled(cfg, name) {
|
|
25
|
-
var _cfg_features
|
|
26
|
-
if (name === 'embed') return
|
|
27
|
-
const enabled = ((
|
|
40
|
+
var _cfg_features;
|
|
41
|
+
if (name === 'embed') return anyPresetEmbeds(cfg);
|
|
42
|
+
const enabled = ((_cfg_features = cfg.features) === null || _cfg_features === void 0 ? void 0 : _cfg_features[name]) !== false;
|
|
28
43
|
if (name === 'rank') return enabled && featureEnabled(cfg, 'links');
|
|
29
44
|
return enabled;
|
|
30
45
|
}
|
|
@@ -39,40 +54,158 @@ export function featureStates(cfg) {
|
|
|
39
54
|
off: FEATURE_NAMES.filter((name)=>!featureEnabled(cfg, name))
|
|
40
55
|
};
|
|
41
56
|
}
|
|
42
|
-
// Resolved embed settings, or null when
|
|
57
|
+
// Resolved embed provider settings, or null when no preset wants embeddings at all.
|
|
43
58
|
export function embedConfig(cfg) {
|
|
44
|
-
var
|
|
45
|
-
|
|
46
|
-
const e = (
|
|
47
|
-
if (!e) return null;
|
|
48
|
-
const o = e === true ? {} : e;
|
|
59
|
+
var _cfg_embed, _e_model, _e_type;
|
|
60
|
+
if (!anyPresetEmbeds(cfg)) return null;
|
|
61
|
+
const e = (_cfg_embed = cfg.embed) !== null && _cfg_embed !== void 0 ? _cfg_embed : {};
|
|
49
62
|
return {
|
|
50
|
-
model: (
|
|
51
|
-
type: (
|
|
52
|
-
url:
|
|
53
|
-
key:
|
|
63
|
+
model: (_e_model = e.model) !== null && _e_model !== void 0 ? _e_model : DEFAULT_EMBED_MODEL,
|
|
64
|
+
type: (_e_type = e.type) !== null && _e_type !== void 0 ? _e_type : 'static',
|
|
65
|
+
url: e.url,
|
|
66
|
+
key: e.key
|
|
54
67
|
};
|
|
55
68
|
}
|
|
56
|
-
// Cache-key string: embed carries its type + model so a model change rebuilds like a toggle
|
|
69
|
+
// Cache-key string: embed carries its type + model so a model change rebuilds like a toggle,
|
|
70
|
+
// and every declared preset carries its include/exclude/semantic so an edit to any of those
|
|
71
|
+
// rebuilds too -- indexing and embedding are both derived from presets, so any change to the
|
|
72
|
+
// derivation inputs must invalidate the cache.
|
|
57
73
|
export function featureSignature(cfg) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
74
|
+
const globalPart = enabledFeatures(cfg).filter((name)=>name !== 'embed').join(',');
|
|
75
|
+
const e = embedConfig(cfg);
|
|
76
|
+
const embedPart = e ? `embed:${e.type}:${e.model}` : 'embed:off';
|
|
77
|
+
// One keyed segment per preset so a rebuild notice can name exactly which preset moved.
|
|
78
|
+
const presetsPart = [
|
|
79
|
+
...presetNames(cfg)
|
|
80
|
+
].sort().map((name)=>{
|
|
81
|
+
var _p_exclude;
|
|
82
|
+
const p = cfg.presets[name];
|
|
83
|
+
const include = [
|
|
84
|
+
...p.include
|
|
85
|
+
].sort().join('+');
|
|
86
|
+
const exclude = [
|
|
87
|
+
...(_p_exclude = p.exclude) !== null && _p_exclude !== void 0 ? _p_exclude : []
|
|
88
|
+
].sort().join('+');
|
|
89
|
+
return `preset:${name}:${include}:${exclude}:${p.semantic === false ? 'off' : 'on'}`;
|
|
90
|
+
}).join('|');
|
|
91
|
+
return [
|
|
92
|
+
`features:${globalPart}`,
|
|
93
|
+
embedPart,
|
|
94
|
+
presetsPart
|
|
95
|
+
].join('|');
|
|
96
|
+
}
|
|
97
|
+
// Looks up a declared preset by name, defaulting to `default`. Throws naming every declared
|
|
98
|
+
// preset when an explicit name is not declared -- the `default` fallback is never unknown
|
|
99
|
+
// itself (validateConfig requires it).
|
|
100
|
+
export function resolvePreset(cfg, name) {
|
|
101
|
+
const presetName = name !== null && name !== void 0 ? name : 'default';
|
|
102
|
+
const preset = cfg.presets[presetName];
|
|
103
|
+
if (!preset) throw new SenseError('PRESET_UNKNOWN', `unknown preset "${presetName}"; declared presets: ${presetNames(cfg).join(', ')}`);
|
|
104
|
+
return {
|
|
105
|
+
name: presetName,
|
|
106
|
+
preset
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
// Resolution precedence: built-ins (k=10, semantic on, whole-index scope) <- named preset's
|
|
110
|
+
// fields (or `default` when none named) <- caller overrides. Each stage overrides only the
|
|
111
|
+
// fields it sets. `opts` here is expected to already carry whatever a saved query and an
|
|
112
|
+
// explicit CLI flag resolved between themselves (src/cli/named.ts does that `cli ?? saved`
|
|
113
|
+
// merge before calling in) -- one `??` per field composes the full four-stage chain, since
|
|
114
|
+
// `??` is associative field-by-field.
|
|
115
|
+
export function resolveSearch(cfg, opts = {}) {
|
|
116
|
+
var _ref, _opts_k, _opts_where, _opts_include;
|
|
117
|
+
const { name: presetName, preset } = resolvePreset(cfg, opts.preset);
|
|
118
|
+
const k = (_ref = (_opts_k = opts.k) !== null && _opts_k !== void 0 ? _opts_k : preset.k) !== null && _ref !== void 0 ? _ref : 10;
|
|
119
|
+
const where = (_opts_where = opts.where) !== null && _opts_where !== void 0 ? _opts_where : preset.where;
|
|
120
|
+
// An explicit include (CLI --include, or a saved search's own `include`) is a full ad hoc
|
|
121
|
+
// scope override -- it replaces the preset's include/exclude pair rather than layering on
|
|
122
|
+
// top of it, the same "replaces, doesn't AND" rule --where already uses.
|
|
123
|
+
const include = (_opts_include = opts.include) !== null && _opts_include !== void 0 ? _opts_include : preset.include;
|
|
124
|
+
const exclude = opts.include ? undefined : preset.exclude;
|
|
125
|
+
const semantic = opts.semantic !== undefined ? opts.semantic : preset.semantic !== false;
|
|
126
|
+
return {
|
|
127
|
+
presetName,
|
|
128
|
+
k,
|
|
129
|
+
where,
|
|
130
|
+
include,
|
|
131
|
+
exclude,
|
|
132
|
+
semantic
|
|
133
|
+
};
|
|
63
134
|
}
|
|
64
135
|
// Pure per-version steps; loadConfig chains them from the file's version up to SUPPORTED_CONFIG_VERSION.
|
|
136
|
+
// Intermediate shapes predate the current Config type (v1 has no features block, v2 has
|
|
137
|
+
// scan/find not presets/search), so steps work loosely-typed and only the final result is cast
|
|
138
|
+
// back to Config.
|
|
65
139
|
const MIGRATIONS = {
|
|
66
140
|
// v1 -> v2: features block introduced, opt-out features enabled (matches the old implicit
|
|
67
141
|
// behavior of `links` etc. not existing). embed stays absent -- opt-in.
|
|
68
142
|
1: (cfg)=>({
|
|
69
143
|
...cfg,
|
|
70
144
|
version: 2,
|
|
71
|
-
features: Object.fromEntries(
|
|
145
|
+
features: Object.fromEntries(V2_OPT_OUT_NAMES.map((name)=>[
|
|
72
146
|
name,
|
|
73
147
|
true
|
|
74
148
|
]))
|
|
75
|
-
})
|
|
149
|
+
}),
|
|
150
|
+
// v2 -> v3: mechanical-minimal, by decision -- only what keeps an existing config loading.
|
|
151
|
+
// No restructuring, no preset inference, no query rewriting; real trees get hand-migrated
|
|
152
|
+
// separately to actually use presets.
|
|
153
|
+
2: (cfg)=>{
|
|
154
|
+
var _prevDefaults_find;
|
|
155
|
+
const { scan, defaults, features, checks, queries, ...rest } = cfg;
|
|
156
|
+
const scanInclude = scan.include;
|
|
157
|
+
const nextQueries = {};
|
|
158
|
+
for (const [name, value] of Object.entries(queries !== null && queries !== void 0 ? queries : {})){
|
|
159
|
+
if (typeof value === 'string') {
|
|
160
|
+
nextQueries[name] = value;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const entry = value;
|
|
164
|
+
const search = {
|
|
165
|
+
search: entry.find
|
|
166
|
+
};
|
|
167
|
+
if (entry.k !== undefined) search.k = entry.k;
|
|
168
|
+
if (entry.where !== undefined) search.where = entry.where;
|
|
169
|
+
// semantic: true was opt-in; it is now the default, so it drops. semantic: false is kept.
|
|
170
|
+
if (entry.semantic === false) search.semantic = false;
|
|
171
|
+
nextQueries[name] = search;
|
|
172
|
+
}
|
|
173
|
+
const prevDefaults = defaults;
|
|
174
|
+
const defaultWhere = prevDefaults === null || prevDefaults === void 0 ? void 0 : (_prevDefaults_find = prevDefaults.find) === null || _prevDefaults_find === void 0 ? void 0 : _prevDefaults_find.where;
|
|
175
|
+
// features.embed: true/absent -> simply removed (v3 default is on). An explicit `false`
|
|
176
|
+
// becomes semantic: false on every preset this migration produces -- there is only ever
|
|
177
|
+
// one (`default`), since v2 has no notion of multiple scopes, but the rule is written to
|
|
178
|
+
// apply to "every migrated preset" rather than hardcoded to `default` in case a later
|
|
179
|
+
// migration step ever produces more than one.
|
|
180
|
+
const prevFeatures = features !== null && features !== void 0 ? features : {};
|
|
181
|
+
const embedWasOff = prevFeatures.embed === false;
|
|
182
|
+
// Object form carried provider settings (model/type/url/key), not a toggle -- they move
|
|
183
|
+
// to the v3 top-level `embed` block verbatim; dropping them would silently switch an
|
|
184
|
+
// api tree back to the built-in static model.
|
|
185
|
+
const embedProvider = typeof prevFeatures.embed === 'object' && prevFeatures.embed !== null ? prevFeatures.embed : undefined;
|
|
186
|
+
const { embed: _embed, ...restFeatures } = prevFeatures;
|
|
187
|
+
const defaultPreset = {
|
|
188
|
+
include: scanInclude
|
|
189
|
+
};
|
|
190
|
+
if (defaultWhere !== undefined) defaultPreset.where = defaultWhere;
|
|
191
|
+
if (embedWasOff) defaultPreset.semantic = false;
|
|
192
|
+
// checks (assertions over saved queries) was removed in v3; its queries still run under
|
|
193
|
+
// `queries`, just without the pass/fail assertion -- reachable now only as a saved query.
|
|
194
|
+
if (checks !== undefined) {
|
|
195
|
+
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');
|
|
196
|
+
}
|
|
197
|
+
const result = {
|
|
198
|
+
...rest,
|
|
199
|
+
version: 3,
|
|
200
|
+
presets: {
|
|
201
|
+
default: defaultPreset
|
|
202
|
+
},
|
|
203
|
+
queries: nextQueries
|
|
204
|
+
};
|
|
205
|
+
if (embedProvider !== undefined) result.embed = embedProvider;
|
|
206
|
+
if (Object.keys(restFeatures).length > 0) result.features = restFeatures;
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
76
209
|
};
|
|
77
210
|
export function migrateConfig(cfg) {
|
|
78
211
|
var _cfg_version;
|
|
@@ -92,15 +225,20 @@ function starterConfig() {
|
|
|
92
225
|
return {
|
|
93
226
|
$schema: 'https://unpkg.com/sensemaking/schema.json',
|
|
94
227
|
version: SUPPORTED_CONFIG_VERSION,
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
228
|
+
presets: {
|
|
229
|
+
default: {
|
|
230
|
+
include: [
|
|
231
|
+
'**/*.md'
|
|
232
|
+
],
|
|
233
|
+
k: 10
|
|
234
|
+
},
|
|
235
|
+
large: {
|
|
236
|
+
include: [
|
|
237
|
+
'**/*.md'
|
|
238
|
+
],
|
|
239
|
+
k: 20
|
|
240
|
+
}
|
|
99
241
|
},
|
|
100
|
-
features: Object.fromEntries(OPT_OUT_NAMES.map((name)=>[
|
|
101
|
-
name,
|
|
102
|
-
true
|
|
103
|
-
])),
|
|
104
242
|
queries: {}
|
|
105
243
|
};
|
|
106
244
|
}
|
|
@@ -125,114 +263,202 @@ export function findConfigPath(startDir) {
|
|
|
125
263
|
}
|
|
126
264
|
// Shape check for hand-edited files: a typo'd config fails with a named error, not a
|
|
127
265
|
// TypeError from whatever code touched the missing field first. `queries` is optional
|
|
128
|
-
// on disk (absent = none); `
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
266
|
+
// on disk (absent = none); `presets` has no usable default.
|
|
267
|
+
// Unknown top-level keys are reported as a soft warning (cli.ts prints it) rather than an
|
|
268
|
+
// error, so a config carrying a stray field still runs -- but unknown keys inside a preset,
|
|
269
|
+
// features, embed, or saved-query block are hard errors: those blocks are small and fully
|
|
270
|
+
// owned, so a typo there is far more likely a mistake than a forward-compat field.
|
|
133
271
|
const KNOWN_KEYS = new Set([
|
|
134
272
|
'$schema',
|
|
135
273
|
'version',
|
|
136
|
-
'
|
|
274
|
+
'presets',
|
|
137
275
|
'features',
|
|
138
|
-
'
|
|
139
|
-
'checks',
|
|
276
|
+
'embed',
|
|
140
277
|
'queries'
|
|
141
278
|
]);
|
|
142
|
-
const
|
|
143
|
-
'include'
|
|
279
|
+
const KNOWN_PRESET_KEYS = new Set([
|
|
280
|
+
'include',
|
|
281
|
+
'exclude',
|
|
282
|
+
'k',
|
|
283
|
+
'semantic',
|
|
284
|
+
'where'
|
|
285
|
+
]);
|
|
286
|
+
const KNOWN_FEATURE_KEYS = new Set([
|
|
287
|
+
'links',
|
|
288
|
+
'sections',
|
|
289
|
+
'rank'
|
|
290
|
+
]);
|
|
291
|
+
const KNOWN_EMBED_KEYS = new Set([
|
|
292
|
+
'model',
|
|
293
|
+
'type',
|
|
294
|
+
'url',
|
|
295
|
+
'key'
|
|
296
|
+
]);
|
|
297
|
+
const SAVED_SEARCH_KEYS = new Set([
|
|
298
|
+
'search',
|
|
299
|
+
'preset',
|
|
300
|
+
'include',
|
|
301
|
+
'where',
|
|
302
|
+
'k',
|
|
303
|
+
'semantic'
|
|
144
304
|
]);
|
|
145
305
|
function unknownConfigKeys(cfg) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
306
|
+
return Object.keys(cfg).filter((k)=>!KNOWN_KEYS.has(k));
|
|
307
|
+
}
|
|
308
|
+
function isNonEmptyStringArray(value) {
|
|
309
|
+
return Array.isArray(value) && value.length > 0 && value.every((g)=>typeof g === 'string' && g.length > 0);
|
|
310
|
+
}
|
|
311
|
+
// Pre-v3 shape check, just enough for migrateConfig to run safely (it reads scan.include
|
|
312
|
+
// directly). The full v3 shape is checked by validateConfig once migration has produced it.
|
|
313
|
+
function validateLegacyScan(parsed, configPath) {
|
|
314
|
+
const cfg = typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) ? parsed : null;
|
|
315
|
+
const scan = cfg === null || cfg === void 0 ? void 0 : cfg.scan;
|
|
316
|
+
if (!cfg || !scan || !isNonEmptyStringArray(scan.include)) {
|
|
317
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: scan.include must be a non-empty array of glob strings`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function validateFeaturesBlock(value, configPath) {
|
|
321
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
322
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: features must be an object of name -> boolean`);
|
|
150
323
|
}
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
324
|
+
const block = value;
|
|
325
|
+
const unknown = Object.keys(block).filter((k)=>!KNOWN_FEATURE_KEYS.has(k));
|
|
326
|
+
if (unknown.length > 0) {
|
|
327
|
+
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 and each preset's "semantic"`);
|
|
328
|
+
}
|
|
329
|
+
for (const [name, v] of Object.entries(block)){
|
|
330
|
+
if (typeof v !== 'boolean') {
|
|
331
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: features.${name} must be a boolean`);
|
|
332
|
+
}
|
|
154
333
|
}
|
|
155
|
-
return unknown;
|
|
156
334
|
}
|
|
157
|
-
|
|
158
|
-
'find',
|
|
159
|
-
'k',
|
|
160
|
-
'where',
|
|
161
|
-
'semantic'
|
|
162
|
-
]);
|
|
163
|
-
// A queries.<name> entry that isn't a SQL string must be a saved find: { find, k?, where?, semantic? }.
|
|
164
|
-
function validateSavedFind(name, value, configPath) {
|
|
335
|
+
function validateEmbedBlock(value, configPath) {
|
|
165
336
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
166
|
-
throw new SenseError('CONFIG_INVALID', `${configPath}:
|
|
337
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: embed must be an object of { model?, type?: "static"|"api", url?, key? }`);
|
|
167
338
|
}
|
|
168
|
-
const
|
|
169
|
-
const unknown = Object.keys(
|
|
339
|
+
const embed = value;
|
|
340
|
+
const unknown = Object.keys(embed).filter((k)=>!KNOWN_EMBED_KEYS.has(k));
|
|
170
341
|
if (unknown.length > 0) {
|
|
171
|
-
throw new SenseError('CONFIG_INVALID', `${configPath}:
|
|
342
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: embed has unknown key(s) ${unknown.join(', ')}`);
|
|
172
343
|
}
|
|
173
|
-
if (typeof
|
|
174
|
-
throw new SenseError('CONFIG_INVALID', `${configPath}:
|
|
344
|
+
if (embed.model !== undefined && typeof embed.model !== 'string') {
|
|
345
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: embed.model must be a string`);
|
|
175
346
|
}
|
|
176
|
-
if (
|
|
177
|
-
throw new SenseError('CONFIG_INVALID', `${configPath}:
|
|
347
|
+
if (embed.type !== undefined && embed.type !== 'static' && embed.type !== 'api') {
|
|
348
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: embed.type must be "static" or "api"`);
|
|
178
349
|
}
|
|
179
|
-
if (
|
|
180
|
-
throw new SenseError('CONFIG_INVALID', `${configPath}:
|
|
350
|
+
if (embed.url !== undefined && typeof embed.url !== 'string') {
|
|
351
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: embed.url must be a string`);
|
|
181
352
|
}
|
|
182
|
-
if (
|
|
183
|
-
throw new SenseError('CONFIG_INVALID', `${configPath}:
|
|
353
|
+
if (embed.key !== undefined && typeof embed.key !== 'string') {
|
|
354
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: embed.key must be a string`);
|
|
184
355
|
}
|
|
185
356
|
}
|
|
357
|
+
function validatePreset(name, value, configPath) {
|
|
358
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
359
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name} must be an object`);
|
|
360
|
+
}
|
|
361
|
+
const preset = value;
|
|
362
|
+
const unknown = Object.keys(preset).filter((k)=>!KNOWN_PRESET_KEYS.has(k));
|
|
363
|
+
if (unknown.length > 0) {
|
|
364
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name} has unknown key(s) ${unknown.join(', ')}; a preset takes include, exclude, k, semantic, where`);
|
|
365
|
+
}
|
|
366
|
+
if (!isNonEmptyStringArray(preset.include)) {
|
|
367
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.include must be a non-empty array of glob strings`);
|
|
368
|
+
}
|
|
369
|
+
if (preset.exclude !== undefined && !isNonEmptyStringArray(preset.exclude)) {
|
|
370
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.exclude must be a non-empty array of glob strings`);
|
|
371
|
+
}
|
|
372
|
+
if (preset.k !== undefined && (typeof preset.k !== 'number' || !Number.isInteger(preset.k) || preset.k <= 0)) {
|
|
373
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.k must be a positive integer`);
|
|
374
|
+
}
|
|
375
|
+
if (preset.semantic !== undefined && typeof preset.semantic !== 'boolean') {
|
|
376
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.semantic must be a boolean`);
|
|
377
|
+
}
|
|
378
|
+
if (preset.where !== undefined && typeof preset.where !== 'string') {
|
|
379
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.where must be a SQL condition string`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// A queries.<name> entry: a SQL string, { sql }, or a saved search { search, preset?, include?, where?, k?, semantic? }.
|
|
383
|
+
function validateSavedQuery(name, value, configPath) {
|
|
384
|
+
if (typeof value === 'string') return;
|
|
385
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
386
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} must be a SQL string, { sql }, or { search, preset?, include?, where?, k?, semantic? }`);
|
|
387
|
+
}
|
|
388
|
+
const entry = value;
|
|
389
|
+
if ('sql' in entry) {
|
|
390
|
+
const unknown = Object.keys(entry).filter((k)=>k !== 'sql');
|
|
391
|
+
if (unknown.length > 0) {
|
|
392
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} has unknown key(s) ${unknown.join(', ')}; a { sql } query takes only sql`);
|
|
393
|
+
}
|
|
394
|
+
if (typeof entry.sql !== 'string' || entry.sql.trim() === '') {
|
|
395
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.sql must be a non-empty string`);
|
|
396
|
+
}
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if ('search' in entry) {
|
|
400
|
+
const unknown = Object.keys(entry).filter((k)=>!SAVED_SEARCH_KEYS.has(k));
|
|
401
|
+
if (unknown.length > 0) {
|
|
402
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} has unknown key(s) ${unknown.join(', ')}; a saved search takes search, preset, include, where, k, semantic`);
|
|
403
|
+
}
|
|
404
|
+
// A saved query saves a question; a scope without a question is just flags.
|
|
405
|
+
if (typeof entry.search !== 'string' || entry.search.trim() === '') {
|
|
406
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.search must be non-empty text`);
|
|
407
|
+
}
|
|
408
|
+
if (entry.preset !== undefined && (typeof entry.preset !== 'string' || entry.preset.length === 0)) {
|
|
409
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.preset must be a preset name`);
|
|
410
|
+
}
|
|
411
|
+
if (entry.include !== undefined && !isNonEmptyStringArray(entry.include)) {
|
|
412
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.include must be a non-empty array of glob strings`);
|
|
413
|
+
}
|
|
414
|
+
if (entry.where !== undefined && typeof entry.where !== 'string') {
|
|
415
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.where must be a SQL condition string`);
|
|
416
|
+
}
|
|
417
|
+
if (entry.k !== undefined && (typeof entry.k !== 'number' || !Number.isInteger(entry.k) || entry.k <= 0)) {
|
|
418
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.k must be a positive integer`);
|
|
419
|
+
}
|
|
420
|
+
if (entry.semantic !== undefined && typeof entry.semantic !== 'boolean') {
|
|
421
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.semantic must be a boolean`);
|
|
422
|
+
}
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} must be a SQL string, { sql }, or { search, preset?, include?, where?, k?, semantic? }`);
|
|
426
|
+
}
|
|
186
427
|
function validateConfig(parsed, configPath) {
|
|
187
|
-
var _cfg_features;
|
|
188
|
-
var _defaults_find;
|
|
189
428
|
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
190
429
|
throw new SenseError('CONFIG_INVALID', `${configPath}: config must be a JSON object`);
|
|
191
430
|
}
|
|
192
431
|
const cfg = parsed;
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
432
|
+
// `checks` (assertions over saved queries) was removed in v3, not merely deprecated: a
|
|
433
|
+
// hand-written v3 config still carrying it gets a named rejection rather than a silent
|
|
434
|
+
// "unknown key" warning, since silence here would hide that the assertion behavior it
|
|
435
|
+
// implies no longer exists.
|
|
436
|
+
if (cfg.checks !== undefined) {
|
|
437
|
+
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`);
|
|
196
438
|
}
|
|
197
|
-
|
|
198
|
-
if (typeof
|
|
199
|
-
throw new SenseError('CONFIG_INVALID', `${configPath}:
|
|
439
|
+
const presets = cfg.presets;
|
|
440
|
+
if (!presets || typeof presets !== 'object' || Array.isArray(presets) || Object.keys(presets).length === 0) {
|
|
441
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: presets must be a non-empty object of preset name -> { include, exclude?, k?, semantic?, where? }`);
|
|
200
442
|
}
|
|
201
|
-
for (const [name, value] of Object.entries(
|
|
202
|
-
|
|
203
|
-
validateSavedFind(name, value, configPath);
|
|
443
|
+
for (const [name, value] of Object.entries(presets)){
|
|
444
|
+
validatePreset(name, value, configPath);
|
|
204
445
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
throw new SenseError('CONFIG_INVALID', `${configPath}: defaults.find.where must be a SQL condition string`);
|
|
446
|
+
if (presets.default === undefined) {
|
|
447
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: presets must include a "default" preset`);
|
|
208
448
|
}
|
|
209
|
-
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
if (typeof checks !== 'object' || checks === null || Array.isArray(checks) || !Object.values(checks).every((v)=>v === 'empty')) {
|
|
213
|
-
throw new SenseError('CONFIG_INVALID', `${configPath}: checks must be an object of query name -> "empty"`);
|
|
214
|
-
}
|
|
215
|
-
const queries = (_cfg_queries = cfg.queries) !== null && _cfg_queries !== void 0 ? _cfg_queries : {};
|
|
216
|
-
for (const name of Object.keys(checks)){
|
|
217
|
-
if (queries[name] === undefined) throw new SenseError('CONFIG_INVALID', `${configPath}: checks names "${name}", which is not a saved query`);
|
|
218
|
-
if (typeof queries[name] !== 'string') throw new SenseError('CONFIG_INVALID', `${configPath}: checks names "${name}", which is a saved find, not a SQL query`);
|
|
219
|
-
}
|
|
449
|
+
if (cfg.queries === undefined) cfg.queries = {};
|
|
450
|
+
if (typeof cfg.queries !== 'object' || cfg.queries === null || Array.isArray(cfg.queries)) {
|
|
451
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: queries must be an object of name -> SQL string, { sql }, or saved search`);
|
|
220
452
|
}
|
|
221
|
-
|
|
222
|
-
|
|
453
|
+
const queries = cfg.queries;
|
|
454
|
+
for (const [name, value] of Object.entries(queries)){
|
|
455
|
+
validateSavedQuery(name, value, configPath);
|
|
223
456
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
'model',
|
|
230
|
-
'url',
|
|
231
|
-
'key'
|
|
232
|
-
].every((f)=>embed[f] === undefined || typeof embed[f] === 'string') && (embed.type === undefined || embed.type === 'static' || embed.type === 'api');
|
|
233
|
-
if (!shapeOk) {
|
|
234
|
-
throw new SenseError('CONFIG_INVALID', `${configPath}: features.${name} must be a boolean${name === 'embed' ? ' or { model?, type?: "static"|"api", url?, key? }' : ''}`);
|
|
235
|
-
}
|
|
457
|
+
if (cfg.features !== undefined) {
|
|
458
|
+
validateFeaturesBlock(cfg.features, configPath);
|
|
459
|
+
}
|
|
460
|
+
if (cfg.embed !== undefined) {
|
|
461
|
+
validateEmbedBlock(cfg.embed, configPath);
|
|
236
462
|
}
|
|
237
463
|
return cfg;
|
|
238
464
|
}
|
|
@@ -250,19 +476,33 @@ export function loadConfig(explicitPath) {
|
|
|
250
476
|
}
|
|
251
477
|
const raw = readFileSync(configPath, 'utf8');
|
|
252
478
|
const parsed = JSON.parse(raw);
|
|
479
|
+
// Ahead of both the version gate and legacy/current shape checks: a non-object file (or
|
|
480
|
+
// an array -- typeof [] is also 'object') is malformed at every version, so it gets one
|
|
481
|
+
// error regardless of which branch would otherwise run.
|
|
482
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
483
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: config must be a JSON object`);
|
|
484
|
+
}
|
|
253
485
|
// Version gate before shape validation: a config written by a newer sense should fail
|
|
254
486
|
// with "requires a newer sense", not with shape errors its own version may not have.
|
|
255
487
|
const version = typeof parsed === 'object' && parsed !== null && typeof parsed.version === 'number' ? parsed.version : 1;
|
|
256
488
|
if (version > SUPPORTED_CONFIG_VERSION) {
|
|
257
489
|
throw new SenseError('CONFIG_VERSION_UNSUPPORTED', `config version ${version} requires a newer sense`);
|
|
258
490
|
}
|
|
259
|
-
|
|
491
|
+
// validateConfig only understands the current v3 shape (presets, saved-query object
|
|
492
|
+
// shapes); a pre-v3 file on disk is scan/find-shaped and must migrate before it can pass
|
|
493
|
+
// that check. Validate just enough of the old shape to migrate safely, migrate, then run
|
|
494
|
+
// the full v3 validateConfig on the migrated result -- the shape that actually lands on
|
|
495
|
+
// disk and in ResolvedConfig.
|
|
496
|
+
let cfg;
|
|
260
497
|
let migratedFrom;
|
|
261
498
|
if (version < SUPPORTED_CONFIG_VERSION) {
|
|
262
|
-
|
|
263
|
-
|
|
499
|
+
validateLegacyScan(parsed, configPath);
|
|
500
|
+
const result = migrateConfig(parsed);
|
|
501
|
+
cfg = validateConfig(result.cfg, configPath);
|
|
264
502
|
migratedFrom = result.from;
|
|
265
503
|
writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}\n`);
|
|
504
|
+
} else {
|
|
505
|
+
cfg = validateConfig(parsed, configPath);
|
|
266
506
|
}
|
|
267
507
|
const unknownKeys = unknownConfigKeys(cfg);
|
|
268
508
|
return {
|