sensemaking 0.15.3 → 0.16.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 +33 -5
- package/dist/cjs/cli/download.js +15 -19
- package/dist/cjs/cli/download.js.map +1 -1
- package/dist/cjs/cli/index.d.cts +1 -1
- package/dist/cjs/cli/index.d.ts +1 -1
- package/dist/cjs/cli/index.js +9 -20
- package/dist/cjs/cli/index.js.map +1 -1
- package/dist/cjs/cli/init.js +31 -5
- package/dist/cjs/cli/init.js.map +1 -1
- package/dist/cjs/cli/map.js +1 -3
- package/dist/cjs/cli/map.js.map +1 -1
- package/dist/cjs/cli/named.js +3 -8
- package/dist/cjs/cli/named.js.map +1 -1
- package/dist/cjs/cli/path.js +4 -7
- package/dist/cjs/cli/path.js.map +1 -1
- package/dist/cjs/cli/peek.js +2 -4
- package/dist/cjs/cli/peek.js.map +1 -1
- package/dist/cjs/cli/related.js +14 -20
- package/dist/cjs/cli/related.js.map +1 -1
- package/dist/cjs/cli/search.js +12 -17
- package/dist/cjs/cli/search.js.map +1 -1
- package/dist/cjs/cli/shared.js +17 -21
- package/dist/cjs/cli/shared.js.map +1 -1
- package/dist/cjs/cli/sql.js +7 -8
- package/dist/cjs/cli/sql.js.map +1 -1
- package/dist/cjs/cli/status.js +344 -102
- package/dist/cjs/cli/status.js.map +1 -1
- package/dist/cjs/cli/watch.js +3 -8
- package/dist/cjs/cli/watch.js.map +1 -1
- package/dist/cjs/cli.js +22 -34
- package/dist/cjs/cli.js.map +1 -1
- package/dist/cjs/column-hint.js +4 -2
- package/dist/cjs/column-hint.js.map +1 -1
- package/dist/cjs/commands/peek.js +5 -3
- package/dist/cjs/commands/peek.js.map +1 -1
- package/dist/cjs/commands/related.js +31 -28
- package/dist/cjs/commands/related.js.map +1 -1
- package/dist/cjs/commands/scope.js +4 -2
- package/dist/cjs/commands/scope.js.map +1 -1
- package/dist/cjs/commands/search.js +36 -108
- package/dist/cjs/commands/search.js.map +1 -1
- package/dist/cjs/commands/signals.d.cts +18 -0
- package/dist/cjs/commands/signals.d.ts +18 -0
- package/dist/cjs/commands/signals.js +299 -0
- package/dist/cjs/commands/signals.js.map +1 -0
- package/dist/cjs/commands/status.d.cts +2 -2
- package/dist/cjs/commands/status.d.ts +2 -2
- package/dist/cjs/commands/status.js +1 -1
- package/dist/cjs/commands/status.js.map +1 -1
- package/dist/cjs/config/access.d.cts +5 -2
- package/dist/cjs/config/access.d.ts +5 -2
- package/dist/cjs/config/access.js +34 -14
- package/dist/cjs/config/access.js.map +1 -1
- package/dist/cjs/config/index.d.cts +4 -1
- package/dist/cjs/config/index.d.ts +4 -1
- package/dist/cjs/config/index.js +12 -2
- package/dist/cjs/config/index.js.map +1 -1
- package/dist/cjs/config/load.d.cts +6 -1
- package/dist/cjs/config/load.d.ts +6 -1
- package/dist/cjs/config/load.js +87 -15
- package/dist/cjs/config/load.js.map +1 -1
- package/dist/cjs/config/resolve.js +1 -1
- package/dist/cjs/config/resolve.js.map +1 -1
- package/dist/cjs/config/signals.d.cts +4 -0
- package/dist/cjs/config/signals.d.ts +4 -0
- package/dist/cjs/config/signals.js +30 -0
- package/dist/cjs/config/signals.js.map +1 -0
- package/dist/cjs/config/types.d.cts +6 -4
- package/dist/cjs/config/types.d.ts +6 -4
- package/dist/cjs/config/types.js +1 -1
- package/dist/cjs/config/types.js.map +1 -1
- package/dist/cjs/config/validate.js +121 -12
- package/dist/cjs/config/validate.js.map +1 -1
- package/dist/cjs/db/open.d.cts +1 -1
- package/dist/cjs/db/open.d.ts +1 -1
- package/dist/cjs/db/open.js +10 -3
- package/dist/cjs/db/open.js.map +1 -1
- package/dist/cjs/db/reconcile.d.cts +1 -0
- package/dist/cjs/db/reconcile.d.ts +1 -0
- package/dist/cjs/db/reconcile.js +20 -3
- package/dist/cjs/db/reconcile.js.map +1 -1
- package/dist/cjs/embed/cohere.d.cts +2 -0
- package/dist/cjs/embed/cohere.d.ts +2 -0
- package/dist/cjs/embed/cohere.js +238 -0
- package/dist/cjs/embed/cohere.js.map +1 -0
- package/dist/cjs/embed/http.d.cts +2 -0
- package/dist/cjs/embed/http.d.ts +2 -0
- package/dist/cjs/embed/http.js +258 -0
- package/dist/cjs/embed/http.js.map +1 -0
- package/dist/cjs/embed/identity.d.cts +13 -0
- package/dist/cjs/embed/identity.d.ts +13 -0
- package/dist/cjs/embed/identity.js +194 -0
- package/dist/cjs/embed/identity.js.map +1 -0
- package/dist/cjs/embed/langfit.d.cts +6 -0
- package/dist/cjs/embed/langfit.d.ts +6 -0
- package/dist/cjs/embed/langfit.js +326 -0
- package/dist/cjs/embed/langfit.js.map +1 -0
- package/dist/cjs/embed/languages.d.cts +3 -0
- package/dist/cjs/embed/languages.d.ts +3 -0
- package/dist/cjs/embed/languages.js +98 -0
- package/dist/cjs/embed/languages.js.map +1 -0
- package/dist/cjs/embed/openai.d.cts +2 -0
- package/dist/cjs/embed/openai.d.ts +2 -0
- package/dist/cjs/embed/openai.js +268 -0
- package/dist/cjs/embed/openai.js.map +1 -0
- package/dist/cjs/embed/query.d.cts +21 -0
- package/dist/cjs/embed/query.d.ts +21 -0
- package/dist/cjs/embed/query.js +610 -0
- package/dist/cjs/embed/query.js.map +1 -0
- package/dist/cjs/embed/registry.d.cts +3 -0
- package/dist/cjs/embed/registry.d.ts +3 -0
- package/dist/cjs/embed/registry.js +114 -0
- package/dist/cjs/embed/registry.js.map +1 -0
- package/dist/cjs/embed/static.d.cts +2 -0
- package/dist/cjs/embed/static.d.ts +2 -0
- package/dist/cjs/embed/static.js +367 -0
- package/dist/cjs/embed/static.js.map +1 -0
- package/dist/cjs/embed/store.d.cts +8 -0
- package/dist/cjs/embed/store.d.ts +8 -0
- package/dist/cjs/embed/store.js +498 -0
- package/dist/cjs/embed/store.js.map +1 -0
- package/dist/cjs/embed/types.d.cts +8 -0
- package/dist/cjs/embed/types.d.ts +8 -0
- package/dist/cjs/embed/types.js +7 -0
- package/dist/cjs/embed/types.js.map +1 -0
- package/dist/cjs/errors.d.cts +1 -1
- package/dist/cjs/errors.d.ts +1 -1
- package/dist/cjs/errors.js +15 -24
- package/dist/cjs/errors.js.map +1 -1
- package/dist/cjs/features/embed.d.cts +4 -27
- package/dist/cjs/features/embed.d.ts +4 -27
- package/dist/cjs/features/embed.js +6 -978
- package/dist/cjs/features/embed.js.map +1 -1
- package/dist/cjs/features/links.js +4 -2
- package/dist/cjs/features/links.js.map +1 -1
- package/dist/cjs/features/rank.js +1 -1
- package/dist/cjs/features/tags.js +4 -2
- package/dist/cjs/features/tags.js.map +1 -1
- package/dist/cjs/fences.js.map +1 -1
- package/dist/cjs/graph.js +5 -3
- package/dist/cjs/graph.js.map +1 -1
- package/dist/cjs/index.d.cts +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/output.d.cts +2 -2
- package/dist/cjs/output.d.ts +2 -2
- package/dist/cjs/output.js +15 -8
- package/dist/cjs/output.js.map +1 -1
- package/dist/cjs/scan.js +13 -8
- package/dist/cjs/scan.js.map +1 -1
- package/dist/cjs/segment.js +67 -14
- package/dist/cjs/segment.js.map +1 -1
- package/dist/cjs/traverse.js +4 -2
- package/dist/cjs/traverse.js.map +1 -1
- package/dist/cjs/watch.js +2 -5
- package/dist/cjs/watch.js.map +1 -1
- package/dist/esm/cli/download.js +9 -8
- package/dist/esm/cli/download.js.map +1 -1
- package/dist/esm/cli/index.d.ts +1 -1
- package/dist/esm/cli/index.js +1 -1
- package/dist/esm/cli/index.js.map +1 -1
- package/dist/esm/cli/init.js +32 -6
- package/dist/esm/cli/init.js.map +1 -1
- package/dist/esm/cli/status.js +36 -8
- package/dist/esm/cli/status.js.map +1 -1
- package/dist/esm/commands/related.js +14 -10
- package/dist/esm/commands/related.js.map +1 -1
- package/dist/esm/commands/search.js +27 -78
- package/dist/esm/commands/search.js.map +1 -1
- package/dist/esm/commands/signals.d.ts +18 -0
- package/dist/esm/commands/signals.js +78 -0
- package/dist/esm/commands/signals.js.map +1 -0
- package/dist/esm/commands/status.d.ts +2 -2
- package/dist/esm/commands/status.js +2 -2
- package/dist/esm/commands/status.js.map +1 -1
- package/dist/esm/config/access.d.ts +5 -2
- package/dist/esm/config/access.js +31 -12
- package/dist/esm/config/access.js.map +1 -1
- package/dist/esm/config/index.d.ts +4 -1
- package/dist/esm/config/index.js +2 -1
- package/dist/esm/config/index.js.map +1 -1
- package/dist/esm/config/load.d.ts +6 -1
- package/dist/esm/config/load.js +67 -9
- package/dist/esm/config/load.js.map +1 -1
- package/dist/esm/config/resolve.js +2 -2
- package/dist/esm/config/resolve.js.map +1 -1
- package/dist/esm/config/signals.d.ts +4 -0
- package/dist/esm/config/signals.js +14 -0
- package/dist/esm/config/signals.js.map +1 -0
- package/dist/esm/config/types.d.ts +6 -4
- package/dist/esm/config/types.js +1 -1
- package/dist/esm/config/types.js.map +1 -1
- package/dist/esm/config/validate.js +58 -11
- package/dist/esm/config/validate.js.map +1 -1
- package/dist/esm/db/open.d.ts +1 -1
- package/dist/esm/db/open.js +7 -2
- package/dist/esm/db/open.js.map +1 -1
- package/dist/esm/db/reconcile.d.ts +1 -0
- package/dist/esm/db/reconcile.js +10 -0
- package/dist/esm/db/reconcile.js.map +1 -1
- package/dist/esm/embed/cohere.d.ts +2 -0
- package/dist/esm/embed/cohere.js +46 -0
- package/dist/esm/embed/cohere.js.map +1 -0
- package/dist/esm/embed/http.d.ts +2 -0
- package/dist/esm/embed/http.js +33 -0
- package/dist/esm/embed/http.js.map +1 -0
- package/dist/esm/embed/identity.d.ts +13 -0
- package/dist/esm/embed/identity.js +92 -0
- package/dist/esm/embed/identity.js.map +1 -0
- package/dist/esm/embed/langfit.d.ts +6 -0
- package/dist/esm/embed/langfit.js +44 -0
- package/dist/esm/embed/langfit.js.map +1 -0
- package/dist/esm/embed/languages.d.ts +3 -0
- package/dist/esm/embed/languages.js +80 -0
- package/dist/esm/embed/languages.js.map +1 -0
- package/dist/esm/embed/openai.d.ts +2 -0
- package/dist/esm/embed/openai.js +49 -0
- package/dist/esm/embed/openai.js.map +1 -0
- package/dist/esm/embed/query.d.ts +21 -0
- package/dist/esm/embed/query.js +181 -0
- package/dist/esm/embed/query.js.map +1 -0
- package/dist/esm/embed/registry.d.ts +3 -0
- package/dist/esm/embed/registry.js +45 -0
- package/dist/esm/embed/registry.js.map +1 -0
- package/dist/esm/embed/static.d.ts +2 -0
- package/dist/esm/embed/static.js +72 -0
- package/dist/esm/embed/static.js.map +1 -0
- package/dist/esm/embed/store.d.ts +8 -0
- package/dist/esm/embed/store.js +123 -0
- package/dist/esm/embed/store.js.map +1 -0
- package/dist/esm/embed/types.d.ts +8 -0
- package/dist/esm/embed/types.js +3 -0
- package/dist/esm/embed/types.js.map +1 -0
- package/dist/esm/errors.d.ts +1 -1
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/features/embed.d.ts +4 -27
- package/dist/esm/features/embed.js +0 -346
- package/dist/esm/features/embed.js.map +1 -1
- package/dist/esm/fences.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/output.d.ts +2 -2
- package/dist/esm/output.js +6 -2
- package/dist/esm/output.js.map +1 -1
- package/dist/esm/scan.js +7 -4
- package/dist/esm/scan.js.map +1 -1
- package/dist/esm/segment.js +49 -19
- package/dist/esm/segment.js.map +1 -1
- package/package.json +9 -1
- package/schema.json +14 -7
- package/skills/sense/SKILL.md +21 -5
- package/skills/sense-setup/EXAMPLES.md +6 -6
- package/skills/sense-setup/SKILL.md +6 -4
package/dist/esm/db/open.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/db/open.ts"],"sourcesContent":["// The Node floor (>=22.20) is explained here and nowhere else: 22.20 is the first release with\n// both FTS5 and row-returning INSERT ... RETURNING. Raise it only for a load-bearing capability.\nimport { mkdirSync, rmSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { DatabaseSync } from 'node:sqlite';\nimport type { Config, ResolvedConfig } from '../config/index.ts';\nimport { contentTokenize, featureSignature, STATE_DIR } from '../config/index.ts';\nimport { SenseError } from '../errors.ts';\nimport { activeFeatures } from '../features/index.ts';\nimport { registerFunctions } from '../sql-functions.ts';\nimport { changedSignatureKeys, rebuildContentTable, reconcile, signatureDiff } from './reconcile.ts';\nimport { getMeta, setMeta } from './shared.ts';\n\nexport const DB_FILENAME = 'cache.db';\n// Cache shape version, independent of the config's own `version`. Bumping it rebuilds\n// existing trees on first query.\nexport const SCHEMA_VERSION = '16';\n\nexport interface OpenResult {\n db: DatabaseSync;\n cfg: ResolvedConfig;\n dbPath: string;\n parsed: number;\n warnings: string[];\n}\n\n// Stemming is English-only, but the segmentation underneath it is what decides coverage:\n// unicode61 splits on spaces, so a language written without them (Chinese, Japanese, Thai)\n// indexes a whole run as one token and word search finds nothing. `content.tokenize` is how\n// such a tree picks trigram instead.\nconst DEFAULT_TOKENIZE = 'porter unicode61';\n\n// FTS5 takes its tokenizer as a string literal inside DDL, where nothing can bind, so a\n// configured value has to be concatenated. Probing a throwaway table is what makes that safe\n// and is also the whole validation: anything the linked SQLite accepts passes, anything else\n// fails here with SQLite's own message rather than against the real table. It means no table\n// of which version added which tokenizer has to be maintained.\nfunction resolveTokenize(db: DatabaseSync, cfg: Config): string {\n const configured = contentTokenize(cfg);\n if (configured === undefined) return DEFAULT_TOKENIZE;\n const literal = configured.replace(/'/g, \"''\");\n try {\n db.exec('DROP TABLE IF EXISTS temp.sense_tokenize_probe');\n db.exec(`CREATE VIRTUAL TABLE temp.sense_tokenize_probe USING fts5(x, tokenize = '${literal}')`);\n db.exec('DROP TABLE IF EXISTS temp.sense_tokenize_probe');\n } catch (err) {\n throw new SenseError('CONFIG_INVALID', `content.tokenize \"${configured}\" is not a tokenizer this SQLite accepts (${(err as Error).message}); the built-in choices are unicode61, ascii, porter, and trigram, each with their own options`);\n }\n return literal;\n}\n\n// The tokenizer the content table was actually built with, from its own DDL -- the one\n// record that cannot desynchronize from the table. NULL when the table does not exist yet.\nfunction storedTokenize(db: DatabaseSync): string | null {\n const row = db.prepare(`SELECT sql FROM sqlite_master WHERE name = 'content'`).get() as { sql: string } | undefined;\n if (!row) return null;\n const m = row.sql.match(/tokenize = '((?:[^']|'')*)'/);\n return m ? m[1] : null;\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, tokenize: string): void {\n db.exec(`CREATE TABLE IF NOT EXISTS frontmatter (\"path\" TEXT PRIMARY KEY, \"_mtime\" REAL, \"_ctime\" REAL, \"_size\" INTEGER, \"_parse_error\" TEXT)`);\n // IF NOT EXISTS is safe against a tokenizer change: open() compares the table's own DDL\n // against the resolved tokenizer before this runs, so a stale table is already gone by now.\n // The three `_seg` sidecars are appended after path, never inserted: bm25(content, ...) and\n // snippet(content, 2, ...) are documented against the first three columns and keep working\n // (FTS5 defaults the weights it was not given). Each carries its field's exploded unspaced\n // runs for text that needs it, and an empty string for text that does not.\n db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS content USING fts5(title, summary, text, path UNINDEXED, title_seg, summary_seg, text_seg, tokenize = '${tokenize}')`);\n // Coverage, not ownership: a path can appear under several presets. path leads the PK so the\n // per-doc delete is an index hit -- keyed the other way, cold builds went quadratic.\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 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 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 (~5s for 500 files at 26k notes). A query\n // that outwaits it still fails loudly.\n db.exec('PRAGMA busy_timeout = 30000');\n registerFunctions(db, contentTokenize(cfg) === undefined);\n\n db.exec('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)');\n\n // Before the rebuild branch below, never after: that branch deletes the cache, so a typo'd\n // tokenizer validated later would cost a full re-index (and re-embed) to reach its own error.\n const tokenize = resolveTokenize(db, cfg);\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 let tokenizeOnlyRebuild = false;\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 db.close();\n clearCache(cfg);\n return open(cfg);\n }\n const changedKeys = changedSignatureKeys(features ?? '', wantFeatures);\n // Only the tokenizer moved: frontmatter, links, sections, and embeddings are file-derived\n // and tokenizer-independent, so they don't need re-deriving. Everything else -- a preset\n // edit, an embed model change -- still takes the full clear/reopen below.\n if (changedKeys.size === 1 && changedKeys.has('tokenize')) {\n console.error('sense: config change (content tokenizer) rebuilds the text index; vectors, links, and sections are kept');\n db.exec('DROP TABLE content');\n tokenizeOnlyRebuild = true;\n } else {\n const changed = signatureDiff(features ?? '', wantFeatures);\n console.error(`sense: config change (${changed}) rebuilds the index`);\n db.close();\n clearCache(cfg);\n return open(cfg);\n }\n }\n\n // Meta can lie after a crash between table creation and the signature write; the table's\n // own DDL cannot. A mismatch here rebuilds no matter what meta says. A tokenize-only rebuild\n // just dropped content above, so storedTokenize sees no table and this guard is skipped\n // naturally rather than needing its own case.\n const stored = storedTokenize(db);\n if (stored !== null && stored !== tokenize) {\n console.error('sense: cache was built with a different content tokenizer; rebuilding the index');\n db.close();\n clearCache(cfg);\n return open(cfg);\n }\n\n ensureSchema(db, cfg, tokenize);\n\n let rebuildWarnings: string[] = [];\n if (tokenizeOnlyRebuild) {\n rebuildWarnings = rebuildContentTable(db, cfg, cfg.baseDir);\n setMeta(db, 'features', wantFeatures);\n }\n\n // 3x the largest reconcile this cache has recorded, floored at 30s and capped at 10min.\n // Installed before reconcile() -- that call is the one that races a watcher's transaction.\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: [...rebuildWarnings, ...warnings] };\n}\n\n// Deletes the cache directory, and only that. The rebuild is not this function's job: the next\n// open() reconciles, which is what running any command already does, so a verb that bundled the\n// two described the half that was not its own. Manual reset for a doubted cache; the schema and\n// config-signature mismatches below reset themselves.\nexport function clearCache(cfg: ResolvedConfig): void {\n rmSync(join(cfg.baseDir, STATE_DIR), { recursive: true, force: true });\n}\n"],"names":["mkdirSync","rmSync","join","DatabaseSync","contentTokenize","featureSignature","STATE_DIR","SenseError","activeFeatures","registerFunctions","changedSignatureKeys","rebuildContentTable","reconcile","signatureDiff","getMeta","setMeta","DB_FILENAME","SCHEMA_VERSION","DEFAULT_TOKENIZE","resolveTokenize","db","cfg","configured","undefined","literal","replace","exec","err","message","storedTokenize","row","prepare","get","m","sql","match","ensureSchema","tokenize","feature","schema","docCount","n","open","stateDir","baseDir","recursive","dbPath","version","features","wantFeatures","tokenizeOnlyRebuild","console","error","close","clearCache","changedKeys","size","has","changed","stored","rebuildWarnings","recordedMaxMs","Number","Math","min","max","parsed","warnings","force"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/db/open.ts"],"sourcesContent":["// The Node floor (>=22.20) is explained here and nowhere else: 22.20 is the first release with\n// both FTS5 and row-returning INSERT ... RETURNING. Raise it only for a load-bearing capability.\nimport { mkdirSync, rmSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { DatabaseSync } from 'node:sqlite';\nimport type { Config, ResolvedConfig } from '../config/index.ts';\nimport { contentTokenize, featureSignature, STATE_DIR } from '../config/index.ts';\nimport { SenseError } from '../errors.ts';\nimport { activeFeatures } from '../features/index.ts';\nimport { registerFunctions } from '../sql-functions.ts';\nimport { changedSignatureKeys, embedIdentityAdopted, rebuildContentTable, reconcile, signatureDiff } from './reconcile.ts';\nimport { getMeta, setMeta } from './shared.ts';\n\nexport const DB_FILENAME = 'cache.db';\n// Cache shape version, independent of the config's own `version`. Bumping it rebuilds\n// existing trees on first query.\nexport const SCHEMA_VERSION = '17';\n\nexport interface OpenResult {\n db: DatabaseSync;\n cfg: ResolvedConfig;\n dbPath: string;\n parsed: number;\n warnings: string[];\n}\n\n// Stemming is English-only, but the segmentation underneath it is what decides coverage:\n// unicode61 splits on spaces, so a language written without them (Chinese, Japanese, Thai)\n// indexes a whole run as one token and word search finds nothing. `content.tokenize` is how\n// such a tree picks trigram instead.\nconst DEFAULT_TOKENIZE = 'porter unicode61';\n\n// FTS5 takes its tokenizer as a string literal inside DDL, where nothing can bind, so a\n// configured value has to be concatenated. Probing a throwaway table is what makes that safe\n// and is also the whole validation: anything the linked SQLite accepts passes, anything else\n// fails here with SQLite's own message rather than against the real table. It means no table\n// of which version added which tokenizer has to be maintained.\nfunction resolveTokenize(db: DatabaseSync, cfg: Config): string {\n const configured = contentTokenize(cfg);\n if (configured === undefined) return DEFAULT_TOKENIZE;\n const literal = configured.replace(/'/g, \"''\");\n try {\n db.exec('DROP TABLE IF EXISTS temp.sense_tokenize_probe');\n db.exec(`CREATE VIRTUAL TABLE temp.sense_tokenize_probe USING fts5(x, tokenize = '${literal}')`);\n db.exec('DROP TABLE IF EXISTS temp.sense_tokenize_probe');\n } catch (err) {\n throw new SenseError('CONFIG_INVALID', `content.tokenize \"${configured}\" is not a tokenizer this SQLite accepts (${(err as Error).message}); the built-in choices are unicode61, ascii, porter, and trigram, each with their own options`);\n }\n return literal;\n}\n\n// The tokenizer the content table was actually built with, from its own DDL -- the one\n// record that cannot desynchronize from the table. NULL when the table does not exist yet.\nfunction storedTokenize(db: DatabaseSync): string | null {\n const row = db.prepare(`SELECT sql FROM sqlite_master WHERE name = 'content'`).get() as { sql: string } | undefined;\n if (!row) return null;\n const m = row.sql.match(/tokenize = '((?:[^']|'')*)'/);\n return m ? m[1] : null;\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, tokenize: string): void {\n db.exec(`CREATE TABLE IF NOT EXISTS frontmatter (\"path\" TEXT PRIMARY KEY, \"_mtime\" REAL, \"_ctime\" REAL, \"_size\" INTEGER, \"_parse_error\" TEXT)`);\n // IF NOT EXISTS is safe against a tokenizer change: open() compares the table's own DDL\n // against the resolved tokenizer before this runs, so a stale table is already gone by now.\n // The three `_seg` sidecars are appended after path, never inserted: bm25(content, ...) and\n // snippet(content, 2, ...) are documented against the first three columns and keep working\n // (FTS5 defaults the weights it was not given). Each carries its field's exploded unspaced\n // runs for text that needs it, and an empty string for text that does not.\n db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS content USING fts5(title, summary, text, path UNINDEXED, title_seg, summary_seg, text_seg, tokenize = '${tokenize}')`);\n // Coverage, not ownership: a path can appear under several presets. path leads the PK so the\n // per-doc delete is an index hit -- keyed the other way, cold builds went quadratic.\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 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 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 (~5s for 500 files at 26k notes). A query\n // that outwaits it still fails loudly.\n db.exec('PRAGMA busy_timeout = 30000');\n registerFunctions(db, contentTokenize(cfg) === undefined);\n\n db.exec('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)');\n\n // Before the rebuild branch below, never after: that branch deletes the cache, so a typo'd\n // tokenizer validated later would cost a full re-index (and re-embed) to reach its own error.\n const tokenize = resolveTokenize(db, cfg);\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 let tokenizeOnlyRebuild = false;\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 db.close();\n clearCache(cfg);\n return open(cfg);\n }\n const changedKeys = changedSignatureKeys(features ?? '', wantFeatures);\n // Only the tokenizer moved: frontmatter, links, sections, and embeddings are file-derived\n // and tokenizer-independent, so they don't need re-deriving. Everything else -- a preset\n // edit, an embed model change -- still takes the full clear/reopen below.\n if (changedKeys.size === 1 && changedKeys.has('tokenize')) {\n console.error('sense: config change (content tokenizer) rebuilds the text index; vectors, links, and sections are kept');\n db.exec('DROP TABLE content');\n tokenizeOnlyRebuild = true;\n } else if (changedKeys.size === 1 && changedKeys.has('embed') && embedIdentityAdopted(features ?? '', wantFeatures)) {\n // First sight of a resolved weight identity: the model itself hasn't changed, so\n // adopt it into meta with no rebuild and no re-embed, mirroring the tokenize precedent.\n console.error(\"sense: recorded the embedding model's resolved identity; vectors are unaffected\");\n setMeta(db, 'features', wantFeatures);\n } else {\n const changed = signatureDiff(features ?? '', wantFeatures);\n console.error(`sense: config change (${changed}) rebuilds the index`);\n db.close();\n clearCache(cfg);\n return open(cfg);\n }\n }\n\n // Meta can lie after a crash between table creation and the signature write; the table's\n // own DDL cannot. A mismatch here rebuilds no matter what meta says. A tokenize-only rebuild\n // just dropped content above, so storedTokenize sees no table and this guard is skipped\n // naturally rather than needing its own case.\n const stored = storedTokenize(db);\n if (stored !== null && stored !== tokenize) {\n console.error('sense: cache was built with a different content tokenizer; rebuilding the index');\n db.close();\n clearCache(cfg);\n return open(cfg);\n }\n\n ensureSchema(db, cfg, tokenize);\n\n let rebuildWarnings: string[] = [];\n if (tokenizeOnlyRebuild) {\n rebuildWarnings = rebuildContentTable(db, cfg, cfg.baseDir);\n setMeta(db, 'features', wantFeatures);\n }\n\n // 3x the largest reconcile this cache has recorded, floored at 30s and capped at 10min.\n // Installed before reconcile() -- that call is the one that races a watcher's transaction.\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: [...rebuildWarnings, ...warnings] };\n}\n\n// Deletes the cache directory, and only that. The rebuild is not this function's job: the next\n// open() reconciles, which is what running any command already does, so a verb that bundled the\n// two described the half that was not its own. Manual reset for a doubted cache; the schema and\n// config-signature mismatches below reset themselves.\nexport function clearCache(cfg: ResolvedConfig): void {\n rmSync(join(cfg.baseDir, STATE_DIR), { recursive: true, force: true });\n}\n"],"names":["mkdirSync","rmSync","join","DatabaseSync","contentTokenize","featureSignature","STATE_DIR","SenseError","activeFeatures","registerFunctions","changedSignatureKeys","embedIdentityAdopted","rebuildContentTable","reconcile","signatureDiff","getMeta","setMeta","DB_FILENAME","SCHEMA_VERSION","DEFAULT_TOKENIZE","resolveTokenize","db","cfg","configured","undefined","literal","replace","exec","err","message","storedTokenize","row","prepare","get","m","sql","match","ensureSchema","tokenize","feature","schema","docCount","n","open","stateDir","baseDir","recursive","dbPath","version","features","wantFeatures","tokenizeOnlyRebuild","console","error","close","clearCache","changedKeys","size","has","changed","stored","rebuildWarnings","recordedMaxMs","Number","Math","min","max","parsed","warnings","force"],"mappings":"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"}
|
|
@@ -5,5 +5,6 @@ export declare function reconcile(db: DatabaseSync, cfg: Config, baseDir: string
|
|
|
5
5
|
warnings: string[];
|
|
6
6
|
};
|
|
7
7
|
export declare function changedSignatureKeys(before: string, after: string): Set<string>;
|
|
8
|
+
export declare function embedIdentityAdopted(before: string, after: string): boolean;
|
|
8
9
|
export declare function signatureDiff(before: string, after: string): string;
|
|
9
10
|
export declare function rebuildContentTable(db: DatabaseSync, cfg: Config, baseDir: string): string[];
|
package/dist/esm/db/reconcile.js
CHANGED
|
@@ -178,6 +178,16 @@ export function changedSignatureKeys(before, after) {
|
|
|
178
178
|
for (const key of a.keys())if (!b.has(key)) changed.add(key);
|
|
179
179
|
return changed;
|
|
180
180
|
}
|
|
181
|
+
// Whether the embed segment only gained its resolved weight identity: same provider and
|
|
182
|
+
// model, no identity recorded before, one now -- adopted into meta without a rebuild.
|
|
183
|
+
export function embedIdentityAdopted(before, after) {
|
|
184
|
+
const embedPart = (sig)=>sig.split('|').find((p)=>p.startsWith('embed:'));
|
|
185
|
+
const b = embedPart(before);
|
|
186
|
+
const a = embedPart(after);
|
|
187
|
+
if (b === undefined || a === undefined) return false;
|
|
188
|
+
const at = a.indexOf('@');
|
|
189
|
+
return b.indexOf('@') === -1 && at !== -1 && a.slice(0, at) === b;
|
|
190
|
+
}
|
|
181
191
|
// Names what moved, for the rebuild notice.
|
|
182
192
|
export function signatureDiff(before, after) {
|
|
183
193
|
const changed = changedSignatureKeys(before, after);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/db/reconcile.ts"],"sourcesContent":["import type { DatabaseSync, StatementSync } from 'node:sqlite';\nimport type { Config } from '../config/index.ts';\nimport { contentTokenize } from '../config/index.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';\nimport { segmentField } from '../segment.ts';\nimport { getColumns, getMeta, quoteIdent, setMeta } from './shared.ts';\n\n// Feature-owned columns (`_rank`) must stay out of the upsert: a reparse would null the last\n// computed value on every touch, not just the reconciles that recompute it.\nconst CORE_FRONTMATTER_COLUMNS = new Set(['path', '_mtime', '_ctime', '_size', '_parse_error']);\n\n// SQLite's compile-time SQLITE_MAX_COLUMN, default 2000 (https://www.sqlite.org/limits.html).\nconst MAX_FRONTMATTER_COLUMNS = 2000;\n\n// Shared by reconcile() and the tokenize-only rebuild in open(), so both prepare the same\n// literal instead of two copies drifting apart.\nconst INSERT_CONTENT_SQL = `INSERT INTO content (rowid, title, summary, text, \"path\", title_seg, summary_seg, text_seg) VALUES ((SELECT rowid FROM frontmatter WHERE \"path\" = ?), ?, ?, ?, ?, ?, ?, ?)`;\n\n// Shared by reconcile() and the tokenize-only rebuild in open(), so content population is\n// defined once. Assumes the frontmatter row for doc.relPath already exists (rowid lookup).\nfunction insertContentRow(insertBody: StatementSync, doc: ParsedDoc, segmenting: boolean): void {\n insertBody.run(doc.relPath, doc.search.title, doc.search.summary, doc.search.text, doc.relPath, segmenting ? segmentField(doc.search.title) : '', segmenting ? segmentField(doc.search.summary) : '', segmenting ? segmentField(doc.search.text) : '');\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 the config names an embedding model).\n const fileFeatures = features.filter((feature) => !feature.enabledForFile || feature.enabledForFile(cfg, file));\n const { doc, warnings: fileWarnings } = parseFile(file, fileFeatures);\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_CONTENT_SQL);\n // A non-default tokenizer means the tree has chosen its own scheme; a phrase query over\n // grapheme runs would be nonsense against trigram, so the sidecars stay empty.\n const segmenting = contentTokenize(cfg) === undefined;\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 === '_ctime') return doc.ctimeMs;\n if (col === '_size') return doc.size;\n // Written per parse, unlike _rank, which a feature pass owns and the upsert skips.\n if (col === '_parse_error') return doc.parseError;\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 // Only for docs that have rows: an unconditional delete made cold crawls quadratic.\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 insertContentRow(insertBody, doc, segmenting);\n // A preset edit forces a full rebuild, so an unchanged doc's coverage is already\n // correct; new docs have nothing to clear, which 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// Segment keys that moved between two feature signatures (see config.featureSignature's\n// format: global features, embed provider, tokenize, then one segment per preset).\nexport function changedSignatureKeys(before: string, after: string): Set<string> {\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 return changed;\n}\n\n// Names what moved, for the rebuild notice.\nexport function signatureDiff(before: string, after: string): string {\n const changed = changedSignatureKeys(before, after);\n const label = (key: string) => (key === 'embed' ? 'embed settings' : key === 'tokenize' ? 'content tokenizer' : key.startsWith('preset:') ? `preset \"${key.slice(7)}\"` : 'features');\n return changed.size === 0 ? 'features' : [...changed].map(label).join(', ');\n}\n\n// The tokenize-only rebuild in open(): content is dropped and repopulated from files already\n// listed in frontmatter, which itself is untouched. Frontmatter, links, sections, and\n// embeddings are file-derived and tokenizer-independent, so they survive. No feature extractors\n// run here -- doc.search (title/summary/text) is all content population needs. Returns\n// parseFile's per-file warnings (e.g. a bad date) so open() can surface them -- mtimes are\n// untouched, so reconcile() never reparses these files and would otherwise never emit them again.\nexport function rebuildContentTable(db: DatabaseSync, cfg: Config, baseDir: string): string[] {\n const known = new Set((db.prepare('SELECT \"path\" FROM frontmatter').all() as Array<{ path: string }>).map((r) => r.path));\n const files = listFiles(cfg, baseDir).filter((f) => known.has(f.relPath));\n const segmenting = contentTokenize(cfg) === undefined;\n const insertBody = db.prepare(INSERT_CONTENT_SQL);\n const warnings: string[] = [];\n db.exec('BEGIN');\n try {\n for (const file of files) {\n const { doc, warnings: fileWarnings } = parseFile(file);\n warnings.push(...fileWarnings);\n insertContentRow(insertBody, doc, segmenting);\n }\n db.exec('COMMIT');\n } catch (err) {\n db.exec('ROLLBACK');\n throw err;\n }\n return warnings;\n}\n"],"names":["contentTokenize","SenseError","activeFeatures","progress","listFiles","parseFile","RESERVED_COLUMNS","segmentField","getColumns","getMeta","quoteIdent","setMeta","CORE_FRONTMATTER_COLUMNS","Set","MAX_FRONTMATTER_COLUMNS","INSERT_CONTENT_SQL","insertContentRow","insertBody","doc","segmenting","run","relPath","search","title","summary","text","reconcile","db","cfg","baseDir","files","currentSet","map","f","existingRows","prepare","all","existing","Map","r","path","vanished","filter","has","toReparse","row","get","_mtime","mtimeMs","_size","size","length","parsed","warnings","features","seenColumns","newColumns","parsedDocs","report","parsedCount","file","fileFeatures","feature","enabledForFile","fileWarnings","tick","push","key","Object","keys","data","add","finish","allColumns","writableColumns","c","insertSql","join","added","delta","reparsed","d","txStart","Date","now","exec","col","delBody","delPresetFiles","insertPresetFile","del","remove","insert","undefined","values","ctimeMs","parseError","presetName","presets","store","extracted","name","afterReconcile","err","durationMs","prevRaw","prevMax","Number","String","changedSignatureKeys","before","after","keyOf","part","startsWith","split","slice","parse","sig","a","b","changed","val","signatureDiff","label","rebuildContentTable","known"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/db/reconcile.ts"],"sourcesContent":["import type { DatabaseSync, StatementSync } from 'node:sqlite';\nimport type { Config } from '../config/index.ts';\nimport { contentTokenize } from '../config/index.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';\nimport { segmentField } from '../segment.ts';\nimport { getColumns, getMeta, quoteIdent, setMeta } from './shared.ts';\n\n// Feature-owned columns (`_rank`) must stay out of the upsert: a reparse would null the last\n// computed value on every touch, not just the reconciles that recompute it.\nconst CORE_FRONTMATTER_COLUMNS = new Set(['path', '_mtime', '_ctime', '_size', '_parse_error']);\n\n// SQLite's compile-time SQLITE_MAX_COLUMN, default 2000 (https://www.sqlite.org/limits.html).\nconst MAX_FRONTMATTER_COLUMNS = 2000;\n\n// Shared by reconcile() and the tokenize-only rebuild in open(), so both prepare the same\n// literal instead of two copies drifting apart.\nconst INSERT_CONTENT_SQL = `INSERT INTO content (rowid, title, summary, text, \"path\", title_seg, summary_seg, text_seg) VALUES ((SELECT rowid FROM frontmatter WHERE \"path\" = ?), ?, ?, ?, ?, ?, ?, ?)`;\n\n// Shared by reconcile() and the tokenize-only rebuild in open(), so content population is\n// defined once. Assumes the frontmatter row for doc.relPath already exists (rowid lookup).\nfunction insertContentRow(insertBody: StatementSync, doc: ParsedDoc, segmenting: boolean): void {\n insertBody.run(doc.relPath, doc.search.title, doc.search.summary, doc.search.text, doc.relPath, segmenting ? segmentField(doc.search.title) : '', segmenting ? segmentField(doc.search.summary) : '', segmenting ? segmentField(doc.search.text) : '');\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 the config names an embedding model).\n const fileFeatures = features.filter((feature) => !feature.enabledForFile || feature.enabledForFile(cfg, file));\n const { doc, warnings: fileWarnings } = parseFile(file, fileFeatures);\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_CONTENT_SQL);\n // A non-default tokenizer means the tree has chosen its own scheme; a phrase query over\n // grapheme runs would be nonsense against trigram, so the sidecars stay empty.\n const segmenting = contentTokenize(cfg) === undefined;\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 === '_ctime') return doc.ctimeMs;\n if (col === '_size') return doc.size;\n // Written per parse, unlike _rank, which a feature pass owns and the upsert skips.\n if (col === '_parse_error') return doc.parseError;\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 // Only for docs that have rows: an unconditional delete made cold crawls quadratic.\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 insertContentRow(insertBody, doc, segmenting);\n // A preset edit forces a full rebuild, so an unchanged doc's coverage is already\n // correct; new docs have nothing to clear, which 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// Segment keys that moved between two feature signatures (see config.featureSignature's\n// format: global features, embed provider, tokenize, then one segment per preset).\nexport function changedSignatureKeys(before: string, after: string): Set<string> {\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 return changed;\n}\n\n// Whether the embed segment only gained its resolved weight identity: same provider and\n// model, no identity recorded before, one now -- adopted into meta without a rebuild.\nexport function embedIdentityAdopted(before: string, after: string): boolean {\n const embedPart = (sig: string) => sig.split('|').find((p) => p.startsWith('embed:'));\n const b = embedPart(before);\n const a = embedPart(after);\n if (b === undefined || a === undefined) return false;\n const at = a.indexOf('@');\n return b.indexOf('@') === -1 && at !== -1 && a.slice(0, at) === b;\n}\n\n// Names what moved, for the rebuild notice.\nexport function signatureDiff(before: string, after: string): string {\n const changed = changedSignatureKeys(before, after);\n const label = (key: string) => (key === 'embed' ? 'embed settings' : key === 'tokenize' ? 'content tokenizer' : key.startsWith('preset:') ? `preset \"${key.slice(7)}\"` : 'features');\n return changed.size === 0 ? 'features' : [...changed].map(label).join(', ');\n}\n\n// The tokenize-only rebuild in open(): content is dropped and repopulated from files already\n// listed in frontmatter, which itself is untouched. Frontmatter, links, sections, and\n// embeddings are file-derived and tokenizer-independent, so they survive. No feature extractors\n// run here -- doc.search (title/summary/text) is all content population needs. Returns\n// parseFile's per-file warnings (e.g. a bad date) so open() can surface them -- mtimes are\n// untouched, so reconcile() never reparses these files and would otherwise never emit them again.\nexport function rebuildContentTable(db: DatabaseSync, cfg: Config, baseDir: string): string[] {\n const known = new Set((db.prepare('SELECT \"path\" FROM frontmatter').all() as Array<{ path: string }>).map((r) => r.path));\n const files = listFiles(cfg, baseDir).filter((f) => known.has(f.relPath));\n const segmenting = contentTokenize(cfg) === undefined;\n const insertBody = db.prepare(INSERT_CONTENT_SQL);\n const warnings: string[] = [];\n db.exec('BEGIN');\n try {\n for (const file of files) {\n const { doc, warnings: fileWarnings } = parseFile(file);\n warnings.push(...fileWarnings);\n insertContentRow(insertBody, doc, segmenting);\n }\n db.exec('COMMIT');\n } catch (err) {\n db.exec('ROLLBACK');\n throw err;\n }\n return warnings;\n}\n"],"names":["contentTokenize","SenseError","activeFeatures","progress","listFiles","parseFile","RESERVED_COLUMNS","segmentField","getColumns","getMeta","quoteIdent","setMeta","CORE_FRONTMATTER_COLUMNS","Set","MAX_FRONTMATTER_COLUMNS","INSERT_CONTENT_SQL","insertContentRow","insertBody","doc","segmenting","run","relPath","search","title","summary","text","reconcile","db","cfg","baseDir","files","currentSet","map","f","existingRows","prepare","all","existing","Map","r","path","vanished","filter","has","toReparse","row","get","_mtime","mtimeMs","_size","size","length","parsed","warnings","features","seenColumns","newColumns","parsedDocs","report","parsedCount","file","fileFeatures","feature","enabledForFile","fileWarnings","tick","push","key","Object","keys","data","add","finish","allColumns","writableColumns","c","insertSql","join","added","delta","reparsed","d","txStart","Date","now","exec","col","delBody","delPresetFiles","insertPresetFile","del","remove","insert","undefined","values","ctimeMs","parseError","presetName","presets","store","extracted","name","afterReconcile","err","durationMs","prevRaw","prevMax","Number","String","changedSignatureKeys","before","after","keyOf","part","startsWith","split","slice","parse","sig","a","b","changed","val","embedIdentityAdopted","embedPart","find","p","at","indexOf","signatureDiff","label","rebuildContentTable","known"],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { SenseError } from '../errors.js';
|
|
2
|
+
import { fetchWithRetry } from './http.js';
|
|
3
|
+
const BATCH_CAP = 96;
|
|
4
|
+
// Cohere's native /v2/embed: input_type distinguishes doc vs query embeddings, which the
|
|
5
|
+
// OpenAI-compatible shape cannot express.
|
|
6
|
+
export async function cohereProvider(model, url, keyEnv) {
|
|
7
|
+
const base = (url !== null && url !== void 0 ? url : 'https://api.cohere.com').replace(/\/+$/, '');
|
|
8
|
+
const headers = {
|
|
9
|
+
'content-type': 'application/json'
|
|
10
|
+
};
|
|
11
|
+
const key = keyEnv ? process.env[keyEnv] : undefined;
|
|
12
|
+
if (key) headers.authorization = `Bearer ${key}`;
|
|
13
|
+
async function post(texts, inputType) {
|
|
14
|
+
const res = await fetchWithRetry(`${base}/v2/embed`, {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers,
|
|
17
|
+
body: JSON.stringify({
|
|
18
|
+
model,
|
|
19
|
+
input_type: inputType,
|
|
20
|
+
texts,
|
|
21
|
+
embedding_types: [
|
|
22
|
+
'float'
|
|
23
|
+
]
|
|
24
|
+
})
|
|
25
|
+
});
|
|
26
|
+
if (!res.ok) throw new SenseError('EMBED_MODEL', `${base}/v2/embed -> HTTP ${res.status}`);
|
|
27
|
+
const body = await res.json();
|
|
28
|
+
// Cohere returns one vector per text in order, with no index field to re-sort by; the
|
|
29
|
+
// count is what a caller mapping positionally still has to be able to trust.
|
|
30
|
+
const vectors = body.embeddings.float;
|
|
31
|
+
if (vectors.length !== texts.length) throw new SenseError('EMBED_MODEL', `${base}/v2/embed returned ${vectors.length} embeddings for ${texts.length} inputs`);
|
|
32
|
+
return vectors.map((v)=>Float32Array.from(v));
|
|
33
|
+
}
|
|
34
|
+
const dims = (await post([
|
|
35
|
+
'dimension probe'
|
|
36
|
+
], 'search_query'))[0].length;
|
|
37
|
+
return {
|
|
38
|
+
id: `cohere:${base}:${model}`,
|
|
39
|
+
dims,
|
|
40
|
+
batchCap: BATCH_CAP,
|
|
41
|
+
embedDocuments: (texts)=>post(texts, 'search_document'),
|
|
42
|
+
embedQuery: async (text)=>(await post([
|
|
43
|
+
text
|
|
44
|
+
], 'search_query'))[0]
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/embed/cohere.ts"],"sourcesContent":["import { SenseError } from '../errors.ts';\nimport { fetchWithRetry } from './http.ts';\nimport type { EmbedProvider } from './types.ts';\n\nconst BATCH_CAP = 96;\n\n// Cohere's native /v2/embed: input_type distinguishes doc vs query embeddings, which the\n// OpenAI-compatible shape cannot express.\nexport async function cohereProvider(model: string, url: string | undefined, keyEnv: string | undefined): Promise<EmbedProvider> {\n const base = (url ?? 'https://api.cohere.com').replace(/\\/+$/, '');\n const headers: Record<string, string> = { 'content-type': 'application/json' };\n const key = keyEnv ? process.env[keyEnv] : undefined;\n if (key) headers.authorization = `Bearer ${key}`;\n\n async function post(texts: string[], inputType: 'search_document' | 'search_query'): Promise<Float32Array[]> {\n const res = await fetchWithRetry(`${base}/v2/embed`, { method: 'POST', headers, body: JSON.stringify({ model, input_type: inputType, texts, embedding_types: ['float'] }) });\n if (!res.ok) throw new SenseError('EMBED_MODEL', `${base}/v2/embed -> HTTP ${res.status}`);\n const body = (await res.json()) as { embeddings: { float: number[][] } };\n // Cohere returns one vector per text in order, with no index field to re-sort by; the\n // count is what a caller mapping positionally still has to be able to trust.\n const vectors = body.embeddings.float;\n if (vectors.length !== texts.length) throw new SenseError('EMBED_MODEL', `${base}/v2/embed returned ${vectors.length} embeddings for ${texts.length} inputs`);\n return vectors.map((v) => Float32Array.from(v));\n }\n\n const dims = (await post(['dimension probe'], 'search_query'))[0].length;\n return { id: `cohere:${base}:${model}`, dims, batchCap: BATCH_CAP, embedDocuments: (texts) => post(texts, 'search_document'), embedQuery: async (text) => (await post([text], 'search_query'))[0] };\n}\n"],"names":["SenseError","fetchWithRetry","BATCH_CAP","cohereProvider","model","url","keyEnv","base","replace","headers","key","process","env","undefined","authorization","post","texts","inputType","res","method","body","JSON","stringify","input_type","embedding_types","ok","status","json","vectors","embeddings","float","length","map","v","Float32Array","from","dims","id","batchCap","embedDocuments","embedQuery","text"],"mappings":"AAAA,SAASA,UAAU,QAAQ,eAAe;AAC1C,SAASC,cAAc,QAAQ,YAAY;AAG3C,MAAMC,YAAY;AAElB,yFAAyF;AACzF,0CAA0C;AAC1C,OAAO,eAAeC,eAAeC,KAAa,EAAEC,GAAuB,EAAEC,MAA0B;IACrG,MAAMC,OAAO,CAACF,gBAAAA,iBAAAA,MAAO,0BAA0BG,OAAO,CAAC,QAAQ;IAC/D,MAAMC,UAAkC;QAAE,gBAAgB;IAAmB;IAC7E,MAAMC,MAAMJ,SAASK,QAAQC,GAAG,CAACN,OAAO,GAAGO;IAC3C,IAAIH,KAAKD,QAAQK,aAAa,GAAG,CAAC,OAAO,EAAEJ,KAAK;IAEhD,eAAeK,KAAKC,KAAe,EAAEC,SAA6C;QAChF,MAAMC,MAAM,MAAMjB,eAAe,GAAGM,KAAK,SAAS,CAAC,EAAE;YAAEY,QAAQ;YAAQV;YAASW,MAAMC,KAAKC,SAAS,CAAC;gBAAElB;gBAAOmB,YAAYN;gBAAWD;gBAAOQ,iBAAiB;oBAAC;iBAAQ;YAAC;QAAG;QAC1K,IAAI,CAACN,IAAIO,EAAE,EAAE,MAAM,IAAIzB,WAAW,eAAe,GAAGO,KAAK,kBAAkB,EAAEW,IAAIQ,MAAM,EAAE;QACzF,MAAMN,OAAQ,MAAMF,IAAIS,IAAI;QAC5B,sFAAsF;QACtF,6EAA6E;QAC7E,MAAMC,UAAUR,KAAKS,UAAU,CAACC,KAAK;QACrC,IAAIF,QAAQG,MAAM,KAAKf,MAAMe,MAAM,EAAE,MAAM,IAAI/B,WAAW,eAAe,GAAGO,KAAK,mBAAmB,EAAEqB,QAAQG,MAAM,CAAC,gBAAgB,EAAEf,MAAMe,MAAM,CAAC,OAAO,CAAC;QAC5J,OAAOH,QAAQI,GAAG,CAAC,CAACC,IAAMC,aAAaC,IAAI,CAACF;IAC9C;IAEA,MAAMG,OAAO,AAAC,CAAA,MAAMrB,KAAK;QAAC;KAAkB,EAAE,eAAc,CAAE,CAAC,EAAE,CAACgB,MAAM;IACxE,OAAO;QAAEM,IAAI,CAAC,OAAO,EAAE9B,KAAK,CAAC,EAAEH,OAAO;QAAEgC;QAAME,UAAUpC;QAAWqC,gBAAgB,CAACvB,QAAUD,KAAKC,OAAO;QAAoBwB,YAAY,OAAOC,OAAS,AAAC,CAAA,MAAM1B,KAAK;gBAAC0B;aAAK,EAAE,eAAc,CAAE,CAAC,EAAE;IAAC;AACpM"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const MAX_ATTEMPTS = 3;
|
|
2
|
+
function sleep(ms) {
|
|
3
|
+
return new Promise((resolve)=>setTimeout(resolve, ms));
|
|
4
|
+
}
|
|
5
|
+
// Shared retry glue for the HTTP providers: 429/5xx/network failure retry with exponential
|
|
6
|
+
// backoff honoring Retry-After; any other status returns immediately for the caller to handle.
|
|
7
|
+
export async function fetchWithRetry(url, init, baseDelayMs = 200) {
|
|
8
|
+
for(let attempt = 0; attempt < MAX_ATTEMPTS; attempt++){
|
|
9
|
+
try {
|
|
10
|
+
const res = await fetch(url, init);
|
|
11
|
+
if (res.ok || res.status !== 429 && res.status < 500 || attempt === MAX_ATTEMPTS - 1) return res;
|
|
12
|
+
const retryAfter = Number(res.headers.get('retry-after'));
|
|
13
|
+
// Capped: a server naming a huge Retry-After must not stall an embed run for it.
|
|
14
|
+
await sleep(retryAfter > 0 ? Math.min(retryAfter * 1000, 10000) : baseDelayMs * 2 ** attempt);
|
|
15
|
+
} catch (err) {
|
|
16
|
+
if (attempt === MAX_ATTEMPTS - 1) throw err;
|
|
17
|
+
await sleep(baseDelayMs * 2 ** attempt);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
throw new Error('unreachable');
|
|
21
|
+
}
|
|
22
|
+
// Any HTTP response means the endpoint is up; only a network failure or the timeout counts as
|
|
23
|
+
// unreachable, so an air-gapped machine never waits more than timeoutMs on `sense status`.
|
|
24
|
+
export async function probeReachable(url, timeoutMs = 1500) {
|
|
25
|
+
try {
|
|
26
|
+
await fetch(url, {
|
|
27
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
28
|
+
});
|
|
29
|
+
return true;
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/embed/http.ts"],"sourcesContent":["const MAX_ATTEMPTS = 3;\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n// Shared retry glue for the HTTP providers: 429/5xx/network failure retry with exponential\n// backoff honoring Retry-After; any other status returns immediately for the caller to handle.\nexport async function fetchWithRetry(url: string, init: RequestInit, baseDelayMs = 200): Promise<Response> {\n for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {\n try {\n const res = await fetch(url, init);\n if (res.ok || (res.status !== 429 && res.status < 500) || attempt === MAX_ATTEMPTS - 1) return res;\n const retryAfter = Number(res.headers.get('retry-after'));\n // Capped: a server naming a huge Retry-After must not stall an embed run for it.\n await sleep(retryAfter > 0 ? Math.min(retryAfter * 1000, 10_000) : baseDelayMs * 2 ** attempt);\n } catch (err) {\n if (attempt === MAX_ATTEMPTS - 1) throw err;\n await sleep(baseDelayMs * 2 ** attempt);\n }\n }\n throw new Error('unreachable');\n}\n\n// Any HTTP response means the endpoint is up; only a network failure or the timeout counts as\n// unreachable, so an air-gapped machine never waits more than timeoutMs on `sense status`.\nexport async function probeReachable(url: string, timeoutMs = 1500): Promise<boolean> {\n try {\n await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });\n return true;\n } catch {\n return false;\n }\n}\n"],"names":["MAX_ATTEMPTS","sleep","ms","Promise","resolve","setTimeout","fetchWithRetry","url","init","baseDelayMs","attempt","res","fetch","ok","status","retryAfter","Number","headers","get","Math","min","err","Error","probeReachable","timeoutMs","signal","AbortSignal","timeout"],"mappings":"AAAA,MAAMA,eAAe;AAErB,SAASC,MAAMC,EAAU;IACvB,OAAO,IAAIC,QAAQ,CAACC,UAAYC,WAAWD,SAASF;AACtD;AAEA,2FAA2F;AAC3F,+FAA+F;AAC/F,OAAO,eAAeI,eAAeC,GAAW,EAAEC,IAAiB,EAAEC,cAAc,GAAG;IACpF,IAAK,IAAIC,UAAU,GAAGA,UAAUV,cAAcU,UAAW;QACvD,IAAI;YACF,MAAMC,MAAM,MAAMC,MAAML,KAAKC;YAC7B,IAAIG,IAAIE,EAAE,IAAKF,IAAIG,MAAM,KAAK,OAAOH,IAAIG,MAAM,GAAG,OAAQJ,YAAYV,eAAe,GAAG,OAAOW;YAC/F,MAAMI,aAAaC,OAAOL,IAAIM,OAAO,CAACC,GAAG,CAAC;YAC1C,iFAAiF;YACjF,MAAMjB,MAAMc,aAAa,IAAII,KAAKC,GAAG,CAACL,aAAa,MAAM,SAAUN,cAAc,KAAKC;QACxF,EAAE,OAAOW,KAAK;YACZ,IAAIX,YAAYV,eAAe,GAAG,MAAMqB;YACxC,MAAMpB,MAAMQ,cAAc,KAAKC;QACjC;IACF;IACA,MAAM,IAAIY,MAAM;AAClB;AAEA,8FAA8F;AAC9F,2FAA2F;AAC3F,OAAO,eAAeC,eAAehB,GAAW,EAAEiB,YAAY,IAAI;IAChE,IAAI;QACF,MAAMZ,MAAML,KAAK;YAAEkB,QAAQC,YAAYC,OAAO,CAACH;QAAW;QAC1D,OAAO;IACT,EAAE,OAAM;QACN,OAAO;IACT;AACF"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const MODEL_FILES: string[];
|
|
2
|
+
export declare const MODEL_FILENAMES: string;
|
|
3
|
+
export declare function isDownloadable(model: string): boolean;
|
|
4
|
+
export declare function refsMainPath(model: string): string;
|
|
5
|
+
export declare function readRef(model: string): string | undefined;
|
|
6
|
+
export declare function writeRef(model: string, sha: string): void;
|
|
7
|
+
export declare function snapshotDir(model: string, sha: string): string;
|
|
8
|
+
export declare function languagesPath(model: string): string;
|
|
9
|
+
export declare function readLanguages(model: string): string[] | undefined;
|
|
10
|
+
export declare function writeLanguages(model: string, languages: string[]): void;
|
|
11
|
+
export declare function modelDir(model: string): string;
|
|
12
|
+
export declare function hasModelFiles(model: string): boolean;
|
|
13
|
+
export declare function modelIdentity(model: string): string | undefined;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
// HF cache layout, no network: ~/.sense/models/models--<org>--<name>/{refs/main,
|
|
5
|
+
// snapshots/<sha>/<files>}, files stored directly (hf_hub's blobs/symlinks layer skipped).
|
|
6
|
+
export const MODEL_FILES = [
|
|
7
|
+
'model.safetensors',
|
|
8
|
+
'tokenizer.json'
|
|
9
|
+
];
|
|
10
|
+
// The pair, for messages that name what a local model directory must contain.
|
|
11
|
+
export const MODEL_FILENAMES = MODEL_FILES.join(' and ');
|
|
12
|
+
// A Hugging Face repo id: one slash, HF's charset. Anything else is a path, used as one rather
|
|
13
|
+
// than resolved against the HF cache.
|
|
14
|
+
const HF_MODEL_ID = /^[A-Za-z0-9][\w.-]*\/[A-Za-z0-9][\w.-]*$/;
|
|
15
|
+
export function isDownloadable(model) {
|
|
16
|
+
return HF_MODEL_ID.test(model);
|
|
17
|
+
}
|
|
18
|
+
function repoDir(model) {
|
|
19
|
+
const [org, name] = model.split('/');
|
|
20
|
+
return join(homedir(), '.sense', 'models', `models--${org}--${name}`);
|
|
21
|
+
}
|
|
22
|
+
export function refsMainPath(model) {
|
|
23
|
+
return join(repoDir(model), 'refs', 'main');
|
|
24
|
+
}
|
|
25
|
+
// What `main` last resolved to, read from disk -- no network. Undefined until some resolution
|
|
26
|
+
// (a download or an explicit prefetch) has written it once.
|
|
27
|
+
export function readRef(model) {
|
|
28
|
+
const p = refsMainPath(model);
|
|
29
|
+
if (!existsSync(p)) return undefined;
|
|
30
|
+
const sha = readFileSync(p, 'utf8').trim();
|
|
31
|
+
return sha.length > 0 ? sha : undefined;
|
|
32
|
+
}
|
|
33
|
+
// A recorded ref pins until its snapshot directory is removed; nothing here re-resolves it.
|
|
34
|
+
export function writeRef(model, sha) {
|
|
35
|
+
mkdirSync(join(repoDir(model), 'refs'), {
|
|
36
|
+
recursive: true
|
|
37
|
+
});
|
|
38
|
+
writeFileSync(refsMainPath(model), sha);
|
|
39
|
+
}
|
|
40
|
+
export function snapshotDir(model, sha) {
|
|
41
|
+
return join(repoDir(model), 'snapshots', sha);
|
|
42
|
+
}
|
|
43
|
+
// Languages describe the repo, not one weight snapshot, so this lives beside refs/, not
|
|
44
|
+
// inside a snapshot dir.
|
|
45
|
+
export function languagesPath(model) {
|
|
46
|
+
return join(repoDir(model), 'languages.json');
|
|
47
|
+
}
|
|
48
|
+
// undefined means "never resolved yet"; an empty array is a resolved fact (checked, none
|
|
49
|
+
// declared) and is cached the same way, so a languageless model is not re-fetched every run.
|
|
50
|
+
export function readLanguages(model) {
|
|
51
|
+
const p = languagesPath(model);
|
|
52
|
+
if (!existsSync(p)) return undefined;
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(readFileSync(p, 'utf8'));
|
|
55
|
+
} catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function writeLanguages(model, languages) {
|
|
60
|
+
mkdirSync(repoDir(model), {
|
|
61
|
+
recursive: true
|
|
62
|
+
});
|
|
63
|
+
writeFileSync(languagesPath(model), JSON.stringify(languages));
|
|
64
|
+
}
|
|
65
|
+
// A local directory the caller manages, or the resolved snapshot once `main` is pinned; an HF
|
|
66
|
+
// id with no recorded ref yet has no known snapshot, so this names the (fileless) repo dir.
|
|
67
|
+
export function modelDir(model) {
|
|
68
|
+
if (!isDownloadable(model)) return model;
|
|
69
|
+
const sha = readRef(model);
|
|
70
|
+
return sha ? snapshotDir(model, sha) : repoDir(model);
|
|
71
|
+
}
|
|
72
|
+
// Both files, so an interrupted download reads as absent and the next one resumes it.
|
|
73
|
+
export function hasModelFiles(model) {
|
|
74
|
+
const dir = modelDir(model);
|
|
75
|
+
return MODEL_FILES.every((file)=>existsSync(join(dir, file)));
|
|
76
|
+
}
|
|
77
|
+
// Weight identity for the signature, no network: a HF id's resolved sha, or the two
|
|
78
|
+
// files' size+mtime for a local path. Undefined when nothing is known yet.
|
|
79
|
+
export function modelIdentity(model) {
|
|
80
|
+
if (isDownloadable(model)) return readRef(model);
|
|
81
|
+
const dir = modelDir(model);
|
|
82
|
+
const parts = [];
|
|
83
|
+
for (const file of MODEL_FILES){
|
|
84
|
+
try {
|
|
85
|
+
const st = statSync(join(dir, file));
|
|
86
|
+
parts.push(`${st.size}:${Math.trunc(st.mtimeMs)}`);
|
|
87
|
+
} catch {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return parts.join(',');
|
|
92
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/embed/identity.ts"],"sourcesContent":["import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\n// HF cache layout, no network: ~/.sense/models/models--<org>--<name>/{refs/main,\n// snapshots/<sha>/<files>}, files stored directly (hf_hub's blobs/symlinks layer skipped).\n\nexport const MODEL_FILES = ['model.safetensors', 'tokenizer.json'];\n// The pair, for messages that name what a local model directory must contain.\nexport const MODEL_FILENAMES = MODEL_FILES.join(' and ');\n\n// A Hugging Face repo id: one slash, HF's charset. Anything else is a path, used as one rather\n// than resolved against the HF cache.\nconst HF_MODEL_ID = /^[A-Za-z0-9][\\w.-]*\\/[A-Za-z0-9][\\w.-]*$/;\n\nexport function isDownloadable(model: string): boolean {\n return HF_MODEL_ID.test(model);\n}\n\nfunction repoDir(model: string): string {\n const [org, name] = model.split('/');\n return join(homedir(), '.sense', 'models', `models--${org}--${name}`);\n}\n\nexport function refsMainPath(model: string): string {\n return join(repoDir(model), 'refs', 'main');\n}\n\n// What `main` last resolved to, read from disk -- no network. Undefined until some resolution\n// (a download or an explicit prefetch) has written it once.\nexport function readRef(model: string): string | undefined {\n const p = refsMainPath(model);\n if (!existsSync(p)) return undefined;\n const sha = readFileSync(p, 'utf8').trim();\n return sha.length > 0 ? sha : undefined;\n}\n\n// A recorded ref pins until its snapshot directory is removed; nothing here re-resolves it.\nexport function writeRef(model: string, sha: string): void {\n mkdirSync(join(repoDir(model), 'refs'), { recursive: true });\n writeFileSync(refsMainPath(model), sha);\n}\n\nexport function snapshotDir(model: string, sha: string): string {\n return join(repoDir(model), 'snapshots', sha);\n}\n\n// Languages describe the repo, not one weight snapshot, so this lives beside refs/, not\n// inside a snapshot dir.\nexport function languagesPath(model: string): string {\n return join(repoDir(model), 'languages.json');\n}\n\n// undefined means \"never resolved yet\"; an empty array is a resolved fact (checked, none\n// declared) and is cached the same way, so a languageless model is not re-fetched every run.\nexport function readLanguages(model: string): string[] | undefined {\n const p = languagesPath(model);\n if (!existsSync(p)) return undefined;\n try {\n return JSON.parse(readFileSync(p, 'utf8'));\n } catch {\n return undefined;\n }\n}\n\nexport function writeLanguages(model: string, languages: string[]): void {\n mkdirSync(repoDir(model), { recursive: true });\n writeFileSync(languagesPath(model), JSON.stringify(languages));\n}\n\n// A local directory the caller manages, or the resolved snapshot once `main` is pinned; an HF\n// id with no recorded ref yet has no known snapshot, so this names the (fileless) repo dir.\nexport function modelDir(model: string): string {\n if (!isDownloadable(model)) return model;\n const sha = readRef(model);\n return sha ? snapshotDir(model, sha) : repoDir(model);\n}\n\n// Both files, so an interrupted download reads as absent and the next one resumes it.\nexport function hasModelFiles(model: string): boolean {\n const dir = modelDir(model);\n return MODEL_FILES.every((file) => existsSync(join(dir, file)));\n}\n\n// Weight identity for the signature, no network: a HF id's resolved sha, or the two\n// files' size+mtime for a local path. Undefined when nothing is known yet.\nexport function modelIdentity(model: string): string | undefined {\n if (isDownloadable(model)) return readRef(model);\n const dir = modelDir(model);\n const parts: string[] = [];\n for (const file of MODEL_FILES) {\n try {\n const st = statSync(join(dir, file));\n parts.push(`${st.size}:${Math.trunc(st.mtimeMs)}`);\n } catch {\n return undefined;\n }\n }\n return parts.join(',');\n}\n"],"names":["existsSync","mkdirSync","readFileSync","statSync","writeFileSync","homedir","join","MODEL_FILES","MODEL_FILENAMES","HF_MODEL_ID","isDownloadable","model","test","repoDir","org","name","split","refsMainPath","readRef","p","undefined","sha","trim","length","writeRef","recursive","snapshotDir","languagesPath","readLanguages","JSON","parse","writeLanguages","languages","stringify","modelDir","hasModelFiles","dir","every","file","modelIdentity","parts","st","push","size","Math","trunc","mtimeMs"],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import type { EmbedProvider } from './types.js';
|
|
3
|
+
type LangCounts = Record<string, number>;
|
|
4
|
+
export declare function languageDistribution(db: DatabaseSync): LangCounts | undefined;
|
|
5
|
+
export declare function checkLanguageFit(db: DatabaseSync, provider: EmbedProvider, texts: string[]): Promise<void>;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { getMeta, setMeta } from '../db/shared.js';
|
|
2
|
+
import { SenseError } from '../errors.js';
|
|
3
|
+
import { toIso3 } from './languages.js';
|
|
4
|
+
const META_KEY = 'embed_languages';
|
|
5
|
+
// franc's own reliability floor is a handful of characters; this is a cheap upper bound, not a
|
|
6
|
+
// per-chunk minimum -- a short chunk is simply more likely to come back 'und' (unclassified).
|
|
7
|
+
const SAMPLE_CHARS = 300;
|
|
8
|
+
// A majority computed over a handful of chunks is noise, not a corpus signal; the decision rule
|
|
9
|
+
// never fires below this many classified chunks, declared languages or not.
|
|
10
|
+
const MIN_CLASSIFIED = 10;
|
|
11
|
+
export function languageDistribution(db) {
|
|
12
|
+
const raw = getMeta(db, META_KEY);
|
|
13
|
+
return raw ? JSON.parse(raw) : undefined;
|
|
14
|
+
}
|
|
15
|
+
function classify(franc, text) {
|
|
16
|
+
const code = franc(text.slice(0, SAMPLE_CHARS));
|
|
17
|
+
return code === 'und' ? undefined : code;
|
|
18
|
+
}
|
|
19
|
+
// Merges chunk texts into the persisted distribution; throws (without persisting) if the model
|
|
20
|
+
// declares languages and a clear majority classified so far is not among them.
|
|
21
|
+
export async function checkLanguageFit(db, provider, texts) {
|
|
22
|
+
var _languageDistribution;
|
|
23
|
+
if (texts.length === 0) return;
|
|
24
|
+
const { franc } = await import('franc-min');
|
|
25
|
+
const persisted = (_languageDistribution = languageDistribution(db)) !== null && _languageDistribution !== void 0 ? _languageDistribution : {};
|
|
26
|
+
const merged = {
|
|
27
|
+
...persisted
|
|
28
|
+
};
|
|
29
|
+
for (const text of texts){
|
|
30
|
+
var _merged_code;
|
|
31
|
+
const code = classify(franc, text);
|
|
32
|
+
if (code) merged[code] = ((_merged_code = merged[code]) !== null && _merged_code !== void 0 ? _merged_code : 0) + 1;
|
|
33
|
+
}
|
|
34
|
+
const total = Object.values(merged).reduce((a, b)=>a + b, 0);
|
|
35
|
+
if (provider.languages && provider.languages.length > 0 && total >= MIN_CLASSIFIED) {
|
|
36
|
+
const declared = new Set(provider.languages.map(toIso3));
|
|
37
|
+
const [majorityLang, majorityCount] = Object.entries(merged).sort((a, b)=>b[1] - a[1])[0];
|
|
38
|
+
if (majorityCount / total >= 0.5 && !declared.has(majorityLang)) {
|
|
39
|
+
const pct = Math.round(majorityCount / total * 100);
|
|
40
|
+
throw new SenseError('EMBED_MODEL_MISMATCH', `embed model ${provider.id} declares languages [${provider.languages.join(', ')}], but ${pct}% of this tree's classified text is "${majorityLang}" (ISO 639-3), which is not among them; choose a model for this tree's languages: \`sense init --model ...\`, see the sense-setup skill or INTEGRATIONS.md`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
setMeta(db, META_KEY, JSON.stringify(merged));
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/embed/langfit.ts"],"sourcesContent":["import type { DatabaseSync } from 'node:sqlite';\nimport { getMeta, setMeta } from '../db/shared.ts';\nimport { SenseError } from '../errors.ts';\nimport { toIso3 } from './languages.ts';\nimport type { EmbedProvider } from './types.ts';\n\nconst META_KEY = 'embed_languages';\n// franc's own reliability floor is a handful of characters; this is a cheap upper bound, not a\n// per-chunk minimum -- a short chunk is simply more likely to come back 'und' (unclassified).\nconst SAMPLE_CHARS = 300;\n// A majority computed over a handful of chunks is noise, not a corpus signal; the decision rule\n// never fires below this many classified chunks, declared languages or not.\nconst MIN_CLASSIFIED = 10;\n\ntype LangCounts = Record<string, number>;\n\nexport function languageDistribution(db: DatabaseSync): LangCounts | undefined {\n const raw = getMeta(db, META_KEY);\n return raw ? JSON.parse(raw) : undefined;\n}\n\nfunction classify(franc: (text: string) => string, text: string): string | undefined {\n const code = franc(text.slice(0, SAMPLE_CHARS));\n return code === 'und' ? undefined : code;\n}\n\n// Merges chunk texts into the persisted distribution; throws (without persisting) if the model\n// declares languages and a clear majority classified so far is not among them.\nexport async function checkLanguageFit(db: DatabaseSync, provider: EmbedProvider, texts: string[]): Promise<void> {\n if (texts.length === 0) return;\n const { franc } = await import('franc-min');\n const persisted = languageDistribution(db) ?? {};\n const merged = { ...persisted };\n for (const text of texts) {\n const code = classify(franc, text);\n if (code) merged[code] = (merged[code] ?? 0) + 1;\n }\n\n const total = Object.values(merged).reduce((a, b) => a + b, 0);\n if (provider.languages && provider.languages.length > 0 && total >= MIN_CLASSIFIED) {\n const declared = new Set(provider.languages.map(toIso3));\n const [majorityLang, majorityCount] = Object.entries(merged).sort((a, b) => b[1] - a[1])[0];\n if (majorityCount / total >= 0.5 && !declared.has(majorityLang)) {\n const pct = Math.round((majorityCount / total) * 100);\n throw new SenseError(\n 'EMBED_MODEL_MISMATCH',\n `embed model ${provider.id} declares languages [${provider.languages.join(', ')}], but ${pct}% of this tree's classified text is \"${majorityLang}\" (ISO 639-3), which is not among them; choose a model for this tree's languages: \\`sense init --model ...\\`, see the sense-setup skill or INTEGRATIONS.md`\n );\n }\n }\n setMeta(db, META_KEY, JSON.stringify(merged));\n}\n"],"names":["getMeta","setMeta","SenseError","toIso3","META_KEY","SAMPLE_CHARS","MIN_CLASSIFIED","languageDistribution","db","raw","JSON","parse","undefined","classify","franc","text","code","slice","checkLanguageFit","provider","texts","length","persisted","merged","total","Object","values","reduce","a","b","languages","declared","Set","map","majorityLang","majorityCount","entries","sort","has","pct","Math","round","id","join","stringify"],"mappings":"AACA,SAASA,OAAO,EAAEC,OAAO,QAAQ,kBAAkB;AACnD,SAASC,UAAU,QAAQ,eAAe;AAC1C,SAASC,MAAM,QAAQ,iBAAiB;AAGxC,MAAMC,WAAW;AACjB,+FAA+F;AAC/F,8FAA8F;AAC9F,MAAMC,eAAe;AACrB,gGAAgG;AAChG,4EAA4E;AAC5E,MAAMC,iBAAiB;AAIvB,OAAO,SAASC,qBAAqBC,EAAgB;IACnD,MAAMC,MAAMT,QAAQQ,IAAIJ;IACxB,OAAOK,MAAMC,KAAKC,KAAK,CAACF,OAAOG;AACjC;AAEA,SAASC,SAASC,KAA+B,EAAEC,IAAY;IAC7D,MAAMC,OAAOF,MAAMC,KAAKE,KAAK,CAAC,GAAGZ;IACjC,OAAOW,SAAS,QAAQJ,YAAYI;AACtC;AAEA,+FAA+F;AAC/F,+EAA+E;AAC/E,OAAO,eAAeE,iBAAiBV,EAAgB,EAAEW,QAAuB,EAAEC,KAAe;QAG7Eb;IAFlB,IAAIa,MAAMC,MAAM,KAAK,GAAG;IACxB,MAAM,EAAEP,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC;IAC/B,MAAMQ,aAAYf,wBAAAA,qBAAqBC,iBAArBD,mCAAAA,wBAA4B,CAAC;IAC/C,MAAMgB,SAAS;QAAE,GAAGD,SAAS;IAAC;IAC9B,KAAK,MAAMP,QAAQK,MAAO;YAEEG;QAD1B,MAAMP,OAAOH,SAASC,OAAOC;QAC7B,IAAIC,MAAMO,MAAM,CAACP,KAAK,GAAG,EAACO,eAAAA,MAAM,CAACP,KAAK,cAAZO,0BAAAA,eAAgB,KAAK;IACjD;IAEA,MAAMC,QAAQC,OAAOC,MAAM,CAACH,QAAQI,MAAM,CAAC,CAACC,GAAGC,IAAMD,IAAIC,GAAG;IAC5D,IAAIV,SAASW,SAAS,IAAIX,SAASW,SAAS,CAACT,MAAM,GAAG,KAAKG,SAASlB,gBAAgB;QAClF,MAAMyB,WAAW,IAAIC,IAAIb,SAASW,SAAS,CAACG,GAAG,CAAC9B;QAChD,MAAM,CAAC+B,cAAcC,cAAc,GAAGV,OAAOW,OAAO,CAACb,QAAQc,IAAI,CAAC,CAACT,GAAGC,IAAMA,CAAC,CAAC,EAAE,GAAGD,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE;QAC3F,IAAIO,gBAAgBX,SAAS,OAAO,CAACO,SAASO,GAAG,CAACJ,eAAe;YAC/D,MAAMK,MAAMC,KAAKC,KAAK,CAAC,AAACN,gBAAgBX,QAAS;YACjD,MAAM,IAAItB,WACR,wBACA,CAAC,YAAY,EAAEiB,SAASuB,EAAE,CAAC,qBAAqB,EAAEvB,SAASW,SAAS,CAACa,IAAI,CAAC,MAAM,OAAO,EAAEJ,IAAI,qCAAqC,EAAEL,aAAa,0JAA0J,CAAC;QAEhT;IACF;IACAjC,QAAQO,IAAIJ,UAAUM,KAAKkC,SAAS,CAACrB;AACvC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// HF model cards declare languages as ISO 639-1 tags (e.g. "zh"); franc-min reports ISO 639-3
|
|
2
|
+
// (e.g. "cmn"). This bridges the two for comparison. Limited to franc-min's own 82-language
|
|
3
|
+
// data (README table, https://github.com/wooorm/franc), so every value here is a code franc-min
|
|
4
|
+
// can actually emit; a declared tag with no entry here is compared as-is.
|
|
5
|
+
const ISO_639_1_TO_3 = {
|
|
6
|
+
ar: 'arb',
|
|
7
|
+
am: 'amh',
|
|
8
|
+
az: 'azj',
|
|
9
|
+
be: 'bel',
|
|
10
|
+
bg: 'bul',
|
|
11
|
+
bn: 'ben',
|
|
12
|
+
bs: 'bos',
|
|
13
|
+
cs: 'ces',
|
|
14
|
+
de: 'deu',
|
|
15
|
+
el: 'ell',
|
|
16
|
+
en: 'eng',
|
|
17
|
+
es: 'spa',
|
|
18
|
+
fa: 'pes',
|
|
19
|
+
fr: 'fra',
|
|
20
|
+
gu: 'guj',
|
|
21
|
+
ha: 'hau',
|
|
22
|
+
hi: 'hin',
|
|
23
|
+
hr: 'hrv',
|
|
24
|
+
hu: 'hun',
|
|
25
|
+
id: 'ind',
|
|
26
|
+
ig: 'ibo',
|
|
27
|
+
it: 'ita',
|
|
28
|
+
ja: 'jpn',
|
|
29
|
+
jv: 'jav',
|
|
30
|
+
kk: 'kaz',
|
|
31
|
+
kn: 'kan',
|
|
32
|
+
ko: 'kor',
|
|
33
|
+
ml: 'mal',
|
|
34
|
+
mr: 'mar',
|
|
35
|
+
ms: 'zlm',
|
|
36
|
+
my: 'mya',
|
|
37
|
+
ne: 'npi',
|
|
38
|
+
nl: 'nld',
|
|
39
|
+
pa: 'pan',
|
|
40
|
+
pl: 'pol',
|
|
41
|
+
pt: 'por',
|
|
42
|
+
ro: 'ron',
|
|
43
|
+
ru: 'rus',
|
|
44
|
+
rw: 'kin',
|
|
45
|
+
si: 'sin',
|
|
46
|
+
so: 'som',
|
|
47
|
+
sr: 'srp',
|
|
48
|
+
su: 'sun',
|
|
49
|
+
sv: 'swe',
|
|
50
|
+
sw: 'swh',
|
|
51
|
+
ta: 'tam',
|
|
52
|
+
te: 'tel',
|
|
53
|
+
th: 'tha',
|
|
54
|
+
tl: 'tgl',
|
|
55
|
+
tr: 'tur',
|
|
56
|
+
uk: 'ukr',
|
|
57
|
+
ur: 'urd',
|
|
58
|
+
uz: 'uzn',
|
|
59
|
+
vi: 'vie',
|
|
60
|
+
yo: 'yor',
|
|
61
|
+
zh: 'cmn',
|
|
62
|
+
zu: 'zul'
|
|
63
|
+
};
|
|
64
|
+
export function isKnownLanguageTag(tag) {
|
|
65
|
+
return tag in ISO_639_1_TO_3;
|
|
66
|
+
}
|
|
67
|
+
// Normalizes a declared or detected language code to ISO 639-3 for comparison; strips a
|
|
68
|
+
// region/script subtag ("zh-cn" -> "zh") first. Codes already in 639-3 form pass through.
|
|
69
|
+
export function toIso3(code) {
|
|
70
|
+
var _ISO_639_1_TO_3_base;
|
|
71
|
+
const base = code.toLowerCase().split('-')[0];
|
|
72
|
+
return (_ISO_639_1_TO_3_base = ISO_639_1_TO_3[base]) !== null && _ISO_639_1_TO_3_base !== void 0 ? _ISO_639_1_TO_3_base : base;
|
|
73
|
+
}
|
|
74
|
+
// Languages this project measured for models whose cards declare none, so the fit check is
|
|
75
|
+
// not silently off for the shipped default; asserted here because it was measured, not attested.
|
|
76
|
+
export const MEASURED_MODEL_LANGUAGES = {
|
|
77
|
+
'minishlab/potion-retrieval-32M': [
|
|
78
|
+
'en'
|
|
79
|
+
]
|
|
80
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/embed/languages.ts"],"sourcesContent":["// HF model cards declare languages as ISO 639-1 tags (e.g. \"zh\"); franc-min reports ISO 639-3\n// (e.g. \"cmn\"). This bridges the two for comparison. Limited to franc-min's own 82-language\n// data (README table, https://github.com/wooorm/franc), so every value here is a code franc-min\n// can actually emit; a declared tag with no entry here is compared as-is.\nconst ISO_639_1_TO_3: Record<string, string> = {\n ar: 'arb',\n am: 'amh',\n az: 'azj',\n be: 'bel',\n bg: 'bul',\n bn: 'ben',\n bs: 'bos',\n cs: 'ces',\n de: 'deu',\n el: 'ell',\n en: 'eng',\n es: 'spa',\n fa: 'pes',\n fr: 'fra',\n gu: 'guj',\n ha: 'hau',\n hi: 'hin',\n hr: 'hrv',\n hu: 'hun',\n id: 'ind',\n ig: 'ibo',\n it: 'ita',\n ja: 'jpn',\n jv: 'jav',\n kk: 'kaz',\n kn: 'kan',\n ko: 'kor',\n ml: 'mal',\n mr: 'mar',\n ms: 'zlm',\n my: 'mya',\n ne: 'npi',\n nl: 'nld',\n pa: 'pan',\n pl: 'pol',\n pt: 'por',\n ro: 'ron',\n ru: 'rus',\n rw: 'kin',\n si: 'sin',\n so: 'som',\n sr: 'srp',\n su: 'sun',\n sv: 'swe',\n sw: 'swh',\n ta: 'tam',\n te: 'tel',\n th: 'tha',\n tl: 'tgl',\n tr: 'tur',\n uk: 'ukr',\n ur: 'urd',\n uz: 'uzn',\n vi: 'vie',\n yo: 'yor',\n zh: 'cmn',\n zu: 'zul',\n};\n\nexport function isKnownLanguageTag(tag: string): boolean {\n return tag in ISO_639_1_TO_3;\n}\n\n// Normalizes a declared or detected language code to ISO 639-3 for comparison; strips a\n// region/script subtag (\"zh-cn\" -> \"zh\") first. Codes already in 639-3 form pass through.\nexport function toIso3(code: string): string {\n const base = code.toLowerCase().split('-')[0];\n return ISO_639_1_TO_3[base] ?? base;\n}\n\n// Languages this project measured for models whose cards declare none, so the fit check is\n// not silently off for the shipped default; asserted here because it was measured, not attested.\nexport const MEASURED_MODEL_LANGUAGES: Record<string, string[]> = {\n 'minishlab/potion-retrieval-32M': ['en'], // BENCHMARKING.md bake-off; token-loss measurements\n};\n"],"names":["ISO_639_1_TO_3","ar","am","az","be","bg","bn","bs","cs","de","el","en","es","fa","fr","gu","ha","hi","hr","hu","id","ig","it","ja","jv","kk","kn","ko","ml","mr","ms","my","ne","nl","pa","pl","pt","ro","ru","rw","si","so","sr","su","sv","sw","ta","te","th","tl","tr","uk","ur","uz","vi","yo","zh","zu","isKnownLanguageTag","tag","toIso3","code","base","toLowerCase","split","MEASURED_MODEL_LANGUAGES"],"mappings":"AAAA,8FAA8F;AAC9F,4FAA4F;AAC5F,gGAAgG;AAChG,0EAA0E;AAC1E,MAAMA,iBAAyC;IAC7CC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,IAAI;AACN;AAEA,OAAO,SAASC,mBAAmBC,GAAW;IAC5C,OAAOA,OAAO3D;AAChB;AAEA,wFAAwF;AACxF,0FAA0F;AAC1F,OAAO,SAAS4D,OAAOC,IAAY;QAE1B7D;IADP,MAAM8D,OAAOD,KAAKE,WAAW,GAAGC,KAAK,CAAC,IAAI,CAAC,EAAE;IAC7C,QAAOhE,uBAAAA,cAAc,CAAC8D,KAAK,cAApB9D,kCAAAA,uBAAwB8D;AACjC;AAEA,2FAA2F;AAC3F,iGAAiG;AACjG,OAAO,MAAMG,2BAAqD;IAChE,kCAAkC;QAAC;KAAK;AAC1C,EAAE"}
|