sensemaking 0.7.2 → 0.8.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 +61 -41
- package/dist/cjs/cli/check.js +386 -0
- package/dist/cjs/cli/check.js.map +1 -0
- package/dist/cjs/{commands → cli}/find.js +3 -4
- package/dist/cjs/cli/find.js.map +1 -0
- package/dist/cjs/cli/index.js.map +1 -0
- package/dist/cjs/{commands → cli}/init.js +3 -0
- package/dist/cjs/cli/init.js.map +1 -0
- package/dist/cjs/{commands → cli}/map.js +2 -2
- package/dist/cjs/cli/map.js.map +1 -0
- package/dist/cjs/{commands → cli}/named.d.cts +1 -1
- package/dist/cjs/{commands → cli}/named.d.ts +1 -1
- package/dist/cjs/cli/named.js +209 -0
- package/dist/cjs/cli/named.js.map +1 -0
- package/dist/cjs/{commands → cli}/peek.js +2 -2
- package/dist/cjs/cli/peek.js.map +1 -0
- package/dist/cjs/cli/query.js.map +1 -0
- package/dist/cjs/cli/rebuild.js.map +1 -0
- package/dist/cjs/{commands → cli}/shared.d.cts +1 -0
- package/dist/{esm/commands → cjs/cli}/shared.d.ts +1 -0
- package/dist/cjs/{commands → cli}/shared.js +9 -0
- package/dist/cjs/cli/shared.js.map +1 -0
- package/dist/cjs/{commands → cli}/status.js +7 -1
- package/dist/cjs/cli/status.js.map +1 -0
- package/dist/cjs/cli/watch.js.map +1 -0
- package/dist/cjs/cli.js +22 -17
- package/dist/cjs/cli.js.map +1 -1
- package/dist/{esm/verbs.d.ts → cjs/commands.d.cts} +5 -4
- package/dist/cjs/{verbs.d.cts → commands.d.ts} +5 -4
- package/dist/cjs/{verbs.js → commands.js} +246 -21
- package/dist/cjs/commands.js.map +1 -0
- package/dist/cjs/config.d.cts +7 -1
- package/dist/cjs/config.d.ts +7 -1
- package/dist/cjs/config.js +75 -24
- 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 +88 -17
- 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 +7 -1
- package/dist/cjs/features/embed.js.map +1 -1
- package/dist/cjs/features/links.js +201 -24
- package/dist/cjs/features/links.js.map +1 -1
- package/dist/cjs/features/rank.js +8 -1
- package/dist/cjs/features/rank.js.map +1 -1
- package/dist/cjs/features/types.d.cts +10 -3
- package/dist/cjs/features/types.d.ts +10 -3
- package/dist/cjs/index.d.cts +3 -3
- package/dist/cjs/index.d.ts +3 -3
- package/dist/cjs/index.js +4 -4
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/output.d.cts +1 -0
- package/dist/cjs/output.d.ts +1 -0
- package/dist/cjs/output.js +2 -1
- package/dist/cjs/output.js.map +1 -1
- package/dist/cjs/progress.d.cts +5 -0
- package/dist/cjs/progress.d.ts +5 -0
- package/dist/cjs/progress.js +54 -0
- package/dist/cjs/progress.js.map +1 -0
- package/dist/cjs/scan.d.cts +1 -0
- package/dist/cjs/scan.d.ts +1 -0
- package/dist/cjs/scan.js +3 -2
- package/dist/cjs/scan.js.map +1 -1
- package/dist/esm/{commands → cli}/check.js +43 -3
- package/dist/esm/cli/check.js.map +1 -0
- package/dist/esm/{commands → cli}/find.js +3 -4
- package/dist/esm/cli/find.js.map +1 -0
- package/dist/esm/cli/index.js.map +1 -0
- package/dist/esm/{commands → cli}/init.js +3 -0
- package/dist/esm/cli/init.js.map +1 -0
- package/dist/esm/{commands → cli}/map.js +1 -1
- package/dist/esm/cli/map.js.map +1 -0
- package/dist/esm/{commands → cli}/named.d.ts +1 -1
- package/dist/esm/cli/named.js +33 -0
- package/dist/esm/cli/named.js.map +1 -0
- package/dist/esm/{commands → cli}/peek.js +1 -1
- package/dist/esm/cli/peek.js.map +1 -0
- package/dist/esm/cli/query.js.map +1 -0
- package/dist/esm/cli/rebuild.js.map +1 -0
- package/dist/{cjs/commands → esm/cli}/shared.d.ts +1 -0
- package/dist/esm/{commands → cli}/shared.js +9 -0
- package/dist/esm/cli/shared.js.map +1 -0
- package/dist/esm/{commands → cli}/status.js +7 -1
- package/dist/esm/cli/status.js.map +1 -0
- package/dist/esm/cli/types.js.map +1 -0
- package/dist/esm/cli/watch.js.map +1 -0
- package/dist/esm/cli.js +8 -6
- package/dist/esm/cli.js.map +1 -1
- package/dist/{cjs/verbs.d.ts → esm/commands.d.ts} +5 -4
- package/dist/esm/{verbs.js → commands.js} +165 -21
- package/dist/esm/commands.js.map +1 -0
- package/dist/esm/config.d.ts +7 -1
- package/dist/esm/config.js +38 -4
- package/dist/esm/config.js.map +1 -1
- package/dist/esm/db.d.ts +1 -1
- package/dist/esm/db.js +78 -16
- 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 +6 -0
- package/dist/esm/features/embed.js.map +1 -1
- package/dist/esm/features/links.js +130 -20
- package/dist/esm/features/links.js.map +1 -1
- package/dist/esm/features/rank.js +8 -1
- package/dist/esm/features/rank.js.map +1 -1
- package/dist/esm/features/types.d.ts +10 -3
- 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 +1 -0
- package/dist/esm/output.js +2 -1
- package/dist/esm/output.js.map +1 -1
- package/dist/esm/progress.d.ts +5 -0
- package/dist/esm/progress.js +44 -0
- package/dist/esm/progress.js.map +1 -0
- package/dist/esm/scan.d.ts +1 -0
- package/dist/esm/scan.js +5 -2
- package/dist/esm/scan.js.map +1 -1
- package/package.json +6 -2
- package/schema.json +19 -4
- package/skills/sense/EXAMPLES.md +9 -7
- package/skills/sense/SKILL.md +43 -27
- package/skills/sense-setup/SKILL.md +16 -2
- package/dist/cjs/commands/check.js +0 -143
- package/dist/cjs/commands/check.js.map +0 -1
- package/dist/cjs/commands/find.js.map +0 -1
- package/dist/cjs/commands/index.js.map +0 -1
- package/dist/cjs/commands/init.js.map +0 -1
- package/dist/cjs/commands/map.js.map +0 -1
- package/dist/cjs/commands/named.js +0 -23
- package/dist/cjs/commands/named.js.map +0 -1
- package/dist/cjs/commands/peek.js.map +0 -1
- package/dist/cjs/commands/query.js.map +0 -1
- package/dist/cjs/commands/rebuild.js.map +0 -1
- package/dist/cjs/commands/shared.js.map +0 -1
- package/dist/cjs/commands/status.js.map +0 -1
- package/dist/cjs/commands/watch.js.map +0 -1
- package/dist/cjs/verbs.js.map +0 -1
- package/dist/esm/commands/check.js.map +0 -1
- package/dist/esm/commands/find.js.map +0 -1
- package/dist/esm/commands/index.js.map +0 -1
- package/dist/esm/commands/init.js.map +0 -1
- package/dist/esm/commands/map.js.map +0 -1
- package/dist/esm/commands/named.js +0 -12
- package/dist/esm/commands/named.js.map +0 -1
- package/dist/esm/commands/peek.js.map +0 -1
- package/dist/esm/commands/query.js.map +0 -1
- package/dist/esm/commands/rebuild.js.map +0 -1
- package/dist/esm/commands/shared.js.map +0 -1
- package/dist/esm/commands/status.js.map +0 -1
- package/dist/esm/commands/types.js.map +0 -1
- package/dist/esm/commands/watch.js.map +0 -1
- package/dist/esm/verbs.js.map +0 -1
- /package/dist/cjs/{commands → cli}/check.d.cts +0 -0
- /package/dist/cjs/{commands → cli}/check.d.ts +0 -0
- /package/dist/cjs/{commands → cli}/find.d.cts +0 -0
- /package/dist/cjs/{commands → cli}/find.d.ts +0 -0
- /package/dist/cjs/{commands → cli}/index.d.cts +0 -0
- /package/dist/cjs/{commands → cli}/index.d.ts +0 -0
- /package/dist/cjs/{commands → cli}/index.js +0 -0
- /package/dist/cjs/{commands → cli}/init.d.cts +0 -0
- /package/dist/cjs/{commands → cli}/init.d.ts +0 -0
- /package/dist/cjs/{commands → cli}/map.d.cts +0 -0
- /package/dist/cjs/{commands → cli}/map.d.ts +0 -0
- /package/dist/cjs/{commands → cli}/peek.d.cts +0 -0
- /package/dist/cjs/{commands → cli}/peek.d.ts +0 -0
- /package/dist/cjs/{commands → cli}/query.d.cts +0 -0
- /package/dist/cjs/{commands → cli}/query.d.ts +0 -0
- /package/dist/cjs/{commands → cli}/query.js +0 -0
- /package/dist/cjs/{commands → cli}/rebuild.d.cts +0 -0
- /package/dist/cjs/{commands → cli}/rebuild.d.ts +0 -0
- /package/dist/cjs/{commands → cli}/rebuild.js +0 -0
- /package/dist/cjs/{commands → cli}/status.d.cts +0 -0
- /package/dist/cjs/{commands → cli}/status.d.ts +0 -0
- /package/dist/cjs/{commands → cli}/types.d.cts +0 -0
- /package/dist/cjs/{commands → cli}/types.d.ts +0 -0
- /package/dist/cjs/{commands → cli}/types.js +0 -0
- /package/dist/cjs/{commands → cli}/types.js.map +0 -0
- /package/dist/cjs/{commands → cli}/watch.d.cts +0 -0
- /package/dist/cjs/{commands → cli}/watch.d.ts +0 -0
- /package/dist/cjs/{commands → cli}/watch.js +0 -0
- /package/dist/esm/{commands → cli}/check.d.ts +0 -0
- /package/dist/esm/{commands → cli}/find.d.ts +0 -0
- /package/dist/esm/{commands → cli}/index.d.ts +0 -0
- /package/dist/esm/{commands → cli}/index.js +0 -0
- /package/dist/esm/{commands → cli}/init.d.ts +0 -0
- /package/dist/esm/{commands → cli}/map.d.ts +0 -0
- /package/dist/esm/{commands → cli}/peek.d.ts +0 -0
- /package/dist/esm/{commands → cli}/query.d.ts +0 -0
- /package/dist/esm/{commands → cli}/query.js +0 -0
- /package/dist/esm/{commands → cli}/rebuild.d.ts +0 -0
- /package/dist/esm/{commands → cli}/rebuild.js +0 -0
- /package/dist/esm/{commands → cli}/status.d.ts +0 -0
- /package/dist/esm/{commands → cli}/types.d.ts +0 -0
- /package/dist/esm/{commands → cli}/types.js +0 -0
- /package/dist/esm/{commands → cli}/watch.d.ts +0 -0
- /package/dist/esm/{commands → cli}/watch.js +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/cli/status.ts"],"sourcesContent":["import { embedConfig, featureStates } from '../config.ts';\nimport { docCount, getMeta, open } from '../db.ts';\nimport { printWarnings } from './shared.ts';\nimport type { Command } from './types.ts';\n\n// Vectors are computed on the first semantic query, not at reconcile, so a tree can be\n// embed-enabled with nothing embedded yet -- reported here because a fast `rebuild` on an\n// embed tree otherwise reads as \"the feature isn't on\".\nfunction vectorState(db: ReturnType<typeof open>['db']): { embedded: number; pending: number } | null {\n try {\n const row = db.prepare('SELECT COUNT(vector) AS embedded, COUNT(*) - COUNT(vector) AS pending FROM embeddings').get() as { embedded: number; pending: number };\n return row;\n } catch {\n return null;\n }\n}\n\nconst status: Command = (ctx) => {\n const cfg = ctx.resolveConfig();\n const { db, dbPath, warnings } = open(cfg);\n printWarnings(warnings);\n\n const features = featureStates(cfg);\n const e = embedConfig(cfg);\n const vectors = e ? vectorState(db) : null;\n const heartbeat = getMeta(db, 'watch_heartbeat');\n // Derived at open() (F): 3x the largest reconcile this cache has ever held its write\n // transaction for, floored at 30s -- read back from the connection rather than\n // recomputed here, so this always reports what open() actually set.\n const busyTimeoutMs = (db.prepare('PRAGMA busy_timeout').get() as { timeout: number }).timeout;\n const result = {\n db: dbPath,\n docs: docCount(db),\n features: features.on,\n featuresOff: features.off,\n embed: e ? { type: e.type, model: e.model, ...(vectors ?? {}) } : null,\n findDefaultWhere: cfg.defaults?.find?.where ?? null,\n watcherHeartbeatSecondsAgo: heartbeat ? Math.round((Date.now() - Date.parse(heartbeat)) / 1000) : null,\n busyTimeoutMs,\n };\n\n if (ctx.format === 'json') {\n console.log(JSON.stringify(result, null, 2));\n } else {\n console.log(`db: ${result.db}`);\n console.log(`docs: ${result.docs}`);\n const off = features.off.length > 0 ? ` · off: ${features.off.map((name) => `${name} (features.${name})`).join(', ')}` : '';\n console.log(`features: ${features.on.join(', ')}${off}`);\n if (e) console.log(`embed: ${e.type} ${e.model}${vectors ? ` (vectors: ${vectors.embedded} embedded, ${vectors.pending} pending — embedded on first --semantic query)` : ''}`);\n if (result.findDefaultWhere) console.log(`find default scope: ${result.findDefaultWhere} (--where replaces it)`);\n console.log(result.watcherHeartbeatSecondsAgo === null ? 'watcher: no watcher' : `watcher: last heartbeat ${result.watcherHeartbeatSecondsAgo}s ago`);\n console.log(`busy_timeout: ${result.busyTimeoutMs}ms (derived: 3x the largest reconcile this cache has recorded, floored at 30000ms)`);\n }\n db.close();\n};\nexport default status;\n"],"names":["embedConfig","featureStates","docCount","getMeta","open","printWarnings","vectorState","db","row","prepare","get","status","ctx","cfg","resolveConfig","dbPath","warnings","features","e","vectors","heartbeat","busyTimeoutMs","timeout","result","docs","on","featuresOff","off","embed","type","model","findDefaultWhere","defaults","find","where","watcherHeartbeatSecondsAgo","Math","round","Date","now","parse","format","console","log","JSON","stringify","length","map","name","join","embedded","pending","close"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/cli/types.ts"],"sourcesContent":["import type { ResolvedConfig } from '../config.ts';\n\n// Handed to every command; argv parsing stays in cli.ts.\nexport interface Ctx {\n name: string;\n rest: string[]; // positionals after the command word\n format: 'table' | 'json';\n values: { config?: string; where?: string; k?: string; force: boolean; semantic: boolean };\n resolveConfig(): ResolvedConfig;\n usageError(message: string): never;\n}\n\nexport type Command = (ctx: Ctx) => Promise<void> | void;\n"],"names":[],"mappings":"AAYA,WAAyD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/cli/watch.ts"],"sourcesContent":["import type { WatchEvent } from '../watch.ts';\nimport { runWatch } from '../watch.ts';\nimport { printWarnings } from './shared.ts';\nimport type { Command } from './types.ts';\n\nconst watch: Command = async (ctx) => {\n await runWatch(ctx.resolveConfig(), {\n force: ctx.values.force,\n onEvent: (event: WatchEvent) => {\n if (event.type === 'started') {\n console.log(`${ctx.name} watch: watching ${event.baseDir}`);\n console.log(`${ctx.name} watch: db ${event.dbPath}`);\n } else if (event.type === 'reconciled') {\n printWarnings(event.warnings);\n if (event.parsed > 0) console.log(`${ctx.name} watch: reconciled, ${event.parsed} file(s) reparsed (${event.total} total)`);\n } else {\n console.error(`${ctx.name} watch: reconcile error: ${event.message}`);\n }\n },\n });\n};\nexport default watch;\n"],"names":["runWatch","printWarnings","watch","ctx","resolveConfig","force","values","onEvent","event","type","console","log","name","baseDir","dbPath","warnings","parsed","total","error","message"],"mappings":"AACA,SAASA,QAAQ,QAAQ,cAAc;AACvC,SAASC,aAAa,QAAQ,cAAc;AAG5C,MAAMC,QAAiB,OAAOC;IAC5B,MAAMH,SAASG,IAAIC,aAAa,IAAI;QAClCC,OAAOF,IAAIG,MAAM,CAACD,KAAK;QACvBE,SAAS,CAACC;YACR,IAAIA,MAAMC,IAAI,KAAK,WAAW;gBAC5BC,QAAQC,GAAG,CAAC,GAAGR,IAAIS,IAAI,CAAC,iBAAiB,EAAEJ,MAAMK,OAAO,EAAE;gBAC1DH,QAAQC,GAAG,CAAC,GAAGR,IAAIS,IAAI,CAAC,WAAW,EAAEJ,MAAMM,MAAM,EAAE;YACrD,OAAO,IAAIN,MAAMC,IAAI,KAAK,cAAc;gBACtCR,cAAcO,MAAMO,QAAQ;gBAC5B,IAAIP,MAAMQ,MAAM,GAAG,GAAGN,QAAQC,GAAG,CAAC,GAAGR,IAAIS,IAAI,CAAC,oBAAoB,EAAEJ,MAAMQ,MAAM,CAAC,mBAAmB,EAAER,MAAMS,KAAK,CAAC,OAAO,CAAC;YAC5H,OAAO;gBACLP,QAAQQ,KAAK,CAAC,GAAGf,IAAIS,IAAI,CAAC,yBAAyB,EAAEJ,MAAMW,OAAO,EAAE;YACtE;QACF;IACF;AACF;AACA,eAAejB,MAAM"}
|
package/dist/esm/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
2
|
import { parseArgs } from 'node:util';
|
|
3
|
-
import { COMMANDS } from './
|
|
3
|
+
import { COMMANDS } from './cli/index.js';
|
|
4
4
|
import { loadConfig, SUPPORTED_CONFIG_VERSION } from './config.js';
|
|
5
|
-
// Parsing and dispatch only. Commands live in src/
|
|
5
|
+
// Parsing and dispatch only. Commands live in src/cli/, one file each, lazy-loaded --
|
|
6
6
|
// nothing tree- or dependency-heavy may be imported at the top of this file.
|
|
7
7
|
// Works from dist/cjs and dist/esm alike: walk up past the dist type-marker package.json.
|
|
8
8
|
function packageVersion() {
|
|
@@ -44,8 +44,7 @@ export default async function cli(argv, name) {
|
|
|
44
44
|
type: 'string'
|
|
45
45
|
},
|
|
46
46
|
semantic: {
|
|
47
|
-
type: 'boolean'
|
|
48
|
-
default: false
|
|
47
|
+
type: 'boolean'
|
|
49
48
|
},
|
|
50
49
|
list: {
|
|
51
50
|
type: 'boolean',
|
|
@@ -103,7 +102,10 @@ export default async function cli(argv, name) {
|
|
|
103
102
|
};
|
|
104
103
|
try {
|
|
105
104
|
if (values.list) {
|
|
106
|
-
|
|
105
|
+
const queries = ctx.resolveConfig().queries;
|
|
106
|
+
for (const queryName of Object.keys(queries).sort()){
|
|
107
|
+
console.log(typeof queries[queryName] === 'string' ? queryName : `${queryName} (find)`);
|
|
108
|
+
}
|
|
107
109
|
return;
|
|
108
110
|
}
|
|
109
111
|
const [first, ...params] = positionals;
|
|
@@ -113,7 +115,7 @@ export default async function cli(argv, name) {
|
|
|
113
115
|
}
|
|
114
116
|
const load = COMMANDS[first];
|
|
115
117
|
if (load) await (await load()).default(ctx);
|
|
116
|
-
else (await import('./
|
|
118
|
+
else await (await import('./cli/named.js')).default(ctx, first, params);
|
|
117
119
|
} catch (err) {
|
|
118
120
|
console.error(err.message);
|
|
119
121
|
process.exit(1);
|
package/dist/esm/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/cli.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport { parseArgs } from 'node:util';\nimport { COMMANDS } from './
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/cli.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport { parseArgs } from 'node:util';\nimport { COMMANDS } from './cli/index.ts';\nimport type { Ctx } from './cli/types.ts';\nimport { loadConfig, SUPPORTED_CONFIG_VERSION } from './config.ts';\n\n// Parsing and dispatch only. Commands live in src/cli/, one file each, lazy-loaded --\n// nothing tree- or dependency-heavy may be imported at the top of this file.\n\n// Works from dist/cjs and dist/esm alike: walk up past the dist type-marker package.json.\nfunction packageVersion(): string {\n const load = createRequire(import.meta.url);\n for (const rel of ['../package.json', '../../package.json', '../../../package.json']) {\n try {\n const pkg = load(rel) as { name?: string; version?: string };\n if (pkg.name === 'sensemaking' && pkg.version) return pkg.version;\n } catch {}\n }\n return 'unknown';\n}\n\nfunction usage(name: string): string {\n return (\n `usage: ${name} <name> [params...] [--format table|json] [--config path]\\n` +\n ` ${name} query \"<sql>\" [params...]\\n` +\n ` ${name} find \"<terms>\" [--where \"<sql>\"] [--k n] [--semantic]\\n` +\n ` ${name} map\\n` +\n ` ${name} peek <path>\\n` +\n ` ${name} --list\\n` +\n ` ${name} init\\n` +\n ` ${name} watch [--force]\\n` +\n ` ${name} status\\n` +\n ` ${name} check\\n` +\n ` ${name} rebuild\\n` +\n ` ${name} --version`\n );\n}\n\n// Thrown errors -> exit 1 with the message verbatim; usage errors exit 2 directly.\nexport default async function cli(argv: string[], name: string): Promise<void> {\n let values: Record<string, string | boolean | undefined>;\n let positionals: string[];\n try {\n ({ values, positionals } = parseArgs({\n args: argv,\n options: {\n format: { type: 'string', default: 'table' },\n config: { type: 'string' },\n where: { type: 'string' },\n k: { type: 'string' },\n semantic: { type: 'boolean' },\n list: { type: 'boolean', default: false },\n force: { type: 'boolean', default: false },\n version: { type: 'boolean', default: false, short: 'v' },\n help: { type: 'boolean', default: false, short: 'h' },\n },\n allowPositionals: true,\n }));\n } catch (err) {\n console.error((err as Error).message);\n console.error(usage(name));\n process.exit(2);\n }\n\n if (values.version) {\n console.log(`v${packageVersion()}`);\n return;\n }\n if (values.help) {\n console.log(usage(name));\n return;\n }\n\n const ctx: Ctx = {\n name,\n rest: positionals.slice(1),\n format: values.format === 'json' ? 'json' : 'table',\n values: values as Ctx['values'],\n resolveConfig() {\n const cfg = loadConfig(values.config as string | undefined);\n if (cfg.migratedFrom !== undefined) {\n console.warn(`${name}: migrated ${cfg.configPath} from config version ${cfg.migratedFrom} to ${SUPPORTED_CONFIG_VERSION}`);\n }\n if (cfg.unknownKeys !== undefined) {\n console.warn(`${name}: ${cfg.configPath} sets ${cfg.unknownKeys.join(', ')}, which this build does not read (no effect); see schema.json for the keys it does`);\n }\n return cfg;\n },\n usageError(message) {\n console.error(message);\n process.exit(2);\n },\n };\n\n try {\n if (values.list) {\n const queries = ctx.resolveConfig().queries;\n for (const queryName of Object.keys(queries).sort()) {\n console.log(typeof queries[queryName] === 'string' ? queryName : `${queryName} (find)`);\n }\n return;\n }\n\n const [first, ...params] = positionals;\n if (!first) {\n console.error(usage(name));\n process.exit(2);\n }\n\n const load = COMMANDS[first];\n if (load) await (await load()).default(ctx);\n else await (await import('./cli/named.ts')).default(ctx, first, params);\n } catch (err) {\n console.error((err as Error).message);\n process.exit(1);\n }\n}\n"],"names":["createRequire","parseArgs","COMMANDS","loadConfig","SUPPORTED_CONFIG_VERSION","packageVersion","load","url","rel","pkg","name","version","usage","cli","argv","values","positionals","args","options","format","type","default","config","where","k","semantic","list","force","short","help","allowPositionals","err","console","error","message","process","exit","log","ctx","rest","slice","resolveConfig","cfg","migratedFrom","undefined","warn","configPath","unknownKeys","join","usageError","queries","queryName","Object","keys","sort","first","params"],"mappings":"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"}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
-
import type {
|
|
2
|
+
import type { FeatureName, ResolvedConfig } from './config.js';
|
|
3
3
|
import type { Row } from './output.js';
|
|
4
4
|
export interface FindOptions {
|
|
5
5
|
k?: number;
|
|
6
6
|
where?: string;
|
|
7
7
|
semantic?: boolean;
|
|
8
8
|
}
|
|
9
|
-
export declare function find(db: DatabaseSync, cfg:
|
|
9
|
+
export declare function find(db: DatabaseSync, cfg: ResolvedConfig, terms: string, opts?: FindOptions): Promise<Row[]>;
|
|
10
10
|
export interface TreeMap {
|
|
11
11
|
docs: {
|
|
12
12
|
count: number;
|
|
@@ -21,7 +21,7 @@ export interface TreeMap {
|
|
|
21
21
|
hubs: Row[];
|
|
22
22
|
recent: Row[];
|
|
23
23
|
}
|
|
24
|
-
export declare function mapTree(db: DatabaseSync, cfg:
|
|
24
|
+
export declare function mapTree(db: DatabaseSync, cfg: ResolvedConfig): TreeMap;
|
|
25
25
|
export interface Peek {
|
|
26
26
|
path: string;
|
|
27
27
|
tokens: number;
|
|
@@ -30,9 +30,10 @@ export interface Peek {
|
|
|
30
30
|
outbound: string[];
|
|
31
31
|
backlinks: string[];
|
|
32
32
|
unresolved: string[];
|
|
33
|
+
sectionsTotal: number;
|
|
33
34
|
outboundTotal: number;
|
|
34
35
|
backlinksTotal: number;
|
|
35
36
|
unresolvedTotal: number;
|
|
36
37
|
off: FeatureName[];
|
|
37
38
|
}
|
|
38
|
-
export declare function peek(db: DatabaseSync, cfg:
|
|
39
|
+
export declare function peek(db: DatabaseSync, cfg: ResolvedConfig, pathArg: string): Peek;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
1
3
|
import posix from 'node:path/posix';
|
|
2
4
|
import { featureEnabled, featureStates } from './config.js';
|
|
3
5
|
import { SenseError } from './errors.js';
|
|
@@ -5,10 +7,108 @@ import { semanticCandidates } from './features/embed.js';
|
|
|
5
7
|
import { linkEdges } from './features/index.js';
|
|
6
8
|
import { personalizedRank } from './graph.js';
|
|
7
9
|
import { searchError } from './search-error.js';
|
|
8
|
-
// The three layer
|
|
10
|
+
// The three layer commands: mapTree (orient), find (locate), peek (structure).
|
|
9
11
|
// Each returns data; cli.ts renders. All of them degrade when a feature is off.
|
|
10
12
|
const WEIGHTED_BM25 = 'bm25(content, 10.0, 5.0, 1.0)';
|
|
11
13
|
const RRF_K = 60;
|
|
14
|
+
// FTS5 snippet() re-tokenizes the whole doc per candidate row, superlinearly: measured
|
|
15
|
+
// 2ms at 4KB docs but 189ms at 64KB for one query's pool, and ~10s for a single 1MB doc
|
|
16
|
+
// (plans/performance-findings.md finding A). 16KB keeps the whole pool's snippet cost in
|
|
17
|
+
// the low-ms range; bigger docs get the JS-computed excerpt, linear and only for the rows
|
|
18
|
+
// actually returned.
|
|
19
|
+
const SNIPPET_BOUND = 16384;
|
|
20
|
+
const EXCERPT_WINDOW = 160;
|
|
21
|
+
// Bare terms from an FTS5 query string: strips operators/quoting so the oversized-doc excerpt
|
|
22
|
+
// scan matches the same words the query matched on, not FTS5 syntax.
|
|
23
|
+
function extractBareTerms(query) {
|
|
24
|
+
const cleaned = query.replace(/"/g, ' ').replace(/[()*]/g, ' ').replace(/\b(AND|OR|NOT|NEAR)\b(\/\d+)?/gi, ' ');
|
|
25
|
+
return cleaned.split(/\s+/).map((tok)=>tok.replace(/^[A-Za-z_]\w*:/, '')) // column filter, e.g. title:term
|
|
26
|
+
.map((tok)=>tok.toLowerCase().trim()).filter((tok)=>tok.length > 0);
|
|
27
|
+
}
|
|
28
|
+
function findOccurrences(haystackLower, terms) {
|
|
29
|
+
const occ = [];
|
|
30
|
+
for (const term of terms){
|
|
31
|
+
let idx = 0;
|
|
32
|
+
for(;;){
|
|
33
|
+
const found = haystackLower.indexOf(term, idx);
|
|
34
|
+
if (found === -1) break;
|
|
35
|
+
occ.push({
|
|
36
|
+
start: found,
|
|
37
|
+
end: found + term.length,
|
|
38
|
+
term
|
|
39
|
+
});
|
|
40
|
+
idx = found + term.length;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Longest span first at equal start, so "tests" beats its substring "test" and the
|
|
44
|
+
// whole word gets highlighted; the emit loop then absorbs the shorter overlap.
|
|
45
|
+
return occ.sort((a, b)=>a.start - b.start || b.end - a.end);
|
|
46
|
+
}
|
|
47
|
+
// Slides a window over the sorted occurrences, same semantics as FTS5's own best-window
|
|
48
|
+
// pick: most distinct terms, ties broken by most total hits.
|
|
49
|
+
function bestWindowStart(occ, windowSize) {
|
|
50
|
+
let best = occ[0].start;
|
|
51
|
+
let bestDistinct = 0;
|
|
52
|
+
let bestCount = 0;
|
|
53
|
+
for(let i = 0; i < occ.length; i++){
|
|
54
|
+
const winEnd = occ[i].start + windowSize;
|
|
55
|
+
const seen = new Set();
|
|
56
|
+
let count = 0;
|
|
57
|
+
for(let j = i; j < occ.length && occ[j].start < winEnd; j++){
|
|
58
|
+
seen.add(occ[j].term);
|
|
59
|
+
count++;
|
|
60
|
+
}
|
|
61
|
+
if (seen.size > bestDistinct || seen.size === bestDistinct && count > bestCount) {
|
|
62
|
+
best = occ[i].start;
|
|
63
|
+
bestDistinct = seen.size;
|
|
64
|
+
bestCount = count;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return best;
|
|
68
|
+
}
|
|
69
|
+
// snippet()-shaped excerpt: matched terms wrapped in «», … at cut edges. Linear in doc
|
|
70
|
+
// length, only run for rows actually returned (<= k), unlike snippet()'s per-row cost.
|
|
71
|
+
function computeExcerpt(text, terms) {
|
|
72
|
+
const occ = findOccurrences(text.toLowerCase(), terms);
|
|
73
|
+
if (occ.length === 0) {
|
|
74
|
+
// This is a raw substring scan, not porter-stemmed: a doc matched only through a
|
|
75
|
+
// stemmed variant (query "negotiate" vs doc "negotiating") finds no occurrence here.
|
|
76
|
+
// Fall back to the doc's start, unmarked, rather than claim a match that isn't there.
|
|
77
|
+
const end = Math.min(text.length, EXCERPT_WINDOW);
|
|
78
|
+
return {
|
|
79
|
+
excerpt: `${text.slice(0, end).replace(/\s+/g, ' ').trim()}${end < text.length ? '…' : ''}`,
|
|
80
|
+
offset: 0
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const start = bestWindowStart(occ, EXCERPT_WINDOW);
|
|
84
|
+
const end = Math.min(text.length, start + EXCERPT_WINDOW);
|
|
85
|
+
let out = '';
|
|
86
|
+
let cursor = start;
|
|
87
|
+
for (const o of occ){
|
|
88
|
+
if (o.start < start || o.end > end) continue;
|
|
89
|
+
// Occurrences of duplicate or substring-overlapping terms ("test tests") can overlap;
|
|
90
|
+
// emitting each would duplicate document text. Keep the first, absorb the rest.
|
|
91
|
+
if (o.start < cursor) continue;
|
|
92
|
+
out += `${text.slice(cursor, o.start)}«${text.slice(o.start, o.end)}»`;
|
|
93
|
+
cursor = o.end;
|
|
94
|
+
}
|
|
95
|
+
out += text.slice(cursor, end);
|
|
96
|
+
const prefix = start > 0 ? '…' : '';
|
|
97
|
+
const suffix = end < text.length ? '…' : '';
|
|
98
|
+
return {
|
|
99
|
+
excerpt: `${prefix}${out.replace(/\s+/g, ' ')}${suffix}`,
|
|
100
|
+
offset: start
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function lineNumberAt(text, offset) {
|
|
104
|
+
let line = 1;
|
|
105
|
+
for(let i = 0; i < offset; i++)if (text.charCodeAt(i) === 10) line++;
|
|
106
|
+
return line;
|
|
107
|
+
}
|
|
108
|
+
function lineRangeFor(db, path, line) {
|
|
109
|
+
const row = db.prepare('SELECT start_line, end_line FROM sections WHERE "path" = ? AND start_line <= ? AND end_line >= ? ORDER BY start_line DESC LIMIT 1').get(path, line, line);
|
|
110
|
+
return row ? `L${row.start_line}-${row.end_line}` : null;
|
|
111
|
+
}
|
|
12
112
|
// Layer 1: BM25 + link-graph expansion, fused by reciprocal rank. `via` says which
|
|
13
113
|
// signal produced each row so the agent knows what evidence it is trusting.
|
|
14
114
|
// Semantic expansion is per-query opt-in: without opts.semantic the result is
|
|
@@ -27,7 +127,9 @@ export async function find(db, cfg, terms, opts = {}) {
|
|
|
27
127
|
const scope = (_opts_where = opts.where) !== null && _opts_where !== void 0 ? _opts_where : (_cfg_defaults = cfg.defaults) === null || _cfg_defaults === void 0 ? void 0 : (_cfg_defaults_find = _cfg_defaults.find) === null || _cfg_defaults_find === void 0 ? void 0 : _cfg_defaults_find.where;
|
|
28
128
|
const whereJoin = scope ? `JOIN frontmatter f ON f."path" = content.path` : '';
|
|
29
129
|
const whereCond = scope ? `AND (${scope})` : '';
|
|
30
|
-
|
|
130
|
+
// Docs past SNIPPET_BOUND skip snippet() entirely -- SQLite
|
|
131
|
+
// short-circuits the untaken CASE branch, so it's never invoked on them.
|
|
132
|
+
const matchSql = `SELECT content.path AS path, CASE WHEN length(content.text) <= ${SNIPPET_BOUND} THEN snippet(content, -1, '«', '»', '…', 10) ELSE NULL END AS hit FROM content ${whereJoin} WHERE content MATCH ? ${whereCond} ORDER BY ${WEIGHTED_BM25} LIMIT ${fetch}`;
|
|
31
133
|
let matchRows;
|
|
32
134
|
try {
|
|
33
135
|
matchRows = db.prepare(matchSql).all(terms);
|
|
@@ -38,6 +140,9 @@ export async function find(db, cfg, terms, opts = {}) {
|
|
|
38
140
|
r.path,
|
|
39
141
|
r.hit
|
|
40
142
|
]));
|
|
143
|
+
// hit === null here means the bound suppressed snippet(), not "no match" -- distinguish
|
|
144
|
+
// from via='link' rows (never in matchRows, so absent from this set) below.
|
|
145
|
+
const oversized = new Set(matchRows.filter((r)=>r.hit === null).map((r)=>r.path));
|
|
41
146
|
const candidates = new Map();
|
|
42
147
|
matchRows.forEach((r, i)=>{
|
|
43
148
|
candidates.set(r.path, {
|
|
@@ -99,10 +204,28 @@ export async function find(db, cfg, terms, opts = {}) {
|
|
|
99
204
|
const insert = db.prepare('INSERT INTO _find ("path", score, via, hit, lines, similarity) VALUES (?, ?, ?, ?, ?, ?)');
|
|
100
205
|
for (const [path, c] of candidates)insert.run(path, c.score, c.via, (_hits_get = hits.get(path)) !== null && _hits_get !== void 0 ? _hits_get : null, (_chunkLines_get = chunkLines.get(path)) !== null && _chunkLines_get !== void 0 ? _chunkLines_get : null, (_chunkSimilarity_get = chunkSimilarity.get(path)) !== null && _chunkSimilarity_get !== void 0 ? _chunkSimilarity_get : null);
|
|
101
206
|
const where = scope ? `WHERE ${scope}` : '';
|
|
102
|
-
|
|
103
|
-
|
|
207
|
+
// lines is always present now: semantic rows carry their chunk's range, oversized-doc
|
|
208
|
+
// lexical rows gain one below, everything else stays null. similarity stays semantic-only.
|
|
209
|
+
const similarityCol = opts.semantic ? ', _find.similarity' : '';
|
|
210
|
+
const rows = db.prepare(`SELECT f."path" AS path, content.title, content.summary, _find.hit, _find.via, round(_find.score, 4) AS score, _find.lines${similarityCol}
|
|
104
211
|
FROM _find JOIN frontmatter f ON f."path" = _find."path" JOIN content ON content.path = _find."path"
|
|
105
212
|
${where} ORDER BY _find.score DESC LIMIT ?`).all(k);
|
|
213
|
+
if (oversized.size > 0) {
|
|
214
|
+
const bareTerms = extractBareTerms(terms);
|
|
215
|
+
for (const row of rows){
|
|
216
|
+
if (row.hit !== null || !oversized.has(row.path)) continue;
|
|
217
|
+
let text;
|
|
218
|
+
try {
|
|
219
|
+
text = readFileSync(join(cfg.baseDir, row.path), 'utf8');
|
|
220
|
+
} catch {
|
|
221
|
+
continue; // vanished since the match; leave hit/lines null rather than throw
|
|
222
|
+
}
|
|
223
|
+
const { excerpt, offset } = computeExcerpt(text, bareTerms);
|
|
224
|
+
row.hit = excerpt;
|
|
225
|
+
if (row.lines == null) row.lines = featureEnabled(cfg, 'sections') ? lineRangeFor(db, row.path, lineNumberAt(text, offset)) : null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return rows;
|
|
106
229
|
}
|
|
107
230
|
const INTERNAL_COLUMNS = new Set([
|
|
108
231
|
'path',
|
|
@@ -110,22 +233,41 @@ const INTERNAL_COLUMNS = new Set([
|
|
|
110
233
|
'_size',
|
|
111
234
|
'_rank'
|
|
112
235
|
]);
|
|
236
|
+
// A result row is capped at SQLITE_MAX_COLUMN (2000, default); two aggregate expressions
|
|
237
|
+
// per field keeps a chunk's row safely under that regardless of how many fields the tree has.
|
|
238
|
+
const MAP_COLUMN_CHUNK = 300;
|
|
239
|
+
function chunk(items, size) {
|
|
240
|
+
const out = [];
|
|
241
|
+
for(let i = 0; i < items.length; i += size)out.push(items.slice(i, i + size));
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
113
244
|
// Layer 0: what is this tree. Fixed-size output regardless of tree size.
|
|
114
245
|
export function mapTree(db, cfg) {
|
|
115
246
|
const docs = db.prepare('SELECT COUNT(*) AS count, COALESCE(SUM("_size"), 0) AS bytes FROM frontmatter').get();
|
|
116
247
|
const columns = db.prepare('PRAGMA table_info(frontmatter)').all().map((r)=>r.name).filter((name)=>!INTERNAL_COLUMNS.has(name));
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
248
|
+
// Observed storage class, not a declared one: these columns are added dynamically and
|
|
249
|
+
// SQLite types per value, so a field can be text in most notes and numeric in a few.
|
|
250
|
+
// Listing every distinct type makes both the type and any drift visible.
|
|
251
|
+
// One aggregate scan per chunk of columns rather than one scan per column: COUNT already
|
|
252
|
+
// ignores NULL, and FILTER keeps typeof() looking only at the non-null rows COUNT counted,
|
|
253
|
+
// matching the old per-column `WHERE ... IS NOT NULL` exactly.
|
|
254
|
+
const allFields = [];
|
|
255
|
+
for (const group of chunk(columns, MAP_COLUMN_CHUNK)){
|
|
256
|
+
const exprs = group.map((name, i)=>{
|
|
257
|
+
const quoted = `"${name.split('"').join('""')}"`;
|
|
258
|
+
return `COUNT(${quoted}) AS n${i}, GROUP_CONCAT(DISTINCT typeof(${quoted})) FILTER (WHERE ${quoted} IS NOT NULL) AS t${i}`;
|
|
259
|
+
});
|
|
260
|
+
const result = db.prepare(`SELECT ${exprs.join(', ')} FROM frontmatter`).get();
|
|
261
|
+
group.forEach((name, i)=>{
|
|
262
|
+
var _result_;
|
|
263
|
+
return allFields.push({
|
|
264
|
+
field: name,
|
|
265
|
+
coverage: result[`n${i}`],
|
|
266
|
+
type: (_result_ = result[`t${i}`]) !== null && _result_ !== void 0 ? _result_ : ''
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
allFields.sort((a, b)=>b.coverage - a.coverage);
|
|
129
271
|
const fields = allFields.slice(0, 20);
|
|
130
272
|
const hubs = featureEnabled(cfg, 'rank') ? db.prepare(`SELECT f."path" AS path, round(f."_rank" * 100, 2) AS rank, content.title FROM frontmatter f JOIN content ON content.path = f."path" WHERE f."_rank" IS NOT NULL ORDER BY f."_rank" DESC LIMIT 8`).all() : [];
|
|
131
273
|
const recent = db.prepare(`SELECT "path", datetime("_mtime" / 1000, 'unixepoch') AS modified FROM frontmatter ORDER BY "_mtime" DESC LIMIT 5`).all();
|
|
@@ -138,7 +280,7 @@ export function mapTree(db, cfg) {
|
|
|
138
280
|
recent
|
|
139
281
|
};
|
|
140
282
|
}
|
|
141
|
-
const
|
|
283
|
+
const PEEK_LIST_LIMIT = 20;
|
|
142
284
|
// Layer 2: everything about one note except its prose -- frontmatter, outline with line
|
|
143
285
|
// ranges + token estimates (so the follow-up Read is a range, not the file), links both ways.
|
|
144
286
|
export function peek(db, cfg, pathArg) {
|
|
@@ -157,7 +299,8 @@ export function peek(db, cfg, pathArg) {
|
|
|
157
299
|
for (const [key, value] of Object.entries(row)){
|
|
158
300
|
if (!INTERNAL_COLUMNS.has(key) && value !== null) frontmatter[key] = value;
|
|
159
301
|
}
|
|
160
|
-
const
|
|
302
|
+
const sectionsTotal = featureEnabled(cfg, 'sections') ? db.prepare('SELECT COUNT(*) AS n FROM sections WHERE "path" = ?').get(path).n : 0;
|
|
303
|
+
const sections = featureEnabled(cfg, 'sections') ? db.prepare('SELECT level, heading, start_line, end_line, tokens FROM sections WHERE "path" = ? ORDER BY idx LIMIT ?').all(path, PEEK_LIST_LIMIT) : [];
|
|
161
304
|
let outbound = [];
|
|
162
305
|
let backlinks = [];
|
|
163
306
|
let unresolved = [];
|
|
@@ -169,16 +312,17 @@ export function peek(db, cfg, pathArg) {
|
|
|
169
312
|
];
|
|
170
313
|
unresolved = out.filter((l)=>l.dst === null).map((l)=>l.target);
|
|
171
314
|
backlinksTotal = db.prepare('SELECT COUNT(DISTINCT src) AS n FROM links WHERE dst = ?').get(path).n;
|
|
172
|
-
backlinks = db.prepare('SELECT DISTINCT src FROM links WHERE dst = ? ORDER BY src LIMIT ?').all(path,
|
|
315
|
+
backlinks = db.prepare('SELECT DISTINCT src FROM links WHERE dst = ? ORDER BY src LIMIT ?').all(path, PEEK_LIST_LIMIT).map((r)=>r.src);
|
|
173
316
|
}
|
|
174
317
|
return {
|
|
175
318
|
path,
|
|
176
319
|
tokens: Math.ceil(((_row__size = row._size) !== null && _row__size !== void 0 ? _row__size : 0) / 4),
|
|
177
320
|
frontmatter,
|
|
178
321
|
sections,
|
|
179
|
-
outbound: outbound.slice(0,
|
|
322
|
+
outbound: outbound.slice(0, PEEK_LIST_LIMIT),
|
|
180
323
|
backlinks,
|
|
181
|
-
unresolved: unresolved.slice(0,
|
|
324
|
+
unresolved: unresolved.slice(0, PEEK_LIST_LIMIT),
|
|
325
|
+
sectionsTotal,
|
|
182
326
|
outboundTotal: outbound.length,
|
|
183
327
|
backlinksTotal,
|
|
184
328
|
unresolvedTotal: unresolved.length,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/commands.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport posix from 'node:path/posix';\nimport type { DatabaseSync } from 'node:sqlite';\nimport type { FeatureName, ResolvedConfig } from './config.ts';\nimport { featureEnabled, featureStates } from './config.ts';\nimport { SenseError } from './errors.ts';\nimport { semanticCandidates } from './features/embed.ts';\nimport { linkEdges } from './features/index.ts';\nimport { personalizedRank } from './graph.ts';\nimport type { Row } from './output.ts';\nimport { searchError } from './search-error.ts';\n\n// The three layer commands: mapTree (orient), find (locate), peek (structure).\n// Each returns data; cli.ts renders. All of them degrade when a feature is off.\n\nconst WEIGHTED_BM25 = 'bm25(content, 10.0, 5.0, 1.0)';\nconst RRF_K = 60;\n\n// FTS5 snippet() re-tokenizes the whole doc per candidate row, superlinearly: measured\n// 2ms at 4KB docs but 189ms at 64KB for one query's pool, and ~10s for a single 1MB doc\n// (plans/performance-findings.md finding A). 16KB keeps the whole pool's snippet cost in\n// the low-ms range; bigger docs get the JS-computed excerpt, linear and only for the rows\n// actually returned.\nconst SNIPPET_BOUND = 16_384;\nconst EXCERPT_WINDOW = 160;\n\n// Bare terms from an FTS5 query string: strips operators/quoting so the oversized-doc excerpt\n// scan matches the same words the query matched on, not FTS5 syntax.\nfunction extractBareTerms(query: string): string[] {\n const cleaned = query\n .replace(/\"/g, ' ')\n .replace(/[()*]/g, ' ')\n .replace(/\\b(AND|OR|NOT|NEAR)\\b(\\/\\d+)?/gi, ' ');\n return cleaned\n .split(/\\s+/)\n .map((tok) => tok.replace(/^[A-Za-z_]\\w*:/, '')) // column filter, e.g. title:term\n .map((tok) => tok.toLowerCase().trim())\n .filter((tok) => tok.length > 0);\n}\n\nfunction findOccurrences(haystackLower: string, terms: string[]): Array<{ start: number; end: number; term: string }> {\n const occ: Array<{ start: number; end: number; term: string }> = [];\n for (const term of terms) {\n let idx = 0;\n for (;;) {\n const found = haystackLower.indexOf(term, idx);\n if (found === -1) break;\n occ.push({ start: found, end: found + term.length, term });\n idx = found + term.length;\n }\n }\n // Longest span first at equal start, so \"tests\" beats its substring \"test\" and the\n // whole word gets highlighted; the emit loop then absorbs the shorter overlap.\n return occ.sort((a, b) => a.start - b.start || b.end - a.end);\n}\n\n// Slides a window over the sorted occurrences, same semantics as FTS5's own best-window\n// pick: most distinct terms, ties broken by most total hits.\nfunction bestWindowStart(occ: Array<{ start: number; end: number; term: string }>, windowSize: number): number {\n let best = occ[0].start;\n let bestDistinct = 0;\n let bestCount = 0;\n for (let i = 0; i < occ.length; i++) {\n const winEnd = occ[i].start + windowSize;\n const seen = new Set<string>();\n let count = 0;\n for (let j = i; j < occ.length && occ[j].start < winEnd; j++) {\n seen.add(occ[j].term);\n count++;\n }\n if (seen.size > bestDistinct || (seen.size === bestDistinct && count > bestCount)) {\n best = occ[i].start;\n bestDistinct = seen.size;\n bestCount = count;\n }\n }\n return best;\n}\n\n// snippet()-shaped excerpt: matched terms wrapped in «», … at cut edges. Linear in doc\n// length, only run for rows actually returned (<= k), unlike snippet()'s per-row cost.\nfunction computeExcerpt(text: string, terms: string[]): { excerpt: string; offset: number } {\n const occ = findOccurrences(text.toLowerCase(), terms);\n if (occ.length === 0) {\n // This is a raw substring scan, not porter-stemmed: a doc matched only through a\n // stemmed variant (query \"negotiate\" vs doc \"negotiating\") finds no occurrence here.\n // Fall back to the doc's start, unmarked, rather than claim a match that isn't there.\n const end = Math.min(text.length, EXCERPT_WINDOW);\n return { excerpt: `${text.slice(0, end).replace(/\\s+/g, ' ').trim()}${end < text.length ? '…' : ''}`, offset: 0 };\n }\n const start = bestWindowStart(occ, EXCERPT_WINDOW);\n const end = Math.min(text.length, start + EXCERPT_WINDOW);\n let out = '';\n let cursor = start;\n for (const o of occ) {\n if (o.start < start || o.end > end) continue;\n // Occurrences of duplicate or substring-overlapping terms (\"test tests\") can overlap;\n // emitting each would duplicate document text. Keep the first, absorb the rest.\n if (o.start < cursor) continue;\n out += `${text.slice(cursor, o.start)}«${text.slice(o.start, o.end)}»`;\n cursor = o.end;\n }\n out += text.slice(cursor, end);\n const prefix = start > 0 ? '…' : '';\n const suffix = end < text.length ? '…' : '';\n return { excerpt: `${prefix}${out.replace(/\\s+/g, ' ')}${suffix}`, offset: start };\n}\n\nfunction lineNumberAt(text: string, offset: number): number {\n let line = 1;\n for (let i = 0; i < offset; i++) if (text.charCodeAt(i) === 10) line++;\n return line;\n}\n\nfunction lineRangeFor(db: DatabaseSync, path: string, line: number): string | null {\n const row = db.prepare('SELECT start_line, end_line FROM sections WHERE \"path\" = ? AND start_line <= ? AND end_line >= ? ORDER BY start_line DESC LIMIT 1').get(path, line, line) as { start_line: number; end_line: number } | undefined;\n return row ? `L${row.start_line}-${row.end_line}` : null;\n}\n\nexport interface FindOptions {\n k?: number;\n where?: string; // SQL fragment against frontmatter alias `f`, e.g. \"f.status = 'active'\"\n semantic?: boolean; // invoke vector expansion (requires features.embed); rows gain via 'vector' and a lines column\n}\n\n// Layer 1: BM25 + link-graph expansion, fused by reciprocal rank. `via` says which\n// signal produced each row so the agent knows what evidence it is trusting.\n// Semantic expansion is per-query opt-in: without opts.semantic the result is\n// byte-for-byte independent of the embed feature.\nexport async function find(db: DatabaseSync, cfg: ResolvedConfig, terms: string, opts: FindOptions = {}): Promise<Row[]> {\n const k = opts.k ?? 10;\n const fetch = Math.max(k * 3, 30);\n\n // Terms pass verbatim to FTS5 MATCH: bare words AND-join, operators are the caller's.\n // Invalid syntax propagates as an error, zero matches return zero -- no silent rewrites.\n // --where applies inside the candidate query (a post-filter over the top-N would drop\n // matches ranked past the pool) and again on the final select for link-derived rows.\n // An explicit --where replaces the tree's declared default rather than ANDing with it,\n // so a caller can always widen back to the whole tree.\n const scope = opts.where ?? cfg.defaults?.find?.where;\n const whereJoin = scope ? `JOIN frontmatter f ON f.\"path\" = content.path` : '';\n const whereCond = scope ? `AND (${scope})` : '';\n // Docs past SNIPPET_BOUND skip snippet() entirely -- SQLite\n // short-circuits the untaken CASE branch, so it's never invoked on them.\n const matchSql = `SELECT content.path AS path, CASE WHEN length(content.text) <= ${SNIPPET_BOUND} THEN snippet(content, -1, '«', '»', '…', 10) ELSE NULL END AS hit FROM content ${whereJoin} WHERE content MATCH ? ${whereCond} ORDER BY ${WEIGHTED_BM25} LIMIT ${fetch}`;\n let matchRows: Array<{ path: string; hit: string | null }>;\n try {\n matchRows = db.prepare(matchSql).all(terms) as Array<{ path: string; hit: string | null }>;\n } catch (err) {\n throw searchError(err as Error, terms, scope);\n }\n\n const hits = new Map(matchRows.map((r) => [r.path, r.hit]));\n // hit === null here means the bound suppressed snippet(), not \"no match\" -- distinguish\n // from via='link' rows (never in matchRows, so absent from this set) below.\n const oversized = new Set(matchRows.filter((r) => r.hit === null).map((r) => r.path));\n const candidates = new Map<string, { score: number; via: string }>();\n matchRows.forEach((r, i) => {\n candidates.set(r.path, { score: 1 / (RRF_K + i), via: 'match' });\n });\n\n const edges = featureEnabled(cfg, 'links') && matchRows.length > 0 ? linkEdges(db) : [];\n if (edges.length > 0) {\n // `linked` gates the label only, not the score: PPR restart mass gives every seed a\n // nonzero rank even without an incident edge, which is not link evidence — but dropping\n // that mass from the score list reweights fusion toward connectivity and measurably\n // wrecks ranking on link-dense corpora (FEVER hit@10 0.997 -> 0.907; fusion-tuning.md).\n const linked = new Set(edges.flat());\n const nodes = (db.prepare('SELECT \"path\" FROM frontmatter').all() as Array<{ path: string }>).map((r) => r.path);\n const seeds = new Map(matchRows.map((r, i) => [r.path, 1 / (i + 1)]));\n const ranked = [...personalizedRank(nodes, edges, seeds)]\n .filter(([, score]) => score > 1e-9)\n .sort((a, b) => b[1] - a[1])\n .slice(0, fetch);\n ranked.forEach(([path], i) => {\n const existing = candidates.get(path);\n if (existing) {\n existing.score += 1 / (RRF_K + i);\n if (linked.has(path)) existing.via = 'match+link';\n } else {\n candidates.set(path, { score: 1 / (RRF_K + i), via: 'link' });\n }\n });\n }\n\n // Vector expansion, invoked only: a third RRF list at the swept flat-region constants\n // (weight 1, pool = fetch). Each row carries its best chunk's line range.\n const chunkLines = new Map<string, string>();\n const chunkSimilarity = new Map<string, number>();\n if (opts.semantic) {\n const vec = await semanticCandidates(db, cfg, terms, fetch);\n vec.forEach(({ path, lines, similarity }, i) => {\n chunkLines.set(path, lines);\n chunkSimilarity.set(path, similarity);\n const existing = candidates.get(path);\n if (existing) {\n existing.score += 1 / (RRF_K + i);\n existing.via = `${existing.via}+vector`;\n } else {\n candidates.set(path, { score: 1 / (RRF_K + i), via: 'vector' });\n }\n });\n }\n\n db.exec('DROP TABLE IF EXISTS _find');\n db.exec('CREATE TEMP TABLE _find (\"path\" TEXT PRIMARY KEY, score REAL, via TEXT, hit TEXT, lines TEXT, similarity REAL)');\n const insert = db.prepare('INSERT INTO _find (\"path\", score, via, hit, lines, similarity) VALUES (?, ?, ?, ?, ?, ?)');\n for (const [path, c] of candidates) insert.run(path, c.score, c.via, hits.get(path) ?? null, chunkLines.get(path) ?? null, chunkSimilarity.get(path) ?? null);\n\n const where = scope ? `WHERE ${scope}` : '';\n // lines is always present now: semantic rows carry their chunk's range, oversized-doc\n // lexical rows gain one below, everything else stays null. similarity stays semantic-only.\n const similarityCol = opts.semantic ? ', _find.similarity' : '';\n const rows = db\n .prepare(\n `SELECT f.\"path\" AS path, content.title, content.summary, _find.hit, _find.via, round(_find.score, 4) AS score, _find.lines${similarityCol}\n FROM _find JOIN frontmatter f ON f.\"path\" = _find.\"path\" JOIN content ON content.path = _find.\"path\"\n ${where} ORDER BY _find.score DESC LIMIT ?`\n )\n .all(k) as Row[];\n\n if (oversized.size > 0) {\n const bareTerms = extractBareTerms(terms);\n for (const row of rows) {\n if (row.hit !== null || !oversized.has(row.path as string)) continue;\n let text: string;\n try {\n text = readFileSync(join(cfg.baseDir, row.path as string), 'utf8');\n } catch {\n continue; // vanished since the match; leave hit/lines null rather than throw\n }\n const { excerpt, offset } = computeExcerpt(text, bareTerms);\n row.hit = excerpt;\n if (row.lines == null) row.lines = featureEnabled(cfg, 'sections') ? lineRangeFor(db, row.path as string, lineNumberAt(text, offset)) : null;\n }\n }\n\n return rows;\n}\n\nexport interface TreeMap {\n docs: { count: number; bytes: number };\n fields: Row[]; // top 20 by coverage; fieldsTotal carries the real count\n fieldsTotal: number;\n features: { on: FeatureName[]; off: FeatureName[] };\n hubs: Row[];\n recent: Row[];\n}\n\nconst INTERNAL_COLUMNS = new Set(['path', '_mtime', '_size', '_rank']);\n\n// A result row is capped at SQLITE_MAX_COLUMN (2000, default); two aggregate expressions\n// per field keeps a chunk's row safely under that regardless of how many fields the tree has.\nconst MAP_COLUMN_CHUNK = 300;\n\nfunction chunk<T>(items: T[], size: number): T[][] {\n const out: T[][] = [];\n for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));\n return out;\n}\n\n// Layer 0: what is this tree. Fixed-size output regardless of tree size.\nexport function mapTree(db: DatabaseSync, cfg: ResolvedConfig): TreeMap {\n const docs = db.prepare('SELECT COUNT(*) AS count, COALESCE(SUM(\"_size\"), 0) AS bytes FROM frontmatter').get() as { count: number; bytes: number };\n\n const columns = (db.prepare('PRAGMA table_info(frontmatter)').all() as Array<{ name: string }>).map((r) => r.name).filter((name) => !INTERNAL_COLUMNS.has(name));\n // Observed storage class, not a declared one: these columns are added dynamically and\n // SQLite types per value, so a field can be text in most notes and numeric in a few.\n // Listing every distinct type makes both the type and any drift visible.\n // One aggregate scan per chunk of columns rather than one scan per column: COUNT already\n // ignores NULL, and FILTER keeps typeof() looking only at the non-null rows COUNT counted,\n // matching the old per-column `WHERE ... IS NOT NULL` exactly.\n const allFields: Row[] = [];\n for (const group of chunk(columns, MAP_COLUMN_CHUNK)) {\n const exprs = group.map((name, i) => {\n const quoted = `\"${name.split('\"').join('\"\"')}\"`;\n return `COUNT(${quoted}) AS n${i}, GROUP_CONCAT(DISTINCT typeof(${quoted})) FILTER (WHERE ${quoted} IS NOT NULL) AS t${i}`;\n });\n const result = db.prepare(`SELECT ${exprs.join(', ')} FROM frontmatter`).get() as Record<string, number | string | null>;\n group.forEach((name, i) => allFields.push({ field: name, coverage: result[`n${i}`] as number, type: (result[`t${i}`] as string) ?? '' }));\n }\n allFields.sort((a, b) => (b.coverage as number) - (a.coverage as number));\n const fields = allFields.slice(0, 20);\n\n const hubs = featureEnabled(cfg, 'rank') ? (db.prepare(`SELECT f.\"path\" AS path, round(f.\"_rank\" * 100, 2) AS rank, content.title FROM frontmatter f JOIN content ON content.path = f.\"path\" WHERE f.\"_rank\" IS NOT NULL ORDER BY f.\"_rank\" DESC LIMIT 8`).all() as Row[]) : [];\n\n const recent = db.prepare(`SELECT \"path\", datetime(\"_mtime\" / 1000, 'unixepoch') AS modified FROM frontmatter ORDER BY \"_mtime\" DESC LIMIT 5`).all() as Row[];\n\n return { docs, fields, fieldsTotal: allFields.length, features: featureStates(cfg), hubs, recent };\n}\n\nexport interface Peek {\n path: string;\n tokens: number;\n frontmatter: Row;\n sections: Row[];\n outbound: string[];\n backlinks: string[];\n unresolved: string[];\n // Totals before truncation: a hub can have thousands of backlinks (or a note thousands of\n // headings), and peek's whole point is bounded output. Query the sections/links tables\n // directly for the full list.\n sectionsTotal: number;\n outboundTotal: number;\n backlinksTotal: number;\n unresolvedTotal: number;\n off: FeatureName[]; // disabled features whose blocks are omitted (not empty)\n}\n\nconst PEEK_LIST_LIMIT = 20;\n\n// Layer 2: everything about one note except its prose -- frontmatter, outline with line\n// ranges + token estimates (so the follow-up Read is a range, not the file), links both ways.\nexport function peek(db: DatabaseSync, cfg: ResolvedConfig, pathArg: string): Peek {\n const paths = (db.prepare('SELECT \"path\" FROM frontmatter').all() as Array<{ path: string }>).map((r) => r.path);\n let path = paths.find((p) => p === pathArg);\n if (!path) {\n const base = posix.basename(pathArg).replace(/\\.md$/i, '').toLowerCase();\n const matches = paths.filter((p) => posix.basename(p).replace(/\\.md$/i, '').toLowerCase() === base);\n if (matches.length === 1) path = matches[0];\n else if (matches.length > 1) throw new SenseError('NOTE_AMBIGUOUS', `\"${pathArg}\" is ambiguous: ${matches.join(', ')}`);\n else throw new SenseError('NOTE_NOT_FOUND', `no note matches \"${pathArg}\"`);\n }\n\n const row = db.prepare('SELECT * FROM frontmatter WHERE \"path\" = ?').get(path) as Row;\n const frontmatter: Row = {};\n for (const [key, value] of Object.entries(row)) {\n if (!INTERNAL_COLUMNS.has(key) && value !== null) frontmatter[key] = value;\n }\n\n const sectionsTotal = featureEnabled(cfg, 'sections') ? (db.prepare('SELECT COUNT(*) AS n FROM sections WHERE \"path\" = ?').get(path) as { n: number }).n : 0;\n const sections = featureEnabled(cfg, 'sections') ? (db.prepare('SELECT level, heading, start_line, end_line, tokens FROM sections WHERE \"path\" = ? ORDER BY idx LIMIT ?').all(path, PEEK_LIST_LIMIT) as Row[]) : [];\n\n let outbound: string[] = [];\n let backlinks: string[] = [];\n let unresolved: string[] = [];\n let backlinksTotal = 0;\n if (featureEnabled(cfg, 'links')) {\n const out = db.prepare('SELECT target, dst FROM links WHERE src = ? ORDER BY target').all(path) as Array<{ target: string; dst: string | null }>;\n outbound = [...new Set(out.filter((l) => l.dst !== null).map((l) => l.dst as string))];\n unresolved = out.filter((l) => l.dst === null).map((l) => l.target);\n backlinksTotal = (db.prepare('SELECT COUNT(DISTINCT src) AS n FROM links WHERE dst = ?').get(path) as { n: number }).n;\n backlinks = (db.prepare('SELECT DISTINCT src FROM links WHERE dst = ? ORDER BY src LIMIT ?').all(path, PEEK_LIST_LIMIT) as Array<{ src: string }>).map((r) => r.src);\n }\n\n return {\n path,\n tokens: Math.ceil(((row._size as number) ?? 0) / 4),\n frontmatter,\n sections,\n outbound: outbound.slice(0, PEEK_LIST_LIMIT),\n backlinks,\n unresolved: unresolved.slice(0, PEEK_LIST_LIMIT),\n sectionsTotal,\n outboundTotal: outbound.length,\n backlinksTotal,\n unresolvedTotal: unresolved.length,\n off: (['sections', 'links'] as FeatureName[]).filter((name) => !featureEnabled(cfg, name)),\n };\n}\n"],"names":["readFileSync","join","posix","featureEnabled","featureStates","SenseError","semanticCandidates","linkEdges","personalizedRank","searchError","WEIGHTED_BM25","RRF_K","SNIPPET_BOUND","EXCERPT_WINDOW","extractBareTerms","query","cleaned","replace","split","map","tok","toLowerCase","trim","filter","length","findOccurrences","haystackLower","terms","occ","term","idx","found","indexOf","push","start","end","sort","a","b","bestWindowStart","windowSize","best","bestDistinct","bestCount","i","winEnd","seen","Set","count","j","add","size","computeExcerpt","text","Math","min","excerpt","slice","offset","out","cursor","o","prefix","suffix","lineNumberAt","line","charCodeAt","lineRangeFor","db","path","row","prepare","get","start_line","end_line","find","cfg","opts","hits","chunkLines","chunkSimilarity","k","fetch","max","scope","where","defaults","whereJoin","whereCond","matchSql","matchRows","all","err","Map","r","hit","oversized","candidates","forEach","set","score","via","edges","linked","flat","nodes","seeds","ranked","existing","has","semantic","vec","lines","similarity","exec","insert","c","run","similarityCol","rows","bareTerms","baseDir","INTERNAL_COLUMNS","MAP_COLUMN_CHUNK","chunk","items","mapTree","docs","columns","name","allFields","group","exprs","quoted","result","field","coverage","type","fields","hubs","recent","fieldsTotal","features","PEEK_LIST_LIMIT","peek","pathArg","paths","p","base","basename","matches","frontmatter","key","value","Object","entries","sectionsTotal","n","sections","outbound","backlinks","unresolved","backlinksTotal","l","dst","target","src","tokens","ceil","_size","outboundTotal","unresolvedTotal","off"],"mappings":"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"}
|
package/dist/esm/config.d.ts
CHANGED
|
@@ -10,6 +10,12 @@ export interface EmbedConfig {
|
|
|
10
10
|
key?: string;
|
|
11
11
|
}
|
|
12
12
|
export declare const DEFAULT_EMBED_MODEL = "minishlab/potion-retrieval-32M";
|
|
13
|
+
export interface SavedFind {
|
|
14
|
+
find: string;
|
|
15
|
+
k?: number;
|
|
16
|
+
where?: string;
|
|
17
|
+
semantic?: boolean;
|
|
18
|
+
}
|
|
13
19
|
export interface Config {
|
|
14
20
|
$schema?: string;
|
|
15
21
|
version?: number;
|
|
@@ -28,7 +34,7 @@ export interface Config {
|
|
|
28
34
|
};
|
|
29
35
|
};
|
|
30
36
|
checks?: Record<string, 'empty'>;
|
|
31
|
-
queries: Record<string, string>;
|
|
37
|
+
queries: Record<string, string | SavedFind>;
|
|
32
38
|
}
|
|
33
39
|
export interface ResolvedConfig extends Config {
|
|
34
40
|
baseDir: string;
|
package/dist/esm/config.js
CHANGED
|
@@ -5,7 +5,7 @@ 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
7
|
export const SUPPORTED_CONFIG_VERSION = 2;
|
|
8
|
-
// Each feature owns its tables, parse-time extraction, and reconcile step;
|
|
8
|
+
// Each feature owns its tables, parse-time extraction, and reconcile step; commands degrade when one is off.
|
|
9
9
|
// links/sections/rank are opt-out (absent = on); embed is opt-in (absent = off) -- most trees don't need vectors.
|
|
10
10
|
const FEATURE_NAMES = [
|
|
11
11
|
'links',
|
|
@@ -31,7 +31,7 @@ export function featureEnabled(cfg, name) {
|
|
|
31
31
|
export function enabledFeatures(cfg) {
|
|
32
32
|
return FEATURE_NAMES.filter((name)=>featureEnabled(cfg, name));
|
|
33
33
|
}
|
|
34
|
-
// Every feature with its current state;
|
|
34
|
+
// Every feature with its current state; commands surface this so "off" and "empty" stay
|
|
35
35
|
// distinguishable in output.
|
|
36
36
|
export function featureStates(cfg) {
|
|
37
37
|
return {
|
|
@@ -154,6 +154,35 @@ function unknownConfigKeys(cfg) {
|
|
|
154
154
|
}
|
|
155
155
|
return unknown;
|
|
156
156
|
}
|
|
157
|
+
const SAVED_FIND_KEYS = new Set([
|
|
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) {
|
|
165
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
166
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} must be a SQL string or { find, k?, where?, semantic? }`);
|
|
167
|
+
}
|
|
168
|
+
const entry = value;
|
|
169
|
+
const unknown = Object.keys(entry).filter((k)=>!SAVED_FIND_KEYS.has(k));
|
|
170
|
+
if (unknown.length > 0) {
|
|
171
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} has unknown key(s) ${unknown.join(', ')}; a saved find takes find, k, where, semantic`);
|
|
172
|
+
}
|
|
173
|
+
if (typeof entry.find !== 'string') {
|
|
174
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.find must be a string`);
|
|
175
|
+
}
|
|
176
|
+
if (entry.k !== undefined && (typeof entry.k !== 'number' || !Number.isInteger(entry.k) || entry.k <= 0)) {
|
|
177
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.k must be a positive integer`);
|
|
178
|
+
}
|
|
179
|
+
if (entry.where !== undefined && typeof entry.where !== 'string') {
|
|
180
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.where must be a SQL condition string`);
|
|
181
|
+
}
|
|
182
|
+
if (entry.semantic !== undefined && typeof entry.semantic !== 'boolean') {
|
|
183
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.semantic must be a boolean`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
157
186
|
function validateConfig(parsed, configPath) {
|
|
158
187
|
var _cfg_features;
|
|
159
188
|
var _defaults_find;
|
|
@@ -166,8 +195,12 @@ function validateConfig(parsed, configPath) {
|
|
|
166
195
|
throw new SenseError('CONFIG_INVALID', `${configPath}: scan.include must be a non-empty array of glob strings`);
|
|
167
196
|
}
|
|
168
197
|
if (cfg.queries === undefined) cfg.queries = {};
|
|
169
|
-
if (typeof cfg.queries !== 'object' || cfg.queries === null || Array.isArray(cfg.queries)
|
|
170
|
-
throw new SenseError('CONFIG_INVALID', `${configPath}: queries must be an object of name -> SQL string`);
|
|
198
|
+
if (typeof cfg.queries !== 'object' || cfg.queries === null || Array.isArray(cfg.queries)) {
|
|
199
|
+
throw new SenseError('CONFIG_INVALID', `${configPath}: queries must be an object of name -> SQL string or saved find`);
|
|
200
|
+
}
|
|
201
|
+
for (const [name, value] of Object.entries(cfg.queries)){
|
|
202
|
+
if (typeof value === 'string') continue;
|
|
203
|
+
validateSavedFind(name, value, configPath);
|
|
171
204
|
}
|
|
172
205
|
const defaults = cfg.defaults;
|
|
173
206
|
if (defaults !== undefined && (typeof 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')) {
|
|
@@ -182,6 +215,7 @@ function validateConfig(parsed, configPath) {
|
|
|
182
215
|
const queries = (_cfg_queries = cfg.queries) !== null && _cfg_queries !== void 0 ? _cfg_queries : {};
|
|
183
216
|
for (const name of Object.keys(checks)){
|
|
184
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`);
|
|
185
219
|
}
|
|
186
220
|
}
|
|
187
221
|
if (cfg.features !== undefined && (typeof cfg.features !== 'object' || cfg.features === null || Array.isArray(cfg.features))) {
|