marchen 0.8.0 → 0.8.2
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/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +174 -323
- package/dist/index.mjs.map +1 -1
- package/dist/search-manager-CnkL4fE8.mjs +3 -0
- package/dist/search-manager-CnkL4fE8.mjs.map +1 -0
- package/package.json +12 -12
- package/dist/search-manager-zPFpsFnI.mjs +0 -3
- package/dist/search-manager-zPFpsFnI.mjs.map +0 -1
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import"node:module";import{dirname as e,join as t}from"node:path";import{mkdir as n}from"node:fs/promises";import{homedir as r}from"node:os";var i=Object.defineProperty,a=(e,t)=>{let n={};for(var r in e)i(n,r,{get:e[r],enumerable:!0});return t||i(n,Symbol.toStringTag,{value:`Module`}),n};const o=`marchen`,s=`changes`,c=`archive`,l=`.metadata.yaml`,u=`config.yaml`,d=`https://hf-mirror.com`,f=`acceptance`,p=`requirement.md`,m=`decision.json`,h=`decision.md`,g=`decision-assets`,_=5*1024*1024,v=`index.html`,y=`rounds`,b=`.serve.pid`,x=7420,S=7430;var C=class extends Error{constructor(e){super(e),this.name=`MarchenError`}},w=class extends C{constructor(e){super(e),this.name=`ValidationError`}},T=class extends C{constructor(e,t){super(e),this.hint=t,this.name=`StateError`}},E=class extends C{constructor(e,t,n){super(e),this.path=t,this.cause=n,this.name=`FileSystemError`}};const D={embed:`hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf`,generate:`hf:tobil/qmd-query-expansion-1.7B-gguf/qmd-query-expansion-1.7B-q4_k_m.gguf`,rerank:`hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf`},O=process.env.XDG_CACHE_HOME?t(process.env.XDG_CACHE_HOME,`qmd`,`models`):t(r(),`.cache`,`qmd`,`models`);var k=a({SearchManager:()=>A}),A=class{store=null;available=null;prepared=!1;constructor(e){this.workspace=e}async isAvailable(){if(this.available!==null)return this.available;try{await import(`@tobilu/qmd`),this.available=!0}catch{this.available=!1}return this.available}async ensureModels(e){await this.applyHfEndpoint();let{resolveModelFile:t}=await import(`node-llama-cpp`);for(let n of[`embed`,`generate`,`rerank`]){let r=D[n],i=r.split(`/`).pop()??r,a=(t,r,a)=>{e?.onProgress?.({model:n,file:i,stage:t,downloadedBytes:r,totalBytes:a})};a(`checking`),await t(r,{directory:O,cli:!1,onProgress:({totalSize:e,downloadedSize:t})=>{a(`downloading`,t,e)}}),a(`ready`)}}async prepare(){this.prepared||=(await this.applyHfEndpoint(),await this.initStore(),!0)}async search(e,t){let n=await this.getStore();await this.ensureIndexed(n);let r=t?.limit??5,i=t?.minScore??.3;return this.hybridSearch(n,e,r,i)}async index(){let e=await this.getStore();await e.update({collections:[`archive`]}),await e.embed()}async indexChange(){let e=await this.getStore();await e.update({collections:[`archive`]}),await e.embed()}async close(){await this.store?.close(),this.store=null,this.prepared=!1}async ensureIndexed(e){(await e.getStatus()).totalDocuments===0&&(await e.update({collections:[`archive`]}),await e.embed())}async hybridSearch(e,t,n,r){return(await e.search({query:t,limit:n,minScore:r})).map(e=>{let t={path:e.displayPath,title:e.title,score:e.score,snippet:e.bestChunk};return e.context?{...t,context:e.context}:t})}async getStore(){return this.prepared||await this.prepare(),this.store}async initStore(){let{createStore:t}=await import(`@tobilu/qmd`);await n(e(this.workspace.searchDbPath),{recursive:!0}),this.store=await t({dbPath:this.workspace.searchDbPath,config:{collections:{archive:{path:this.workspace.archiveDir,pattern:`**/*.md`}}}}),await this.store.addContext(`archive`,`/`,`Marchen 变更历史归档,包含 proposal(动机)、design(设计决策)、specs(规格)、tasks(任务清单)`)}async applyHfEndpoint(){if(process.env.HF_ENDPOINT)return;let e=await this.resolveHfEndpoint();process.env.HF_ENDPOINT=e}async resolveHfEndpoint(){try{return(await this.workspace.readConfig()).models?.endpoint??`https://hf-mirror.com`}catch{return d}}};export{o as C,l as S,b as _,T as a,u as b,_ as c,f as d,h as f,y as g,p as h,C as i,m as l,v as m,k as n,w as o,S as p,E as r,g as s,A as t,x as u,c as v,d as x,s as y};
|
|
3
|
+
//# sourceMappingURL=search-manager-CnkL4fE8.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-manager-CnkL4fE8.mjs","names":[],"sources":["../../../packages/shared/src/constants.ts","../../../packages/shared/src/errors.ts","../../../packages/core/src/qmd-models.ts","../../../packages/core/src/search-manager.ts"],"sourcesContent":["/** 规范目录名称 */\nexport const SPEC_DIRECTORY_NAME = 'marchen'\n\n/** 变更目录名称 */\nexport const CHANGE_DIRECTORY_NAME = 'changes'\n\n/** 归档目录名称 */\nexport const ARCHIVE_DIRECTORY_NAME = 'archive'\n\n/** 元数据文件名常量 */\nexport const METADATA_FILE_NAME = '.metadata.yaml'\n\n/** 工作区配置文件名 */\nexport const CONFIG_FILE_NAME = 'config.yaml'\n\n/** HuggingFace 下载源默认值(国内镜像) */\nexport const DEFAULT_HF_ENDPOINT = 'https://hf-mirror.com'\n\n/** 变更目录下的验收附属夹 */\nexport const ACCEPTANCE_DIRECTORY_NAME = 'acceptance'\n\n/** 验收目标(第一轮写下后不可改) */\nexport const ACCEPTANCE_REQUIREMENT_FILE = 'requirement.md'\n\n/** 人的整单签核 */\nexport const ACCEPTANCE_DECISION_FILE = 'decision.json'\n\n/** 旧版整单签核(只读兼容) */\nexport const ACCEPTANCE_LEGACY_DECISION_FILE = 'decision.md'\n\n/** 待修改项附图目录 */\nexport const ACCEPTANCE_DECISION_ASSETS_DIRECTORY = 'decision-assets'\n\n/** 单张待修改附图上限(字节) */\nexport const ACCEPTANCE_DECISION_ASSET_MAX_BYTES = 5 * 1024 * 1024\n\n/** 灌好的离线页 */\nexport const ACCEPTANCE_PAGE_FILE = 'index.html'\n\n/** 轮次目录 */\nexport const ACCEPTANCE_ROUNDS_DIRECTORY = 'rounds'\n\n/** serve 锁:pid / port / token */\nexport const ACCEPTANCE_SERVE_LOCK_FILE = '.serve.pid'\n\n/** 默认回环端口,占用则 7421–7430 */\nexport const ACCEPTANCE_DEFAULT_PORT = 7420\n\n/** 端口尝试上限(含默认) */\nexport const ACCEPTANCE_MAX_PORT = 7430\n","/**\n * Marchen 统一错误基类\n *\n * 所有业务错误的根类,CLI 层可用 instanceof MarchenError 兜底捕获\n */\nexport class MarchenError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'MarchenError'\n }\n}\n\n/**\n * 用户输入校验错误\n *\n * 名称不合法、变更已存在/不存在等用户操作问题\n */\nexport class ValidationError extends MarchenError {\n constructor(message: string) {\n super(message)\n this.name = 'ValidationError'\n }\n}\n\n/**\n * 状态前置条件错误\n *\n * 工作区未初始化等系统状态不满足的场景,可附带操作建议\n */\nexport class StateError extends MarchenError {\n constructor(\n message: string,\n public readonly hint?: string,\n ) {\n super(message)\n this.name = 'StateError'\n }\n}\n\n/**\n * 文件系统操作错误\n *\n * 文件/目录不存在、YAML 解析失败等 IO 异常\n */\nexport class FileSystemError extends MarchenError {\n constructor(\n message: string,\n public readonly path: string,\n public readonly cause?: Error,\n ) {\n super(message)\n this.name = 'FileSystemError'\n }\n}\n","import { homedir } from 'node:os'\nimport { join } from 'node:path'\n\n/** QMD 模型类型 */\nexport type QmdModelKind = 'embed' | 'generate' | 'rerank'\n\n/** 模型下载阶段 */\nexport type ModelDownloadStage =\n | 'checking'\n | 'downloading'\n | 'verifying'\n | 'ready'\n\n/** 单次模型下载进度 */\nexport interface ModelDownloadProgress {\n readonly model: QmdModelKind\n readonly file: string\n readonly stage: ModelDownloadStage\n readonly downloadedBytes?: number | undefined\n readonly totalBytes?: number | null | undefined\n}\n\n/**\n * QMD 模型 HuggingFace URI\n *\n * 跟随 @tobilu/qmd@2.5.1 的 DEFAULT_EMBED_MODEL / DEFAULT_GENERATE_MODEL /\n * DEFAULT_RERANK_MODEL。qmd 升级时需 review 是否同步更新。\n *\n * 不通过 import 自动跟随的原因:qmd 包入口未 re-export 这些常量,\n * 且默认模型变更属于语义大事件(向量维度可能变、索引需重建),\n * 显式跟随比自动跟随更安全。\n */\nexport const QMD_MODEL_URIS: Record<QmdModelKind, string> = {\n embed: 'hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf',\n generate:\n 'hf:tobil/qmd-query-expansion-1.7B-gguf/qmd-query-expansion-1.7B-q4_k_m.gguf',\n rerank:\n 'hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf',\n}\n\n/**\n * QMD 默认模型缓存目录\n *\n * 必须与 qmd 内部 `MODEL_CACHE_DIR` 完全一致,否则 createStore 找不到我们\n * 下载的文件会触发二次下载。qmd `createStore` 未透传 modelCacheDir 配置,\n * 此目录由 qmd 单方面决定,本项目不暴露为可配置项。\n */\nexport const QMD_MODEL_CACHE_DIR = process.env.XDG_CACHE_HOME\n ? join(process.env.XDG_CACHE_HOME, 'qmd', 'models')\n : join(homedir(), '.cache', 'qmd', 'models')\n","import type { HybridQueryResult, QMDStore } from '@tobilu/qmd'\nimport type {\n ModelDownloadProgress,\n ModelDownloadStage,\n QmdModelKind,\n} from './qmd-models.js'\nimport type { Workspace } from './workspace.js'\nimport { mkdir } from 'node:fs/promises'\nimport { dirname } from 'node:path'\nimport { DEFAULT_HF_ENDPOINT } from '@marchen/shared'\nimport { QMD_MODEL_CACHE_DIR, QMD_MODEL_URIS } from './qmd-models.js'\n\n/** 搜索结果项 */\nexport interface SearchResult {\n readonly path: string\n readonly title: string\n readonly score: number\n readonly snippet: string\n readonly context?: string\n}\n\n/** 搜索选项 */\nexport interface SearchOptions {\n readonly limit?: number\n readonly minScore?: number\n}\n\n/** ensureModels 选项 */\nexport interface EnsureModelsOptions {\n readonly onProgress?: (progress: ModelDownloadProgress) => void\n}\n\n/**\n * 搜索管理器\n *\n * 封装 qmd SDK,提供 Hybrid Search、模型下载和索引管理接口。\n * 调用方在 search.enabled: true 时才应使用此类。\n */\nexport class SearchManager {\n private store: QMDStore | null = null\n private available: boolean | null = null\n private prepared = false\n\n constructor(private readonly workspace: Workspace) {}\n\n /** 检测 qmd SDK 是否可用 */\n async isAvailable(): Promise<boolean> {\n if (this.available !== null) return this.available\n try {\n await import('@tobilu/qmd')\n this.available = true\n } catch {\n this.available = false\n }\n return this.available\n }\n\n /**\n * 下载三个 QMD 模型到 qmd 默认缓存目录。\n *\n * 通过 node-llama-cpp 的 `resolveModelFile` 完成下载,下载源由\n * `HF_ENDPOINT` 环境变量决定,缓存目录强制对齐 qmd 默认值\n * (见 qmd-models.ts 的 QMD_MODEL_CACHE_DIR 注释)。\n *\n * 进度通过 onProgress 回调上报,封装为现有 ModelDownloadProgress 格式。\n */\n async ensureModels(options?: EnsureModelsOptions): Promise<void> {\n await this.applyHfEndpoint()\n\n const { resolveModelFile } = await import('node-llama-cpp')\n\n const kinds: QmdModelKind[] = ['embed', 'generate', 'rerank']\n for (const model of kinds) {\n const uri = QMD_MODEL_URIS[model]\n const file = uri.split('/').pop() ?? uri\n const emit = (\n stage: ModelDownloadStage,\n downloadedBytes?: number,\n totalBytes?: number | null,\n ): void => {\n options?.onProgress?.({\n model,\n file,\n stage,\n downloadedBytes,\n totalBytes,\n })\n }\n\n emit('checking')\n await resolveModelFile(uri, {\n directory: QMD_MODEL_CACHE_DIR,\n cli: false,\n onProgress: ({ totalSize, downloadedSize }) => {\n emit('downloading', downloadedSize, totalSize)\n },\n })\n emit('ready')\n }\n }\n\n /**\n * 准备搜索引擎。\n *\n * 设置 HF_ENDPOINT 环境变量并初始化 qmd store,不再下载模型。\n * 幂等,重复调用立即返回。\n */\n async prepare(): Promise<void> {\n if (this.prepared) return\n await this.applyHfEndpoint()\n await this.initStore()\n this.prepared = true\n }\n\n /** 搜索归档内容 */\n async search(\n query: string,\n options?: SearchOptions,\n ): Promise<SearchResult[]> {\n const store = await this.getStore()\n await this.ensureIndexed(store)\n const limit = options?.limit ?? 5\n const minScore = options?.minScore ?? 0.3\n\n return this.hybridSearch(store, query, limit, minScore)\n }\n\n /** 全量索引(扫描 + embedding) */\n async index(): Promise<void> {\n const store = await this.getStore()\n await store.update({ collections: ['archive'] })\n await store.embed()\n }\n\n /** 增量索引(archive 后调用) */\n async indexChange(): Promise<void> {\n const store = await this.getStore()\n await store.update({ collections: ['archive'] })\n await store.embed()\n }\n\n /** 释放资源 */\n async close(): Promise<void> {\n await this.store?.close()\n this.store = null\n this.prepared = false\n }\n\n /** 索引为空时自动触发首次扫描 */\n private async ensureIndexed(store: QMDStore): Promise<void> {\n const status = await store.getStatus()\n if (status.totalDocuments === 0) {\n await store.update({ collections: ['archive'] })\n await store.embed()\n }\n }\n\n /** Hybrid Search(BM25 + Vector + Reranking) */\n private async hybridSearch(\n store: QMDStore,\n query: string,\n limit: number,\n minScore: number,\n ): Promise<SearchResult[]> {\n const results = await store.search({ query, limit, minScore })\n return results.map((r: HybridQueryResult) => {\n const result: SearchResult = {\n path: r.displayPath,\n title: r.title,\n score: r.score,\n snippet: r.bestChunk,\n }\n if (r.context) {\n return { ...result, context: r.context }\n }\n return result\n })\n }\n\n /** 确保 store 就绪,未 prepare 时自动触发 */\n private async getStore(): Promise<QMDStore> {\n if (!this.prepared) await this.prepare()\n return this.store!\n }\n\n /** 初始化 qmd store */\n private async initStore(): Promise<void> {\n const { createStore } = await import('@tobilu/qmd')\n await mkdir(dirname(this.workspace.searchDbPath), { recursive: true })\n this.store = await createStore({\n dbPath: this.workspace.searchDbPath,\n config: {\n collections: {\n archive: {\n path: this.workspace.archiveDir,\n pattern: '**/*.md',\n },\n },\n },\n })\n await this.store.addContext(\n 'archive',\n '/',\n 'Marchen 变更历史归档,包含 proposal(动机)、design(设计决策)、specs(规格)、tasks(任务清单)',\n )\n }\n\n /**\n * 解析并应用 HF 端点到 process.env.HF_ENDPOINT。\n *\n * 优先级:已设置的 env > config.models.endpoint > DEFAULT_HF_ENDPOINT。\n * 不覆盖已存在的 env 值,保证用户能临时覆盖配置。\n */\n private async applyHfEndpoint(): Promise<void> {\n if (process.env.HF_ENDPOINT) return\n const endpoint = await this.resolveHfEndpoint()\n process.env.HF_ENDPOINT = endpoint\n }\n\n /** 从 workspace config 读取 endpoint,缺失时返回默认值 */\n private async resolveHfEndpoint(): Promise<string> {\n try {\n const config = await this.workspace.readConfig()\n return config.models?.endpoint ?? DEFAULT_HF_ENDPOINT\n } catch {\n return DEFAULT_HF_ENDPOINT\n }\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "marchen",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "A harness for AI coding agents — structured workflows and long-term memory",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -15,6 +15,14 @@
|
|
|
15
15
|
"files": [
|
|
16
16
|
"dist"
|
|
17
17
|
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsdown --config tsdown.config.ts",
|
|
20
|
+
"dev": "tsdown --config tsdown.config.ts --watch",
|
|
21
|
+
"lint": "eslint .",
|
|
22
|
+
"lint:fix": "eslint . --fix",
|
|
23
|
+
"test": "vitest run --config vitest.config.ts",
|
|
24
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
25
|
+
},
|
|
18
26
|
"dependencies": {
|
|
19
27
|
"@clack/prompts": "^0.10.1",
|
|
20
28
|
"@tobilu/qmd": "^2.1.0",
|
|
@@ -24,15 +32,7 @@
|
|
|
24
32
|
"picocolors": "^1.1.1"
|
|
25
33
|
},
|
|
26
34
|
"devDependencies": {
|
|
27
|
-
"@marchen/core": "
|
|
28
|
-
"@marchen/shared": "
|
|
29
|
-
},
|
|
30
|
-
"scripts": {
|
|
31
|
-
"build": "tsdown --config tsdown.config.ts",
|
|
32
|
-
"dev": "tsdown --config tsdown.config.ts --watch",
|
|
33
|
-
"lint": "eslint .",
|
|
34
|
-
"lint:fix": "eslint . --fix",
|
|
35
|
-
"test": "vitest run --config vitest.config.ts",
|
|
36
|
-
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
35
|
+
"@marchen/core": "workspace:*",
|
|
36
|
+
"@marchen/shared": "workspace:*"
|
|
37
37
|
}
|
|
38
|
-
}
|
|
38
|
+
}
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import"node:module";import{dirname as e,join as t}from"node:path";import{mkdir as n}from"node:fs/promises";import{homedir as r}from"node:os";var i=Object.defineProperty,a=(e,t)=>{let n={};for(var r in e)i(n,r,{get:e[r],enumerable:!0});return t||i(n,Symbol.toStringTag,{value:`Module`}),n};const o=`marchen`,s=`changes`,c=`archive`,l=`.metadata.yaml`,u=`config.yaml`,d=`https://hf-mirror.com`,f={embed:`hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf`,generate:`hf:tobil/qmd-query-expansion-1.7B-gguf/qmd-query-expansion-1.7B-q4_k_m.gguf`,rerank:`hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf`},p=process.env.XDG_CACHE_HOME?t(process.env.XDG_CACHE_HOME,`qmd`,`models`):t(r(),`.cache`,`qmd`,`models`);var m=a({SearchManager:()=>h}),h=class{store=null;available=null;prepared=!1;constructor(e){this.workspace=e}async isAvailable(){if(this.available!==null)return this.available;try{await import(`@tobilu/qmd`),this.available=!0}catch{this.available=!1}return this.available}async ensureModels(e){await this.applyHfEndpoint();let{resolveModelFile:t}=await import(`node-llama-cpp`);for(let n of[`embed`,`generate`,`rerank`]){let r=f[n],i=r.split(`/`).pop()??r,a=(t,r,a)=>{e?.onProgress?.({model:n,file:i,stage:t,downloadedBytes:r,totalBytes:a})};a(`checking`),await t(r,{directory:p,cli:!1,onProgress:({totalSize:e,downloadedSize:t})=>{a(`downloading`,t,e)}}),a(`ready`)}}async prepare(){this.prepared||=(await this.applyHfEndpoint(),await this.initStore(),!0)}async search(e,t){let n=await this.getStore();await this.ensureIndexed(n);let r=t?.limit??5,i=t?.minScore??.3;return this.hybridSearch(n,e,r,i)}async index(){let e=await this.getStore();await e.update({collections:[`archive`]}),await e.embed()}async indexChange(){let e=await this.getStore();await e.update({collections:[`archive`]}),await e.embed()}async close(){await this.store?.close(),this.store=null,this.prepared=!1}async ensureIndexed(e){(await e.getStatus()).totalDocuments===0&&(await e.update({collections:[`archive`]}),await e.embed())}async hybridSearch(e,t,n,r){return(await e.search({query:t,limit:n,minScore:r})).map(e=>{let t={path:e.displayPath,title:e.title,score:e.score,snippet:e.bestChunk};return e.context?{...t,context:e.context}:t})}async getStore(){return this.prepared||await this.prepare(),this.store}async initStore(){let{createStore:t}=await import(`@tobilu/qmd`);await n(e(this.workspace.searchDbPath),{recursive:!0}),this.store=await t({dbPath:this.workspace.searchDbPath,config:{collections:{archive:{path:this.workspace.archiveDir,pattern:`**/*.md`}}}}),await this.store.addContext(`archive`,`/`,`Marchen 变更历史归档,包含 proposal(动机)、design(设计决策)、specs(规格)、tasks(任务清单)`)}async applyHfEndpoint(){if(process.env.HF_ENDPOINT)return;let e=await this.resolveHfEndpoint();process.env.HF_ENDPOINT=e}async resolveHfEndpoint(){try{return(await this.workspace.readConfig()).models?.endpoint??`https://hf-mirror.com`}catch{return d}}};export{u as a,o as c,s as i,m as n,d as o,c as r,l as s,h as t};
|
|
3
|
-
//# sourceMappingURL=search-manager-zPFpsFnI.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"search-manager-zPFpsFnI.mjs","names":[],"sources":["../../../packages/shared/src/constants.ts","../../../packages/core/src/qmd-models.ts","../../../packages/core/src/search-manager.ts"],"sourcesContent":["/** 规范目录名称 */\nexport const SPEC_DIRECTORY_NAME = 'marchen'\n\n/** 变更目录名称 */\nexport const CHANGE_DIRECTORY_NAME = 'changes'\n\n/** 归档目录名称 */\nexport const ARCHIVE_DIRECTORY_NAME = 'archive'\n\n/** 元数据文件名常量 */\nexport const METADATA_FILE_NAME = '.metadata.yaml'\n\n/** 工作区配置文件名 */\nexport const CONFIG_FILE_NAME = 'config.yaml'\n\n/** HuggingFace 下载源默认值(国内镜像) */\nexport const DEFAULT_HF_ENDPOINT = 'https://hf-mirror.com'\n","import { homedir } from 'node:os'\nimport { join } from 'node:path'\n\n/** QMD 模型类型 */\nexport type QmdModelKind = 'embed' | 'generate' | 'rerank'\n\n/** 模型下载阶段 */\nexport type ModelDownloadStage =\n | 'checking'\n | 'downloading'\n | 'verifying'\n | 'ready'\n\n/** 单次模型下载进度 */\nexport interface ModelDownloadProgress {\n readonly model: QmdModelKind\n readonly file: string\n readonly stage: ModelDownloadStage\n readonly downloadedBytes?: number | undefined\n readonly totalBytes?: number | null | undefined\n}\n\n/**\n * QMD 模型 HuggingFace URI\n *\n * 跟随 @tobilu/qmd@2.5.1 的 DEFAULT_EMBED_MODEL / DEFAULT_GENERATE_MODEL /\n * DEFAULT_RERANK_MODEL。qmd 升级时需 review 是否同步更新。\n *\n * 不通过 import 自动跟随的原因:qmd 包入口未 re-export 这些常量,\n * 且默认模型变更属于语义大事件(向量维度可能变、索引需重建),\n * 显式跟随比自动跟随更安全。\n */\nexport const QMD_MODEL_URIS: Record<QmdModelKind, string> = {\n embed: 'hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf',\n generate:\n 'hf:tobil/qmd-query-expansion-1.7B-gguf/qmd-query-expansion-1.7B-q4_k_m.gguf',\n rerank:\n 'hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf',\n}\n\n/**\n * QMD 默认模型缓存目录\n *\n * 必须与 qmd 内部 `MODEL_CACHE_DIR` 完全一致,否则 createStore 找不到我们\n * 下载的文件会触发二次下载。qmd `createStore` 未透传 modelCacheDir 配置,\n * 此目录由 qmd 单方面决定,本项目不暴露为可配置项。\n */\nexport const QMD_MODEL_CACHE_DIR = process.env.XDG_CACHE_HOME\n ? join(process.env.XDG_CACHE_HOME, 'qmd', 'models')\n : join(homedir(), '.cache', 'qmd', 'models')\n","import type { HybridQueryResult, QMDStore } from '@tobilu/qmd'\nimport type {\n ModelDownloadProgress,\n ModelDownloadStage,\n QmdModelKind,\n} from './qmd-models.js'\nimport type { Workspace } from './workspace.js'\nimport { mkdir } from 'node:fs/promises'\nimport { dirname } from 'node:path'\nimport { DEFAULT_HF_ENDPOINT } from '@marchen/shared'\nimport { QMD_MODEL_CACHE_DIR, QMD_MODEL_URIS } from './qmd-models.js'\n\n/** 搜索结果项 */\nexport interface SearchResult {\n readonly path: string\n readonly title: string\n readonly score: number\n readonly snippet: string\n readonly context?: string\n}\n\n/** 搜索选项 */\nexport interface SearchOptions {\n readonly limit?: number\n readonly minScore?: number\n}\n\n/** ensureModels 选项 */\nexport interface EnsureModelsOptions {\n readonly onProgress?: (progress: ModelDownloadProgress) => void\n}\n\n/**\n * 搜索管理器\n *\n * 封装 qmd SDK,提供 Hybrid Search、模型下载和索引管理接口。\n * 调用方在 search.enabled: true 时才应使用此类。\n */\nexport class SearchManager {\n private store: QMDStore | null = null\n private available: boolean | null = null\n private prepared = false\n\n constructor(private readonly workspace: Workspace) {}\n\n /** 检测 qmd SDK 是否可用 */\n async isAvailable(): Promise<boolean> {\n if (this.available !== null) return this.available\n try {\n await import('@tobilu/qmd')\n this.available = true\n } catch {\n this.available = false\n }\n return this.available\n }\n\n /**\n * 下载三个 QMD 模型到 qmd 默认缓存目录。\n *\n * 通过 node-llama-cpp 的 `resolveModelFile` 完成下载,下载源由\n * `HF_ENDPOINT` 环境变量决定,缓存目录强制对齐 qmd 默认值\n * (见 qmd-models.ts 的 QMD_MODEL_CACHE_DIR 注释)。\n *\n * 进度通过 onProgress 回调上报,封装为现有 ModelDownloadProgress 格式。\n */\n async ensureModels(options?: EnsureModelsOptions): Promise<void> {\n await this.applyHfEndpoint()\n\n const { resolveModelFile } = await import('node-llama-cpp')\n\n const kinds: QmdModelKind[] = ['embed', 'generate', 'rerank']\n for (const model of kinds) {\n const uri = QMD_MODEL_URIS[model]\n const file = uri.split('/').pop() ?? uri\n const emit = (\n stage: ModelDownloadStage,\n downloadedBytes?: number,\n totalBytes?: number | null,\n ): void => {\n options?.onProgress?.({\n model,\n file,\n stage,\n downloadedBytes,\n totalBytes,\n })\n }\n\n emit('checking')\n await resolveModelFile(uri, {\n directory: QMD_MODEL_CACHE_DIR,\n cli: false,\n onProgress: ({ totalSize, downloadedSize }) => {\n emit('downloading', downloadedSize, totalSize)\n },\n })\n emit('ready')\n }\n }\n\n /**\n * 准备搜索引擎。\n *\n * 设置 HF_ENDPOINT 环境变量并初始化 qmd store,不再下载模型。\n * 幂等,重复调用立即返回。\n */\n async prepare(): Promise<void> {\n if (this.prepared) return\n await this.applyHfEndpoint()\n await this.initStore()\n this.prepared = true\n }\n\n /** 搜索归档内容 */\n async search(\n query: string,\n options?: SearchOptions,\n ): Promise<SearchResult[]> {\n const store = await this.getStore()\n await this.ensureIndexed(store)\n const limit = options?.limit ?? 5\n const minScore = options?.minScore ?? 0.3\n\n return this.hybridSearch(store, query, limit, minScore)\n }\n\n /** 全量索引(扫描 + embedding) */\n async index(): Promise<void> {\n const store = await this.getStore()\n await store.update({ collections: ['archive'] })\n await store.embed()\n }\n\n /** 增量索引(archive 后调用) */\n async indexChange(): Promise<void> {\n const store = await this.getStore()\n await store.update({ collections: ['archive'] })\n await store.embed()\n }\n\n /** 释放资源 */\n async close(): Promise<void> {\n await this.store?.close()\n this.store = null\n this.prepared = false\n }\n\n /** 索引为空时自动触发首次扫描 */\n private async ensureIndexed(store: QMDStore): Promise<void> {\n const status = await store.getStatus()\n if (status.totalDocuments === 0) {\n await store.update({ collections: ['archive'] })\n await store.embed()\n }\n }\n\n /** Hybrid Search(BM25 + Vector + Reranking) */\n private async hybridSearch(\n store: QMDStore,\n query: string,\n limit: number,\n minScore: number,\n ): Promise<SearchResult[]> {\n const results = await store.search({ query, limit, minScore })\n return results.map((r: HybridQueryResult) => {\n const result: SearchResult = {\n path: r.displayPath,\n title: r.title,\n score: r.score,\n snippet: r.bestChunk,\n }\n if (r.context) {\n return { ...result, context: r.context }\n }\n return result\n })\n }\n\n /** 确保 store 就绪,未 prepare 时自动触发 */\n private async getStore(): Promise<QMDStore> {\n if (!this.prepared) await this.prepare()\n return this.store!\n }\n\n /** 初始化 qmd store */\n private async initStore(): Promise<void> {\n const { createStore } = await import('@tobilu/qmd')\n await mkdir(dirname(this.workspace.searchDbPath), { recursive: true })\n this.store = await createStore({\n dbPath: this.workspace.searchDbPath,\n config: {\n collections: {\n archive: {\n path: this.workspace.archiveDir,\n pattern: '**/*.md',\n },\n },\n },\n })\n await this.store.addContext(\n 'archive',\n '/',\n 'Marchen 变更历史归档,包含 proposal(动机)、design(设计决策)、specs(规格)、tasks(任务清单)',\n )\n }\n\n /**\n * 解析并应用 HF 端点到 process.env.HF_ENDPOINT。\n *\n * 优先级:已设置的 env > config.models.endpoint > DEFAULT_HF_ENDPOINT。\n * 不覆盖已存在的 env 值,保证用户能临时覆盖配置。\n */\n private async applyHfEndpoint(): Promise<void> {\n if (process.env.HF_ENDPOINT) return\n const endpoint = await this.resolveHfEndpoint()\n process.env.HF_ENDPOINT = endpoint\n }\n\n /** 从 workspace config 读取 endpoint,缺失时返回默认值 */\n private async resolveHfEndpoint(): Promise<string> {\n try {\n const config = await this.workspace.readConfig()\n return config.models?.endpoint ?? DEFAULT_HF_ENDPOINT\n } catch {\n return DEFAULT_HF_ENDPOINT\n }\n }\n}\n"],"mappings":"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"}
|