simple-dynamsoft-mcp 7.6.1 → 7.7.1

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
@@ -305,6 +305,8 @@ The server exposes this minimal tool surface:
305
305
 
306
306
  Typical doc Q&A flow: `search` -> `read_document` with the exact `doc://` URI -> answer from the returned text and cite its public URL. Clients that support MCP resources can also fetch the same `doc://` / `sample://` URIs with `resources/read`.
307
307
 
308
+ DBR searches also cover the shared Capture Vision API pages that the DBR docs link to (for example `CaptureVisionRouter.capture()` and `getSimplifiedSettings()`/`updateSettings()`); these hits keep their official `capture-vision/docs` URL and are labelled `shared Capture Vision API used by DBR` in search results. Doc URIs are path-based (`doc://dbr/web/web/<version>/user-guide/use-in-framework`) and change only when the docs move or the SDK version changes.
309
+
308
310
  ## Advanced Configuration And Operator Docs
309
311
 
310
312
  Advanced settings, CI/runbook details, and maintenance workflows live in:
@@ -28,8 +28,8 @@
28
28
  "branch": "main",
29
29
  "owner": "dynamsoft-docs",
30
30
  "repo": "barcode-reader-docs-server",
31
- "commit": "7aa8985f9918460466b3dde1426f5ad1342fa93e",
32
- "archiveUrl": "https://codeload.github.com/dynamsoft-docs/barcode-reader-docs-server/zip/7aa8985f9918460466b3dde1426f5ad1342fa93e"
31
+ "commit": "f82b5b92fb039064be3d355f9aeaacf0947b7096",
32
+ "archiveUrl": "https://codeload.github.com/dynamsoft-docs/barcode-reader-docs-server/zip/f82b5b92fb039064be3d355f9aeaacf0947b7096"
33
33
  },
34
34
  {
35
35
  "name": "data/documentation/capture-vision-docs",
@@ -38,8 +38,8 @@
38
38
  "branch": "main",
39
39
  "owner": "dynamsoft-docs",
40
40
  "repo": "capture-vision-docs",
41
- "commit": "f3ddf9614ca3d5f153bdc780cd740732d30d3de9",
42
- "archiveUrl": "https://codeload.github.com/dynamsoft-docs/capture-vision-docs/zip/f3ddf9614ca3d5f153bdc780cd740732d30d3de9"
41
+ "commit": "428c0080975aee70f15e09deda09ca548a4f9a93",
42
+ "archiveUrl": "https://codeload.github.com/dynamsoft-docs/capture-vision-docs/zip/428c0080975aee70f15e09deda09ca548a4f9a93"
43
43
  },
44
44
  {
45
45
  "name": "data/documentation/capture-vision-docs-js",
@@ -68,8 +68,8 @@
68
68
  "branch": "main",
69
69
  "owner": "dynamsoft-docs",
70
70
  "repo": "capture-vision-docs-server",
71
- "commit": "1e7455238c8fe8f935640c1c31c0c433798ce1f0",
72
- "archiveUrl": "https://codeload.github.com/dynamsoft-docs/capture-vision-docs-server/zip/1e7455238c8fe8f935640c1c31c0c433798ce1f0"
71
+ "commit": "4138672377f9ac99380bc985d28fbbe457c31c2b",
72
+ "archiveUrl": "https://codeload.github.com/dynamsoft-docs/capture-vision-docs-server/zip/4138672377f9ac99380bc985d28fbbe457c31c2b"
73
73
  },
74
74
  {
75
75
  "name": "data/documentation/document-viewer-docs",
@@ -108,8 +108,8 @@
108
108
  "branch": "master",
109
109
  "owner": "dynamsoft-docs",
110
110
  "repo": "web-twain-docs",
111
- "commit": "3cb367e1e733a6556902f328b6b51fad0280d2b9",
112
- "archiveUrl": "https://codeload.github.com/dynamsoft-docs/web-twain-docs/zip/3cb367e1e733a6556902f328b6b51fad0280d2b9"
111
+ "commit": "256fe47f8b83c79cbf8f23034fe77eab65a3fc7b",
112
+ "archiveUrl": "https://codeload.github.com/dynamsoft-docs/web-twain-docs/zip/256fe47f8b83c79cbf8f23034fe77eab65a3fc7b"
113
113
  },
