@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 +95 -34
- package/assets/skill/SKILL.md +3 -1
- package/assets/skill/cli-reference.md +2 -1
- package/package.json +3 -1
- package/src/bench/cjk-fingerprint.ts +42 -0
- package/src/bench/cjk-metrics.ts +110 -0
- package/src/bench/types.ts +119 -0
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|

|
|
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
|
|
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
|
-
|
|
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** |
|
|
806
|
-
| **Privacy First** |
|
|
807
|
-
| **MCP Server** |
|
|
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** |
|
|
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
|
|
822
|
-
| :--------------------- |
|
|
823
|
-
| Qwen3-Embedding-0.6B | Embeddings
|
|
824
|
-
| Qwen3-Reranker-0.6B |
|
|
825
|
-
| Qwen3 / Qwen2.5 family | Query expansion
|
|
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 |
|
|
830
|
-
| :----------- |
|
|
831
|
-
| `slim-tuned` |
|
|
832
|
-
| `slim` |
|
|
833
|
-
| `balanced` |
|
|
834
|
-
| `quality` |
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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
|
|
package/assets/skill/SKILL.md
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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.
|
|
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
|
+
});
|
package/src/bench/types.ts
CHANGED
|
@@ -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
|
+
}
|