ko-qmd 2.8.3-ko.10
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/CHANGELOG.md +1937 -0
- package/LICENSE +21 -0
- package/README.en.md +184 -0
- package/README.md +292 -0
- package/bin/qmd +192 -0
- package/dist/ast.d.ts +65 -0
- package/dist/ast.js +51 -0
- package/dist/bench/bench.d.ts +41 -0
- package/dist/bench/bench.js +12 -0
- package/dist/bench/score.d.ts +33 -0
- package/dist/bench/score.js +1 -0
- package/dist/bench/types.d.ts +101 -0
- package/dist/bench/types.js +1 -0
- package/dist/cli/build-info.json +4 -0
- package/dist/cli/embed-lock.d.ts +24 -0
- package/dist/cli/embed-lock.js +2 -0
- package/dist/cli/formatter.d.ts +120 -0
- package/dist/cli/formatter.js +60 -0
- package/dist/cli/mcp-pid.d.ts +33 -0
- package/dist/cli/mcp-pid.js +1 -0
- package/dist/cli/qmd.d.ts +101 -0
- package/dist/cli/qmd.js +165 -0
- package/dist/cli/version.d.ts +42 -0
- package/dist/cli/version.js +1 -0
- package/dist/collections.d.ts +162 -0
- package/dist/collections.js +1 -0
- package/dist/db.d.ts +63 -0
- package/dist/db.js +2 -0
- package/dist/filters.d.ts +46 -0
- package/dist/filters.js +1 -0
- package/dist/grep.d.ts +85 -0
- package/dist/grep.js +9 -0
- package/dist/hangul.d.ts +102 -0
- package/dist/hangul.js +1 -0
- package/dist/index.d.ts +270 -0
- package/dist/index.js +1 -0
- package/dist/llm.d.ts +584 -0
- package/dist/llm.js +40 -0
- package/dist/maintenance.d.ts +33 -0
- package/dist/maintenance.js +1 -0
- package/dist/mcp/daemon-health.d.ts +17 -0
- package/dist/mcp/daemon-health.js +1 -0
- package/dist/mcp/origin-guard.d.ts +67 -0
- package/dist/mcp/origin-guard.js +1 -0
- package/dist/mcp/server.d.ts +131 -0
- package/dist/mcp/server.js +35 -0
- package/dist/memory.d.ts +74 -0
- package/dist/memory.js +13 -0
- package/dist/metadata-filter.d.ts +74 -0
- package/dist/metadata-filter.js +1 -0
- package/dist/metadata-store.d.ts +45 -0
- package/dist/metadata-store.js +68 -0
- package/dist/metadata.d.ts +61 -0
- package/dist/metadata.js +1 -0
- package/dist/paths.d.ts +1 -0
- package/dist/paths.js +1 -0
- package/dist/query-log.d.ts +156 -0
- package/dist/query-log.js +2 -0
- package/dist/refresh.d.ts +60 -0
- package/dist/refresh.js +1 -0
- package/dist/rerank-input.d.ts +46 -0
- package/dist/rerank-input.js +5 -0
- package/dist/store.d.ts +1267 -0
- package/dist/store.js +421 -0
- package/dist/trust.d.ts +152 -0
- package/dist/trust.js +2 -0
- package/package.json +124 -0
- package/scripts/build.mjs +130 -0
- package/scripts/check-package-grammars.mjs +29 -0
- package/scripts/package-smoke.mjs +65 -0
- package/scripts/test-all.mjs +42 -0
- package/skills/qmd/SKILL.md +333 -0
- package/skills/qmd/references/mcp-setup.md +122 -0
- package/skills/release/SKILL.md +141 -0
- package/skills/release/scripts/install-hooks.sh +38 -0
- package/skills/release/scripts/release-context.sh +129 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collections configuration management
|
|
3
|
+
*
|
|
4
|
+
* This module manages the YAML-based collection configuration at ~/.config/qmd/index.yml.
|
|
5
|
+
* Collections define which directories to index and their associated contexts.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Context definitions for a collection
|
|
9
|
+
* Key is path prefix (e.g., "/", "/2024", "/Board of Directors")
|
|
10
|
+
* Value is the context description
|
|
11
|
+
*/
|
|
12
|
+
export type ContextMap = Record<string, string>;
|
|
13
|
+
/**
|
|
14
|
+
* A single collection configuration
|
|
15
|
+
*/
|
|
16
|
+
export interface Collection {
|
|
17
|
+
path: string;
|
|
18
|
+
pattern: string;
|
|
19
|
+
ignore?: string[];
|
|
20
|
+
context?: ContextMap;
|
|
21
|
+
update?: string;
|
|
22
|
+
includeByDefault?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Model configuration for embedding, reranking, and generation
|
|
26
|
+
*/
|
|
27
|
+
export interface ModelsConfig {
|
|
28
|
+
embed?: string;
|
|
29
|
+
rerank?: string;
|
|
30
|
+
generate?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The complete configuration file structure
|
|
34
|
+
*/
|
|
35
|
+
export interface CollectionConfig {
|
|
36
|
+
global_context?: string;
|
|
37
|
+
editor_uri?: string;
|
|
38
|
+
editor_uri_template?: string;
|
|
39
|
+
editorUri?: string;
|
|
40
|
+
"editor-uri"?: string;
|
|
41
|
+
collections: Record<string, Collection>;
|
|
42
|
+
models?: ModelsConfig;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Collection with its name (for return values)
|
|
46
|
+
*/
|
|
47
|
+
export interface NamedCollection extends Collection {
|
|
48
|
+
name: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Set the config source for SDK mode.
|
|
52
|
+
* - File path: load/save from a specific YAML file
|
|
53
|
+
* - Inline config: use an in-memory CollectionConfig (saveConfig updates in place, no file I/O)
|
|
54
|
+
* - undefined: reset to default file-based config
|
|
55
|
+
*/
|
|
56
|
+
export declare function setConfigSource(source?: {
|
|
57
|
+
configPath?: string;
|
|
58
|
+
config?: CollectionConfig;
|
|
59
|
+
}): void;
|
|
60
|
+
/**
|
|
61
|
+
* Set the current index name for config file lookup
|
|
62
|
+
* Config file will be ~/.config/qmd/{indexName}.yml
|
|
63
|
+
*/
|
|
64
|
+
export declare function setConfigIndexName(name: string): void;
|
|
65
|
+
export declare function getConfigDir(): string;
|
|
66
|
+
/**
|
|
67
|
+
* Find a project-local QMD config by walking upward from startDir.
|
|
68
|
+
* The local config lives at .qmd/index.yaml or .qmd/index.yml and,
|
|
69
|
+
* when used by the CLI, keeps both config and index DB writes inside
|
|
70
|
+
* the project instead of the global ~/.config / ~/.cache locations.
|
|
71
|
+
*/
|
|
72
|
+
export declare function findLocalConfigPath(startDir?: string): string | undefined;
|
|
73
|
+
/** Return the local SQLite index path paired with a local .qmd/index.yaml file. */
|
|
74
|
+
export declare function getLocalDbPath(configPath: string): string;
|
|
75
|
+
/**
|
|
76
|
+
* Load configuration from the configured source.
|
|
77
|
+
* - Inline config: returns the in-memory object directly
|
|
78
|
+
* - File-based: reads from YAML file (default ~/.config/qmd/index.yml)
|
|
79
|
+
* Returns empty config if file doesn't exist
|
|
80
|
+
*/
|
|
81
|
+
export declare function loadConfig(): CollectionConfig;
|
|
82
|
+
/**
|
|
83
|
+
* Save configuration to the configured source.
|
|
84
|
+
* - Inline config: updates the in-memory object (no file I/O)
|
|
85
|
+
* - File-based: writes to YAML file (default ~/.config/qmd/index.yml)
|
|
86
|
+
*/
|
|
87
|
+
export declare function saveConfig(config: CollectionConfig): void;
|
|
88
|
+
/**
|
|
89
|
+
* Get a specific collection by name
|
|
90
|
+
* Returns null if not found
|
|
91
|
+
*/
|
|
92
|
+
export declare function getCollection(name: string): NamedCollection | null;
|
|
93
|
+
/**
|
|
94
|
+
* List all collections
|
|
95
|
+
*/
|
|
96
|
+
export declare function listCollections(): NamedCollection[];
|
|
97
|
+
/**
|
|
98
|
+
* Get collections that are included by default in queries
|
|
99
|
+
*/
|
|
100
|
+
export declare function getDefaultCollections(): NamedCollection[];
|
|
101
|
+
/**
|
|
102
|
+
* Get collection names that are included by default
|
|
103
|
+
*/
|
|
104
|
+
export declare function getDefaultCollectionNames(): string[];
|
|
105
|
+
/**
|
|
106
|
+
* Update a collection's settings
|
|
107
|
+
*/
|
|
108
|
+
export declare function updateCollectionSettings(name: string, settings: {
|
|
109
|
+
update?: string | null;
|
|
110
|
+
includeByDefault?: boolean;
|
|
111
|
+
}): boolean;
|
|
112
|
+
/**
|
|
113
|
+
* Add or update a collection
|
|
114
|
+
*/
|
|
115
|
+
export declare function addCollection(name: string, path: string, pattern?: string, opts?: {
|
|
116
|
+
ignore?: string[];
|
|
117
|
+
}): void;
|
|
118
|
+
/**
|
|
119
|
+
* Remove a collection
|
|
120
|
+
*/
|
|
121
|
+
export declare function removeCollection(name: string): boolean;
|
|
122
|
+
/**
|
|
123
|
+
* Rename a collection
|
|
124
|
+
*/
|
|
125
|
+
export declare function renameCollection(oldName: string, newName: string): boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Get global context
|
|
128
|
+
*/
|
|
129
|
+
export declare function getGlobalContext(): string | undefined;
|
|
130
|
+
/**
|
|
131
|
+
* Set global context
|
|
132
|
+
*/
|
|
133
|
+
export declare function setGlobalContext(context: string | undefined): void;
|
|
134
|
+
/**
|
|
135
|
+
* Add or update a context for a specific path in a collection
|
|
136
|
+
*/
|
|
137
|
+
export declare function addContext(collectionName: string, pathPrefix: string, contextText: string): boolean;
|
|
138
|
+
/**
|
|
139
|
+
* Remove a context from a collection
|
|
140
|
+
*/
|
|
141
|
+
export declare function removeContext(collectionName: string, pathPrefix: string): boolean;
|
|
142
|
+
/**
|
|
143
|
+
* List all contexts across all collections
|
|
144
|
+
*/
|
|
145
|
+
export declare function listAllContexts(): Array<{
|
|
146
|
+
collection: string;
|
|
147
|
+
path: string;
|
|
148
|
+
context: string;
|
|
149
|
+
}>;
|
|
150
|
+
/**
|
|
151
|
+
* Find best matching context for a given collection and path
|
|
152
|
+
* Returns the most specific matching context (longest path prefix match)
|
|
153
|
+
*/
|
|
154
|
+
export declare function findContextForPath(collectionName: string, filePath: string): string | undefined;
|
|
155
|
+
/**
|
|
156
|
+
* Get the config file path (useful for error messages)
|
|
157
|
+
*/
|
|
158
|
+
export declare function getConfigPath(): string;
|
|
159
|
+
/**
|
|
160
|
+
* Check if config file exists
|
|
161
|
+
*/
|
|
162
|
+
export declare function configExists(): boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var P=Object.defineProperty;var i=(t,e)=>P(t,"name",{value:e,configurable:!0});import{existsSync as a,mkdirSync as m,readFileSync as O,writeFileSync as v}from"fs";import{join as s,dirname as h,resolve as D}from"path";import{qmdHomedir as w}from"./paths.js";import b from"yaml";let y="index",r={type:"file"};function B(t){if(!t){r={type:"file"};return}t.config?(t.config.collections||(t.config.collections={}),r={type:"inline",config:t.config}):t.configPath?r={type:"file",path:t.configPath}:r={type:"file"}}i(B,"setConfigSource");function E(t){const e=/^[a-zA-Z]:[\\/]/.test(t)||/^\\/.test(t);e||/[\\/]/.test(t)?y=(e?t:D(process.cwd(),t)).replace(/[:\\/]+/g,"_").replace(/^_+/,""):y=t}i(E,"setConfigIndexName");function _(){return process.env.QMD_CONFIG_DIR?process.env.QMD_CONFIG_DIR:process.env.XDG_CONFIG_HOME?s(process.env.XDG_CONFIG_HOME,"qmd"):s(w(),".config","qmd")}i(_,"getConfigDir");function x(){return s(_(),`${y}.yml`)}i(x,"getConfigFilePath");function M(t=process.cwd()){let e=D(t);for(;;){const n=s(e,".qmd"),o=s(n,"index.yaml");if(a(o))return o;const c=s(n,"index.yml");if(a(c))return c;const l=h(e);if(l===e)return;e=l}}i(M,"findLocalConfigPath");function W(t){return s(h(t),"index.sqlite")}i(W,"getLocalDbPath");function A(){const t=_();a(t)||m(t,{recursive:!0})}i(A,"ensureConfigDir");function f(){if(r.type==="inline")return r.config;const t=r.path||x();if(!a(t))return{collections:{}};try{const e=O(t,"utf-8"),o=b.parse(e)??{collections:{}};return o.collections||(o.collections={}),o}catch(e){throw new Error(`Failed to parse ${t}: ${e}`)}}i(f,"loadConfig");function u(t){if(r.type==="inline"){r.config=t;return}const e=r.path||x(),n=h(e);a(n)||m(n,{recursive:!0});try{const o=b.stringify(t,{indent:2,lineWidth:0});v(e,o,"utf-8")}catch(o){throw new Error(`Failed to write ${e}: ${o}`)}}i(u,"saveConfig");function z(t){const n=f().collections[t];return n?{name:t,...n}:null}i(z,"getCollection");function F(){const t=f();return Object.entries(t.collections).map(([e,n])=>({name:e,...n}))}i(F,"listCollections");function $(){return F().filter(t=>t.includeByDefault!==!1)}i($,"getDefaultCollections");function H(){return $().map(t=>t.name)}i(H,"getDefaultCollectionNames");function L(t,e){const n=f(),o=n.collections[t];return o?(e.update!==void 0&&(e.update===null?delete o.update:o.update=e.update),e.includeByDefault!==void 0&&(e.includeByDefault===!0?delete o.includeByDefault:o.includeByDefault=e.includeByDefault),u(n),!0):!1}i(L,"updateCollectionSettings");function N(t,e,n="**/*.md",o){const c=f(),l=c.collections[t];c.collections[t]={...l,path:e,pattern:n,...o?.ignore!==void 0?{ignore:o.ignore}:{}},u(c)}i(N,"addCollection");function k(t){const e=f();return e.collections[t]?(delete e.collections[t],u(e),!0):!1}i(k,"removeCollection");function Q(t,e){const n=f();if(!n.collections[t])return!1;if(n.collections[e])throw new Error(`Collection '${e}' already exists`);return n.collections[e]=n.collections[t],delete n.collections[t],u(n),!0}i(Q,"renameCollection");function R(){return f().global_context}i(R,"getGlobalContext");function X(t){const e=f();e.global_context=t,u(e)}i(X,"setGlobalContext");function Y(t,e,n){const o=f(),c=o.collections[t];return c?(c.context||(c.context={}),c.context[e]=n,u(o),!0):!1}i(Y,"addContext");function Z(t,e){const n=f(),o=n.collections[t];return o?.context?.[e]?(delete o.context[e],Object.keys(o.context).length===0&&delete o.context,u(n),!0):!1}i(Z,"removeContext");function J(){const t=f(),e=[];t.global_context&&e.push({collection:"*",path:"/",context:t.global_context});for(const[n,o]of Object.entries(t.collections))if(o.context)for(const[c,l]of Object.entries(o.context))e.push({collection:n,path:c,context:l});return e}i(J,"listAllContexts");function K(t,e){const n=f(),o=n.collections[t];if(!o?.context)return n.global_context;const c=[];for(const[l,d]of Object.entries(o.context)){const C=e.startsWith("/")?e:`/${e}`,g=l.startsWith("/")?l:`/${l}`,p=g!=="/"?g.replace(/\/+$/,""):g;(C===p||C.startsWith(p==="/"?"/":`${p}/`))&&c.push({prefix:p,context:d})}return c.length>0?(c.sort((l,d)=>d.prefix.length-l.prefix.length),c[0].context):n.global_context}i(K,"findContextForPath");function T(){return r.type==="inline"?"<inline>":r.path||x()}i(T,"getConfigPath");function U(){if(r.type==="inline")return!0;const t=r.path||x();return a(t)}i(U,"configExists");export{N as addCollection,Y as addContext,U as configExists,K as findContextForPath,M as findLocalConfigPath,z as getCollection,_ as getConfigDir,T as getConfigPath,H as getDefaultCollectionNames,$ as getDefaultCollections,R as getGlobalContext,W as getLocalDbPath,J as listAllContexts,F as listCollections,f as loadConfig,k as removeCollection,Z as removeContext,Q as renameCollection,u as saveConfig,E as setConfigIndexName,B as setConfigSource,X as setGlobalContext,L as updateCollectionSettings};
|
package/dist/db.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* db.ts - Cross-runtime SQLite compatibility layer
|
|
3
|
+
*
|
|
4
|
+
* Provides a unified Database export that works under both Bun (bun:sqlite)
|
|
5
|
+
* and Node.js (better-sqlite3). The APIs are nearly identical — the main
|
|
6
|
+
* difference is the import path.
|
|
7
|
+
*
|
|
8
|
+
* On macOS, Apple's system SQLite is compiled with SQLITE_OMIT_LOAD_EXTENSION,
|
|
9
|
+
* which prevents loading native extensions like sqlite-vec. When running under
|
|
10
|
+
* Bun we call Database.setCustomSQLite() to swap in Homebrew's full-featured
|
|
11
|
+
* SQLite build before creating any database instances.
|
|
12
|
+
*/
|
|
13
|
+
export declare const isBun: boolean;
|
|
14
|
+
export type SQLiteValue = string | number | bigint | Buffer | Uint8Array | Float32Array | null;
|
|
15
|
+
export type SQLiteParams = readonly SQLiteValue[];
|
|
16
|
+
/**
|
|
17
|
+
* Open a SQLite database. Works with both bun:sqlite and better-sqlite3.
|
|
18
|
+
*
|
|
19
|
+
* `bun:sqlite` and `better-sqlite3` both default `busy_timeout` to 0, so
|
|
20
|
+
* concurrent writers throw `SQLITE_BUSY` instead of waiting. WAL improves
|
|
21
|
+
* read-while-write concurrency but does not serialise writers. Setting the
|
|
22
|
+
* timeout at connection open makes parallel processes (e.g. an `update` or
|
|
23
|
+
* `query` racing a long `embed`, or a first-open schema migration racing any
|
|
24
|
+
* routine command) queue at batch boundaries instead of failing on contact.
|
|
25
|
+
*
|
|
26
|
+
* WAL is enabled here too (with a bounded retry) so connection-level pragmas
|
|
27
|
+
* live in one place and the cold-database journal migration survives concurrent
|
|
28
|
+
* opens.
|
|
29
|
+
*
|
|
30
|
+
* Default 120_000 ms outlasts the worst-case batch commit on a multi-GB
|
|
31
|
+
* index. Override with `QMD_SQLITE_BUSY_TIMEOUT` (value in milliseconds; `0`
|
|
32
|
+
* restores the upstream fail-fast behaviour). See
|
|
33
|
+
* https://bun.sh/docs/api/sqlite#busy-timeout.
|
|
34
|
+
*/
|
|
35
|
+
export declare function openDatabase(path: string): Database;
|
|
36
|
+
/**
|
|
37
|
+
* Common subset of the Database interface used throughout QMD.
|
|
38
|
+
*/
|
|
39
|
+
export interface Database {
|
|
40
|
+
exec(sql: string): void;
|
|
41
|
+
prepare(sql: string): Statement;
|
|
42
|
+
loadExtension(path: string): void;
|
|
43
|
+
transaction<T extends (...args: SQLiteValue[]) => unknown>(fn: T): T & {
|
|
44
|
+
immediate: T;
|
|
45
|
+
};
|
|
46
|
+
close(): void;
|
|
47
|
+
}
|
|
48
|
+
export interface Statement {
|
|
49
|
+
run(...params: SQLiteValue[]): {
|
|
50
|
+
changes: number;
|
|
51
|
+
lastInsertRowid: number | bigint;
|
|
52
|
+
};
|
|
53
|
+
get<T = unknown>(...params: SQLiteValue[]): T | undefined;
|
|
54
|
+
all<T = unknown>(...params: SQLiteValue[]): T[];
|
|
55
|
+
iterate<T = unknown>(...params: SQLiteValue[]): IterableIterator<T>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Load the sqlite-vec extension into a database.
|
|
59
|
+
*
|
|
60
|
+
* Throws with platform-specific fix instructions when the extension is
|
|
61
|
+
* unavailable.
|
|
62
|
+
*/
|
|
63
|
+
export declare function loadSqliteVec(db: Database): void;
|
package/dist/db.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var u=Object.defineProperty;var s=(e,t)=>u(e,"name",{value:t,configurable:!0});const r="Bun"in globalThis;let l,i;if(r){const t=(await import("bun:sqlite")).Database;if(process.platform==="darwin"){const n=["/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib","/usr/local/opt/sqlite/lib/libsqlite3.dylib"];for(const o of n)try{t.setCustomSQLite(o);break}catch{}}l=t;try{const{getLoadablePath:n}=await import("sqlite-vec"),o=n(),a=new t(":memory:");a.loadExtension(o),a.close(),i=s(c=>c.loadExtension(o),"_sqliteVecLoad")}catch{i=null}}else{l=(await import("better-sqlite3")).default;const e=await import("sqlite-vec");i=s(t=>e.load(t),"_sqliteVecLoad")}function b(e){if(typeof e!="object"||e===null)return!1;const t=e.code;if(t==="SQLITE_BUSY"||t==="SQLITE_BUSY_SNAPSHOT")return!0;const n=e.message;return typeof n=="string"&&/database is locked|database is busy|SQLITE_BUSY/i.test(n)}s(b,"isBusyError");function m(e){Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,e)}s(m,"sleepSync");function d(e,t){const n=Date.now()+Math.max(t,0);for(let o=0;;o++)try{e.exec("PRAGMA journal_mode = WAL");return}catch(a){if(!b(a)||Date.now()>=n)throw a;m(Math.min(5+o,25))}}s(d,"enableWal");function p(e){const t=new l(e),n=process.env.QMD_SQLITE_BUSY_TIMEOUT,o=n!==void 0&&n!==""?Number(n):Number.NaN,a=Number.isFinite(o)&&o>=0?Math.floor(o):12e4;return t.exec(`PRAGMA busy_timeout = ${a}`),d(t,a),t}s(p,"openDatabase");function w(e){if(!i){const t=r&&process.platform==="darwin"?`On macOS with Bun, install Homebrew SQLite: brew install sqlite
|
|
2
|
+
Or install qmd with npm instead: npm install -g ko-qmd`:"Ensure the sqlite-vec native module is installed correctly.";throw new Error(`sqlite-vec extension is unavailable. ${t}`)}i(e)}s(w,"loadSqliteVec");export{r as isBun,w as loadSqliteVec,p as openDatabase};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* filters.ts — narrowing a search to part of the corpus.
|
|
3
|
+
*
|
|
4
|
+
* A query can already be scoped to collections. These are the two other questions people
|
|
5
|
+
* actually ask of their own notes: "only under this path" and "only what I touched lately".
|
|
6
|
+
* Parsing lives here, away from SQL, because the failure that matters is a typo in a flag:
|
|
7
|
+
* `--since 7dd` has to say so rather than quietly search everything.
|
|
8
|
+
*/
|
|
9
|
+
export type DocumentFilter = {
|
|
10
|
+
/** Globs against `collection/path`, the same string results print. `!` prefix excludes. */
|
|
11
|
+
readonly paths?: readonly string[];
|
|
12
|
+
/** ISO timestamps compared against `documents.modified_at`. */
|
|
13
|
+
readonly sinceIso?: string;
|
|
14
|
+
readonly untilIso?: string;
|
|
15
|
+
};
|
|
16
|
+
/** True when the filter would actually narrow anything. */
|
|
17
|
+
export declare const isNarrowing: (filter?: DocumentFilter) => filter is DocumentFilter;
|
|
18
|
+
/**
|
|
19
|
+
* A point in time, from either a span back from now (`7d`, `3h`, `2w`, `6mo`) or a date
|
|
20
|
+
* (`2026-09-01`, `2026-09-01T10:00:00Z`). Throws on anything else — a filter that silently
|
|
21
|
+
* means "no filter" is worse than an error, because the result still looks like an answer.
|
|
22
|
+
*/
|
|
23
|
+
export declare const parseTimeSpec: (spec: string, now?: Date) => string;
|
|
24
|
+
type PathMatcher = (displayPath: string) => boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Compile path globs into one predicate.
|
|
27
|
+
*
|
|
28
|
+
* Positive patterns are an OR: a document has to be under one of them. Negative (`!`)
|
|
29
|
+
* patterns then veto, so `--path 'journals/**' --path '!journals/2024/**'` reads the way it
|
|
30
|
+
* looks. Negatives alone mean "everything except these".
|
|
31
|
+
*/
|
|
32
|
+
export declare const compilePathMatcher: (patterns: readonly string[]) => PathMatcher;
|
|
33
|
+
/** Whether a document's `collection/path` survives the filter's path globs. */
|
|
34
|
+
export declare const matchesPaths: (displayPath: string, filter?: DocumentFilter) => boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Build a filter from raw CLI or MCP input. Returns undefined when nothing was asked for, so
|
|
37
|
+
* callers can keep the unfiltered fast paths.
|
|
38
|
+
*/
|
|
39
|
+
export declare const buildDocumentFilter: (input: {
|
|
40
|
+
path?: readonly string[];
|
|
41
|
+
since?: string;
|
|
42
|
+
until?: string;
|
|
43
|
+
}, now?: Date) => DocumentFilter | undefined;
|
|
44
|
+
/** How the filter reads back to a user when it removed everything. */
|
|
45
|
+
export declare const describeFilter: (filter: DocumentFilter) => string;
|
|
46
|
+
export {};
|
package/dist/filters.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var p=Object.defineProperty;var c=(e,t)=>p(e,"name",{value:t,configurable:!0});import m from"picomatch";const w=c(e=>e!==void 0&&((e.paths?.length??0)>0||e.sinceIso!==void 0||e.untilIso!==void 0),"isNarrowing"),_=/^(\d+)\s*(m|h|d|w|mo|y)$/i,g={m:6e4,h:36e5,d:864e5,w:6048e5,mo:2592e6,y:31536e6},f=c((e,t=new Date)=>{const s=e.trim();if(s==="")throw new Error("empty time filter");const o=_.exec(s);if(o){const a=Number(o[1]),d=o[2].toLowerCase();return new Date(t.getTime()-a*g[d]).toISOString()}const r=/^(\d{4})-(\d{2})-(\d{2})$/.exec(s);if(r){const[,a,d,h]=r,n=new Date(Number(a),Number(d)-1,Number(h));if(Number.isNaN(n.getTime()))throw new Error(`not a date: '${e}'`);return n.toISOString()}const u=new Date(s);if(Number.isNaN(u.getTime()))throw new Error(`not a time: '${e}' \u2014 use a span like '7d' or a date like '2026-09-01'`);return u.toISOString()},"parseTimeSpec"),l=new Map,N=c(e=>{const t=e.join("\0"),s=l.get(t);if(s)return s;const o=[],r=[];for(const n of e){const i=n.trim();i!==""&&(i.startsWith("!")?r.push(i.slice(1)):o.push(i))}const u=c(n=>{if(n.includes("*"))return[n];const i=n.replace(/\/+$/,"");return i===""||i===n?i===n?[n,`${n}/**`]:[n]:[i,`${i}/**`]},"asPrefix"),a=o.length===0?()=>!0:m(o.flatMap(u),{dot:!0}),d=r.length===0?()=>!1:m(r.flatMap(u),{dot:!0}),h=c(n=>a(n)&&!d(n),"matcher");return l.set(t,h),h},"compilePathMatcher"),$=c((e,t)=>!t?.paths||t.paths.length===0?!0:N(t.paths)(e),"matchesPaths"),b=c((e,t=new Date)=>{const s=(e.path??[]).filter(a=>a.trim()!==""),o=e.since!==void 0?f(e.since,t):void 0,r=e.until!==void 0?f(e.until,t):void 0;if(o!==void 0&&r!==void 0&&o>r)throw new Error("--since is after --until");const u={...s.length>0?{paths:s}:{},...o?{sinceIso:o}:{},...r?{untilIso:r}:{}};return w(u)?u:void 0},"buildDocumentFilter"),S=c(e=>{const t=[];return e.paths&&e.paths.length>0&&t.push(`path ${e.paths.map(s=>`'${s}'`).join(", ")}`),e.sinceIso&&t.push(`since ${e.sinceIso}`),e.untilIso&&t.push(`until ${e.untilIso}`),t.join(", ")},"describeFilter");export{b as buildDocumentFilter,N as compilePathMatcher,S as describeFilter,w as isNarrowing,$ as matchesPaths,f as parseTimeSpec};
|
package/dist/grep.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* grep.ts — exact and regex matching over indexed document bodies.
|
|
3
|
+
*
|
|
4
|
+
* This is the escape hatch for the failure this fork keeps hitting: a query finds nothing
|
|
5
|
+
* because tokenization never produced the term the document contains. BM25 and vector search
|
|
6
|
+
* both answer "what is this about"; sometimes the question is "where does this string
|
|
7
|
+
* appear", and no amount of ranking answers that.
|
|
8
|
+
*
|
|
9
|
+
* It is not a replacement for ripgrep. At a terminal with a shell, ripgrep is usually the
|
|
10
|
+
* right tool and faster. This earns its place for the caller with no shell — an MCP client
|
|
11
|
+
* whose only tools are `query`, `get` and `multi_get` — and for the caller who wants one
|
|
12
|
+
* corpus definition: it searches exactly what the index holds, honours collection scoping,
|
|
13
|
+
* and returns `qmd://` paths, docids and line numbers that `qmd get file.md:120:40` accepts.
|
|
14
|
+
*/
|
|
15
|
+
import type { Database } from "./db.js";
|
|
16
|
+
import { type DocumentFilter } from "./filters.js";
|
|
17
|
+
import { type MetadataFilter } from "./metadata-filter.js";
|
|
18
|
+
export type GrepLine = {
|
|
19
|
+
/** 1-indexed line number within the document body, as `qmd get path:N:count` takes it. */
|
|
20
|
+
readonly line: number;
|
|
21
|
+
readonly text: string;
|
|
22
|
+
};
|
|
23
|
+
export type GrepFileMatch = {
|
|
24
|
+
readonly filepath: string;
|
|
25
|
+
readonly displayPath: string;
|
|
26
|
+
readonly title: string;
|
|
27
|
+
readonly hash: string;
|
|
28
|
+
readonly docid: string;
|
|
29
|
+
readonly collectionName: string;
|
|
30
|
+
readonly lines: readonly GrepLine[];
|
|
31
|
+
/** Matches found in this file, which can exceed `lines.length` once the per-file cap bites. */
|
|
32
|
+
readonly matchCount: number;
|
|
33
|
+
};
|
|
34
|
+
export type GrepOptions = {
|
|
35
|
+
/** Max files to return. The scan stops once this many have matched. */
|
|
36
|
+
readonly limit?: number;
|
|
37
|
+
/** Max lines reported per file. */
|
|
38
|
+
readonly maxLinesPerFile?: number;
|
|
39
|
+
/** Force case sensitivity. Left unset, smart case decides. */
|
|
40
|
+
readonly caseSensitive?: boolean;
|
|
41
|
+
/** Treat the pattern as a literal string rather than a regular expression. */
|
|
42
|
+
readonly fixedString?: boolean;
|
|
43
|
+
readonly filter?: DocumentFilter;
|
|
44
|
+
readonly collections?: readonly string[];
|
|
45
|
+
/** Metadata filter (`--filter` JSON AST); same semantics as searchFTS. */
|
|
46
|
+
readonly metadataFilter?: MetadataFilter;
|
|
47
|
+
};
|
|
48
|
+
export type GrepResult = {
|
|
49
|
+
readonly files: readonly GrepFileMatch[];
|
|
50
|
+
/** Documents read, whether or not they matched — what "no results" was measured against. */
|
|
51
|
+
readonly scanned: number;
|
|
52
|
+
/** True when `limit` stopped the scan, so the corpus was not read to the end. */
|
|
53
|
+
readonly truncated: boolean;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Cap on pattern length.
|
|
57
|
+
*
|
|
58
|
+
* A user-supplied pattern compiled as a JS `RegExp` is a ReDoS surface, and in the daemon a
|
|
59
|
+
* catastrophic backtrack is not a slow query — it blocks the only thread, so no timer fires
|
|
60
|
+
* and every session on the machine stops getting answers. The containment here is structural
|
|
61
|
+
* rather than a timeout: the scan runs line by line, so the worst case is bounded by the
|
|
62
|
+
* longest line rather than by the whole document, and the exponents that make a backtrack
|
|
63
|
+
* catastrophic need a long input to bite. A 1KB line and a bounded pattern keep that product
|
|
64
|
+
* small. Patterns longer than this are rejected outright.
|
|
65
|
+
*/
|
|
66
|
+
export declare const MAX_PATTERN_LENGTH = 1000;
|
|
67
|
+
/**
|
|
68
|
+
* Compile a user pattern.
|
|
69
|
+
*
|
|
70
|
+
* Smart case: a pattern containing an uppercase letter matches case-sensitively, anything
|
|
71
|
+
* else matches either case. Hangul has no case, so Korean patterns land on the
|
|
72
|
+
* case-insensitive side either way, which is what a Korean user wants.
|
|
73
|
+
*/
|
|
74
|
+
export declare const compileGrepPattern: (pattern: string, options?: {
|
|
75
|
+
caseSensitive?: boolean;
|
|
76
|
+
fixedString?: boolean;
|
|
77
|
+
}) => RegExp;
|
|
78
|
+
/**
|
|
79
|
+
* Scan indexed bodies for a pattern.
|
|
80
|
+
*
|
|
81
|
+
* Documents are read in keyset-paginated batches rather than into one array — a vault's
|
|
82
|
+
* worth of bodies does not belong in memory at once, and the scan stops as soon as `limit`
|
|
83
|
+
* files have matched, so a common pattern costs a few batches rather than the corpus.
|
|
84
|
+
*/
|
|
85
|
+
export declare const grepDocuments: (db: Database, pattern: RegExp, options?: GrepOptions) => GrepResult;
|
package/dist/grep.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
var w=Object.defineProperty;var r=(t,o)=>w(t,"name",{value:o,configurable:!0});import{isNarrowing as b,matchesPaths as O}from"./filters.js";import{compileMetadataFilter as P}from"./metadata-filter.js";import{METADATA_EXTRACTION_VERSION as C}from"./metadata.js";const I=1e3,_=4e3,D=20,y=20,$=200,H=r(t=>t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"escapeRegExp"),B=r((t,o={})=>{if(t==="")throw new Error("empty pattern");if(t.length>I)throw new Error(`pattern is ${t.length} characters, over the ${I} limit`);const n=o.fixedString?H(t):t,s=/[A-Z]/.test(t),f=o.caseSensitive??s;try{return new RegExp(n,f?"g":"gi")}catch(a){throw new Error(`not a regular expression: ${a instanceof Error?a.message:String(a)}`)}},"compileGrepPattern"),U=r((t,o)=>{if(t.length<=_)return o.lastIndex=0,o.test(t);const n=_-I;for(let s=0;s<t.length;s+=n)if(o.lastIndex=0,o.test(t.slice(s,s+_)))return!0;return!1},"lineMatches"),G=r((t,o,n={})=>{const s=r((e,i)=>e===void 0||!Number.isFinite(e)?i:Math.max(1,Math.floor(e)),"clampCount"),f=s(n.limit,D),a=s(n.maxLinesPerFile,y),c=n.filter,F=b(c),h=n.collections,N=n.metadataFilter,l=["d.active = 1","d.id > ?"],d=[];let L="";if(N){const e=P(N,"d");L=" LEFT JOIN document_metadata dm ON dm.document_id = d.id",l.push(`dm.extraction_version = ${C}`,"dm.extraction_error IS NULL",e.sql),d.push(...e.params)}h&&h.length>0&&(l.push(`d.collection IN (${h.map(()=>"?").join(",")})`),d.push(...h.map(String))),c?.sinceIso&&(l.push("d.modified_at >= ?"),d.push(c.sinceIso)),c?.untilIso&&(l.push("d.modified_at <= ?"),d.push(c.untilIso));const R=t.prepare(`
|
|
2
|
+
SELECT d.id, d.collection, d.path, d.title, d.hash, d.modified_at, content.doc as body
|
|
3
|
+
FROM documents d
|
|
4
|
+
JOIN content ON content.hash = d.hash${L}
|
|
5
|
+
WHERE ${l.join(" AND ")}
|
|
6
|
+
ORDER BY d.id
|
|
7
|
+
LIMIT ?
|
|
8
|
+
`),u=[];let x=0,A=!1,T=0;t:for(;;){const e=R.all(T,...d,$);if(e.length===0)break;T=e[e.length-1].id;for(const i of e){const p=`${i.collection}/${i.path}`;if(F&&!O(p,c))continue;x++;const E=[];let g=0;const M=(i.body??"").split(`
|
|
9
|
+
`);for(let m=0;m<M.length;m++){const S=M[m];U(S,o)&&(g++,E.length<a&&E.push({line:m+1,text:S}))}if(g!==0&&(u.push({filepath:`qmd://${p}`,displayPath:p,title:i.title,hash:i.hash,docid:i.hash.slice(0,6),collectionName:i.collection,lines:E,matchCount:g}),u.length>=f)){A=!0;break t}}if(e.length<$)break}return{files:u,scanned:x,truncated:A}},"grepDocuments");export{I as MAX_PATTERN_LENGTH,B as compileGrepPattern,G as grepDocuments};
|
package/dist/hangul.d.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hangul.ts - Korean (Hangul) handling for FTS5 indexing and queries.
|
|
3
|
+
*
|
|
4
|
+
* ko-qmd patch stack: all Hangul-specific logic lives here so store.ts keeps
|
|
5
|
+
* only one-line call sites. Han and kana appear only as CJK-side characters in
|
|
6
|
+
* estimateCharsPerToken's density count — indexing and query logic never sees them.
|
|
7
|
+
*/
|
|
8
|
+
/** True when `text` has any Hangul syllable or jamo, script-mixed text included. */
|
|
9
|
+
export declare const containsHangul: (text: string) => boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Syllable bigrams of every Hangul run in `text`, space-joined in run order
|
|
12
|
+
* (`검색을 개선` → `검색 색을 개선`). Appended after the character-spaced field
|
|
13
|
+
* text at index time: the character tokens keep their original adjacency, so
|
|
14
|
+
* existing phrase queries match exactly as before, and bigram phrases from
|
|
15
|
+
* one run stay contiguous.
|
|
16
|
+
*/
|
|
17
|
+
export declare function hangulBigramTail(text: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Whether two texts share a Hangul syllable bigram (`검색을 고치기` and `문서 검색`
|
|
20
|
+
* share `검색`). Two Korean sentences about the same thing overlap in syllables long
|
|
21
|
+
* before they overlap in whole words, because the words carry different particles and
|
|
22
|
+
* endings, so a bigram is the smallest unit that says "these are about the same thing"
|
|
23
|
+
* without stemming both sides. Returns false when either side has no Hangul run of two
|
|
24
|
+
* or more syllables.
|
|
25
|
+
*/
|
|
26
|
+
export declare function sharesHangulBigram(a: string, b: string): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Drop the excluded clause of a Korean negation query and keep what follows the
|
|
29
|
+
* last marker (`tf-idf 말고 순위 공식` → `순위 공식`), so retrieval and rerank
|
|
30
|
+
* never see the concept the user ruled out. Returns the query unchanged when a
|
|
31
|
+
* side would be empty or hold only `-term` exclusions, a `"` phrase is present, or
|
|
32
|
+
* no marker stands as its own token — a marker glued to X (`이거말고`) is not split.
|
|
33
|
+
*/
|
|
34
|
+
export declare function stripHangulNegation(query: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Strip one trailing particle (or 기) from a pure-Hangul word when the stem
|
|
37
|
+
* keeps at least two syllables (`검색을` → `검색`, `책을` stays). Returns null
|
|
38
|
+
* when nothing is stripped.
|
|
39
|
+
*/
|
|
40
|
+
export declare function stripHangulParticle(word: string): string | null;
|
|
41
|
+
/**
|
|
42
|
+
* Strip one trailing verbal/nominalizing ending (`토큰화하는` → `토큰화`,
|
|
43
|
+
* `검색하기` → `검색`). Returns null when nothing is stripped. Endings are tried
|
|
44
|
+
* before particles so `하기` wins over the bare `기` that would leave `검색하`.
|
|
45
|
+
*/
|
|
46
|
+
export declare function stripHangulEnding(word: string): string | null;
|
|
47
|
+
/**
|
|
48
|
+
* Stems worth querying for one Hangul word, longest first and without the word
|
|
49
|
+
* itself: an ending (`토큰화하는` → `토큰화`), then particles, twice, so a chain
|
|
50
|
+
* like `청킹에서의` reaches `청킹`. Two rounds cover the chains Korean actually
|
|
51
|
+
* stacks in queries (`에서 + 의`, `으로 + 는`) without unwinding real syllables.
|
|
52
|
+
*/
|
|
53
|
+
export declare function hangulStems(word: string): string[];
|
|
54
|
+
/**
|
|
55
|
+
* FTS5 expression for a plain (unquoted) Hangul query term: for the word and
|
|
56
|
+
* each of its stems, a bigram phrase (from hangulBigramTail) OR the character
|
|
57
|
+
* phrase (`검색` → `("검색" OR "검 색")`). The character phrase keeps spacing
|
|
58
|
+
* variants matching (`간격반복` over `간격 반복`), which bigrams cannot span; the
|
|
59
|
+
* bigram phrase adds its BM25 weight. A loanword (hangulLoanwordForms) also ORs
|
|
60
|
+
* its Latin spelling. Returns null for single syllables with no loanword entry and
|
|
61
|
+
* for non-Hangul or mixed-script terms, so the caller keeps its default character
|
|
62
|
+
* phrase.
|
|
63
|
+
*/
|
|
64
|
+
export declare function hangulTermQuery(term: string): string | null;
|
|
65
|
+
/**
|
|
66
|
+
* Latin spellings for a Hangul loanword (`서치` → `search`), looked up on the word and on
|
|
67
|
+
* its particle-stripped stems so `서치를` bridges too. Empty when nothing matches.
|
|
68
|
+
*/
|
|
69
|
+
export declare function hangulLoanwordForms(term: string): string[];
|
|
70
|
+
/**
|
|
71
|
+
* FTS5 expression for a term that glues Hangul to Latin or digits
|
|
72
|
+
* (`SKILL.md계약의핵심`, `hook이유일한hardboundary다`, `auto-dream의발화조건`).
|
|
73
|
+
* The index already splits scripts — hangulBigramTail only sees Hangul runs and the
|
|
74
|
+
* porter tokenizer splits the rest — so the query has to split too, or the glued term
|
|
75
|
+
* is searched as one phrase that appears nowhere. Each Hangul run goes through
|
|
76
|
+
* hangulTermQuery, each Latin/digit run becomes a prefix term, and the runs are ANDed.
|
|
77
|
+
* Separators (`.`, `-`, `/`, `+`) are dropped with the rest of the non-alphanumerics.
|
|
78
|
+
* Returns null when the term is not mixed script, so callers keep their own handling.
|
|
79
|
+
*/
|
|
80
|
+
export declare function hangulMixedQuery(term: string): string | null;
|
|
81
|
+
/**
|
|
82
|
+
* Estimated characters per token for chunk sizing, judged from the script
|
|
83
|
+
* makeup of `text` alone — a regex count, no tokenizer call. CJK (Hangul,
|
|
84
|
+
* Han, kana) runs at 1.6 chars/token (measured 1.64 on Korean prose,
|
|
85
|
+
* 2026-09-21); everything else keeps whatever ratio the caller was already
|
|
86
|
+
* using, so non-CJK chunk boundaries do not move. That ratio differs by call
|
|
87
|
+
* site — indexing sized its first pass at 3.0, the search paths at 4.0
|
|
88
|
+
* (`CHUNK_SIZE_CHARS / CHUNK_SIZE_TOKENS`) — so each passes its own through
|
|
89
|
+
* `otherCharsPerToken` rather than sharing one default. Because token counts add across
|
|
90
|
+
* scripts, the blend is the harmonic mean `(c + o) / (c/1.6 + o/3.0)` — a
|
|
91
|
+
* linear interpolation of the two ratios would overestimate a half-and-half
|
|
92
|
+
* document by about 10% (2.30 vs 2.09) and call the resplit safety net back
|
|
93
|
+
* into action. The count is whole-text, not a leading sample: a Korean
|
|
94
|
+
* document that opens with an English code block would otherwise be judged
|
|
95
|
+
* Latin. Empty text returns `otherCharsPerToken`, the caller's previous behavior.
|
|
96
|
+
*
|
|
97
|
+
* `otherCharsPerToken` must be positive — zero or negative yields a non-finite
|
|
98
|
+
* ratio and NaN budgets downstream. Both in-repo call sites pass a literal
|
|
99
|
+
* (3.0 when indexing, `CHUNK_SIZE_CHARS / CHUNK_SIZE_TOKENS` on the search
|
|
100
|
+
* paths), so this is a precondition, not a runtime check.
|
|
101
|
+
*/
|
|
102
|
+
export declare function estimateCharsPerToken(text: string, otherCharsPerToken?: number): number;
|
package/dist/hangul.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var g=Object.defineProperty;var i=(n,t)=>g(n,"name",{value:t,configurable:!0});const a=/^\p{Script=Hangul}+$/u,s=/\p{Script=Hangul}+/gu,h=/\p{Script=Hangul}/u,m=["\uC5D0\uAC8C","\uC5D0\uC11C","\uC73C\uB85C","\uAE4C\uC9C0","\uBD80\uD130","\uCC98\uB7FC","\uC740","\uB294","\uC774","\uAC00","\uC744","\uB97C","\uC5D0","\uB85C","\uC758","\uB3C4","\uC640","\uACFC","\uAE30"],d=["\uD558\uB824\uB294","\uB418\uB824\uB294","\uD558\uBA74\uC11C","\uB418\uBA74\uC11C","\uD558\uB294","\uD558\uAE30","\uD55C\uB2E4","\uD588\uB2E4","\uD558\uBA70","\uD558\uACE0","\uD558\uC5EC","\uD574\uC11C","\uD558\uC9C0","\uD558\uB2E4","\uB418\uB294","\uB418\uAE30","\uB41C\uB2E4","\uB410\uB2E4","\uB418\uBA70","\uB418\uACE0","\uB418\uC5B4","\uB3FC\uC11C","\uB418\uB2E4","\uD55C","\uB41C"],b=2,S=new Set(["\uB9D0\uACE0","\uBE7C\uACE0","\uC81C\uC678\uD558\uACE0","\uC81C\uC678\uD55C"]);function l(n){const t=Array.from(n);return t.slice(1).map((e,o)=>t[o]+e)}i(l,"syllableBigrams");const k=i(n=>h.test(n),"containsHangul");function w(n){return(n.match(s)??[]).flatMap(l).join(" ")}i(w,"hangulBigramTail");function M(n,t){const e=new Set((n.match(s)??[]).flatMap(l));if(e.size===0)return!1;for(const o of t.match(s)??[])for(const r of l(o))if(e.has(r))return!0;return!1}i(M,"sharesHangulBigram");function A(n){if(n.includes('"')||!k(n))return null;const t=n.trim().split(/\s+/),e=t.findLastIndex(r=>S.has(r));if(e<1||e===t.length-1)return null;const o=t.slice(e+1);return o.every(r=>r.startsWith("-"))?null:{excluded:t.slice(0,e),kept:o}}i(A,"splitHangulNegation");function $(n){const t=A(n);return t?t.kept.join(" "):n}i($,"stripHangulNegation");function N(n){return u(n,m)}i(N,"stripHangulParticle");function H(n){return u(n,d)}i(H,"stripHangulEnding");function u(n,t){if(!a.test(n))return null;const e=Array.from(n);for(const o of t){const r=e.length-Array.from(o).length;if(!(r<2)&&n.endsWith(o))return e.slice(0,r).join("")}return null}i(u,"stripOnce");function p(n){const t=[];let e=n;for(let o=0;o<2;o++){const r=H(e)??N(e);if(r===null||r===e)break;t.push(r),e=r}return[...t,...t.flatMap(_)]}i(p,"hangulStems");function _(n){return n.endsWith("\uD558")?[`${n.slice(0,-1)}\uD574`]:n.endsWith("\uD574")?[`${n.slice(0,-1)}\uD558`]:[]}i(_,"contractions");function R(n){if(!a.test(n))return null;const t=p(n),e=f([n,...t]).map(r=>`"${r}"`);if(Array.from(n).length<2)return e.length>0?`(${[`"${n}"`,...e].join(" OR ")})`:null;const o=i(r=>[`"${l(r).join(" ")}"`,`"${Array.from(r).join(" ")}"`],"phrases");return`(${[...t.flatMap(o),...o(n),...e].join(" OR ")})`}i(R,"hangulTermQuery");const x={\uAC00\uB4DC:["guard"],\uAC00\uC774\uB4DC:["guide"],\uAC8C\uC774\uD2B8:["gate"],\uACE8\uB4E0:["golden"],\uADF8\uB798\uD504:["graph"],\uB178\uD2B8:["note"],\uB370\uBAAC:["daemon"],\uB370\uC774\uD130:["data"],\uB370\uC774\uD130\uC14B:["dataset"],\uB514\uBC14\uC774\uC2A4:["device"],\uB514\uC2DC\uC804:["decision"],\uB7AD\uD06C:["rank"],\uB7AD\uD0B9:["ranking"],\uB9AC\uC561\uD2B8:["react"],\uB808\uC2DC\uD504\uB85C\uCEEC:["reciprocal"],\uB808\uCF54\uB4DC:["record"],\uB85C\uADF8:["log"],\uB9AC\uB7AD\uCEE4:["reranker"],\uB9AC\uB7AD\uD06C:["rerank"],\uB9AC\uB7AD\uD0B9:["reranking"],\uB9AC\uD2B8\uB9AC\uBC8C:["retrieval"],\uB9AC\uD53C\uD2F0\uC158:["repetition"],\uB9B0\uD2B8:["lint"],\uB8E8\uD504:["loop"],\uB9C8\uC774\uADF8\uB808\uC774\uC158:["migration"],\uBA54\uD0C0\uB370\uC774\uD130:["metadata"],\uBAA8\uB378:["model"],\uBBF8\uD305:["meeting"],\uBC14\uC774\uADF8\uB7A8:["bigram"],\uBC31\uB9C1\uD06C:["backlink"],\uBCA1\uD130:["vector"],\uBCA4\uCE58:["bench"],\uBCA4\uCE58\uB9C8\uD06C:["benchmark"],\uBCD1\uB82C:["parallel"],\uC11C\uBC84:["server"],\uC11C\uCE58:["search"],\uC138\uC158:["session"],\uC2A4\uD0A4\uB9C8:["schema"],\uC2A4\uD0AC:["skill"],\uC2A4\uD398\uC774\uC2A4\uB4DC:["spaced"],\uC2F1\uD06C:["sync"],\uC544\uCE74\uC774\uBE0C:["archive"],\uC5B4\uD39C\uB4DC:["append"],\uC5D0\uC774\uC804\uD2B8:["agent"],\uC5D4\uADF8\uB7A8:["ngram","n gram"],\uC624\uD1A0\uBA54\uC774\uC158:["automation"],\uC628\uB9AC:["only"],\uC635\uC2DC\uB514\uC5B8:["obsidian"],\uC6CC\uD06C\uD50C\uB85C:["workflow"],\uC6CC\uD06C\uD50C\uB85C\uC6B0:["workflow"],\uC6F9:["web"],\uC704\uD0A4:["wiki"],\uC704\uD0A4\uB9C1\uD06C:["wikilink"],\uC720\uB2C8\uADF8\uB7A8:["unigram"],\uC775\uC2A4\uD32C\uC158:["expansion"],\uC778\uB371\uC2A4:["index"],\uC778\uBC84\uD2F0\uB4DC:["inverted"],\uC778\uC81C\uC2A4\uD2B8:["ingest"],\uC778\uCF54\uB354:["encoder"],\uC784\uBCA0\uB529:["embedding"],\uC9C0\uC5F0:["latency"],\uCCAD\uD06C:["chunk"],\uCCAD\uD0B9:["chunking"],\uCE90\uC2DC:["cache"],\uCEE8\uD14D\uC2A4\uD2B8:["context"],\uCEEC\uB809\uC158:["collection"],\uCF54\uC5B4:["core"],\uCFFC\uB9AC:["query"],\uD06C\uB85C\uC2A4:["cross"],\uD074\uB77C\uC774\uC5B8\uD2B8:["client"],\uD15C\uD50C\uB9BF:["template"],\uD1A0\uD06C\uB098\uC774\uC81C\uC774\uC158:["tokenization"],\uD1A0\uD070:["token"],\uD1A0\uD53D:["topic"],\uD2B8\uB798\uD0B9:["tracking"],\uD30C\uC774\uD504\uB77C\uC778:["pipeline"],\uD328\uB110:["panel"],\uD314\uB808\uD2B8:["palette"],\uD3EC\uC988:["pause"],\uD4E8\uC804:["fusion"],\uD504\uB85C\uBC84\uB10C\uC2A4:["provenance"],\uD504\uB85C\uBE44\uB10C\uC2A4:["provenance"],\uD504\uB85C\uD544:["profile"],\uD504\uB860\uD2B8\uB9E4\uD130:["frontmatter"],\uD504\uB86C\uD504\uD2B8:["prompt"],\uD50C\uB7EC\uADF8\uC778:["plugin"],\uD544\uD130:["filter"],\uD558\uB124\uC2A4:["harness"],\uD558\uC774\uBE0C\uB9AC\uB4DC:["hybrid"],\uD6C5:["hook"]};function j(n){return f([n,...p(n)])}i(j,"hangulLoanwordForms");function f(n){return[...new Set(n.flatMap(t=>x[t]??[]))]}i(f,"loanwordForms");function v(n){const t=n.match(/\p{Script=Hangul}+|[[\p{L}\p{N}]--[\p{Script=Hangul}]]+/gv)??[],e=t.filter(r=>a.test(r));if(e.length===0||e.length===t.length)return null;const o=t.flatMap(r=>{if(a.test(r))return[R(r)??`"${Array.from(r).join(" ")}"`];const c=r.replace(/[^\p{L}\p{N}'_]/gu,"").toLowerCase();return c?[`"${c}"*`]:[]});return o.length>0?`(${o.join(" AND ")})`:null}i(v,"hangulMixedQuery");const E=1.6,T=3,L=/\p{Script=Hangul}|\p{Script=Han}|\p{Script=Hiragana}|\p{Script=Katakana}/gu;function O(n,t=T){const e=(n.match(L)??[]).length,o=[...n].length;if(o===0)return t;const r=o-e;return o/(e/E+r/t)}i(O,"estimateCharsPerToken");export{k as containsHangul,O as estimateCharsPerToken,w as hangulBigramTail,j as hangulLoanwordForms,v as hangulMixedQuery,p as hangulStems,R as hangulTermQuery,M as sharesHangulBigram,H as stripHangulEnding,$ as stripHangulNegation,N as stripHangulParticle};
|