114
114
  {
115
115
  "name": "data/samples/document-scanner-javascript",
@@ -148,8 +148,8 @@
148
148
  "branch": "main",
149
149
  "owner": "Dynamsoft",
150
150
  "repo": "barcode-reader-c-cpp-samples",
151
- "commit": "8d4b48cc525d65569ae63c2317c907a06b5e8322",
152
- "archiveUrl": "https://codeload.github.com/Dynamsoft/barcode-reader-c-cpp-samples/zip/8d4b48cc525d65569ae63c2317c907a06b5e8322"
151
+ "commit": "96e6242b43253a930b1edf6bbe5b0dbcadf1bb5a",
152
+ "archiveUrl": "https://codeload.github.com/Dynamsoft/barcode-reader-c-cpp-samples/zip/96e6242b43253a930b1edf6bbe5b0dbcadf1bb5a"
153
153
  },
154
154
  {
155
155
  "name": "data/samples/dynamsoft-barcode-reader-dotnet",
@@ -158,8 +158,8 @@
158
158
  "branch": "main",
159
159
  "owner": "Dynamsoft",
160
160
  "repo": "barcode-reader-dotnet-samples",
161
- "commit": "641cdf391b514e67a75f5bb6bc25ace068af8b38",
162
- "archiveUrl": "https://codeload.github.com/Dynamsoft/barcode-reader-dotnet-samples/zip/641cdf391b514e67a75f5bb6bc25ace068af8b38"
161
+ "commit": "49e205da121046998d8633c80ff2e31d3d3594f3",
162
+ "archiveUrl": "https://codeload.github.com/Dynamsoft/barcode-reader-dotnet-samples/zip/49e205da121046998d8633c80ff2e31d3d3594f3"
163
163
  },
164
164
  {
165
165
  "name": "data/samples/dynamsoft-barcode-reader-flutter",
@@ -178,8 +178,8 @@
178
178
  "branch": "main",
179
179
  "owner": "Dynamsoft",
180
180
  "repo": "barcode-reader-java-samples",
181
- "commit": "caab3b6c1737f465047f84bbb77f827f19bb8523",
182
- "archiveUrl": "https://codeload.github.com/Dynamsoft/barcode-reader-java-samples/zip/caab3b6c1737f465047f84bbb77f827f19bb8523"
181
+ "commit": "fbadec4609803fb3ec542b20b630adb99481cfe2",
182
+ "archiveUrl": "https://codeload.github.com/Dynamsoft/barcode-reader-java-samples/zip/fbadec4609803fb3ec542b20b630adb99481cfe2"
183
183
  },
184
184
  {
185
185
  "name": "data/samples/dynamsoft-barcode-reader-maui",
@@ -208,8 +208,8 @@
208
208
  "branch": "master",
209
209
  "owner": "Dynamsoft",
210
210
  "repo": "barcode-reader-python-samples",
211
- "commit": "a754aab9e4fb9630519382402f32250c94d8643b",
212
- "archiveUrl": "https://codeload.github.com/Dynamsoft/barcode-reader-python-samples/zip/a754aab9e4fb9630519382402f32250c94d8643b"
211
+ "commit": "bd63acaaf5d8b427aee7d3d1c8b316dc3021b9f5",
212
+ "archiveUrl": "https://codeload.github.com/Dynamsoft/barcode-reader-python-samples/zip/bd63acaaf5d8b427aee7d3d1c8b316dc3021b9f5"
213
213
  },
214
214
  {
215
215
  "name": "data/samples/dynamsoft-barcode-reader-react-native",
@@ -228,8 +228,8 @@
228
228
  "branch": "main",
229
229
  "owner": "Dynamsoft",
230
230
  "repo": "capture-vision-cpp-samples",
231
- "commit": "9bd516bc1961425eed02b2308505a9a6c775b38d",
232
- "archiveUrl": "https://codeload.github.com/Dynamsoft/capture-vision-cpp-samples/zip/9bd516bc1961425eed02b2308505a9a6c775b38d"
231
+ "commit": "cfd0fc6f4fee070e188955dd0cccfab39ff2c02d",
232
+ "archiveUrl": "https://codeload.github.com/Dynamsoft/capture-vision-cpp-samples/zip/cfd0fc6f4fee070e188955dd0cccfab39ff2c02d"
233
233
  },
234
234
  {
235
235
  "name": "data/samples/dynamsoft-capture-vision-dotnet",
@@ -238,8 +238,8 @@
238
238
  "branch": "main",
239
239
  "owner": "Dynamsoft",
240
240
  "repo": "capture-vision-dotnet-samples",
241
- "commit": "871ffea41164e08ed1315157c3feb7912915fa40",
242
- "archiveUrl": "https://codeload.github.com/Dynamsoft/capture-vision-dotnet-samples/zip/871ffea41164e08ed1315157c3feb7912915fa40"
241
+ "commit": "f3fe6aecc7c3c628f4241fe3a52ed448680c9d64",
242
+ "archiveUrl": "https://codeload.github.com/Dynamsoft/capture-vision-dotnet-samples/zip/f3fe6aecc7c3c628f4241fe3a52ed448680c9d64"
243
243
  },
