@gmickel/gno 1.14.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -4
- 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
|
@@ -94,7 +94,7 @@ gno daemon --detach # headless continuous indexing (background; --status / --st
|
|
|
94
94
|
|
|
95
95
|
<!-- public-truth:current-version -->
|
|
96
96
|
|
|
97
|
-
> Current release: **v1.
|
|
97
|
+
> Current release: **v1.16.0** — see [CHANGELOG.md](./CHANGELOG.md)
|
|
98
98
|
|
|
99
99
|
<!-- /public-truth -->
|
|
100
100
|
|
|
@@ -1036,9 +1036,30 @@ official production GGUF was validated for Nemotron.
|
|
|
1036
1036
|
These small fixture results support keeping Qwen as the built-in default; they
|
|
1037
1037
|
do not establish general language superiority. Query-language classification
|
|
1038
1038
|
supports a broader set than the indexed-document detector (`en`, `de`, `fr`,
|
|
1039
|
-
`it`, `zh`, `ja`, `ko`), and the committed fixture covers only five
|
|
1040
|
-
|
|
1041
|
-
|
|
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 -->
|
|
1042
1063
|
|
|
1043
1064
|
---
|
|
1044
1065
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gmickel/gno",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.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,8 +78,10 @@
|
|
|
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",
|
|
84
|
+
"eval:agentic": "bun evals/agentic/cli.ts",
|
|
83
85
|
"eval:watch": "bun --bun evalite watch",
|
|
84
86
|
"research:finetune:bootstrap": "bun research/finetune/scripts/bootstrap-promotion-fixtures.ts",
|
|
85
87
|
"research:finetune:autonomous:noop": "bun research/finetune/autonomous/scripts/noop-run.ts",
|
|
@@ -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
|
+
}
|