@gmickel/gno 1.13.0 → 1.15.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 CHANGED
@@ -92,7 +92,12 @@ gno daemon --detach # headless continuous indexing (background; --status / --st
92
92
 
93
93
  ## What's New
94
94
 
95
- > Latest release: see [CHANGELOG.md](./CHANGELOG.md)
95
+ <!-- public-truth:current-version -->
96
+
97
+ > Current release: **v1.15.0** — see [CHANGELOG.md](./CHANGELOG.md)
98
+
99
+ <!-- /public-truth -->
100
+
96
101
  > Full release history: [CHANGELOG.md](./CHANGELOG.md)
97
102
 
98
103
  - **Retrieval-proven activation**: `gno status`, `gno doctor`, REST, and the
@@ -109,7 +114,9 @@ gno daemon --detach # headless continuous indexing (background; --status / --st
109
114
  - **Publish to [gno.sh](https://gno.sh/publish)**: new `gno publish export` CLI and Web UI action produce a self-contained artifact you upload to the hosted reader — public, secret, invite-only, or locally encrypted before upload
110
115
  - **Retrieval Quality Upgrade**: stronger BM25 lexical handling, code-aware chunking, terminal result hyperlinks, and per-collection model overrides
111
116
  - **Code Embedding Benchmarks**: new benchmark workflow across canonical, real-GNO, and pinned OSS slices for comparing alternate embedding models
112
- - **Default Embed Model**: built-in presets now use `Qwen3-Embedding-0.6B-GGUF` after it beat `bge-m3` on both code and multilingual prose benchmark lanes
117
+ <!-- public-truth:default-embed-model -->
118
+ - **Default Embed Model**: all four built-in presets use `Qwen3-Embedding-0.6B-GGUF`; see the dated, fixture-scoped evidence below
119
+ <!-- /public-truth -->
113
120
  - **Regression Fixes**: tightened phrase/negation/hyphen/underscore BM25 behavior, cleaned non-TTY hyperlink output, improved `gno doctor` chunking and embedding fingerprint visibility, and fixed the embedding autoresearch harness
114
121
 
115
122
  ### Upgrading Existing Collections
@@ -201,7 +208,11 @@ Manage the detached process with `gno daemon --status` and `gno daemon --stop`.
201
208
 
202
209
  ### Install GNO
203
210
 
204
- Requires [Bun](https://bun.sh/) >= 1.3.0.
211
+ <!-- public-truth:runtime -->
212
+
213
+ Requires [Bun](https://bun.sh/) >=1.3.0.
214
+
215
+ <!-- /public-truth -->
205
216
 
206
217
  ```bash
207
218
  bun install -g @gmickel/gno
@@ -225,10 +236,15 @@ when its structured activation state is degraded. `gno doctor` exits 2 when any
225
236
  configured folder fails the lexical proof; semantic models may still be pending
226
237
  without blocking BM25 search.
227
238
 
228
- **Windows**: current validated target is `windows-x64`, with a packaged
239
+ <!-- public-truth:supported-platforms -->
240
+
241
+ GNO supports macOS, Linux, and Windows. The current validated Windows target is
242
+ `windows-x64`, with a packaged
229
243
  desktop beta zip now published on GitHub Releases. See
230
244
  [docs/WINDOWS.md](./docs/WINDOWS.md) for support scope and validation notes.
231
245
 
246
+ <!-- /public-truth -->
247
+
232
248
  Keep an index fresh continuously without opening the Web UI:
233
249
 
234
250
  ```bash
@@ -532,7 +548,9 @@ Connect GNO to Claude Desktop, Cursor, Raycast, and more:
532
548
 
533
549
  ![GNO MCP](./assets/screenshots/mcp.jpg)
534
550
 
535
- GNO exposes tools via [Model Context Protocol](https://modelcontextprotocol.io):
551
+ GNO exposes 17 tools by default via [Model Context Protocol](https://modelcontextprotocol.io),
552
+ including the core retrieval tools below. Starting MCP with `--enable-write`
553
+ adds 11 opt-in mutation tools, for 28 total.
536
554
 
537
555
  | Tool | Description |
538
556
  | :-------------- | :------------------------------------ |
@@ -547,7 +565,9 @@ GNO exposes tools via [Model Context Protocol](https://modelcontextprotocol.io):
547
565
  | `gno_graph` | Get knowledge graph (nodes and edges) |
548
566
  | `gno_status` | Index health check |
549
567
 
550
- **Design**: MCP tools are retrieval-only. Your AI assistant (Claude, GPT-4) synthesizes answers from retrieved context. Best retrieval (GNO) + best reasoning (your LLM).
568
+ **Design**: Default MCP mode is read-only: retrieval, graph, status, and job
569
+ inspection. Your AI assistant synthesizes answers from retrieved context. Write
570
+ tools are available only through the explicit `--enable-write` opt-in.
551
571
 
552
572
  [MCP setup guide →](https://gno.sh/docs/MCP/)
553
573
 
@@ -644,7 +664,10 @@ Interactive visualization of document connections. Wiki links, markdown links, a
644
664
 
645
665
  Ask questions in natural language. GNO searches your documents and synthesizes answers with inline citations linking to sources.
646
666
 
647
- Everything runs locally. No cloud, no accounts, no data leaving your machine.
667
+ The Web UI and local-model path run on your machine with no account or
668
+ telemetry. Network access occurs when GNO downloads models, when you configure
669
+ an HTTP model backend, or when you explicitly upload an exported artifact to
670
+ gno.sh.
648
671
 
649
672
  > **Detailed docs**: [Web UI Guide](https://gno.sh/docs/WEB-UI/)
650
673
 
@@ -802,13 +825,13 @@ graph TD
802
825
  | **REST API** | HTTP API for custom tools and integrations |
803
826
  | **Multi-Format** | Markdown, PDF, DOCX, XLSX, PPTX, plain text |
804
827
  | **Local LLM** | AI answers via llama.cpp, no API keys |
805
- | **Remote Inference** | Offload to GPU servers via HTTP (llama-server, Ollama, LocalAI) |
806
- | **Privacy First** | 100% offline, zero telemetry, your data stays yours |
807
- | **MCP Server** | Works with Claude Desktop, Cursor, Zed, + 8 more |
828
+ | **Remote Inference** | Optional HTTP endpoints for embedding, reranking, expansion, and generation |
829
+ | **Privacy First** | Local by default; no telemetry; network use is explicit or model provisioning |
830
+ | **MCP Server** | 10 automatic client targets; 17 read-only tools, 28 with writes enabled |
808
831
  | **Collections** | Organize sources with patterns, excludes, contexts |
809
832
  | **Tag Filtering** | Frontmatter tags with hierarchical paths, filter via `--tags-any`/`--tags-all` |
810
833
  | **Note Linking** | Wiki links, backlinks, related notes, cross-collection navigation |
811
- | **Multilingual** | 30+ languages, auto-detection, cross-lingual search |
834
+ | **Multilingual** | Query classification, 7-language document detection, multilingual embeddings |
812
835
  | **Incremental** | SHA-256 tracking, only changed files re-indexed |
813
836
  | **Keyboard First** | ⌘N capture, ⌘K search, ⌘/ shortcuts, ⌘S save |
814
837
 
@@ -818,20 +841,20 @@ graph TD
818
841
 
819
842
  Models auto-download on first use to `~/.cache/gno/models/`. GNO validates cached GGUF files before loading and removes intercepted HTML/non-GGUF cache entries with a clear recovery error. For deterministic startup, set `GNO_NO_AUTO_DOWNLOAD=1` and use `gno models pull` explicitly. Alternatively, offload to a GPU server on your network using HTTP backends.
820
843
 
821
- | Model | Purpose | Size |
822
- | :--------------------- | :------------------------------------ | :----------- |
823
- | Qwen3-Embedding-0.6B | Embeddings (multilingual) | ~640MB |
824
- | Qwen3-Reranker-0.6B | Cross-encoder reranking (32K context) | ~700MB |
825
- | Qwen3 / Qwen2.5 family | Query expansion + AI answers | ~600MB-2.5GB |
844
+ | Model | Purpose |
845
+ | :--------------------- | :----------------------------------------------- |
846
+ | Qwen3-Embedding-0.6B | Embeddings |
847
+ | Qwen3-Reranker-0.6B | Best-chunk-per-document cross-encoder reranking |
848
+ | Qwen3 / Qwen2.5 family | Query expansion and standalone answer generation |
826
849
 
827
850
  ### Model Presets
828
851
 
829
- | Preset | Disk | Best For |
830
- | :----------- | :----- | :------------------------------------------------------ |
831
- | `slim-tuned` | ~1GB | Current default, tuned retrieval in a compact footprint |
832
- | `slim` | ~1GB | Fast, good quality |
833
- | `balanced` | ~2GB | Slightly larger model |
834
- | `quality` | ~2.5GB | Best answers |
852
+ | Preset | Best For |
853
+ | :----------- | :------------------------------------------- |
854
+ | `slim-tuned` | Current default; tuned query expansion |
855
+ | `slim` | Untuned slim query expansion |
856
+ | `balanced` | Qwen2.5 3B expansion and answers |
857
+ | `quality` | Qwen3 4B expansion and standalone AI answers |
835
858
 
836
859
  ```bash
837
860
  gno models use slim-tuned
@@ -849,7 +872,9 @@ GNO now has a published promoted retrieval model for the default slim path:
849
872
 
850
873
  Use it when you want the tuned retrieval expansion path immediately, without running local fine-tuning yourself.
851
874
 
852
- For private/internal products, use the same workflow but keep the final GGUF private and point `gen:` at a `file:` URI instead of publishing to Hugging Face.
875
+ For private/internal products, use the same workflow but keep the final GGUF
876
+ private and point `expand:` at a `file:` URI instead of publishing it to
877
+ Hugging Face. The `gen:` role remains the standalone answer model.
853
878
 
854
879
  See:
855
880
 
@@ -873,7 +898,10 @@ models:
873
898
  gen: "http://192.168.1.100:8083/v1/chat/completions#qwen3-4b"
874
899
  ```
875
900
 
876
- Works with llama-server, Ollama, LocalAI, vLLM, or any OpenAI-compatible server.
901
+ The HTTP adapter expects the OpenAI-compatible endpoint shapes documented in
902
+ [Configuration](./docs/CONFIGURATION.md). Remote servers receive the query,
903
+ chunk, or answer context sent to their configured model role; they are outside
904
+ GNO's local trust boundary.
877
905
 
878
906
  > **Configuration**: [Model Setup](https://gno.sh/docs/CONFIGURATION/)
879
907
 
@@ -989,16 +1017,49 @@ bun run bench:general-embeddings --candidate bge-m3-incumbent --write
989
1017
  bun run bench:general-embeddings --candidate qwen3-embedding-0.6b --write
990
1018
  ```
991
1019
 
992
- Current signal on the public multilingual FastAPI-docs fixture:
993
-
994
- - `bge-m3`: vector nDCG@10 `0.3508`, hybrid nDCG@10 `0.6756`
995
- - `Qwen3-Embedding-0.6B-GGUF`: vector nDCG@10 `0.9891`, hybrid nDCG@10 `0.9891`
996
-
997
- Interpretation:
998
-
999
- - Qwen is now the strongest general multilingual embedding model we have tested
1000
- - built-in presets now use Qwen by default
1001
- - existing users may need to run `gno embed` again after upgrading so current collections catch up
1020
+ <!-- public-truth:general-embedding-benchmark -->
1021
+
1022
+ The immutable April 2026 FastAPI-docs run used 15 documents in five corpus
1023
+ languages (`en`, `de`, `fr`, `es`, `zh`) and 13 queries:
1024
+
1025
+ - [bge-m3 incumbent](./evals/fixtures/general-embedding-benchmark/2026-04-06-bge-m3-incumbent.md): vector nDCG@10 `0.3503`, hybrid nDCG@10 `0.642`
1026
+ - [Qwen3 Embedding 0.6B](./evals/fixtures/general-embedding-benchmark/2026-04-06-qwen3-embedding-0-6b.md): vector nDCG@10 `0.8594`, hybrid nDCG@10 `0.947`
1027
+ <!-- /public-truth -->
1028
+
1029
+ A separate [July 2026 Nemotron screen](./research/embeddings/2026-07-21-nemotron-3-embed-1b.md)
1030
+ reran the same 13-query multilingual lane after runtime/profile changes. It
1031
+ measured Qwen at `0.9891` vector / `0.9891` hybrid nDCG@10 and Nemotron 3 Embed
1032
+ 1B at `0.9023` / `0.9461`. Nemotron used a temporary PyTorch HTTP adapter;
1033
+ Qwen used GNO's production GGUF path. Their timings are not comparable, and no
1034
+ official production GGUF was validated for Nemotron.
1035
+
1036
+ These small fixture results support keeping Qwen as the built-in default; they
1037
+ do not establish general language superiority. Query-language classification
1038
+ supports a broader set than the indexed-document detector (`en`, `de`, `fr`,
1039
+ `it`, `zh`, `ja`, `ko`), and the committed semantic fixture covers only five
1040
+ languages.
1041
+
1042
+ <!-- public-truth:cjk-lexical-benchmark -->
1043
+
1044
+ Lexical fallback has separate evidence. The immutable
1045
+ [July 22, 2026 CJK result](./evals/fixtures/cjk-lexical-benchmark/2026-07-22.md)
1046
+ uses 21 synthetic documents and 25 same-language queries across Chinese,
1047
+ Japanese, and Korean. Production BM25 lexical results and frozen floors:
1048
+
1049
+ - Chinese: baseline Recall@10 `0.2222`, nDCG@10 `0.1481`, zero-result `0.7778`; promotion Recall@10 `0.4722`, nDCG@10 `0.3981`, maximum zero-result `0.5278`
1050
+ - Japanese: baseline Recall@10 `0.125`, nDCG@10 `0.125`, zero-result `0.875`; promotion Recall@10 `0.375`, nDCG@10 `0.375`, maximum zero-result `0.625`
1051
+ - Korean: baseline Recall@10 `0.5`, nDCG@10 `0.5`, zero-result `0.5`; promotion Recall@10 `0.75`, nDCG@10 `0.75`, maximum zero-result `0.25`
1052
+
1053
+ The
1054
+ [promotion-gates.md](./evals/fixtures/cjk-lexical-benchmark/promotion-gates.md)
1055
+ also bind MRR, non-regression, and cost requirements. This lexical result does
1056
+ not reduce or replace the semantic evidence above. All positive qrels use
1057
+ relevance `3`, so
1058
+ nDCG measures placement but not distinctions among positive gain grades.
1059
+ Production tokenization is unchanged; improvements remain gated work for
1060
+ `fn-109`.
1061
+
1062
+ <!-- /public-truth -->
1002
1063
 
1003
1064
  ---
1004
1065
 
@@ -6,7 +6,9 @@ allowed-tools: Bash(gno:*) Read
6
6
 
7
7
  # GNO - Local Knowledge Engine
8
8
 
9
- Fast local semantic search. Index once, search instantly. No cloud, no API keys.
9
+ Fast local semantic search. Index once, search instantly. Local inference needs
10
+ no API key; configured HTTP backends and explicit gno.sh publishing are separate
11
+ network boundaries.
10
12
 
11
13
  ## When to Use This Skill
12
14
 
@@ -453,7 +453,8 @@ gno models list [--json|--md]
453
453
  gno models use <preset>
454
454
  ```
455
455
 
456
- Presets: `slim` (~1GB), `balanced` (~2GB), `quality` (~2.5GB)
456
+ Built-ins: `slim-tuned` (default), `slim`, `balanced`, `quality`. Actual
457
+ download/cache use depends on the selected artifacts and existing shared cache.
457
458
 
458
459
  ### gno models pull
459
460
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gmickel/gno",
3
- "version": "1.13.0",
3
+ "version": "1.15.0",
4
4
  "description": "Local semantic search for your documents. Index Markdown, PDF, and Office files with hybrid BM25 + vector search.",
5
5
  "keywords": [
6
6
  "embeddings",
@@ -78,6 +78,7 @@
78
78
  "bench:general-embeddings:write": "bun scripts/general-embedding-benchmark.ts --write",
79
79
  "bench:cpu-embeddings": "bun scripts/cpu-embed-autoresearch.ts",
80
80
  "bench:cpu-embeddings:native-batch-probe": "bun scripts/native-embedding-batch-probe.ts",
81
+ "bench:cjk-lexical": "bun scripts/cjk-lexical-benchmark.ts",
81
82
  "eval:retrieval-candidates": "bun scripts/retrieval-candidate-benchmark.ts",
82
83
  "eval:retrieval-candidates:write": "bun scripts/retrieval-candidate-benchmark.ts --write",
83
84
  "eval:watch": "bun --bun evalite watch",
@@ -118,6 +119,7 @@
118
119
  "research:finetune:mlx:fuse": "bun research/finetune/scripts/fuse-mlx.ts",
119
120
  "research:finetune:mlx:smoke": "bun research/finetune/scripts/smoke-mlx.ts",
120
121
  "reset": "bun run src/index.ts reset --confirm",
122
+ "docs:truth": "bun run scripts/public-truth.ts",
121
123
  "docs:verify": "bun run scripts/docs-verify.ts",
122
124
  "website:install": "cd website && bundle install",
123
125
  "website:dev": "cd website && make serve",
@@ -0,0 +1,42 @@
1
+ const canonicalize = (value: unknown): unknown => {
2
+ if (Array.isArray(value)) return value.map(canonicalize);
3
+ if (value !== null && typeof value === "object") {
4
+ return Object.fromEntries(
5
+ Object.entries(value as Record<string, unknown>)
6
+ .sort(([left], [right]) => left.localeCompare(right))
7
+ .map(([key, child]) => [key, canonicalize(child)])
8
+ );
9
+ }
10
+ return value;
11
+ };
12
+
13
+ export const stableJson = (value: unknown): string =>
14
+ JSON.stringify(canonicalize(value));
15
+
16
+ export const fingerprint = (value: unknown): string => {
17
+ const hasher = new Bun.CryptoHasher("sha256");
18
+ hasher.update(stableJson(value));
19
+ return hasher.digest("hex");
20
+ };
21
+
22
+ const withoutVolatileFields = (value: unknown, parentKey?: string): unknown => {
23
+ if (Array.isArray(value)) {
24
+ return value.map((child) => withoutVolatileFields(child, parentKey));
25
+ }
26
+ if (value !== null && typeof value === "object") {
27
+ return Object.fromEntries(
28
+ Object.entries(value as Record<string, unknown>)
29
+ .filter(
30
+ ([key]) =>
31
+ key !== "generatedAt" &&
32
+ !key.endsWith("Ms") &&
33
+ !(parentKey === "fingerprints" && key === "result")
34
+ )
35
+ .map(([key, child]) => [key, withoutVolatileFields(child, key)])
36
+ );
37
+ }
38
+ return value;
39
+ };
40
+
41
+ export const fingerprintStableResult = (value: unknown): string =>
42
+ fingerprint(withoutVolatileFields(value));
@@ -0,0 +1,110 @@
1
+ import type {
2
+ CjkBenchCaseResult,
3
+ CjkBenchFailure,
4
+ CjkBenchLanguage,
5
+ CjkBenchLanguageResult,
6
+ CjkBenchMetrics,
7
+ } from "./types";
8
+
9
+ import { computeMrr, computeNdcg, computeRecall } from "./metrics";
10
+
11
+ const round = (value: number, places = 4): number =>
12
+ Number(value.toFixed(places));
13
+
14
+ const average = (values: number[]): number =>
15
+ values.length === 0
16
+ ? 0
17
+ : values.reduce((sum, value) => sum + value, 0) / values.length;
18
+
19
+ export const summarizeLatency = (
20
+ values: number[]
21
+ ): { p50Ms: number; p95Ms: number; meanMs: number } => {
22
+ if (values.length === 0) {
23
+ return { p50Ms: 0, p95Ms: 0, meanMs: 0 };
24
+ }
25
+ const sorted = [...values].sort((left, right) => left - right);
26
+ const percentile = (percent: number): number => {
27
+ const index = Math.ceil((percent / 100) * sorted.length) - 1;
28
+ return sorted[Math.max(0, Math.min(sorted.length - 1, index))] ?? 0;
29
+ };
30
+ return {
31
+ p50Ms: round(percentile(50), 2),
32
+ p95Ms: round(percentile(95), 2),
33
+ meanMs: round(average(values), 2),
34
+ };
35
+ };
36
+
37
+ export const buildCjkCaseResult = (input: {
38
+ queryId: string;
39
+ language: CjkBenchLanguage;
40
+ category: CjkBenchCaseResult["category"];
41
+ query: string;
42
+ expected: string[];
43
+ judgments: Array<{ docid: string; relevance: number }>;
44
+ topDocs: string[];
45
+ warmLatencyMs: number;
46
+ error?: string;
47
+ }): CjkBenchCaseResult => ({
48
+ queryId: input.queryId,
49
+ language: input.language,
50
+ category: input.category,
51
+ query: input.query,
52
+ expected: input.expected,
53
+ topDocs: input.topDocs.slice(0, 10),
54
+ metrics: {
55
+ recallAt5: round(computeRecall(input.topDocs, input.expected, 5)),
56
+ recallAt10: round(computeRecall(input.topDocs, input.expected, 10)),
57
+ mrr: round(computeMrr(input.topDocs, input.expected)),
58
+ ndcgAt10: round(computeNdcg(input.topDocs, input.judgments, 10)),
59
+ },
60
+ zeroResult: input.topDocs.length === 0,
61
+ warmLatencyMs: round(input.warmLatencyMs, 2),
62
+ ...(input.error ? { error: input.error } : {}),
63
+ });
64
+
65
+ export const summarizeCjkMetrics = (
66
+ cases: CjkBenchCaseResult[]
67
+ ): CjkBenchMetrics => ({
68
+ recallAt5: round(average(cases.map((item) => item.metrics.recallAt5))),
69
+ recallAt10: round(average(cases.map((item) => item.metrics.recallAt10))),
70
+ mrr: round(average(cases.map((item) => item.metrics.mrr))),
71
+ ndcgAt10: round(average(cases.map((item) => item.metrics.ndcgAt10))),
72
+ zeroResultRate: round(
73
+ average(cases.map((item) => (item.zeroResult ? 1 : 0)))
74
+ ),
75
+ });
76
+
77
+ export const classifyCjkFailures = (
78
+ cases: CjkBenchCaseResult[]
79
+ ): CjkBenchFailure[] =>
80
+ cases.flatMap((item) => {
81
+ if (item.metrics.recallAt5 === 1) {
82
+ return [];
83
+ }
84
+ const reason = item.zeroResult
85
+ ? "zero-result"
86
+ : item.metrics.recallAt10 === 0
87
+ ? "not-in-top-10"
88
+ : "below-rank-5";
89
+ return [
90
+ {
91
+ queryId: item.queryId,
92
+ language: item.language,
93
+ category: item.category,
94
+ reason,
95
+ query: item.query,
96
+ expected: item.expected,
97
+ topDocs: item.topDocs.slice(0, 5),
98
+ },
99
+ ];
100
+ });
101
+
102
+ export const summarizeCjkLanguage = (
103
+ language: CjkBenchLanguage,
104
+ cases: CjkBenchCaseResult[]
105
+ ): CjkBenchLanguageResult => ({
106
+ language,
107
+ queryCount: cases.length,
108
+ metrics: summarizeCjkMetrics(cases),
109
+ failures: classifyCjkFailures(cases),
110
+ });
@@ -94,3 +94,122 @@ export interface BenchOutput {
94
94
  export type BenchResult =
95
95
  | { success: true; data: BenchOutput }
96
96
  | { success: false; error: string; isValidation?: boolean };
97
+
98
+ export const CJK_BENCH_LANGUAGES = ["zh", "ja", "ko"] as const;
99
+ export type CjkBenchLanguage = (typeof CJK_BENCH_LANGUAGES)[number];
100
+
101
+ export type CjkBenchCategory =
102
+ | "exact-term"
103
+ | "filename"
104
+ | "identifier"
105
+ | "mixed-script"
106
+ | "normalization"
107
+ | "punctuation"
108
+ | "ranking"
109
+ | "token-boundary";
110
+
111
+ export type CjkBenchLane =
112
+ | "bm25"
113
+ | "hybrid-no-models"
114
+ | "substring-raw"
115
+ | "substring-nfc";
116
+
117
+ export interface CjkBenchMetrics {
118
+ recallAt5: number;
119
+ recallAt10: number;
120
+ mrr: number;
121
+ ndcgAt10: number;
122
+ zeroResultRate: number;
123
+ }
124
+
125
+ export interface CjkBenchLatency {
126
+ coldQueryMs: number;
127
+ warmQuery: {
128
+ p50Ms: number;
129
+ p95Ms: number;
130
+ meanMs: number;
131
+ };
132
+ }
133
+
134
+ export interface CjkBenchFailure {
135
+ queryId: string;
136
+ language: CjkBenchLanguage;
137
+ category: CjkBenchCategory;
138
+ reason: "below-rank-5" | "not-in-top-10" | "zero-result";
139
+ query: string;
140
+ expected: string[];
141
+ topDocs: string[];
142
+ }
143
+
144
+ export interface CjkBenchCaseResult {
145
+ queryId: string;
146
+ language: CjkBenchLanguage;
147
+ category: CjkBenchCategory;
148
+ query: string;
149
+ expected: string[];
150
+ normalization?: {
151
+ form: "NFC" | "NFKC";
152
+ source: string;
153
+ target: string;
154
+ };
155
+ topDocs: string[];
156
+ metrics: Omit<CjkBenchMetrics, "zeroResultRate">;
157
+ zeroResult: boolean;
158
+ warmLatencyMs: number;
159
+ error?: string;
160
+ }
161
+
162
+ export interface CjkBenchLanguageResult {
163
+ language: CjkBenchLanguage;
164
+ queryCount: number;
165
+ metrics: CjkBenchMetrics;
166
+ failures: CjkBenchFailure[];
167
+ }
168
+
169
+ export interface CjkBenchLaneResult {
170
+ id: CjkBenchLane;
171
+ description: string;
172
+ config: Record<string, string | number | boolean | null>;
173
+ queryCount: number;
174
+ metrics: CjkBenchMetrics;
175
+ latency: CjkBenchLatency;
176
+ languages: CjkBenchLanguageResult[];
177
+ cases: CjkBenchCaseResult[];
178
+ }
179
+
180
+ export interface CjkBenchOutput {
181
+ schemaVersion: 1;
182
+ generatedAt: string;
183
+ benchmark: "gno-cjk-lexical-degradation";
184
+ corpus: {
185
+ fixtureVersion: number;
186
+ documentCount: number;
187
+ queryCount: number;
188
+ languages: CjkBenchLanguage[];
189
+ provenance: string;
190
+ fingerprint: string;
191
+ };
192
+ runtime: {
193
+ bun: string;
194
+ platform: string;
195
+ arch: string;
196
+ sqlite: string;
197
+ };
198
+ index: {
199
+ tokenizer: string;
200
+ buildMs: number;
201
+ bytes: number;
202
+ pageCount: number;
203
+ pageSize: number;
204
+ vocabularyTerms: number;
205
+ vocabularyDocuments: number;
206
+ tokenOccurrences: number;
207
+ };
208
+ fingerprints: {
209
+ config: string;
210
+ runtime: string;
211
+ tokenizer: string;
212
+ result: string;
213
+ };
214
+ lanes: CjkBenchLaneResult[];
215
+ }