244
244
  {
245
245
  "name": "data/samples/dynamsoft-capture-vision-flutter",
@@ -258,8 +258,8 @@
258
258
  "branch": "main",
259
259
  "owner": "Dynamsoft",
260
260
  "repo": "capture-vision-java-samples",
261
- "commit": "760f9e3f5fed9c74d813ffc30fbcd9a5a96bf576",
262
- "archiveUrl": "https://codeload.github.com/Dynamsoft/capture-vision-java-samples/zip/760f9e3f5fed9c74d813ffc30fbcd9a5a96bf576"
261
+ "commit": "83660139a8d32b0b319b6bc0b213b827437ddc43",
262
+ "archiveUrl": "https://codeload.github.com/Dynamsoft/capture-vision-java-samples/zip/83660139a8d32b0b319b6bc0b213b827437ddc43"
263
263
  },
264
264
  {
265
265
  "name": "data/samples/dynamsoft-capture-vision-maui",
@@ -298,8 +298,8 @@
298
298
  "branch": "main",
299
299
  "owner": "Dynamsoft",
300
300
  "repo": "capture-vision-python-samples",
301
- "commit": "f54b2d281b8325e8765f535be8ef6ebf157f4a54",
302
- "archiveUrl": "https://codeload.github.com/Dynamsoft/capture-vision-python-samples/zip/f54b2d281b8325e8765f535be8ef6ebf157f4a54"
301
+ "commit": "6f350f5d68e3ddcd90041bf46cac5541677f04b5",
302
+ "archiveUrl": "https://codeload.github.com/Dynamsoft/capture-vision-python-samples/zip/6f350f5d68e3ddcd90041bf46cac5541677f04b5"
303
303
  },
