sensemaking 0.7.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -61
- package/dist/cjs/cli/check.js +367 -0
- package/dist/cjs/cli/check.js.map +1 -0
- package/dist/cjs/{commands → cli}/index.js +2 -2
- 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 +219 -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/cli/search.d.cts +3 -0
- package/dist/cjs/cli/search.d.ts +3 -0
- package/dist/cjs/{commands/find.js → cli/search.js} +9 -8
- package/dist/cjs/cli/search.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 +36 -13
- package/dist/cjs/cli/status.js.map +1 -0
- package/dist/cjs/{commands → cli}/types.d.cts +3 -1
- package/dist/{esm/commands → cjs/cli}/types.d.ts +3 -1
- package/dist/cjs/cli/watch.js.map +1 -0
- package/dist/cjs/cli.js +42 -17
- package/dist/cjs/cli.js.map +1 -1
- package/dist/{esm/verbs.d.ts → cjs/commands.d.cts} +15 -5
- package/dist/cjs/{verbs.d.cts → commands.d.ts} +15 -5
- package/dist/cjs/{verbs.js → commands.js} +329 -39
- package/dist/cjs/commands.js.map +1 -0
- package/dist/cjs/config.d.cts +45 -12
- package/dist/cjs/config.d.ts +45 -12
- package/dist/cjs/config.js +464 -125
- 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 +293 -76
- 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 +19 -3
- 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 +12 -4
- package/dist/cjs/features/types.d.ts +12 -4
- package/dist/cjs/index.d.cts +3 -3
- package/dist/cjs/index.d.ts +3 -3
- package/dist/cjs/index.js +9 -6
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/output.d.cts +16 -0
- package/dist/cjs/output.d.ts +16 -0
- package/dist/cjs/output.js +27 -5
- 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 +4 -0
- package/dist/cjs/scan.d.ts +4 -0
- package/dist/cjs/scan.js +87 -6
- package/dist/cjs/scan.js.map +1 -1
- package/dist/esm/{commands → cli}/check.js +41 -22
- package/dist/esm/cli/check.js.map +1 -0
- package/dist/esm/{commands → cli}/index.js +1 -1
- 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 +41 -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/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/{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 +17 -11
- package/dist/esm/cli/status.js.map +1 -0
- package/dist/{cjs/commands → esm/cli}/types.d.ts +3 -1
- package/dist/esm/cli/types.js.map +1 -0
- package/dist/esm/cli/watch.js.map +1 -0
- package/dist/esm/cli.js +24 -6
- package/dist/esm/cli.js.map +1 -1
- package/dist/{cjs/verbs.d.ts → esm/commands.d.ts} +15 -5
- package/dist/esm/commands.js +386 -0
- package/dist/esm/commands.js.map +1 -0
- package/dist/esm/config.d.ts +45 -12
- package/dist/esm/config.js +367 -93
- package/dist/esm/config.js.map +1 -1
- package/dist/esm/db.d.ts +1 -1
- package/dist/esm/db.js +126 -17
- 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 +18 -2
- 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 +12 -4
- 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 +16 -0
- package/dist/esm/output.js +20 -5
- 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 +4 -0
- package/dist/esm/scan.js +37 -6
- package/dist/esm/scan.js.map +1 -1
- package/package.json +12 -2
- package/schema.json +85 -50
- package/skills/sense/EXAMPLES.md +16 -12
- package/skills/sense/SKILL.md +74 -55
- package/skills/sense-setup/EXAMPLES.md +121 -0
- package/skills/sense-setup/SKILL.md +73 -54
- package/dist/cjs/commands/check.js +0 -143
- package/dist/cjs/commands/check.js.map +0 -1
- package/dist/cjs/commands/find.d.cts +0 -3
- package/dist/cjs/commands/find.d.ts +0 -3
- 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.d.ts +0 -3
- package/dist/esm/commands/find.js +0 -15
- 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 +0 -190
- 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}/index.d.cts +0 -0
- /package/dist/cjs/{commands → cli}/index.d.ts +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.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}/index.d.ts +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.js +0 -0
- /package/dist/esm/{commands → cli}/watch.d.ts +0 -0
- /package/dist/esm/{commands → cli}/watch.js +0 -0
package/dist/esm/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/config.ts"],"sourcesContent":["import { existsSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { SenseError } from './errors.ts';\n\nexport const CONFIG_FILENAME = 'sense.config.json';\nexport const STATE_DIR = '.sense';\n\n// Highest sense.config.json `version` this build understands. Older versions auto-migrate on load.\nexport const SUPPORTED_CONFIG_VERSION = 2;\n\n// Each feature owns its tables, parse-time extraction, and reconcile step; verbs degrade when one is off.\n// links/sections/rank are opt-out (absent = on); embed is opt-in (absent = off) -- most trees don't need vectors.\nconst FEATURE_NAMES = ['links', 'sections', 'rank', 'embed'] as const;\nconst OPT_OUT_NAMES = ['links', 'sections', 'rank'] as const;\nexport type FeatureName = (typeof FEATURE_NAMES)[number];\n\n// `true` = all defaults (static type, default model). A local `model` path skips the network.\nexport interface EmbedConfig {\n model?: string;\n type?: 'static' | 'api';\n url?: string; // api type: OpenAI-compatible base URL, e.g. http://localhost:11434/v1\n key?: string; // api type: name of the env var holding the bearer token, if any\n}\n\nexport const DEFAULT_EMBED_MODEL = 'minishlab/potion-retrieval-32M';\n\nexport interface Config {\n // Editor-only pointer to schema.json; never read by sense.\n $schema?: string;\n version?: number;\n scan: { include: string[] };\n features?: { links?: boolean; sections?: boolean; rank?: boolean; embed?: boolean | EmbedConfig };\n // Tree-declared default scope for `find`. A --where on the invocation replaces it, so a\n // caller can always reach the whole tree (`--where \"1=1\"`).\n defaults?: { find?: { where?: string } };\n // Assertions over saved queries: `checks: { \"dead-links\": \"empty\" }` makes `sense check`\n // fail when that query returns rows -- for invariant queries where 0 rows is the pass.\n checks?: Record<string, 'empty'>;\n queries: Record<string, string>;\n}\n\nexport interface ResolvedConfig extends Config {\n baseDir: string;\n configPath: string | null;\n // Keys the file declares that this build does not read; cli reports them.\n unknownKeys?: string[];\n // Set when loadConfig auto-migrated the file on disk; cli reports it.\n migratedFrom?: number;\n}\n\n// Opt-out features: absent block or key means enabled. `rank` additionally requires `links`.\n// `embed` is the opposite: enabled only when the config says so.\nexport function featureEnabled(cfg: Config, name: FeatureName): boolean {\n if (name === 'embed') return Boolean(cfg.features?.embed);\n const enabled = cfg.features?.[name] !== false;\n if (name === 'rank') return enabled && featureEnabled(cfg, 'links');\n return enabled;\n}\n\nexport function enabledFeatures(cfg: Config): FeatureName[] {\n return FEATURE_NAMES.filter((name) => featureEnabled(cfg, name));\n}\n\n// Every feature with its current state; verbs surface this so \"off\" and \"empty\" stay\n// distinguishable in output.\nexport function featureStates(cfg: Config): { on: FeatureName[]; off: FeatureName[] } {\n return {\n on: FEATURE_NAMES.filter((name) => featureEnabled(cfg, name)),\n off: FEATURE_NAMES.filter((name) => !featureEnabled(cfg, name)),\n };\n}\n\n// Resolved embed settings, or null when the feature is off.\nexport function embedConfig(cfg: Config): { model: string; type: 'static' | 'api'; url?: string; key?: string } | null {\n const e = cfg.features?.embed;\n if (!e) return null;\n const o = e === true ? {} : e;\n return { model: o.model ?? DEFAULT_EMBED_MODEL, type: o.type ?? 'static', url: o.url, key: o.key };\n}\n\n// Cache-key string: embed carries its type + model so a model change rebuilds like a toggle.\nexport function featureSignature(cfg: Config): string {\n return enabledFeatures(cfg)\n .map((name) => {\n if (name !== 'embed') return name;\n const e = embedConfig(cfg);\n return `embed:${e?.type}:${e?.model}`;\n })\n .join(',');\n}\n\n// Pure per-version steps; loadConfig chains them from the file's version up to SUPPORTED_CONFIG_VERSION.\nconst MIGRATIONS: Record<number, (cfg: Config) => Config> = {\n // v1 -> v2: features block introduced, opt-out features enabled (matches the old implicit\n // behavior of `links` etc. not existing). embed stays absent -- opt-in.\n 1: (cfg) => ({ ...cfg, version: 2, features: Object.fromEntries(OPT_OUT_NAMES.map((name) => [name, true])) as Config['features'] }),\n};\n\nexport function migrateConfig(cfg: Config): { cfg: Config; from: number } {\n const from = cfg.version ?? 1;\n let current = cfg;\n for (let v = from; v < SUPPORTED_CONFIG_VERSION; v++) {\n const step = MIGRATIONS[v];\n if (!step) throw new SenseError('CONFIG_VERSION_UNSUPPORTED', `no migration from config version ${v}`);\n current = step(current);\n }\n return { cfg: current, from };\n}\n\nfunction starterConfig(): Config {\n return {\n $schema: 'https://unpkg.com/sensemaking/schema.json',\n version: SUPPORTED_CONFIG_VERSION,\n scan: { include: ['**/*.md'] },\n features: Object.fromEntries(OPT_OUT_NAMES.map((name) => [name, true])) as Config['features'],\n queries: {},\n };\n}\n\n// Refuses to overwrite an existing config.\nexport function initConfig(dir: string): string {\n const configPath = join(dir, CONFIG_FILENAME);\n if (existsSync(configPath)) {\n throw new SenseError('CONFIG_EXISTS', `${CONFIG_FILENAME} already exists in ${dir}`);\n }\n writeFileSync(configPath, `${JSON.stringify(starterConfig(), null, 2)}\\n`);\n return configPath;\n}\n\nexport function findConfigPath(startDir: string): string | null {\n let dir = resolve(startDir);\n for (;;) {\n const candidate = join(dir, CONFIG_FILENAME);\n if (existsSync(candidate)) return candidate;\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\n// Shape check for hand-edited files: a typo'd config fails with a named error, not a\n// TypeError from whatever code touched the missing field first. `queries` is optional\n// on disk (absent = none); `scan.include` has no usable default.\n// Keys each block actually reads. A key outside these sets is accepted and ignored, which\n// looks identical to working -- one field report set `scan.exclude`, saw no change in doc\n// count and no warning, and reasonably concluded the filter had applied. Reported as a\n// warning rather than an error so a config carrying an unknown key still runs.\nconst KNOWN_KEYS = new Set(['$schema', 'version', 'scan', 'features', 'defaults', 'checks', 'queries']);\nconst KNOWN_SCAN_KEYS = new Set(['include']);\n\nfunction unknownConfigKeys(cfg: Record<string, unknown>): string[] {\n const unknown = Object.keys(cfg)\n .filter((k) => !KNOWN_KEYS.has(k))\n .map((k) => k);\n const scan = cfg.scan as Record<string, unknown> | undefined;\n if (scan && typeof scan === 'object' && !Array.isArray(scan)) {\n unknown.push(\n ...Object.keys(scan)\n .filter((k) => !KNOWN_SCAN_KEYS.has(k))\n .map((k) => `scan.${k}`)\n );\n }\n const features = cfg.features as Record<string, unknown> | undefined;\n if (features && typeof features === 'object' && !Array.isArray(features)) {\n unknown.push(\n ...Object.keys(features)\n .filter((k) => !(FEATURE_NAMES as readonly string[]).includes(k))\n .map((k) => `features.${k}`)\n );\n }\n return unknown;\n}\n\nfunction validateConfig(parsed: unknown, configPath: string): Config {\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: config must be a JSON object`);\n }\n const cfg = parsed as Record<string, unknown>;\n const scan = cfg.scan as { include?: unknown } | undefined;\n if (!scan || !Array.isArray(scan.include) || scan.include.length === 0 || !scan.include.every((g) => typeof g === 'string')) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: scan.include must be a non-empty array of glob strings`);\n }\n if (cfg.queries === undefined) cfg.queries = {};\n if (typeof cfg.queries !== 'object' || cfg.queries === null || Array.isArray(cfg.queries) || !Object.values(cfg.queries).every((q) => typeof q === 'string')) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries must be an object of name -> SQL string`);\n }\n const defaults = cfg.defaults as { find?: { where?: unknown } } | undefined;\n if (defaults !== undefined && (typeof defaults !== 'object' || defaults === null || Array.isArray(defaults) || (defaults.find !== undefined && typeof defaults.find?.where !== 'string'))) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: defaults.find.where must be a SQL condition string`);\n }\n const checks = cfg.checks as Record<string, unknown> | undefined;\n if (checks !== undefined) {\n if (typeof checks !== 'object' || checks === null || Array.isArray(checks) || !Object.values(checks).every((v) => v === 'empty')) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: checks must be an object of query name -> \"empty\"`);\n }\n const queries = (cfg.queries ?? {}) as Record<string, unknown>;\n for (const name of Object.keys(checks)) {\n if (queries[name] === undefined) throw new SenseError('CONFIG_INVALID', `${configPath}: checks names \"${name}\", which is not a saved query`);\n }\n }\n if (cfg.features !== undefined && (typeof cfg.features !== 'object' || cfg.features === null || Array.isArray(cfg.features))) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: features must be an object of name -> boolean`);\n }\n for (const [name, value] of Object.entries(cfg.features ?? {})) {\n if (typeof value === 'boolean') continue;\n // embed alone takes an object form: { model?, type?: static|api, url?, key? }.\n const embed = value as Record<string, unknown>;\n const shapeOk = name === 'embed' && typeof value === 'object' && value !== null && !Array.isArray(value) && ['model', 'url', 'key'].every((f) => embed[f] === undefined || typeof embed[f] === 'string') && (embed.type === undefined || embed.type === 'static' || embed.type === 'api');\n if (!shapeOk) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: features.${name} must be a boolean${name === 'embed' ? ' or { model?, type?: \"static\"|\"api\", url?, key? }' : ''}`);\n }\n }\n return cfg as unknown as Config;\n}\n\nexport function loadConfig(explicitPath?: string): ResolvedConfig {\n let configPath: string;\n if (explicitPath) {\n configPath = resolve(process.cwd(), explicitPath);\n if (!existsSync(configPath)) throw new SenseError('CONFIG_NOT_FOUND', `config not found: ${configPath}`);\n } else {\n const found = findConfigPath(process.cwd());\n if (!found) {\n throw new SenseError('CONFIG_NOT_FOUND', `could not find ${CONFIG_FILENAME} in ${process.cwd()} or any parent directory`);\n }\n configPath = found;\n }\n\n const raw = readFileSync(configPath, 'utf8');\n const parsed: unknown = JSON.parse(raw);\n\n // Version gate before shape validation: a config written by a newer sense should fail\n // with \"requires a newer sense\", not with shape errors its own version may not have.\n const version = typeof parsed === 'object' && parsed !== null && typeof (parsed as { version?: unknown }).version === 'number' ? (parsed as { version: number }).version : 1;\n if (version > SUPPORTED_CONFIG_VERSION) {\n throw new SenseError('CONFIG_VERSION_UNSUPPORTED', `config version ${version} requires a newer sense`);\n }\n\n let cfg = validateConfig(parsed, configPath);\n\n let migratedFrom: number | undefined;\n if (version < SUPPORTED_CONFIG_VERSION) {\n const result = migrateConfig(cfg);\n cfg = result.cfg;\n migratedFrom = result.from;\n writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}\\n`);\n }\n\n const unknownKeys = unknownConfigKeys(cfg as unknown as Record<string, unknown>);\n return { ...cfg, baseDir: dirname(configPath), configPath, migratedFrom, unknownKeys: unknownKeys.length > 0 ? unknownKeys : undefined };\n}\n"],"names":["existsSync","readFileSync","writeFileSync","dirname","join","resolve","SenseError","CONFIG_FILENAME","STATE_DIR","SUPPORTED_CONFIG_VERSION","FEATURE_NAMES","OPT_OUT_NAMES","DEFAULT_EMBED_MODEL","featureEnabled","cfg","name","Boolean","features","embed","enabled","enabledFeatures","filter","featureStates","on","off","embedConfig","o","e","model","type","url","key","featureSignature","map","MIGRATIONS","version","Object","fromEntries","migrateConfig","from","current","v","step","starterConfig","$schema","scan","include","queries","initConfig","dir","configPath","JSON","stringify","findConfigPath","startDir","candidate","parent","KNOWN_KEYS","Set","KNOWN_SCAN_KEYS","unknownConfigKeys","unknown","keys","k","has","Array","isArray","push","includes","validateConfig","parsed","defaults","length","every","g","undefined","values","q","find","where","checks","value","entries","shapeOk","f","loadConfig","explicitPath","process","cwd","found","raw","parse","migratedFrom","result","unknownKeys","baseDir"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/config.ts"],"sourcesContent":["import { existsSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { SenseError } from './errors.ts';\n\nexport const CONFIG_FILENAME = 'sense.config.json';\nexport const STATE_DIR = '.sense';\n\n// Highest sense.config.json `version` this build understands. Older versions auto-migrate on load.\nexport const SUPPORTED_CONFIG_VERSION = 3;\n\n// Each feature owns its tables, parse-time extraction, and reconcile step; commands degrade when one is off.\n// links/sections/rank are opt-out toggles in the top-level `features` block; embed is not a\n// member of that block -- it is derived from whether any declared preset has semantic on.\nconst FEATURE_NAMES = ['links', 'sections', 'rank', 'embed'] as const;\n// v1 -> v2 migration only: the features v2 introduced as opt-out (embed was opt-in then).\nconst V2_OPT_OUT_NAMES = ['links', 'sections', 'rank'] as const;\nexport type FeatureName = (typeof FEATURE_NAMES)[number];\n\n// `embed` config is provider settings only -- presence toggles nothing. Vector participation\n// is controlled per preset by `semantic`.\nexport interface EmbedConfig {\n model?: string;\n type?: 'static' | 'api';\n url?: string; // api type: OpenAI-compatible base URL, e.g. http://localhost:11434/v1\n key?: string; // api type: name of the env var holding the bearer token, if any\n}\n\nexport const DEFAULT_EMBED_MODEL = 'minishlab/potion-retrieval-32M';\n\n// A named, self-contained file-selection scope. include/exclude are globby patterns resolved\n// relative to the config file. No inheritance between presets -- every field a preset wants\n// it declares itself. Presets may overlap freely; they are views, not partitions.\nexport interface Preset {\n include: string[];\n exclude?: string[];\n k?: number; // result count for `search` scoped to this preset; default 10\n semantic?: boolean; // vectors on/off for files this preset covers. Default true; only ever written as false.\n where?: string; // standing SQL condition against frontmatter alias `f`\n}\n\n// A saved `search` invocation: `sense <name>` runs like\n// `sense search <search> [--preset] [--include] [--where] [--k] [--semantic]`.\nexport interface SavedSearch {\n search: string;\n preset?: string;\n include?: string[];\n where?: string;\n k?: number;\n semantic?: boolean;\n}\n\n// A queries.<name> entry: raw SQL, an explicit SQL wrapper, or a saved search.\nexport type SavedQuery = string | { sql: string } | SavedSearch;\n\nexport interface Config {\n // Editor-only pointer to schema.json; never read by sense.\n $schema?: string;\n version?: number;\n // File selection, index-time, and per-preset search defaults. A file is indexed iff any\n // preset's include/exclude covers it (union); embedded iff any covering preset has\n // semantic !== false. `default` is used when a command names no preset.\n presets: Record<string, Preset>;\n // Global feature defaults; absent block or key means enabled. embed is not a member here --\n // see Preset.semantic.\n features?: { links?: boolean; sections?: boolean; rank?: boolean };\n // Embed provider settings only. Presence alone does not enable embedding -- see Preset.semantic.\n embed?: EmbedConfig;\n queries: Record<string, SavedQuery>;\n}\n\nexport interface ResolvedConfig extends Config {\n baseDir: string;\n configPath: string | null;\n // Keys the file declares that this build does not read; cli reports them.\n unknownKeys?: string[];\n // Set when loadConfig auto-migrated the file on disk; cli reports it.\n migratedFrom?: number;\n}\n\nexport function presetNames(cfg: Config): string[] {\n return Object.keys(cfg.presets);\n}\n\n// A preset's own resolved vector participation: absent or true means on, false means off.\nexport function presetSemanticEnabled(cfg: Config, name: string): boolean {\n return cfg.presets[name]?.semantic !== false;\n}\n\n// Whether embedding needs to run at all: true when at least one declared preset wants vectors.\nexport function anyPresetEmbeds(cfg: Config): boolean {\n return presetNames(cfg).some((name) => presetSemanticEnabled(cfg, name));\n}\n\n// Opt-out features (default on): absent block or key means enabled. `rank` additionally\n// requires `links`. `embed` is derived, not a features-block member: on iff any declared\n// preset has semantic on.\nexport function featureEnabled(cfg: Config, name: FeatureName): boolean {\n if (name === 'embed') return anyPresetEmbeds(cfg);\n const enabled = cfg.features?.[name] !== false;\n if (name === 'rank') return enabled && featureEnabled(cfg, 'links');\n return enabled;\n}\n\nexport function enabledFeatures(cfg: Config): FeatureName[] {\n return FEATURE_NAMES.filter((name) => featureEnabled(cfg, name));\n}\n\n// Every feature with its current state; commands surface this so \"off\" and \"empty\" stay\n// distinguishable in output.\nexport function featureStates(cfg: Config): { on: FeatureName[]; off: FeatureName[] } {\n return {\n on: FEATURE_NAMES.filter((name) => featureEnabled(cfg, name)),\n off: FEATURE_NAMES.filter((name) => !featureEnabled(cfg, name)),\n };\n}\n\n// Resolved embed provider settings, or null when no preset wants embeddings at all.\nexport function embedConfig(cfg: Config): { model: string; type: 'static' | 'api'; url?: string; key?: string } | null {\n if (!anyPresetEmbeds(cfg)) return null;\n const e = cfg.embed ?? {};\n return { model: e.model ?? DEFAULT_EMBED_MODEL, type: e.type ?? 'static', url: e.url, key: e.key };\n}\n\n// Cache-key string: embed carries its type + model so a model change rebuilds like a toggle,\n// and every declared preset carries its include/exclude/semantic so an edit to any of those\n// rebuilds too -- indexing and embedding are both derived from presets, so any change to the\n// derivation inputs must invalidate the cache.\nexport function featureSignature(cfg: Config): string {\n const globalPart = enabledFeatures(cfg)\n .filter((name) => name !== 'embed')\n .join(',');\n const e = embedConfig(cfg);\n const embedPart = e ? `embed:${e.type}:${e.model}` : 'embed:off';\n // One keyed segment per preset so a rebuild notice can name exactly which preset moved.\n const presetsPart = [...presetNames(cfg)]\n .sort()\n .map((name) => {\n const p = cfg.presets[name];\n const include = [...p.include].sort().join('+');\n const exclude = [...(p.exclude ?? [])].sort().join('+');\n return `preset:${name}:${include}:${exclude}:${p.semantic === false ? 'off' : 'on'}`;\n })\n .join('|');\n return [`features:${globalPart}`, embedPart, presetsPart].join('|');\n}\n\n// Looks up a declared preset by name, defaulting to `default`. Throws naming every declared\n// preset when an explicit name is not declared -- the `default` fallback is never unknown\n// itself (validateConfig requires it).\nexport function resolvePreset(cfg: Config, name?: string): { name: string; preset: Preset } {\n const presetName = name ?? 'default';\n const preset = cfg.presets[presetName];\n if (!preset) throw new SenseError('PRESET_UNKNOWN', `unknown preset \"${presetName}\"; declared presets: ${presetNames(cfg).join(', ')}`);\n return { name: presetName, preset };\n}\n\nexport interface SearchOverrides {\n preset?: string;\n k?: number;\n where?: string;\n include?: string[];\n semantic?: boolean;\n}\n\nexport interface EffectiveSearch {\n presetName: string;\n k: number;\n where?: string;\n include: string[];\n exclude?: string[];\n semantic: boolean;\n}\n\n// Resolution precedence: built-ins (k=10, semantic on, whole-index scope) <- named preset's\n// fields (or `default` when none named) <- caller overrides. Each stage overrides only the\n// fields it sets. `opts` here is expected to already carry whatever a saved query and an\n// explicit CLI flag resolved between themselves (src/cli/named.ts does that `cli ?? saved`\n// merge before calling in) -- one `??` per field composes the full four-stage chain, since\n// `??` is associative field-by-field.\nexport function resolveSearch(cfg: Config, opts: SearchOverrides = {}): EffectiveSearch {\n const { name: presetName, preset } = resolvePreset(cfg, opts.preset);\n const k = opts.k ?? preset.k ?? 10;\n const where = opts.where ?? preset.where;\n // An explicit include (CLI --include, or a saved search's own `include`) is a full ad hoc\n // scope override -- it replaces the preset's include/exclude pair rather than layering on\n // top of it, the same \"replaces, doesn't AND\" rule --where already uses.\n const include = opts.include ?? preset.include;\n const exclude = opts.include ? undefined : preset.exclude;\n const semantic = opts.semantic !== undefined ? opts.semantic : preset.semantic !== false;\n return { presetName, k, where, include, exclude, semantic };\n}\n\n// Pure per-version steps; loadConfig chains them from the file's version up to SUPPORTED_CONFIG_VERSION.\n// Intermediate shapes predate the current Config type (v1 has no features block, v2 has\n// scan/find not presets/search), so steps work loosely-typed and only the final result is cast\n// back to Config.\nconst MIGRATIONS: Record<number, (cfg: Record<string, unknown>) => Record<string, unknown>> = {\n // v1 -> v2: features block introduced, opt-out features enabled (matches the old implicit\n // behavior of `links` etc. not existing). embed stays absent -- opt-in.\n 1: (cfg) => ({ ...cfg, version: 2, features: Object.fromEntries(V2_OPT_OUT_NAMES.map((name) => [name, true])) }),\n // v2 -> v3: mechanical-minimal, by decision -- only what keeps an existing config loading.\n // No restructuring, no preset inference, no query rewriting; real trees get hand-migrated\n // separately to actually use presets.\n 2: (cfg) => {\n const { scan, defaults, features, checks, queries, ...rest } = cfg;\n const scanInclude = (scan as { include: string[] }).include;\n\n const nextQueries: Record<string, unknown> = {};\n for (const [name, value] of Object.entries((queries as Record<string, unknown> | undefined) ?? {})) {\n if (typeof value === 'string') {\n nextQueries[name] = value;\n continue;\n }\n const entry = value as { find: string; k?: number; where?: string; semantic?: boolean };\n const search: SavedSearch = { search: entry.find };\n if (entry.k !== undefined) search.k = entry.k;\n if (entry.where !== undefined) search.where = entry.where;\n // semantic: true was opt-in; it is now the default, so it drops. semantic: false is kept.\n if (entry.semantic === false) search.semantic = false;\n nextQueries[name] = search;\n }\n\n const prevDefaults = defaults as { find?: { where?: string } } | undefined;\n const defaultWhere = prevDefaults?.find?.where;\n\n // features.embed: true/absent -> simply removed (v3 default is on). An explicit `false`\n // becomes semantic: false on every preset this migration produces -- there is only ever\n // one (`default`), since v2 has no notion of multiple scopes, but the rule is written to\n // apply to \"every migrated preset\" rather than hardcoded to `default` in case a later\n // migration step ever produces more than one.\n const prevFeatures = (features as Record<string, unknown> | undefined) ?? {};\n const embedWasOff = prevFeatures.embed === false;\n // Object form carried provider settings (model/type/url/key), not a toggle -- they move\n // to the v3 top-level `embed` block verbatim; dropping them would silently switch an\n // api tree back to the built-in static model.\n const embedProvider = typeof prevFeatures.embed === 'object' && prevFeatures.embed !== null ? prevFeatures.embed : undefined;\n const { embed: _embed, ...restFeatures } = prevFeatures;\n\n const defaultPreset: Record<string, unknown> = { include: scanInclude };\n if (defaultWhere !== undefined) defaultPreset.where = defaultWhere;\n if (embedWasOff) defaultPreset.semantic = false;\n\n // checks (assertions over saved queries) was removed in v3; its queries still run under\n // `queries`, just without the pass/fail assertion -- reachable now only as a saved query.\n if (checks !== undefined) {\n 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');\n }\n\n const result: Record<string, unknown> = { ...rest, version: 3, presets: { default: defaultPreset }, queries: nextQueries };\n if (embedProvider !== undefined) result.embed = embedProvider;\n if (Object.keys(restFeatures).length > 0) result.features = restFeatures;\n return result;\n },\n};\n\nexport function migrateConfig(cfg: Config): { cfg: Config; from: number } {\n const from = cfg.version ?? 1;\n let current: Record<string, unknown> = cfg as unknown as Record<string, unknown>;\n for (let v = from; v < SUPPORTED_CONFIG_VERSION; v++) {\n const step = MIGRATIONS[v];\n if (!step) throw new SenseError('CONFIG_VERSION_UNSUPPORTED', `no migration from config version ${v}`);\n current = step(current);\n }\n return { cfg: current as unknown as Config, from };\n}\n\nfunction starterConfig(): Config {\n return {\n $schema: 'https://unpkg.com/sensemaking/schema.json',\n version: SUPPORTED_CONFIG_VERSION,\n presets: {\n default: { include: ['**/*.md'], k: 10 },\n large: { include: ['**/*.md'], k: 20 },\n },\n queries: {},\n };\n}\n\n// Refuses to overwrite an existing config.\nexport function initConfig(dir: string): string {\n const configPath = join(dir, CONFIG_FILENAME);\n if (existsSync(configPath)) {\n throw new SenseError('CONFIG_EXISTS', `${CONFIG_FILENAME} already exists in ${dir}`);\n }\n writeFileSync(configPath, `${JSON.stringify(starterConfig(), null, 2)}\\n`);\n return configPath;\n}\n\nexport function findConfigPath(startDir: string): string | null {\n let dir = resolve(startDir);\n for (;;) {\n const candidate = join(dir, CONFIG_FILENAME);\n if (existsSync(candidate)) return candidate;\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\n// Shape check for hand-edited files: a typo'd config fails with a named error, not a\n// TypeError from whatever code touched the missing field first. `queries` is optional\n// on disk (absent = none); `presets` has no usable default.\n// Unknown top-level keys are reported as a soft warning (cli.ts prints it) rather than an\n// error, so a config carrying a stray field still runs -- but unknown keys inside a preset,\n// features, embed, or saved-query block are hard errors: those blocks are small and fully\n// owned, so a typo there is far more likely a mistake than a forward-compat field.\nconst KNOWN_KEYS = new Set(['$schema', 'version', 'presets', 'features', 'embed', 'queries']);\nconst KNOWN_PRESET_KEYS = new Set(['include', 'exclude', 'k', 'semantic', 'where']);\nconst KNOWN_FEATURE_KEYS = new Set(['links', 'sections', 'rank']);\nconst KNOWN_EMBED_KEYS = new Set(['model', 'type', 'url', 'key']);\nconst SAVED_SEARCH_KEYS = new Set(['search', 'preset', 'include', 'where', 'k', 'semantic']);\n\nfunction unknownConfigKeys(cfg: Record<string, unknown>): string[] {\n return Object.keys(cfg).filter((k) => !KNOWN_KEYS.has(k));\n}\n\nfunction isNonEmptyStringArray(value: unknown): value is string[] {\n return Array.isArray(value) && value.length > 0 && value.every((g) => typeof g === 'string' && g.length > 0);\n}\n\n// Pre-v3 shape check, just enough for migrateConfig to run safely (it reads scan.include\n// directly). The full v3 shape is checked by validateConfig once migration has produced it.\nfunction validateLegacyScan(parsed: unknown, configPath: string): void {\n const cfg = typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) ? (parsed as Record<string, unknown>) : null;\n const scan = cfg?.scan as { include?: unknown } | undefined;\n if (!cfg || !scan || !isNonEmptyStringArray(scan.include)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: scan.include must be a non-empty array of glob strings`);\n }\n}\n\nfunction validateFeaturesBlock(value: unknown, configPath: string): void {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: features must be an object of name -> boolean`);\n }\n const block = value as Record<string, unknown>;\n const unknown = Object.keys(block).filter((k) => !KNOWN_FEATURE_KEYS.has(k));\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: features has unknown key(s) ${unknown.join(', ')}; embed is not a features key -- see the top-level \"embed\" block and each preset's \"semantic\"`);\n }\n for (const [name, v] of Object.entries(block)) {\n if (typeof v !== 'boolean') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: features.${name} must be a boolean`);\n }\n }\n}\n\nfunction validateEmbedBlock(value: unknown, configPath: string): void {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed must be an object of { model?, type?: \"static\"|\"api\", url?, key? }`);\n }\n const embed = value as Record<string, unknown>;\n const unknown = Object.keys(embed).filter((k) => !KNOWN_EMBED_KEYS.has(k));\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed has unknown key(s) ${unknown.join(', ')}`);\n }\n if (embed.model !== undefined && typeof embed.model !== 'string') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.model must be a string`);\n }\n if (embed.type !== undefined && embed.type !== 'static' && embed.type !== 'api') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.type must be \"static\" or \"api\"`);\n }\n if (embed.url !== undefined && typeof embed.url !== 'string') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.url must be a string`);\n }\n if (embed.key !== undefined && typeof embed.key !== 'string') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.key must be a string`);\n }\n}\n\nfunction validatePreset(name: string, value: unknown, configPath: string): void {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name} must be an object`);\n }\n const preset = value as Record<string, unknown>;\n const unknown = Object.keys(preset).filter((k) => !KNOWN_PRESET_KEYS.has(k));\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name} has unknown key(s) ${unknown.join(', ')}; a preset takes include, exclude, k, semantic, where`);\n }\n if (!isNonEmptyStringArray(preset.include)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.include must be a non-empty array of glob strings`);\n }\n if (preset.exclude !== undefined && !isNonEmptyStringArray(preset.exclude)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.exclude must be a non-empty array of glob strings`);\n }\n if (preset.k !== undefined && (typeof preset.k !== 'number' || !Number.isInteger(preset.k) || preset.k <= 0)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.k must be a positive integer`);\n }\n if (preset.semantic !== undefined && typeof preset.semantic !== 'boolean') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.semantic must be a boolean`);\n }\n if (preset.where !== undefined && typeof preset.where !== 'string') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.where must be a SQL condition string`);\n }\n}\n\n// A queries.<name> entry: a SQL string, { sql }, or a saved search { search, preset?, include?, where?, k?, semantic? }.\nfunction validateSavedQuery(name: string, value: unknown, configPath: string): void {\n if (typeof value === 'string') return;\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} must be a SQL string, { sql }, or { search, preset?, include?, where?, k?, semantic? }`);\n }\n const entry = value as Record<string, unknown>;\n if ('sql' in entry) {\n const unknown = Object.keys(entry).filter((k) => k !== 'sql');\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} has unknown key(s) ${unknown.join(', ')}; a { sql } query takes only sql`);\n }\n if (typeof entry.sql !== 'string' || entry.sql.trim() === '') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.sql must be a non-empty string`);\n }\n return;\n }\n if ('search' in entry) {\n const unknown = Object.keys(entry).filter((k) => !SAVED_SEARCH_KEYS.has(k));\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} has unknown key(s) ${unknown.join(', ')}; a saved search takes search, preset, include, where, k, semantic`);\n }\n // A saved query saves a question; a scope without a question is just flags.\n if (typeof entry.search !== 'string' || entry.search.trim() === '') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.search must be non-empty text`);\n }\n if (entry.preset !== undefined && (typeof entry.preset !== 'string' || entry.preset.length === 0)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.preset must be a preset name`);\n }\n if (entry.include !== undefined && !isNonEmptyStringArray(entry.include)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.include must be a non-empty array of glob strings`);\n }\n if (entry.where !== undefined && typeof entry.where !== 'string') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.where must be a SQL condition string`);\n }\n if (entry.k !== undefined && (typeof entry.k !== 'number' || !Number.isInteger(entry.k) || entry.k <= 0)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.k must be a positive integer`);\n }\n if (entry.semantic !== undefined && typeof entry.semantic !== 'boolean') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.semantic must be a boolean`);\n }\n return;\n }\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} must be a SQL string, { sql }, or { search, preset?, include?, where?, k?, semantic? }`);\n}\n\nfunction validateConfig(parsed: unknown, configPath: string): Config {\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: config must be a JSON object`);\n }\n const cfg = parsed as Record<string, unknown>;\n\n // `checks` (assertions over saved queries) was removed in v3, not merely deprecated: a\n // hand-written v3 config still carrying it gets a named rejection rather than a silent\n // \"unknown key\" warning, since silence here would hide that the assertion behavior it\n // implies no longer exists.\n if (cfg.checks !== undefined) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: checks was removed in v3 -- sense check no longer asserts on saved queries; a returned row set is the reader's judgment`);\n }\n\n const presets = cfg.presets as Record<string, unknown> | undefined;\n if (!presets || typeof presets !== 'object' || Array.isArray(presets) || Object.keys(presets).length === 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets must be a non-empty object of preset name -> { include, exclude?, k?, semantic?, where? }`);\n }\n for (const [name, value] of Object.entries(presets)) {\n validatePreset(name, value, configPath);\n }\n if (presets.default === undefined) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets must include a \"default\" preset`);\n }\n\n if (cfg.queries === undefined) cfg.queries = {};\n if (typeof cfg.queries !== 'object' || cfg.queries === null || Array.isArray(cfg.queries)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries must be an object of name -> SQL string, { sql }, or saved search`);\n }\n const queries = cfg.queries as Record<string, unknown>;\n for (const [name, value] of Object.entries(queries)) {\n validateSavedQuery(name, value, configPath);\n }\n\n if (cfg.features !== undefined) {\n validateFeaturesBlock(cfg.features, configPath);\n }\n if (cfg.embed !== undefined) {\n validateEmbedBlock(cfg.embed, configPath);\n }\n\n return cfg as unknown as Config;\n}\n\nexport function loadConfig(explicitPath?: string): ResolvedConfig {\n let configPath: string;\n if (explicitPath) {\n configPath = resolve(process.cwd(), explicitPath);\n if (!existsSync(configPath)) throw new SenseError('CONFIG_NOT_FOUND', `config not found: ${configPath}`);\n } else {\n const found = findConfigPath(process.cwd());\n if (!found) {\n throw new SenseError('CONFIG_NOT_FOUND', `could not find ${CONFIG_FILENAME} in ${process.cwd()} or any parent directory`);\n }\n configPath = found;\n }\n\n const raw = readFileSync(configPath, 'utf8');\n const parsed: unknown = JSON.parse(raw);\n\n // Ahead of both the version gate and legacy/current shape checks: a non-object file (or\n // an array -- typeof [] is also 'object') is malformed at every version, so it gets one\n // error regardless of which branch would otherwise run.\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: config must be a JSON object`);\n }\n\n // Version gate before shape validation: a config written by a newer sense should fail\n // with \"requires a newer sense\", not with shape errors its own version may not have.\n const version = typeof parsed === 'object' && parsed !== null && typeof (parsed as { version?: unknown }).version === 'number' ? (parsed as { version: number }).version : 1;\n if (version > SUPPORTED_CONFIG_VERSION) {\n throw new SenseError('CONFIG_VERSION_UNSUPPORTED', `config version ${version} requires a newer sense`);\n }\n\n // validateConfig only understands the current v3 shape (presets, saved-query object\n // shapes); a pre-v3 file on disk is scan/find-shaped and must migrate before it can pass\n // that check. Validate just enough of the old shape to migrate safely, migrate, then run\n // the full v3 validateConfig on the migrated result -- the shape that actually lands on\n // disk and in ResolvedConfig.\n let cfg: Config;\n let migratedFrom: number | undefined;\n if (version < SUPPORTED_CONFIG_VERSION) {\n validateLegacyScan(parsed, configPath);\n const result = migrateConfig(parsed as unknown as Config);\n cfg = validateConfig(result.cfg, configPath);\n migratedFrom = result.from;\n writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}\\n`);\n } else {\n cfg = validateConfig(parsed, configPath);\n }\n\n const unknownKeys = unknownConfigKeys(cfg as unknown as Record<string, unknown>);\n return { ...cfg, baseDir: dirname(configPath), configPath, migratedFrom, unknownKeys: unknownKeys.length > 0 ? unknownKeys : undefined };\n}\n"],"names":["existsSync","readFileSync","writeFileSync","dirname","join","resolve","SenseError","CONFIG_FILENAME","STATE_DIR","SUPPORTED_CONFIG_VERSION","FEATURE_NAMES","V2_OPT_OUT_NAMES","DEFAULT_EMBED_MODEL","presetNames","cfg","Object","keys","presets","presetSemanticEnabled","name","semantic","anyPresetEmbeds","some","featureEnabled","enabled","features","enabledFeatures","filter","featureStates","on","off","embedConfig","e","embed","model","type","url","key","featureSignature","globalPart","embedPart","presetsPart","sort","map","p","include","exclude","resolvePreset","presetName","preset","resolveSearch","opts","k","where","undefined","MIGRATIONS","version","fromEntries","prevDefaults","scan","defaults","checks","queries","rest","scanInclude","nextQueries","value","entries","entry","search","find","defaultWhere","prevFeatures","embedWasOff","embedProvider","_embed","restFeatures","defaultPreset","console","error","result","default","length","migrateConfig","from","current","v","step","starterConfig","$schema","large","initConfig","dir","configPath","JSON","stringify","findConfigPath","startDir","candidate","parent","KNOWN_KEYS","Set","KNOWN_PRESET_KEYS","KNOWN_FEATURE_KEYS","KNOWN_EMBED_KEYS","SAVED_SEARCH_KEYS","unknownConfigKeys","has","isNonEmptyStringArray","Array","isArray","every","g","validateLegacyScan","parsed","validateFeaturesBlock","block","unknown","validateEmbedBlock","validatePreset","Number","isInteger","validateSavedQuery","sql","trim","validateConfig","loadConfig","explicitPath","process","cwd","found","raw","parse","migratedFrom","unknownKeys","baseDir"],"mappings":"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"}
|
package/dist/esm/db.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DatabaseSync } from 'node:sqlite';
|
|
2
2
|
import type { Config, ResolvedConfig } from './config.js';
|
|
3
3
|
export declare const DB_FILENAME = "cache.db";
|
|
4
|
-
export declare const SCHEMA_VERSION = "
|
|
4
|
+
export declare const SCHEMA_VERSION = "8";
|
|
5
5
|
export interface OpenResult {
|
|
6
6
|
db: DatabaseSync;
|
|
7
7
|
cfg: ResolvedConfig;
|
package/dist/esm/db.js
CHANGED
|
@@ -2,13 +2,30 @@ import { mkdirSync, rmSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { DatabaseSync } from 'node:sqlite';
|
|
4
4
|
import { featureSignature, STATE_DIR } from './config.js';
|
|
5
|
+
import { SenseError } from './errors.js';
|
|
5
6
|
import { activeFeatures } from './features/index.js';
|
|
6
|
-
import {
|
|
7
|
+
import { progress } from './progress.js';
|
|
8
|
+
import { listFiles, parseFile, RESERVED_COLUMNS } from './scan.js';
|
|
9
|
+
// path/_mtime/_size are core: every reparse legitimately rewrites them. Every other
|
|
10
|
+
// RESERVED_COLUMNS entry that shows up as a real frontmatter column (currently only
|
|
11
|
+
// rank's `_rank`) is feature-owned -- scan.ts already refuses to let frontmatter set it,
|
|
12
|
+
// so it must never appear in the upsert below, or a reparse would blow its last computed
|
|
13
|
+
// value away with NULL on every touch, not just the reconciles that recompute it.
|
|
14
|
+
const CORE_FRONTMATTER_COLUMNS = new Set([
|
|
15
|
+
'path',
|
|
16
|
+
'_mtime',
|
|
17
|
+
'_size'
|
|
18
|
+
]);
|
|
7
19
|
// Rows -> SQLite: core schema, reconcile loop, has(). Parsing lives in scan.ts;
|
|
8
20
|
// everything beyond frontmatter + content lives in src/features/.
|
|
9
21
|
export const DB_FILENAME = 'cache.db';
|
|
10
22
|
// Cache shape version, independent of the config's own `version`.
|
|
11
|
-
|
|
23
|
+
// 7: presets replace layers -- frontmatter drops the `layer` column, a new
|
|
24
|
+
// preset_files(preset, path) table tracks per-preset coverage for status/map (was: 6,
|
|
25
|
+
// frontmatter gains the `layer` column).
|
|
26
|
+
export const SCHEMA_VERSION = '8';
|
|
27
|
+
// SQLite's compile-time SQLITE_MAX_COLUMN, default 2000 (https://www.sqlite.org/limits.html).
|
|
28
|
+
const MAX_FRONTMATTER_COLUMNS = 2000;
|
|
12
29
|
function quoteIdent(name) {
|
|
13
30
|
return `"${name.split('"').join('""')}"`;
|
|
14
31
|
}
|
|
@@ -43,6 +60,14 @@ function getColumns(db) {
|
|
|
43
60
|
function ensureSchema(db, cfg) {
|
|
44
61
|
db.exec(`CREATE TABLE IF NOT EXISTS frontmatter ("path" TEXT PRIMARY KEY, "_mtime" REAL, "_size" INTEGER)`);
|
|
45
62
|
db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS content USING fts5(title, summary, text, path UNINDEXED, tokenize = 'porter unicode61')`);
|
|
63
|
+
// Coverage, not ownership: a path can appear under several presets. Rebuilt per-file
|
|
64
|
+
// alongside frontmatter/content at reconcile so status/map can report matched/embedded
|
|
65
|
+
// counts per preset without recomputing globs at read time.
|
|
66
|
+
// path leads the PK so the per-doc delete in reconcile is an index hit -- keyed the other
|
|
67
|
+
// way it scans the whole table per doc, which made cold builds quadratic (measured 3x cost
|
|
68
|
+
// per note-count doubling at 13k/26k). Coverage-by-preset reads get their own index.
|
|
69
|
+
db.exec(`CREATE TABLE IF NOT EXISTS preset_files ("path" TEXT, preset TEXT, PRIMARY KEY ("path", preset))`);
|
|
70
|
+
db.exec('CREATE INDEX IF NOT EXISTS preset_files_preset ON preset_files(preset)');
|
|
46
71
|
for (const feature of activeFeatures(cfg))feature.schema(db);
|
|
47
72
|
if (getMeta(db, 'schema_version') === null) setMeta(db, 'schema_version', SCHEMA_VERSION);
|
|
48
73
|
if (getMeta(db, 'features') === null) setMeta(db, 'features', featureSignature(cfg));
|
|
@@ -84,8 +109,16 @@ export function reconcile(db, cfg, baseDir) {
|
|
|
84
109
|
const newColumns = [];
|
|
85
110
|
const parsedDocs = [];
|
|
86
111
|
const warnings = [];
|
|
112
|
+
// Bulk reparses (a sync, a cold build) are the long silences a query can hit; short
|
|
113
|
+
// reconciles stay silent (progress() has a threshold).
|
|
114
|
+
const report = progress('reparsing files', toReparse.length);
|
|
115
|
+
let parsedCount = 0;
|
|
87
116
|
for (const file of toReparse){
|
|
88
|
-
|
|
117
|
+
// A doc only gets extract/store from features that apply to it (currently: embed, via
|
|
118
|
+
// FileStat.embed -- true iff a covering preset has semantic on).
|
|
119
|
+
const fileFeatures = features.filter((feature)=>!feature.enabledForFile || feature.enabledForFile(cfg, file));
|
|
120
|
+
const { doc, warnings: fileWarnings } = parseFile(file, fileFeatures);
|
|
121
|
+
report.tick(++parsedCount);
|
|
89
122
|
warnings.push(...fileWarnings);
|
|
90
123
|
for (const key of Object.keys(doc.data)){
|
|
91
124
|
if (!seenColumns.has(key)) {
|
|
@@ -95,63 +128,124 @@ export function reconcile(db, cfg, baseDir) {
|
|
|
95
128
|
}
|
|
96
129
|
parsedDocs.push(doc);
|
|
97
130
|
}
|
|
131
|
+
report.finish();
|
|
98
132
|
const allColumns = [
|
|
99
133
|
...seenColumns
|
|
100
134
|
];
|
|
101
|
-
|
|
135
|
+
// Fence before ALTERing: SQLite's own failure past this point is a raw
|
|
136
|
+
// "too many columns on sqlite_altertab_frontmatter" with no indication of the boundary or the levers.
|
|
137
|
+
if (allColumns.length > MAX_FRONTMATTER_COLUMNS) {
|
|
138
|
+
throw new SenseError('COLUMN_LIMIT', `frontmatter would need ${allColumns.length} columns, crossing SQLite's compile-time SQLITE_MAX_COLUMN limit (default ${MAX_FRONTMATTER_COLUMNS}; see https://www.sqlite.org/limits.html). Narrow the presets' include globs so fewer/other files are indexed, or fix whatever is generating unbounded frontmatter keys.`);
|
|
139
|
+
}
|
|
140
|
+
// Columns the frontmatter upsert actually writes: core + parsed frontmatter keys, never a
|
|
141
|
+
// feature-owned reserved column (see CORE_FRONTMATTER_COLUMNS above).
|
|
142
|
+
const writableColumns = allColumns.filter((c)=>CORE_FRONTMATTER_COLUMNS.has(c) || !RESERVED_COLUMNS.has(c));
|
|
143
|
+
// ON CONFLICT UPDATE (not OR REPLACE) keeps the row's rowid stable across reparses --
|
|
144
|
+
// content rows are coupled to that rowid below.
|
|
145
|
+
const insertSql = `INSERT INTO frontmatter (${writableColumns.map(quoteIdent).join(', ')}) VALUES (${writableColumns.map(()=>'?').join(', ')}) ON CONFLICT("path") DO UPDATE SET ${writableColumns.filter((c)=>c !== 'path').map((c)=>`${quoteIdent(c)} = excluded.${quoteIdent(c)}`).join(', ')}`;
|
|
146
|
+
const added = toReparse.filter((f)=>!existing.has(f.relPath)).map((f)=>f.relPath);
|
|
147
|
+
const delta = {
|
|
148
|
+
files,
|
|
149
|
+
reparsed: parsedDocs.map((d)=>d.relPath),
|
|
150
|
+
added,
|
|
151
|
+
vanished
|
|
152
|
+
};
|
|
153
|
+
const txStart = Date.now();
|
|
102
154
|
db.exec('BEGIN');
|
|
103
155
|
try {
|
|
104
156
|
var _feature_afterReconcile;
|
|
105
157
|
for (const col of newColumns)db.exec(`ALTER TABLE frontmatter ADD COLUMN ${quoteIdent(col)}`);
|
|
106
|
-
// FTS5 has no upsert, so delete-before-insert into `content
|
|
107
|
-
|
|
158
|
+
// FTS5 has no upsert, so delete-before-insert into `content`; coupled to the
|
|
159
|
+
// frontmatter rowid (indexed via its PRIMARY KEY) instead of the UNINDEXED `path`
|
|
160
|
+
// column, which a per-row DELETE would otherwise scan the whole table to find.
|
|
161
|
+
const delBody = db.prepare(`DELETE FROM content WHERE rowid = (SELECT rowid FROM frontmatter WHERE "path" = ?)`);
|
|
162
|
+
const delPresetFiles = db.prepare(`DELETE FROM preset_files WHERE "path" = ?`);
|
|
163
|
+
const insertPresetFile = db.prepare(`INSERT INTO preset_files ("path", preset) VALUES (?, ?)`);
|
|
108
164
|
if (vanished.length > 0) {
|
|
109
165
|
const del = db.prepare(`DELETE FROM frontmatter WHERE "path" = ?`);
|
|
110
166
|
for (const path of vanished){
|
|
111
167
|
var _feature_remove;
|
|
112
|
-
|
|
168
|
+
// content delete must run first: it looks up the frontmatter rowid by path, which
|
|
169
|
+
// the frontmatter delete below would otherwise have already removed.
|
|
113
170
|
delBody.run(path);
|
|
114
|
-
|
|
171
|
+
del.run(path);
|
|
172
|
+
delPresetFiles.run(path);
|
|
173
|
+
for (const feature of features)(_feature_remove = feature.remove) === null || _feature_remove === void 0 ? void 0 : _feature_remove.call(feature, db, path, delta);
|
|
115
174
|
}
|
|
116
175
|
}
|
|
117
176
|
if (parsedDocs.length > 0) {
|
|
118
177
|
const insert = db.prepare(insertSql);
|
|
119
|
-
const insertBody = db.prepare(`INSERT INTO content (title, summary, text, "path") VALUES (?, ?, ?, ?)`);
|
|
178
|
+
const insertBody = db.prepare(`INSERT INTO content (rowid, title, summary, text, "path") VALUES ((SELECT rowid FROM frontmatter WHERE "path" = ?), ?, ?, ?, ?)`);
|
|
120
179
|
for (const doc of parsedDocs){
|
|
121
180
|
var _feature_store;
|
|
122
|
-
const values =
|
|
181
|
+
const values = writableColumns.map((col)=>{
|
|
123
182
|
var _doc_data_col;
|
|
124
183
|
if (col === 'path') return doc.relPath;
|
|
125
184
|
if (col === '_mtime') return doc.mtimeMs;
|
|
126
185
|
if (col === '_size') return doc.size;
|
|
127
186
|
return (_doc_data_col = doc.data[col]) !== null && _doc_data_col !== void 0 ? _doc_data_col : null;
|
|
128
187
|
});
|
|
188
|
+
// Frontmatter upsert first: content's rowid lookup below depends on this row existing.
|
|
129
189
|
insert.run(...values);
|
|
130
|
-
// Delete-before-insert only for docs that have rows: an FTS5 DELETE by
|
|
131
|
-
//
|
|
132
|
-
//
|
|
190
|
+
// Delete-before-insert only for docs that have rows: an FTS5 DELETE by rowid on a
|
|
191
|
+
// cold build (empty table, nothing to delete) is wasted work, and doing it
|
|
192
|
+
// unconditionally previously made the crawl quadratic when it scanned by column --
|
|
133
193
|
// measured 4x time per note-count doubling at 13k/26k notes.
|
|
134
194
|
if (existing.has(doc.relPath)) {
|
|
135
195
|
var _feature_remove1;
|
|
136
196
|
delBody.run(doc.relPath);
|
|
137
|
-
for (const feature of features)(_feature_remove1 = feature.remove) === null || _feature_remove1 === void 0 ? void 0 : _feature_remove1.call(feature, db, doc.relPath);
|
|
197
|
+
for (const feature of features)(_feature_remove1 = feature.remove) === null || _feature_remove1 === void 0 ? void 0 : _feature_remove1.call(feature, db, doc.relPath, delta);
|
|
138
198
|
}
|
|
139
|
-
insertBody.run(doc.search.title, doc.search.summary, doc.search.text, doc.relPath);
|
|
140
|
-
|
|
199
|
+
insertBody.run(doc.relPath, doc.search.title, doc.search.summary, doc.search.text, doc.relPath);
|
|
200
|
+
// Coverage is glob-derived, not content-derived, but only reparsed/added docs are
|
|
201
|
+
// touched here: a preset edit changes featureSignature and forces a full rebuild
|
|
202
|
+
// (see open()), so an unchanged doc's coverage is already correct on disk. New docs
|
|
203
|
+
// have no rows to clear -- skipping the delete keeps cold builds linear.
|
|
204
|
+
if (existing.has(doc.relPath)) delPresetFiles.run(doc.relPath);
|
|
205
|
+
for (const presetName of doc.presets)insertPresetFile.run(doc.relPath, presetName);
|
|
206
|
+
for (const feature of features)(_feature_store = feature.store) === null || _feature_store === void 0 ? void 0 : _feature_store.call(feature, db, doc.relPath, doc.extracted[feature.name], delta);
|
|
141
207
|
}
|
|
142
208
|
}
|
|
143
|
-
for (const feature of features)(_feature_afterReconcile = feature.afterReconcile) === null || _feature_afterReconcile === void 0 ? void 0 : _feature_afterReconcile.call(feature, db,
|
|
209
|
+
for (const feature of features)(_feature_afterReconcile = feature.afterReconcile) === null || _feature_afterReconcile === void 0 ? void 0 : _feature_afterReconcile.call(feature, db, delta);
|
|
144
210
|
db.exec('COMMIT');
|
|
145
211
|
} catch (err) {
|
|
146
212
|
db.exec('ROLLBACK');
|
|
147
213
|
throw err;
|
|
148
214
|
}
|
|
215
|
+
// Reconcile's own write-transaction duration, for open()'s derived busy_timeout (F):
|
|
216
|
+
// keep the observed max so a big watcher reconcile's lock hold is what the next open bounds its wait against.
|
|
217
|
+
const durationMs = Date.now() - txStart;
|
|
218
|
+
const prevRaw = getMeta(db, 'reconcile_max_ms');
|
|
219
|
+
// -1, not 0, so a genuinely 0ms first reconcile (sub-millisecond, common on a tiny tree)
|
|
220
|
+
// still gets recorded instead of losing to the "nothing recorded yet" default.
|
|
221
|
+
const prevMax = prevRaw === null ? -1 : Number(prevRaw);
|
|
222
|
+
if (durationMs > prevMax) setMeta(db, 'reconcile_max_ms', String(durationMs));
|
|
149
223
|
return {
|
|
150
224
|
parsed: parsedDocs.length,
|
|
151
225
|
warnings
|
|
152
226
|
};
|
|
153
227
|
}
|
|
228
|
+
// Names what moved between two feature signatures (see config.featureSignature's format:
|
|
229
|
+
// global features, embed provider, then one segment per preset) for the rebuild notice.
|
|
230
|
+
function signatureDiff(before, after) {
|
|
231
|
+
// Segment keys: `features`, `embed`, `preset:<name>` (config.featureSignature's format).
|
|
232
|
+
const keyOf = (part)=>part.startsWith('preset:') ? part.split(':').slice(0, 2).join(':') : part.split(':')[0];
|
|
233
|
+
const parse = (sig)=>new Map(sig.split('|').map((part)=>[
|
|
234
|
+
keyOf(part),
|
|
235
|
+
part
|
|
236
|
+
]));
|
|
237
|
+
const a = parse(before);
|
|
238
|
+
const b = parse(after);
|
|
239
|
+
const changed = new Set();
|
|
240
|
+
for (const [key, val] of b)if (a.get(key) !== val) changed.add(key);
|
|
241
|
+
for (const key of a.keys())if (!b.has(key)) changed.add(key);
|
|
242
|
+
const label = (key)=>key === 'embed' ? 'embed settings' : key.startsWith('preset:') ? `preset "${key.slice(7)}"` : 'features';
|
|
243
|
+
return changed.size === 0 ? 'features' : [
|
|
244
|
+
...changed
|
|
245
|
+
].map(label).join(', ');
|
|
246
|
+
}
|
|
154
247
|
export function open(cfg) {
|
|
248
|
+
var _getMeta;
|
|
155
249
|
const stateDir = join(cfg.baseDir, STATE_DIR);
|
|
156
250
|
mkdirSync(stateDir, {
|
|
157
251
|
recursive: true
|
|
@@ -172,6 +266,14 @@ export function open(cfg) {
|
|
|
172
266
|
const features = getMeta(db, 'features');
|
|
173
267
|
const wantFeatures = featureSignature(cfg);
|
|
174
268
|
if (version !== null && version !== SCHEMA_VERSION || features !== null && features !== wantFeatures) {
|
|
269
|
+
// Indexing derives from presets, so a config edit rebuilding the cache must say so and
|
|
270
|
+
// name what changed -- silent rebuilds make derived indexing look like a hang or a bug.
|
|
271
|
+
if (version !== null && version !== SCHEMA_VERSION) {
|
|
272
|
+
console.error('sense: cache format changed (new sensemaking version); rebuilding the index');
|
|
273
|
+
} else {
|
|
274
|
+
const changed = signatureDiff(features !== null && features !== void 0 ? features : '', wantFeatures);
|
|
275
|
+
console.error(`sense: config change (${changed}) rebuilds the index`);
|
|
276
|
+
}
|
|
175
277
|
db.close();
|
|
176
278
|
rmSync(stateDir, {
|
|
177
279
|
recursive: true,
|
|
@@ -180,6 +282,13 @@ export function open(cfg) {
|
|
|
180
282
|
return open(cfg);
|
|
181
283
|
}
|
|
182
284
|
ensureSchema(db, cfg);
|
|
285
|
+
// Derived from reconcile's own recorded max (F): 3x the largest reconcile this cache has
|
|
286
|
+
// ever held its write transaction for, floored at the 30s default and capped at 10min so
|
|
287
|
+
// one pathological build can't pin every later open to an unbounded wait. Installed
|
|
288
|
+
// before reconcile() below -- this open's own reconcile is exactly the operation that
|
|
289
|
+
// races a concurrent watcher's transaction and needs the derived wait.
|
|
290
|
+
const recordedMaxMs = Number((_getMeta = getMeta(db, 'reconcile_max_ms')) !== null && _getMeta !== void 0 ? _getMeta : '0');
|
|
291
|
+
db.exec(`PRAGMA busy_timeout = ${Math.min(Math.max(30000, 3 * recordedMaxMs), 600000)}`);
|
|
183
292
|
const { parsed, warnings } = reconcile(db, cfg, cfg.baseDir);
|
|
184
293
|
return {
|
|
185
294
|
db,
|
package/dist/esm/db.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/db.ts"],"sourcesContent":["import { mkdirSync, rmSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { DatabaseSync } from 'node:sqlite';\nimport type { Config, ResolvedConfig } from './config.ts';\nimport { featureSignature, STATE_DIR } from './config.ts';\nimport { activeFeatures } from './features/index.ts';\nimport type { ParsedDoc } from './scan.ts';\nimport { listFiles, parseFile } from './scan.ts';\n\n// Rows -> SQLite: core schema, reconcile loop, has(). Parsing lives in scan.ts;\n// everything beyond frontmatter + content lives in src/features/.\n\nexport const DB_FILENAME = 'cache.db';\n// Cache shape version, independent of the config's own `version`.\nexport const SCHEMA_VERSION = '4'; // 4: frontmatter scalars store as INTEGER/REAL/TEXT by YAML type (was: all numbers REAL)\n\nexport interface OpenResult {\n db: DatabaseSync;\n cfg: ResolvedConfig;\n dbPath: string;\n parsed: number;\n warnings: string[];\n}\n\nfunction quoteIdent(name: string): string {\n return `\"${name.split('\"').join('\"\"')}\"`;\n}\n\n// has(field, value): JSON-array field -> membership, string field -> substring, NULL -> false.\nfunction registerFunctions(db: DatabaseSync): void {\n db.function('has', { deterministic: true, varargs: false }, (field: unknown, value: unknown): number => {\n if (field === null || field === undefined) return 0;\n\n const needle = String(value);\n\n if (typeof field === 'string') {\n if (field.startsWith('[')) {\n try {\n const parsed = JSON.parse(field);\n if (Array.isArray(parsed)) {\n return parsed.some((item) => String(item) === needle) ? 1 : 0;\n }\n } catch {}\n }\n return field.includes(needle) ? 1 : 0;\n }\n\n return String(field).includes(needle) ? 1 : 0;\n });\n}\n\nfunction getColumns(db: DatabaseSync): Set<string> {\n const rows = db.prepare('PRAGMA table_info(frontmatter)').all() as Array<{ name: string }>;\n return new Set(rows.map((r) => r.name));\n}\n\n// Content is a separate table (not a column on frontmatter) so `SELECT * FROM frontmatter`\n// can't dump file text into context. Features add their own tables after the core ones.\nfunction ensureSchema(db: DatabaseSync, cfg: Config): void {\n db.exec(`CREATE TABLE IF NOT EXISTS frontmatter (\"path\" TEXT PRIMARY KEY, \"_mtime\" REAL, \"_size\" INTEGER)`);\n db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS content USING fts5(title, summary, text, path UNINDEXED, tokenize = 'porter unicode61')`);\n for (const feature of activeFeatures(cfg)) feature.schema(db);\n if (getMeta(db, 'schema_version') === null) setMeta(db, 'schema_version', SCHEMA_VERSION);\n if (getMeta(db, 'features') === null) setMeta(db, 'features', featureSignature(cfg));\n}\n\nexport function getMeta(db: DatabaseSync, key: string): string | null {\n const row = db.prepare('SELECT value FROM meta WHERE key = ?').get(key) as { value: string } | undefined;\n return row ? row.value : null;\n}\n\nexport function setMeta(db: DatabaseSync, key: string, value: string | null): void {\n if (value === null) {\n db.prepare('DELETE FROM meta WHERE key = ?').run(key);\n return;\n }\n db.prepare('INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value').run(key, value);\n}\n\nexport function docCount(db: DatabaseSync): number {\n const row = db.prepare('SELECT COUNT(*) AS n FROM frontmatter').get() as { n: number };\n return row.n;\n}\n\nexport function reconcile(db: DatabaseSync, cfg: Config, baseDir: string): { parsed: number; warnings: string[] } {\n const files = listFiles(cfg, baseDir);\n const currentSet = new Set(files.map((f) => f.relPath));\n\n const existingRows = db.prepare(`SELECT \"path\", \"_mtime\", \"_size\" FROM frontmatter`).all() as Array<{\n path: string;\n _mtime: number;\n _size: number;\n }>;\n const existing = new Map(existingRows.map((r) => [r.path, r]));\n const vanished = existingRows.filter((r) => !currentSet.has(r.path)).map((r) => r.path);\n\n const toReparse = files.filter((f) => {\n const row = existing.get(f.relPath);\n return !row || row._mtime !== f.mtimeMs || row._size !== f.size;\n });\n\n if (vanished.length === 0 && toReparse.length === 0) return { parsed: 0, warnings: [] };\n\n const features = activeFeatures(cfg);\n const seenColumns = getColumns(db);\n const newColumns: string[] = [];\n const parsedDocs: ParsedDoc[] = [];\n const warnings: string[] = [];\n\n for (const file of toReparse) {\n const { doc, warnings: fileWarnings } = parseFile(file, features);\n warnings.push(...fileWarnings);\n for (const key of Object.keys(doc.data)) {\n if (!seenColumns.has(key)) {\n seenColumns.add(key);\n newColumns.push(key);\n }\n }\n parsedDocs.push(doc);\n }\n\n const allColumns = [...seenColumns];\n const insertSql = `INSERT OR REPLACE INTO frontmatter (${allColumns.map(quoteIdent).join(', ')}) VALUES (${allColumns.map(() => '?').join(', ')})`;\n\n db.exec('BEGIN');\n try {\n for (const col of newColumns) db.exec(`ALTER TABLE frontmatter ADD COLUMN ${quoteIdent(col)}`);\n // FTS5 has no upsert, so delete-before-insert into `content`.\n const delBody = db.prepare(`DELETE FROM content WHERE \"path\" = ?`);\n if (vanished.length > 0) {\n const del = db.prepare(`DELETE FROM frontmatter WHERE \"path\" = ?`);\n for (const path of vanished) {\n del.run(path);\n delBody.run(path);\n for (const feature of features) feature.remove?.(db, path);\n }\n }\n if (parsedDocs.length > 0) {\n const insert = db.prepare(insertSql);\n const insertBody = db.prepare(`INSERT INTO content (title, summary, text, \"path\") VALUES (?, ?, ?, ?)`);\n for (const doc of parsedDocs) {\n const values = allColumns.map((col) => {\n if (col === 'path') return doc.relPath;\n if (col === '_mtime') return doc.mtimeMs;\n if (col === '_size') return doc.size;\n return doc.data[col] ?? null;\n });\n insert.run(...values);\n // Delete-before-insert only for docs that have rows: an FTS5 DELETE by column\n // cannot use an index and scans the whole table, so running it per doc on a\n // cold build (empty table, nothing to delete) made the crawl quadratic --\n // measured 4x time per note-count doubling at 13k/26k notes.\n if (existing.has(doc.relPath)) {\n delBody.run(doc.relPath);\n for (const feature of features) feature.remove?.(db, doc.relPath);\n }\n insertBody.run(doc.search.title, doc.search.summary, doc.search.text, doc.relPath);\n for (const feature of features) feature.store?.(db, doc.relPath, doc.extracted[feature.name]);\n }\n }\n for (const feature of features) feature.afterReconcile?.(db, files);\n db.exec('COMMIT');\n } catch (err) {\n db.exec('ROLLBACK');\n throw err;\n }\n\n return { parsed: parsedDocs.length, warnings };\n}\n\nexport function open(cfg: ResolvedConfig): OpenResult {\n const stateDir = join(cfg.baseDir, STATE_DIR);\n mkdirSync(stateDir, { recursive: true });\n const dbPath = join(stateDir, DB_FILENAME);\n\n const db = new DatabaseSync(dbPath);\n db.exec('PRAGMA journal_mode = WAL');\n // Covers a concurrent watcher's bulk reconcile: the write transaction for 500 changed\n // files measures ~5s at 26k notes, so 5s expired exactly at the boundary and queries\n // racing the watcher got SQLITE_BUSY. 30s bounds the wait at ~3x the largest measured\n // reconcile; a query that outwaits it still fails loudly.\n db.exec('PRAGMA busy_timeout = 30000');\n registerFunctions(db);\n\n db.exec('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)');\n\n // Schema-version or feature-set mismatch: reconcile only reparses changed files, so an\n // old cache can't be patched incrementally -- rebuild instead (cheap: nothing expensive lives here).\n const version = getMeta(db, 'schema_version');\n const features = getMeta(db, 'features');\n const wantFeatures = featureSignature(cfg);\n if ((version !== null && version !== SCHEMA_VERSION) || (features !== null && features !== wantFeatures)) {\n db.close();\n rmSync(stateDir, { recursive: true, force: true });\n return open(cfg);\n }\n\n ensureSchema(db, cfg);\n const { parsed, warnings } = reconcile(db, cfg, cfg.baseDir);\n\n return { db, cfg, dbPath, parsed, warnings };\n}\n\n// Manual reset for a doubted cache.\nexport function rebuild(cfg: ResolvedConfig): OpenResult {\n rmSync(join(cfg.baseDir, STATE_DIR), { recursive: true, force: true });\n return open(cfg);\n}\n"],"names":["mkdirSync","rmSync","join","DatabaseSync","featureSignature","STATE_DIR","activeFeatures","listFiles","parseFile","DB_FILENAME","SCHEMA_VERSION","quoteIdent","name","split","registerFunctions","db","function","deterministic","varargs","field","value","undefined","needle","String","startsWith","parsed","JSON","parse","Array","isArray","some","item","includes","getColumns","rows","prepare","all","Set","map","r","ensureSchema","cfg","exec","feature","schema","getMeta","setMeta","key","row","get","run","docCount","n","reconcile","baseDir","files","currentSet","f","relPath","existingRows","existing","Map","path","vanished","filter","has","toReparse","_mtime","mtimeMs","_size","size","length","warnings","features","seenColumns","newColumns","parsedDocs","file","doc","fileWarnings","push","Object","keys","data","add","allColumns","insertSql","col","delBody","del","remove","insert","insertBody","values","search","title","summary","text","store","extracted","afterReconcile","err","open","stateDir","recursive","dbPath","version","wantFeatures","close","force","rebuild"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/db.ts"],"sourcesContent":["import { mkdirSync, rmSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { DatabaseSync } from 'node:sqlite';\nimport type { Config, ResolvedConfig } from './config.ts';\nimport { featureSignature, STATE_DIR } from './config.ts';\nimport { SenseError } from './errors.ts';\nimport { activeFeatures } from './features/index.ts';\nimport type { ReconcileDelta } from './features/types.ts';\nimport { progress } from './progress.ts';\nimport type { ParsedDoc } from './scan.ts';\nimport { listFiles, parseFile, RESERVED_COLUMNS } from './scan.ts';\n\n// path/_mtime/_size are core: every reparse legitimately rewrites them. Every other\n// RESERVED_COLUMNS entry that shows up as a real frontmatter column (currently only\n// rank's `_rank`) is feature-owned -- scan.ts already refuses to let frontmatter set it,\n// so it must never appear in the upsert below, or a reparse would blow its last computed\n// value away with NULL on every touch, not just the reconciles that recompute it.\nconst CORE_FRONTMATTER_COLUMNS = new Set(['path', '_mtime', '_size']);\n\n// Rows -> SQLite: core schema, reconcile loop, has(). Parsing lives in scan.ts;\n// everything beyond frontmatter + content lives in src/features/.\n\nexport const DB_FILENAME = 'cache.db';\n// Cache shape version, independent of the config's own `version`.\n// 7: presets replace layers -- frontmatter drops the `layer` column, a new\n// preset_files(preset, path) table tracks per-preset coverage for status/map (was: 6,\n// frontmatter gains the `layer` column).\nexport const SCHEMA_VERSION = '8';\n\n// SQLite's compile-time SQLITE_MAX_COLUMN, default 2000 (https://www.sqlite.org/limits.html).\nconst MAX_FRONTMATTER_COLUMNS = 2000;\n\nexport interface OpenResult {\n db: DatabaseSync;\n cfg: ResolvedConfig;\n dbPath: string;\n parsed: number;\n warnings: string[];\n}\n\nfunction quoteIdent(name: string): string {\n return `\"${name.split('\"').join('\"\"')}\"`;\n}\n\n// has(field, value): JSON-array field -> membership, string field -> substring, NULL -> false.\nfunction registerFunctions(db: DatabaseSync): void {\n db.function('has', { deterministic: true, varargs: false }, (field: unknown, value: unknown): number => {\n if (field === null || field === undefined) return 0;\n\n const needle = String(value);\n\n if (typeof field === 'string') {\n if (field.startsWith('[')) {\n try {\n const parsed = JSON.parse(field);\n if (Array.isArray(parsed)) {\n return parsed.some((item) => String(item) === needle) ? 1 : 0;\n }\n } catch {}\n }\n return field.includes(needle) ? 1 : 0;\n }\n\n return String(field).includes(needle) ? 1 : 0;\n });\n}\n\nfunction getColumns(db: DatabaseSync): Set<string> {\n const rows = db.prepare('PRAGMA table_info(frontmatter)').all() as Array<{ name: string }>;\n return new Set(rows.map((r) => r.name));\n}\n\n// Content is a separate table (not a column on frontmatter) so `SELECT * FROM frontmatter`\n// can't dump file text into context. Features add their own tables after the core ones.\nfunction ensureSchema(db: DatabaseSync, cfg: Config): void {\n db.exec(`CREATE TABLE IF NOT EXISTS frontmatter (\"path\" TEXT PRIMARY KEY, \"_mtime\" REAL, \"_size\" INTEGER)`);\n db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS content USING fts5(title, summary, text, path UNINDEXED, tokenize = 'porter unicode61')`);\n // Coverage, not ownership: a path can appear under several presets. Rebuilt per-file\n // alongside frontmatter/content at reconcile so status/map can report matched/embedded\n // counts per preset without recomputing globs at read time.\n // path leads the PK so the per-doc delete in reconcile is an index hit -- keyed the other\n // way it scans the whole table per doc, which made cold builds quadratic (measured 3x cost\n // per note-count doubling at 13k/26k). Coverage-by-preset reads get their own index.\n db.exec(`CREATE TABLE IF NOT EXISTS preset_files (\"path\" TEXT, preset TEXT, PRIMARY KEY (\"path\", preset))`);\n db.exec('CREATE INDEX IF NOT EXISTS preset_files_preset ON preset_files(preset)');\n for (const feature of activeFeatures(cfg)) feature.schema(db);\n if (getMeta(db, 'schema_version') === null) setMeta(db, 'schema_version', SCHEMA_VERSION);\n if (getMeta(db, 'features') === null) setMeta(db, 'features', featureSignature(cfg));\n}\n\nexport function getMeta(db: DatabaseSync, key: string): string | null {\n const row = db.prepare('SELECT value FROM meta WHERE key = ?').get(key) as { value: string } | undefined;\n return row ? row.value : null;\n}\n\nexport function setMeta(db: DatabaseSync, key: string, value: string | null): void {\n if (value === null) {\n db.prepare('DELETE FROM meta WHERE key = ?').run(key);\n return;\n }\n db.prepare('INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value').run(key, value);\n}\n\nexport function docCount(db: DatabaseSync): number {\n const row = db.prepare('SELECT COUNT(*) AS n FROM frontmatter').get() as { n: number };\n return row.n;\n}\n\nexport function reconcile(db: DatabaseSync, cfg: Config, baseDir: string): { parsed: number; warnings: string[] } {\n const files = listFiles(cfg, baseDir);\n const currentSet = new Set(files.map((f) => f.relPath));\n\n const existingRows = db.prepare(`SELECT \"path\", \"_mtime\", \"_size\" FROM frontmatter`).all() as Array<{\n path: string;\n _mtime: number;\n _size: number;\n }>;\n const existing = new Map(existingRows.map((r) => [r.path, r]));\n const vanished = existingRows.filter((r) => !currentSet.has(r.path)).map((r) => r.path);\n\n const toReparse = files.filter((f) => {\n const row = existing.get(f.relPath);\n return !row || row._mtime !== f.mtimeMs || row._size !== f.size;\n });\n\n if (vanished.length === 0 && toReparse.length === 0) return { parsed: 0, warnings: [] };\n\n const features = activeFeatures(cfg);\n const seenColumns = getColumns(db);\n const newColumns: string[] = [];\n const parsedDocs: ParsedDoc[] = [];\n const warnings: string[] = [];\n\n // Bulk reparses (a sync, a cold build) are the long silences a query can hit; short\n // reconciles stay silent (progress() has a threshold).\n const report = progress('reparsing files', toReparse.length);\n let parsedCount = 0;\n for (const file of toReparse) {\n // A doc only gets extract/store from features that apply to it (currently: embed, via\n // FileStat.embed -- true iff a covering preset has semantic on).\n const fileFeatures = features.filter((feature) => !feature.enabledForFile || feature.enabledForFile(cfg, file));\n const { doc, warnings: fileWarnings } = parseFile(file, fileFeatures);\n report.tick(++parsedCount);\n warnings.push(...fileWarnings);\n for (const key of Object.keys(doc.data)) {\n if (!seenColumns.has(key)) {\n seenColumns.add(key);\n newColumns.push(key);\n }\n }\n parsedDocs.push(doc);\n }\n report.finish();\n\n const allColumns = [...seenColumns];\n // Fence before ALTERing: SQLite's own failure past this point is a raw\n // \"too many columns on sqlite_altertab_frontmatter\" with no indication of the boundary or the levers.\n if (allColumns.length > MAX_FRONTMATTER_COLUMNS) {\n throw new SenseError(\n 'COLUMN_LIMIT',\n `frontmatter would need ${allColumns.length} columns, crossing SQLite's compile-time SQLITE_MAX_COLUMN limit (default ${MAX_FRONTMATTER_COLUMNS}; see https://www.sqlite.org/limits.html). Narrow the presets' include globs so fewer/other files are indexed, or fix whatever is generating unbounded frontmatter keys.`\n );\n }\n // Columns the frontmatter upsert actually writes: core + parsed frontmatter keys, never a\n // feature-owned reserved column (see CORE_FRONTMATTER_COLUMNS above).\n const writableColumns = allColumns.filter((c) => CORE_FRONTMATTER_COLUMNS.has(c) || !RESERVED_COLUMNS.has(c));\n // ON CONFLICT UPDATE (not OR REPLACE) keeps the row's rowid stable across reparses --\n // content rows are coupled to that rowid below.\n const insertSql = `INSERT INTO frontmatter (${writableColumns.map(quoteIdent).join(', ')}) VALUES (${writableColumns.map(() => '?').join(', ')}) ON CONFLICT(\"path\") DO UPDATE SET ${writableColumns\n .filter((c) => c !== 'path')\n .map((c) => `${quoteIdent(c)} = excluded.${quoteIdent(c)}`)\n .join(', ')}`;\n\n const added = toReparse.filter((f) => !existing.has(f.relPath)).map((f) => f.relPath);\n const delta: ReconcileDelta = { files, reparsed: parsedDocs.map((d) => d.relPath), added, vanished };\n\n const txStart = Date.now();\n db.exec('BEGIN');\n try {\n for (const col of newColumns) db.exec(`ALTER TABLE frontmatter ADD COLUMN ${quoteIdent(col)}`);\n // FTS5 has no upsert, so delete-before-insert into `content`; coupled to the\n // frontmatter rowid (indexed via its PRIMARY KEY) instead of the UNINDEXED `path`\n // column, which a per-row DELETE would otherwise scan the whole table to find.\n const delBody = db.prepare(`DELETE FROM content WHERE rowid = (SELECT rowid FROM frontmatter WHERE \"path\" = ?)`);\n const delPresetFiles = db.prepare(`DELETE FROM preset_files WHERE \"path\" = ?`);\n const insertPresetFile = db.prepare(`INSERT INTO preset_files (\"path\", preset) VALUES (?, ?)`);\n if (vanished.length > 0) {\n const del = db.prepare(`DELETE FROM frontmatter WHERE \"path\" = ?`);\n for (const path of vanished) {\n // content delete must run first: it looks up the frontmatter rowid by path, which\n // the frontmatter delete below would otherwise have already removed.\n delBody.run(path);\n del.run(path);\n delPresetFiles.run(path);\n for (const feature of features) feature.remove?.(db, path, delta);\n }\n }\n if (parsedDocs.length > 0) {\n const insert = db.prepare(insertSql);\n const insertBody = db.prepare(`INSERT INTO content (rowid, title, summary, text, \"path\") VALUES ((SELECT rowid FROM frontmatter WHERE \"path\" = ?), ?, ?, ?, ?)`);\n for (const doc of parsedDocs) {\n const values = writableColumns.map((col) => {\n if (col === 'path') return doc.relPath;\n if (col === '_mtime') return doc.mtimeMs;\n if (col === '_size') return doc.size;\n return doc.data[col] ?? null;\n });\n // Frontmatter upsert first: content's rowid lookup below depends on this row existing.\n insert.run(...values);\n // Delete-before-insert only for docs that have rows: an FTS5 DELETE by rowid on a\n // cold build (empty table, nothing to delete) is wasted work, and doing it\n // unconditionally previously made the crawl quadratic when it scanned by column --\n // measured 4x time per note-count doubling at 13k/26k notes.\n if (existing.has(doc.relPath)) {\n delBody.run(doc.relPath);\n for (const feature of features) feature.remove?.(db, doc.relPath, delta);\n }\n insertBody.run(doc.relPath, doc.search.title, doc.search.summary, doc.search.text, doc.relPath);\n // Coverage is glob-derived, not content-derived, but only reparsed/added docs are\n // touched here: a preset edit changes featureSignature and forces a full rebuild\n // (see open()), so an unchanged doc's coverage is already correct on disk. New docs\n // have no rows to clear -- skipping the delete keeps cold builds linear.\n if (existing.has(doc.relPath)) delPresetFiles.run(doc.relPath);\n for (const presetName of doc.presets) insertPresetFile.run(doc.relPath, presetName);\n for (const feature of features) feature.store?.(db, doc.relPath, doc.extracted[feature.name], delta);\n }\n }\n for (const feature of features) feature.afterReconcile?.(db, delta);\n db.exec('COMMIT');\n } catch (err) {\n db.exec('ROLLBACK');\n throw err;\n }\n\n // Reconcile's own write-transaction duration, for open()'s derived busy_timeout (F):\n // keep the observed max so a big watcher reconcile's lock hold is what the next open bounds its wait against.\n const durationMs = Date.now() - txStart;\n const prevRaw = getMeta(db, 'reconcile_max_ms');\n // -1, not 0, so a genuinely 0ms first reconcile (sub-millisecond, common on a tiny tree)\n // still gets recorded instead of losing to the \"nothing recorded yet\" default.\n const prevMax = prevRaw === null ? -1 : Number(prevRaw);\n if (durationMs > prevMax) setMeta(db, 'reconcile_max_ms', String(durationMs));\n\n return { parsed: parsedDocs.length, warnings };\n}\n\n// Names what moved between two feature signatures (see config.featureSignature's format:\n// global features, embed provider, then one segment per preset) for the rebuild notice.\nfunction signatureDiff(before: string, after: string): string {\n // Segment keys: `features`, `embed`, `preset:<name>` (config.featureSignature's format).\n const keyOf = (part: string) => (part.startsWith('preset:') ? part.split(':').slice(0, 2).join(':') : part.split(':')[0]);\n const parse = (sig: string) => new Map(sig.split('|').map((part) => [keyOf(part), part]));\n const a = parse(before);\n const b = parse(after);\n const changed = new Set<string>();\n for (const [key, val] of b) if (a.get(key) !== val) changed.add(key);\n for (const key of a.keys()) if (!b.has(key)) changed.add(key);\n const label = (key: string) => (key === 'embed' ? 'embed settings' : key.startsWith('preset:') ? `preset \"${key.slice(7)}\"` : 'features');\n return changed.size === 0 ? 'features' : [...changed].map(label).join(', ');\n}\n\nexport function open(cfg: ResolvedConfig): OpenResult {\n const stateDir = join(cfg.baseDir, STATE_DIR);\n mkdirSync(stateDir, { recursive: true });\n const dbPath = join(stateDir, DB_FILENAME);\n\n const db = new DatabaseSync(dbPath);\n db.exec('PRAGMA journal_mode = WAL');\n // Covers a concurrent watcher's bulk reconcile: the write transaction for 500 changed\n // files measures ~5s at 26k notes, so 5s expired exactly at the boundary and queries\n // racing the watcher got SQLITE_BUSY. 30s bounds the wait at ~3x the largest measured\n // reconcile; a query that outwaits it still fails loudly.\n db.exec('PRAGMA busy_timeout = 30000');\n registerFunctions(db);\n\n db.exec('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)');\n\n // Schema-version or feature-set mismatch: reconcile only reparses changed files, so an\n // old cache can't be patched incrementally -- rebuild instead (cheap: nothing expensive lives here).\n const version = getMeta(db, 'schema_version');\n const features = getMeta(db, 'features');\n const wantFeatures = featureSignature(cfg);\n if ((version !== null && version !== SCHEMA_VERSION) || (features !== null && features !== wantFeatures)) {\n // Indexing derives from presets, so a config edit rebuilding the cache must say so and\n // name what changed -- silent rebuilds make derived indexing look like a hang or a bug.\n if (version !== null && version !== SCHEMA_VERSION) {\n console.error('sense: cache format changed (new sensemaking version); rebuilding the index');\n } else {\n const changed = signatureDiff(features ?? '', wantFeatures);\n console.error(`sense: config change (${changed}) rebuilds the index`);\n }\n db.close();\n rmSync(stateDir, { recursive: true, force: true });\n return open(cfg);\n }\n\n ensureSchema(db, cfg);\n\n // Derived from reconcile's own recorded max (F): 3x the largest reconcile this cache has\n // ever held its write transaction for, floored at the 30s default and capped at 10min so\n // one pathological build can't pin every later open to an unbounded wait. Installed\n // before reconcile() below -- this open's own reconcile is exactly the operation that\n // races a concurrent watcher's transaction and needs the derived wait.\n const recordedMaxMs = Number(getMeta(db, 'reconcile_max_ms') ?? '0');\n db.exec(`PRAGMA busy_timeout = ${Math.min(Math.max(30000, 3 * recordedMaxMs), 600_000)}`);\n\n const { parsed, warnings } = reconcile(db, cfg, cfg.baseDir);\n\n return { db, cfg, dbPath, parsed, warnings };\n}\n\n// Manual reset for a doubted cache.\nexport function rebuild(cfg: ResolvedConfig): OpenResult {\n rmSync(join(cfg.baseDir, STATE_DIR), { recursive: true, force: true });\n return open(cfg);\n}\n"],"names":["mkdirSync","rmSync","join","DatabaseSync","featureSignature","STATE_DIR","SenseError","activeFeatures","progress","listFiles","parseFile","RESERVED_COLUMNS","CORE_FRONTMATTER_COLUMNS","Set","DB_FILENAME","SCHEMA_VERSION","MAX_FRONTMATTER_COLUMNS","quoteIdent","name","split","registerFunctions","db","function","deterministic","varargs","field","value","undefined","needle","String","startsWith","parsed","JSON","parse","Array","isArray","some","item","includes","getColumns","rows","prepare","all","map","r","ensureSchema","cfg","exec","feature","schema","getMeta","setMeta","key","row","get","run","docCount","n","reconcile","baseDir","files","currentSet","f","relPath","existingRows","existing","Map","path","vanished","filter","has","toReparse","_mtime","mtimeMs","_size","size","length","warnings","features","seenColumns","newColumns","parsedDocs","report","parsedCount","file","fileFeatures","enabledForFile","doc","fileWarnings","tick","push","Object","keys","data","add","finish","allColumns","writableColumns","c","insertSql","added","delta","reparsed","d","txStart","Date","now","col","delBody","delPresetFiles","insertPresetFile","del","remove","insert","insertBody","values","search","title","summary","text","presetName","presets","store","extracted","afterReconcile","err","durationMs","prevRaw","prevMax","Number","signatureDiff","before","after","keyOf","part","slice","sig","a","b","changed","val","label","open","stateDir","recursive","dbPath","version","wantFeatures","console","error","close","force","recordedMaxMs","Math","min","max","rebuild"],"mappings":"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"}
|
package/dist/esm/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type SenseErrorCode = 'CONFIG_NOT_FOUND' | 'CONFIG_EXISTS' | 'CONFIG_INVALID' | 'CONFIG_VERSION_UNSUPPORTED' | 'WATCH_ACTIVE' | 'NOTE_NOT_FOUND' | 'NOTE_AMBIGUOUS' | 'EMBED_DISABLED' | 'EMBED_MODEL' | 'SEARCH_SYNTAX';
|
|
1
|
+
export type SenseErrorCode = 'CONFIG_NOT_FOUND' | 'CONFIG_EXISTS' | 'CONFIG_INVALID' | 'CONFIG_VERSION_UNSUPPORTED' | 'WATCH_ACTIVE' | 'NOTE_NOT_FOUND' | 'NOTE_AMBIGUOUS' | 'EMBED_DISABLED' | 'EMBED_MODEL' | 'SEARCH_SYNTAX' | 'COLUMN_LIMIT' | 'PRESET_UNKNOWN';
|
|
2
2
|
export declare class SenseError extends Error {
|
|
3
3
|
code: SenseErrorCode;
|
|
4
4
|
constructor(code: SenseErrorCode, message: string);
|
package/dist/esm/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/errors.ts"],"sourcesContent":["// Library code throws; only cli.ts prints and exits.\nexport type SenseErrorCode = 'CONFIG_NOT_FOUND' | 'CONFIG_EXISTS' | 'CONFIG_INVALID' | 'CONFIG_VERSION_UNSUPPORTED' | 'WATCH_ACTIVE' | 'NOTE_NOT_FOUND' | 'NOTE_AMBIGUOUS' | 'EMBED_DISABLED' | 'EMBED_MODEL' | 'SEARCH_SYNTAX';\n\nexport class SenseError extends Error {\n code: SenseErrorCode;\n\n constructor(code: SenseErrorCode, message: string) {\n super(message);\n this.name = 'SenseError';\n this.code = code;\n }\n}\n"],"names":["SenseError","Error","code","message","name"],"mappings":"AAAA,qDAAqD;AAGrD,OAAO,MAAMA,mBAAmBC;IAG9B,YAAYC,IAAoB,EAAEC,OAAe,CAAE;QACjD,KAAK,CAACA;QACN,IAAI,CAACC,IAAI,GAAG;QACZ,IAAI,CAACF,IAAI,GAAGA;IACd;AACF"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/errors.ts"],"sourcesContent":["// Library code throws; only cli.ts prints and exits.\nexport type SenseErrorCode = 'CONFIG_NOT_FOUND' | 'CONFIG_EXISTS' | 'CONFIG_INVALID' | 'CONFIG_VERSION_UNSUPPORTED' | 'WATCH_ACTIVE' | 'NOTE_NOT_FOUND' | 'NOTE_AMBIGUOUS' | 'EMBED_DISABLED' | 'EMBED_MODEL' | 'SEARCH_SYNTAX' | 'COLUMN_LIMIT' | 'PRESET_UNKNOWN';\n\nexport class SenseError extends Error {\n code: SenseErrorCode;\n\n constructor(code: SenseErrorCode, message: string) {\n super(message);\n this.name = 'SenseError';\n this.code = code;\n }\n}\n"],"names":["SenseError","Error","code","message","name"],"mappings":"AAAA,qDAAqD;AAGrD,OAAO,MAAMA,mBAAmBC;IAG9B,YAAYC,IAAoB,EAAEC,OAAe,CAAE;QACjD,KAAK,CAACA;QACN,IAAI,CAACC,IAAI,GAAG;QACZ,IAAI,CAACF,IAAI,GAAGA;IACd;AACF"}
|
|
@@ -3,6 +3,7 @@ import { homedir } from 'node:os';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { embedConfig } from '../config.js';
|
|
5
5
|
import { SenseError } from '../errors.js';
|
|
6
|
+
import { progress } from '../progress.js';
|
|
6
7
|
import { parseFile } from '../scan.js';
|
|
7
8
|
// embeddings(path, chunk, start_line, end_line, scale, vector): heading-based chunks,
|
|
8
9
|
// int8 vectors with a per-vector dequantization scale, vector NULL = not yet embedded.
|
|
@@ -60,9 +61,15 @@ export const embed = {
|
|
|
60
61
|
remove (db, path) {
|
|
61
62
|
db.prepare('DELETE FROM embeddings WHERE "path" = ?').run(path);
|
|
62
63
|
},
|
|
64
|
+
// A doc covered only by semantic-false presets never had extract() run for it (db.ts's
|
|
65
|
+
// per-file filter skips it), so extracted is undefined here -- store nothing, i.e. no rows.
|
|
63
66
|
store (db, path, extracted) {
|
|
67
|
+
if (!extracted) return;
|
|
64
68
|
const insert = db.prepare('INSERT INTO embeddings ("path", chunk, start_line, end_line, scale, vector) VALUES (?, ?, ?, ?, NULL, NULL)');
|
|
65
69
|
extracted.forEach((c, idx)=>insert.run(path, idx, c.startLine, c.endLine));
|
|
70
|
+
},
|
|
71
|
+
enabledForFile (_cfg, file) {
|
|
72
|
+
return file.embed;
|
|
66
73
|
}
|
|
67
74
|
};
|
|
68
75
|
// --- static type: Model2Vec safetensors + pure-JS tokenizer, model cached in ~/.cache ---
|
|
@@ -171,7 +178,7 @@ const providers = new Map();
|
|
|
171
178
|
function getProvider(cfg) {
|
|
172
179
|
var _e_url;
|
|
173
180
|
const e = embedConfig(cfg);
|
|
174
|
-
if (!e) throw new SenseError('EMBED_DISABLED', 'semantic
|
|
181
|
+
if (!e) throw new SenseError('EMBED_DISABLED', 'semantic search needs at least one preset with vectors on (semantic !== false); every declared preset in sense.config.json has "semantic": false');
|
|
175
182
|
const sig = `${e.type}:${e.model}:${(_e_url = e.url) !== null && _e_url !== void 0 ? _e_url : ''}`;
|
|
176
183
|
let p = providers.get(sig);
|
|
177
184
|
if (!p) {
|
|
@@ -216,11 +223,15 @@ export async function embedPending(db, cfg, baseDir) {
|
|
|
216
223
|
for (const [path, chunkIdxs] of byPath){
|
|
217
224
|
let chunks;
|
|
218
225
|
try {
|
|
226
|
+
// presets/embed are irrelevant here -- re-deriving chunk text for a doc that already
|
|
227
|
+
// has embeddings rows means it was covered by a semantic-on preset at reconcile time.
|
|
219
228
|
chunks = parseFile({
|
|
220
229
|
relPath: path,
|
|
221
230
|
absPath: join(baseDir, path),
|
|
222
231
|
mtimeMs: 0,
|
|
223
|
-
size: 0
|
|
232
|
+
size: 0,
|
|
233
|
+
presets: [],
|
|
234
|
+
embed: true
|
|
224
235
|
}, [
|
|
225
236
|
embed
|
|
226
237
|
]).doc.extracted.embed;
|
|
@@ -234,6 +245,9 @@ export async function embedPending(db, cfg, baseDir) {
|
|
|
234
245
|
});
|
|
235
246
|
}
|
|
236
247
|
const update = db.prepare('UPDATE embeddings SET scale = ?, vector = ? WHERE "path" = ? AND chunk = ?');
|
|
248
|
+
// The lazy build is the one long silence a first --semantic query hits (measured 23s at
|
|
249
|
+
// 26k notes); progress makes it distinguishable from a hang.
|
|
250
|
+
const report = progress('embedding chunks', jobs.length);
|
|
237
251
|
for(let i = 0; i < jobs.length; i += BATCH){
|
|
238
252
|
const batch = jobs.slice(i, i + BATCH);
|
|
239
253
|
const vectors = await provider.embed(batch.map((j)=>j.text));
|
|
@@ -250,7 +264,9 @@ export async function embedPending(db, cfg, baseDir) {
|
|
|
250
264
|
db.exec('ROLLBACK');
|
|
251
265
|
throw err;
|
|
252
266
|
}
|
|
267
|
+
report.tick(Math.min(i + BATCH, jobs.length));
|
|
253
268
|
}
|
|
269
|
+
report.finish();
|
|
254
270
|
}
|
|
255
271
|
// Top candidates by cosine for a semantic find: best chunk per file, its line range
|
|
256
272
|
// riding along. FTS5 operators in the terms are lexical syntax, not meaning -- stripped
|