304
304
  {
305
305
  "name": "data/samples/dynamsoft-capture-vision-react-native",
@@ -120,7 +120,7 @@
120
120
  "dbr-server": {
121
121
  "name": "Dynamsoft Barcode Reader Server/Desktop SDK",
122
122
  "description": "Barcode scanning for server/desktop platforms including Python, .NET, Java, C++, and Node.js.",
123
- "version": "11.6.4000",
123
+ "version": "11.6.5000",
124
124
  "default_platform": "python",
125
125
  "snippet_path": "dynamsoft-barcode-reader/server",
126
126
  "platforms": {
@@ -255,7 +255,7 @@
255
255
  "dcv-core": {
256
256
  "name": "Dynamsoft Capture Vision Core",
257
257
  "description": "Unified architecture documentation for Capture Vision capabilities across barcode, MRZ, document normalization, and parsing workflows.",
258
- "version": "3.6.4000",
258
+ "version": "3.6.5000",
259
259
  "default_platform": "core",
260
260
  "snippet_path": "dynamsoft-capture-vision/core",
261
261
  "platforms": {
@@ -270,7 +270,7 @@
270
270
  "dcv-server": {
271
271
  "name": "Dynamsoft Capture Vision Server/Desktop SDK",
272
272
  "description": "Capture Vision workflows for Python, .NET, Java, C++, and Node.js, including MRZ, document normalization, and driver license scenarios.",
273
- "version": "3.6.4000",
273
+ "version": "3.6.5000",
274
274
  "default_platform": "python",
275
275
  "snippet_path": "dynamsoft-capture-vision/server",
276
276
  "platforms": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simple-dynamsoft-mcp",
3
- "version": "7.6.1",
3
+ "version": "7.7.1",
4
4
  "description": "MCP server for Dynamsoft SDKs - Capture Vision, Barcode Reader (Mobile/Python/Web), Dynamic Web TWAIN, and Document Viewer. Provides documentation, code snippets, and API guidance.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -24,13 +24,15 @@
24
24
  "scripts": {
25
25
  "start": "node src/index.js",
26
26
  "test": "npm run test:lite",
27
- "test:unit": "node --test test/unit/gemini-retry.test.js test/unit/profile-config.test.js test/unit/lexical-provider.test.js test/unit/hydration-mode.test.js test/unit/hydration-policy.test.js test/unit/repo-map.test.js test/unit/download-utils.test.js test/unit/logging.test.js test/unit/request-logging.test.js test/unit/request-log-file.test.js test/unit/rag-signature-manifest.test.js test/unit/startup-timing.test.js test/unit/http-startup-readiness.test.js test/unit/resource-index-startup.test.js test/unit/create-server.test.js test/unit/server-helpers.test.js test/unit/update-sdk-versions.test.js test/unit/version-policy.test.js test/unit/query-expansion.test.js test/unit/docs-loader-enrich.test.js",
27
+ "test:unit": "node --test test/unit/gemini-retry.test.js test/unit/profile-config.test.js test/unit/lexical-provider.test.js test/unit/hydration-mode.test.js test/unit/hydration-policy.test.js test/unit/repo-map.test.js test/unit/download-utils.test.js test/unit/logging.test.js test/unit/request-logging.test.js test/unit/request-log-file.test.js test/unit/rag-signature-manifest.test.js test/unit/startup-timing.test.js test/unit/http-startup-readiness.test.js test/unit/resource-index-startup.test.js test/unit/create-server.test.js test/unit/server-helpers.test.js test/unit/update-sdk-versions.test.js test/unit/version-policy.test.js test/unit/query-expansion.test.js test/unit/docs-loader-enrich.test.js test/unit/shared-docs.test.js test/unit/vector-cache.test.js test/unit/doc-slugs.test.js",
28
28
  "test:lite": "npm run test:stdio && npm run test:http && npm run test:package",
29
29
  "test:lexical": "node --test test/integration/stdio.test.js test/integration/http.test.js",
30
30
  "test:gemini": "node scripts/run-gemini-tests.mjs",
31
31
  "test:stdio": "node --test test/integration/stdio.test.js",
32
32
  "test:http": "node --test test/integration/http.test.js",
33
33
  "test:package": "node --test test/integration/package-runtime.test.js",
34
+ "test:retrieval": "node --test test/integration/retrieval-quality.test.js",
35
+ "eval:retrieval": "node scripts/eval-retrieval.mjs",
34
36
  "test:contract": "node --test test/contract/contract.test.js",
35
37
  "check:version-drift": "node scripts/check-version-drift.mjs",
36
38
  "test:lazy": "node --test test/integration/lazy-hydration.test.js",
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ // Measures search relevance against the golden query set.
3
+ //
4
+ // node scripts/eval-retrieval.mjs [--provider=lexical|gemini] [--case=<id>] [--show] [--json=<path>]
5
+ //
6
+ // The provider is passed through RAG_PROVIDER before the RAG layer loads, with
7
+ // fallback disabled so a failing provider is reported instead of masked.
8
+
9
+ import { readFileSync, writeFileSync } from "node:fs";
10
+ import { dirname, join } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ const args = Object.fromEntries(
14
+ process.argv.slice(2).map((arg) => {
15
+ const [key, ...rest] = arg.replace(/^--/, "").split("=");
16
+ return [key, rest.length ? rest.join("=") : "true"];
17
+ })
18
+ );
19
+ const provider = args.provider || "lexical";
20
+ process.env.RAG_PROVIDER = provider;
21
+ process.env.RAG_FALLBACK = "none";
22
+ process.env.RAG_PREWARM = "false";
23
+
24
+ const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
25
+ const goldenPath = args.golden || join(repoRoot, "test", "fixtures", "retrieval-golden.json");
26
+ const golden = JSON.parse(readFileSync(goldenPath, "utf8"));
27
+ const cases = args.case ? golden.cases.filter((item) => item.id === args.case) : golden.cases;
28
+ if (cases.length === 0) {
29
+ console.error(`No golden cases matched ${args.case}`);
30
+ process.exit(1);
31
+ }
32
+
33
+ const { ensureResourceIndexReady } = await import("../src/server/resource-index.js");
34
+ const { searchResources } = await import("../src/rag/index.js");
35
+ const { runRetrievalEval } = await import("./lib/retrieval-eval.mjs");
36
+ const { isResultInScope } = await import("./lib/scope-matcher.mjs");
37
+
38
+ ensureResourceIndexReady();
39
+ const report = await runRetrievalEval({ searchResources, cases, k: golden.k || 5, isInScope: isResultInScope });
40
+ report.provider = provider;
41
+
42
+ const fmt = (value) => (value === null || value === undefined ? "n/a" : value.toFixed(3));
43
+ for (const item of report.cases) {
44
+ const flag = item.gap ? "GAP " : item.requiredHit === false ? "MISS" : "ok ";
45
+ console.log(`${flag} ${item.id.padEnd(34)} recall=${fmt(item.recall)} mrr=${fmt(item.mrr)} ndcg=${fmt(item.ndcg)} dup=${item.duplicates} off=${item.offScope} rn=${item.releaseNotes} ms=${item.latencyMs.toFixed(0)}`);
46
+ if (args.show || item.requiredHit === false) {
47
+ for (const hit of item.top) {
48
+ console.log(` ${hit.rank}. [g${hit.grade}] [${hit.product}] ${hit.title} :: ${hit.url || hit.uri}`);
49
+ }
50
+ for (const url of item.requiredMissing) console.log(` missing: ${url}`);
51
+ }
52
+ }
53
+
54
+ const s = report.summary;
55
+ console.log(`\nprovider=${provider} cases=${s.cases} (gap=${s.gapCases}) k=${report.k}`);
56
+ console.log(`recall@${report.k}=${fmt(s.recallAtK)} mrr=${fmt(s.mrr)} ndcg@${report.k}=${fmt(s.ndcgAtK)} required_hit=${fmt(s.requiredHitRate)}`);
57
+ console.log(`duplicate_rate=${fmt(s.duplicateRate)} off_scope_rate=${fmt(s.offScopeRate)} release_notes_rate=${fmt(s.releaseNotesRate)}`);
58
+ const lat = s.latencyMs;
59
+ console.log(`latency_ms first_query=${lat.firstQuery.toFixed(0)} first_pass p50=${lat.p50FirstPass.toFixed(0)} p95=${lat.p95FirstPass.toFixed(0)} | warm p50=${lat.p50Warm.toFixed(0)} p95=${lat.p95Warm.toFixed(0)}`);
60
+
61
+ if (args.json) {
62
+ writeFileSync(args.json, JSON.stringify(report, null, 2));
63
+ console.log(`wrote ${args.json}`);
64
+ }
@@ -0,0 +1,134 @@
1
+ // Retrieval-quality metrics for the golden query set (test/fixtures/retrieval-golden.json).
2
+ // Cases grade relevant documents by public URL (not doc:// URI) so judgments
3
+ // survive URI-scheme and index-order changes.
4
+
5
+ function normalizeUrl(url) {
6
+ return String(url || "").split("#")[0].split("?")[0].replace(/\/index\.html$/i, "/").toLowerCase();
7
+ }
8
+
9
+ function gradeOf(result, relevant) {
10
+ const key = normalizeUrl(result.url);
11
+ if (!key) return 0;
12
+ return relevant.get(key) || 0;
13
+ }
14
+
15
+ function dcg(gains) {
16
+ return gains.reduce((sum, gain, index) => sum + ((2 ** gain) - 1) / Math.log2(index + 2), 0);
17
+ }
18
+
19
+ function percentile(values, p) {
20
+ if (values.length === 0) return 0;
21
+ const sorted = [...values].sort((a, b) => a - b);
22
+ const index = Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1);
23
+ return sorted[Math.max(0, index)];
24
+ }
25
+
26
+ // `isInScope(result, filters)` (when given) applies the server's own
27
+ // product/edition/platform matching so cross-platform leakage is counted too.
28
+ function isOffScope(result, testCase, isInScope) {
29
+ const product = testCase.filters?.product;
30
+ if (product && result.product !== product) return true;
31
+ if (isInScope && !isInScope(result, testCase.filters || {})) return true;
32
+ const prefixes = testCase.allowedUrlPrefixes;
33
+ if (Array.isArray(prefixes) && prefixes.length > 0 && result.url) {
34
+ return !prefixes.some((prefix) => String(result.url).startsWith(prefix));
35
+ }
36
+ return false;
37
+ }
38
+
39
+ function isReleaseNotes(result) {
40
+ return /\/release-notes?\//i.test(String(result.url || ""));
41
+ }
42
+
43
+ function scoreCase(testCase, results, k, isInScope) {
44
+ const relevant = new Map(
45
+ Object.entries(testCase.relevant || {}).map(([url, grade]) => [normalizeUrl(url), Number(grade) || 0])
46
+ );
47
+ const top = results.slice(0, k);
48
+ const gains = top.map((result) => gradeOf(result, relevant));
49
+ const positiveGains = [...relevant.values()].filter((g) => g > 0).sort((a, b) => b - a);
50
+ const idealGains = positiveGains.slice(0, k);
51
+ const relevantCount = positiveGains.length;
52
+
53
+ const firstHit = gains.findIndex((gain) => gain > 0);
54
+ const found = new Set(top.filter((result) => gradeOf(result, relevant) > 0).map((r) => normalizeUrl(r.url)));
55
+ const required = [...relevant.entries()].filter(([, grade]) => grade >= 3).map(([url]) => url);
56
+ const requiredFound = required.filter((url) => found.has(url));
57
+
58
+ const urls = top.map((result) => normalizeUrl(result.url || result.uri));
59
+ const duplicates = urls.length - new Set(urls).size;
60
+
61
+ return {
62
+ id: testCase.id,
63
+ gap: relevantCount === 0,
64
+ recall: relevantCount ? found.size / relevantCount : null,
65
+ mrr: relevantCount ? (firstHit >= 0 ? 1 / (firstHit + 1) : 0) : null,
66
+ ndcg: relevantCount ? dcg(gains) / dcg(idealGains) : null,
67
+ requiredHit: required.length ? requiredFound.length === required.length : null,
68
+ requiredMissing: required.filter((url) => !found.has(url)),
69
+ duplicates,
70
+ offScope: top.filter((result) => isOffScope(result, testCase, isInScope)).length,
71
+ releaseNotes: top.filter(isReleaseNotes).length,
72
+ returned: top.length,
73
+ top: top.map((result, index) => ({
74
+ rank: index + 1,
75
+ grade: gains[index],
76
+ product: result.product,
77
+ title: result.title,
78
+ url: result.url || "",
79
+ uri: result.uri
80
+ }))
81
+ };
82
+ }
83
+
84
+ function mean(values) {
85
+ const present = values.filter((value) => value !== null && value !== undefined);
86
+ if (present.length === 0) return null;
87
+ return present.reduce((sum, value) => sum + value, 0) / present.length;
88
+ }
89
+
90
+ function summarize(caseScores, firstPass, warmPass) {
91
+ const totalReturned = caseScores.reduce((sum, item) => sum + item.returned, 0) || 1;
92
+ const required = caseScores.filter((item) => item.requiredHit !== null);
93
+ return {
94
+ cases: caseScores.length,
95
+ gapCases: caseScores.filter((item) => item.gap).length,
96
+ recallAtK: mean(caseScores.map((item) => item.recall)),
97
+ mrr: mean(caseScores.map((item) => item.mrr)),
98
+ ndcgAtK: mean(caseScores.map((item) => item.ndcg)),
99
+ requiredHitRate: required.length ? required.filter((item) => item.requiredHit).length / required.length : null,
100
+ duplicateRate: caseScores.reduce((sum, item) => sum + item.duplicates, 0) / totalReturned,
101
+ offScopeRate: caseScores.reduce((sum, item) => sum + item.offScope, 0) / totalReturned,
102
+ releaseNotesRate: caseScores.reduce((sum, item) => sum + item.releaseNotes, 0) / totalReturned,
103
+ // First pass pays index loading and per-scope index builds; the warm pass
104
+ // repeats the same queries, so it reflects steady-state search cost.
105
+ latencyMs: {
106
+ firstQuery: firstPass[0] ?? 0,
107
+ p50FirstPass: percentile(firstPass, 50),
108
+ p95FirstPass: percentile(firstPass, 95),
109
+ p50Warm: percentile(warmPass, 50),
110
+ p95Warm: percentile(warmPass, 95)
111
+ }
112
+ };
113
+ }
114
+
115
+ async function runRetrievalEval({ searchResources, cases, k = 5, isInScope }) {
116
+ const caseScores = [];
117
+ const firstPass = [];
118
+ const warmPass = [];
119
+ for (const testCase of cases) {
120
+ const startedAt = performance.now();
121
+ const results = await searchResources({ query: testCase.query, ...(testCase.filters || {}), limit: k, throwOnError: true });
122
+ const latencyMs = performance.now() - startedAt;
123
+ firstPass.push(latencyMs);
124
+ caseScores.push({ ...scoreCase(testCase, results, k, isInScope), latencyMs });
125
+ }
126
+ for (const testCase of cases) {
127
+ const startedAt = performance.now();
128
+ await searchResources({ query: testCase.query, ...(testCase.filters || {}), limit: k, throwOnError: true });
129
+ warmPass.push(performance.now() - startedAt);
130
+ }
131
+ return { k, summary: summarize(caseScores, firstPass, warmPass), cases: caseScores };
132
+ }
133
+
134
+ export { normalizeUrl, scoreCase, summarize, runRetrievalEval };
@@ -0,0 +1,17 @@
1
+ // Scope check for retrieval evaluation, using the same normalizers and
2
+ // matchers the search path applies to filters.
3
+ import {
4
+ normalizeProduct,
5
+ normalizePlatform,
6
+ normalizeEdition,
7
+ editionMatches,
8
+ platformMatches
9
+ } from "../../src/server/resource-index.js";
10
+ import { normalizeSearchFilters, entryMatchesScope } from "../../src/rag/search-utils.js";
11
+
12
+ function isResultInScope(result, rawFilters) {
13
+ const filters = normalizeSearchFilters(rawFilters, { normalizeProduct, normalizePlatform, normalizeEdition });
14
+ return entryMatchesScope(result, filters, { editionMatches, platformMatches });
15
+ }
16
+
17
+ export { isResultInScope };
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createHash } from "node:crypto";
3
- import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
3
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
4
4
  import { basename, join, resolve } from "node:path";
5
5
 
6
6
  function toPosixPath(path) {
@@ -32,7 +32,11 @@ if (!existsSync(cacheDir)) {
32
32
  throw new Error(`RAG cache directory not found after prebuild: ${cacheDir}`);
33
33
  }
34
34
 
35
- const cacheFiles = readdirSync(cacheDir, { withFileTypes: true })
35
+ // Each cache is a JSON header plus a sibling `.f32` vector file (see
36
+ // src/rag/vector-cache.js); both ship in the release archive. A header with no
37
+ // `.f32` is a pre-7.7 all-JSON cache (e.g. restored from an older CI cache):
38
+ // the runtime cannot load it, so it is deleted rather than shipped.
39
+ const allHeaders = readdirSync(cacheDir, { withFileTypes: true })
36
40
  .filter((entry) => entry.isFile())
37
41
  .map((entry) => join(cacheDir, entry.name))
38
42
  .filter((path) => {
@@ -44,10 +48,23 @@ const cacheFiles = readdirSync(cacheDir, { withFileTypes: true })
44
48
  })
45
49
  .sort();
46
50
 
51
+ const toVectorFile = (path) => path.replace(/\.json$/i, ".f32");
52
+ const cacheFiles = [];
53
+ for (const path of allHeaders) {
54
+ if (existsSync(toVectorFile(path))) {
55
+ cacheFiles.push(path);
56
+ } else {
57
+ rmSync(path, { force: true });
58
+ console.log(`[rag-prebuild] removed legacy cache without vectors: ${basename(path)}`);
59
+ }
60
+ }
61
+
47
62
  if (cacheFiles.length === 0) {
48
- throw new Error(`No cache JSON files found in ${cacheDir}.`);
63
+ throw new Error(`No cache files with vectors found in ${cacheDir}.`);
49
64
  }
50
65
 
66
+ const vectorFiles = cacheFiles.map(toVectorFile);
67
+
51
68
  let indexSignature = "";
52
69
  let indexCacheKey = "";
53
70
  const geminiCacheFile = cacheFiles.find((path) => /rag-gemini-.*\.json$/i.test(path));
@@ -69,7 +86,7 @@ const manifest = {
69
86
  indexSignature,
70
87
  indexCacheKey,
71
88
  cacheDir: toPosixPath(cacheDir),
72
- files: cacheFiles.map((path) => {
89
+ files: [...cacheFiles, ...vectorFiles].sort().map((path) => {
73
90
  const stats = statSync(path);
74
91
  return {
75
92
  name: toPosixPath(path.slice(cacheDir.length + 1)),
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { resourceIndex, readResourceContent } from "../src/server/resource-index.js";
3
+ import { ensureResourceIndexReady, resourceIndex, readResourceContent } from "../src/server/resource-index.js";
4
4
 
5
5
  function parsePositiveInt(value, fallback) {
6
6
  const parsed = Number.parseInt(String(value ?? ""), 10);
@@ -9,6 +9,7 @@ function parsePositiveInt(value, fallback) {
9
9
  }
10
10
 
11
11
  async function main() {
12
+ ensureResourceIndexReady();
12
13
  const concurrency = parsePositiveInt(process.env.DOC_VERIFY_CONCURRENCY, 8);
13
14
  const docs = resourceIndex.filter((entry) => entry.type === "doc");
14
15
  const total = docs.length;
@@ -3,14 +3,33 @@ import { getHydrationProduct } from "../server/public-offerings.js";
3
3
 
4
4
  const REPO_MAP = {
5
5
  dbr: {
6
+ // DBR docs index the Capture Vision pages they link to (shared APIs such as
7
+ // CaptureVisionRouter), so each DBR edition also needs the matching DCV
8
+ // edition repo plus DCV core.
6
9
  docs: {
7
- web: ["documentation/barcode-reader-docs-js"],
8
- mobile: ["documentation/barcode-reader-docs-mobile"],
9
- server: ["documentation/barcode-reader-docs-server"],
10
+ web: [
11
+ "documentation/barcode-reader-docs-js",
12
+ "documentation/capture-vision-docs-js",
13
+ "documentation/capture-vision-docs"
14
+ ],
15
+ mobile: [
16
+ "documentation/barcode-reader-docs-mobile",
17
+ "documentation/capture-vision-docs-mobile",
18
+ "documentation/capture-vision-docs"
19
+ ],
20
+ server: [
21
+ "documentation/barcode-reader-docs-server",
22
+ "documentation/capture-vision-docs-server",
23
+ "documentation/capture-vision-docs"
24
+ ],
10
25
  any: [
11
26
  "documentation/barcode-reader-docs-js",
12
27
  "documentation/barcode-reader-docs-mobile",
13
- "documentation/barcode-reader-docs-server"
28
+ "documentation/barcode-reader-docs-server",
29
+ "documentation/capture-vision-docs",
30
+ "documentation/capture-vision-docs-js",
31
+ "documentation/capture-vision-docs-mobile",
32
+ "documentation/capture-vision-docs-server"
14
33
  ]
15
34
  },
16
35
  samples: {
package/src/rag/config.js CHANGED
@@ -63,12 +63,10 @@ const ragConfig = {
63
63
  modelCacheDir: readEnvValue("RAG_MODEL_CACHE_DIR", join(dataRoot, ".rag-cache", "models")),
64
64
  chunkSize: readIntEnv("RAG_CHUNK_SIZE", 1200),
65
65
  chunkOverlap: readIntEnv("RAG_CHUNK_OVERLAP", 200),
66
- // Keep the total embedded-chunk count within V8's ~512MB max-string limit,
67
- // which the prebuilt Gemini cache hits via a single JSON.stringify of ~3072-dim
68
- // vectors. At 24 the release prebuild overflowed ("Invalid string length"); 6 is
69
- // the count the pre-v7.4 release built successfully on this data. Raising this
70
- // needs the ceiling-proof cache format tracked in the v7.5 follow-ups.
71
- maxChunksPerDoc: readIntEnv("RAG_MAX_CHUNKS_PER_DOC", 6),
66
+ // 0 = embed every chunk of every doc (~19.7k chunks vs ~10.3k at the old cap
67
+ // of 6). Each chunk is one Gemini embedding per release prebuild; the vector
68
+ // cache is header + float32 (src/rag/vector-cache.js), so there is no size cap.
69
+ maxChunksPerDoc: readIntEnv("RAG_MAX_CHUNKS_PER_DOC", 0),
72
70
  maxTextChars: readIntEnv("RAG_MAX_TEXT_CHARS", 4000),
73
71
  minScore: readFloatEnv("RAG_MIN_SCORE", 0.2),
74
72
  includeScore: readBoolEnv("RAG_INCLUDE_SCORE", false),
package/src/rag/index.js CHANGED
@@ -102,7 +102,22 @@ function refreshRagIndexes() {
102
102
  logRag(`indexes refreshed resources=${resourceIndex.length}`);
103
103
  }
104
104
 
105
- async function searchResources({ query, product, edition, platform, type, limit }) {
105
+ function dedupeByPublicUrl(results, limit) {
106
+ const seen = new Set();
107
+ const unique = [];
108
+ for (const entry of results) {
109
+ const key = entry.url || entry.uri;
110
+ if (seen.has(key)) continue;
111
+ seen.add(key);
112
+ unique.push(entry);
113
+ if (limit && unique.length >= limit) break;
114
+ }
115
+ return unique;
116
+ }
117
+
118
+ // `throwOnError` makes an all-providers failure throw instead of returning [];
119
+ // evaluation uses it so a broken provider cannot pass as zero relevance.
120
+ async function searchResources({ query, product, edition, platform, type, limit, throwOnError = false }) {
106
121
  const startedAt = Date.now();
107
122
  const filters = normalizeSearchFilters({ product, edition, platform, type }, {
108
123
  normalizeProduct,
@@ -133,7 +148,11 @@ async function searchResources({ query, product, edition, platform, type, limit
133
148
  for (const name of providers) {
134
149
  try {
135
150
  const provider = await providerOrchestrator.loadSearchProvider(name);
136
- const results = await provider.search(searchQuery, filters, maxResults);
151
+ // Shared pages (e.g. Capture Vision core reference) are indexed once per
152
+ // DBR edition, so an edition-less search can hit the same public page
153
+ // several times. Over-fetch, then keep the best hit per public URL.
154
+ const candidates = await provider.search(searchQuery, filters, maxResults ? maxResults * 3 : undefined);
155
+ const results = dedupeByPublicUrl(candidates, maxResults);
137
156
  if (!ragLogState.providerFirstUse.has(name)) {
138
157
  ragLogState.providerFirstUse.add(name);
139
158
  logRag("provider_selected", {
@@ -191,6 +210,7 @@ async function searchResources({ query, product, edition, platform, type, limit
191
210
  latency_bucket: latencyBucket(elapsedMs),
192
211
  error: lastError.message
193
212
  }, { level: "error" });
213
+ if (throwOnError) throw lastError;
194
214
  }
195
215
  return [];
196
216
  }