@syndash/research-vault-mcp 1.1.3 → 1.1.5
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/CHANGELOG.md +23 -2
- package/README.md +35 -7
- package/dist/server.js +59 -16
- package/package.json +2 -2
- package/src/evidence_metadata.ts +63 -3
- package/src/vault.ts +16 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 1.1.5 — 2026-06-08
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- `vault_search` now separates read/index/analysis state with `readability_verdict`, `index_verdict`, and `analysis_verdict`.
|
|
10
|
+
- Readable search hits without `lastAnalyzedAt` now return `analysis_verdict: "NOT_ANALYZED"` without turning the overall search guidance into a scary `FLAG`.
|
|
11
|
+
- Exact-id search hits are marked with `matched_fields: ["id_exact"]`.
|
|
12
|
+
- Slash-heavy titles/categories are easier to find through punctuation-normalized matching, such as `Music/Rhythm` via `music rhythm`.
|
|
13
|
+
|
|
14
|
+
### Verified
|
|
15
|
+
|
|
16
|
+
- `bun test packages/research-vault-mcp/__tests__/vault_evidence_metadata.test.ts`
|
|
17
|
+
|
|
18
|
+
## 1.1.4 — 2026-05-10
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Published the package source under the canonical Dash Research Vault repo path: `packages/research-vault-mcp`.
|
|
23
|
+
- Updated README language to treat Dash Research Vault as the package home and Evensong as research provenance, not the npm package repository.
|
|
24
|
+
- Updated package metadata description and release docs to match the public npm source path.
|
|
25
|
+
|
|
5
26
|
## 1.1.3 — 2026-05-10
|
|
6
27
|
|
|
7
28
|
### Added
|
|
@@ -24,8 +45,8 @@
|
|
|
24
45
|
- Default MCP transport is now `stdio`, matching command-launched MCP clients.
|
|
25
46
|
- The npm bin is a Node-compatible launcher that delegates server execution to Bun.
|
|
26
47
|
- Published package includes `dist/server.js` via `prepack` build and `files` allowlist.
|
|
27
|
-
- README now documents
|
|
28
|
-
- Package metadata now
|
|
48
|
+
- README now documents install commands, Claude config, Bun runtime requirement, and explicit SSE mode.
|
|
49
|
+
- Package metadata now includes public repository information and Apache-2.0 package license.
|
|
29
50
|
|
|
30
51
|
### Verified
|
|
31
52
|
|
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# @syndash/research-vault-mcp
|
|
2
2
|
|
|
3
|
-
Research Vault is the
|
|
3
|
+
Research Vault MCP is the installable server for **Dash Research Vault**. It gives MCP-compatible agents a public-safe way to search, inspect, and operate a markdown knowledge vault.
|
|
4
4
|
|
|
5
|
-
It is not the whole Evensong
|
|
5
|
+
It is not the whole vault template or the whole Evensong research project. This package is the runtime adapter: MCP tools, read-only defaults, bounded reads, public-surface redaction, and optional mutation profiles for operator-approved sessions.
|
|
6
|
+
|
|
7
|
+
Source: <https://github.com/Fearvox/dash-research-vault/tree/main/packages/research-vault-mcp>
|
|
6
8
|
|
|
7
9
|
## Install
|
|
8
10
|
|
|
@@ -77,7 +79,7 @@ Set the vault location with an environment variable before launching your MCP cl
|
|
|
77
79
|
export VAULT_ROOT=/path/to/research-vault
|
|
78
80
|
```
|
|
79
81
|
|
|
80
|
-
The package is designed for markdown-based knowledge bases. Keep private vault contents outside the public
|
|
82
|
+
The package is designed for markdown-based knowledge bases. Keep private vault contents outside the public Dash Research Vault repo.
|
|
81
83
|
|
|
82
84
|
## MCP Profiles
|
|
83
85
|
|
|
@@ -93,6 +95,31 @@ Mutation tools are hidden and blocked in `readonly`. `MCP_PROFILE=full` enables
|
|
|
93
95
|
|
|
94
96
|
`vault_get` is bounded by default: it returns an excerpt unless the operator approves `include_content:true`, and even full-content requests are capped by `max_chars`. Search, status, and batch responses include `agent_guidance` plus evidence metadata for provenance, freshness, profile, and public-safety state.
|
|
95
97
|
|
|
98
|
+
## Search and read semantics
|
|
99
|
+
|
|
100
|
+
`vault_search` is an index/readability surface. It answers whether the query matched saved knowledge entries and whether those entries are readable. It does not treat missing analysis metadata as missing content.
|
|
101
|
+
|
|
102
|
+
Search matching covers:
|
|
103
|
+
|
|
104
|
+
- exact note IDs, marked with `matched_fields: ["id_exact"]`
|
|
105
|
+
- titles, note content, IDs, and categories
|
|
106
|
+
- punctuation-normalized text, so slash-heavy categories such as `software-engineering/game-design` can be found with `software engineering game design`
|
|
107
|
+
|
|
108
|
+
Search results separate the relevant states:
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"readability_verdict": "PASS",
|
|
113
|
+
"index_verdict": "PASS",
|
|
114
|
+
"analysis_verdict": "NOT_ANALYZED",
|
|
115
|
+
"freshness_verdict": "PASS"
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
That means the note matched and is readable, but has no `lastAnalyzedAt` yet. Treat `NOT_ANALYZED` as an analysis caveat, not a broken read. Stale or invalid analysis timestamps still produce a `FLAG`.
|
|
120
|
+
|
|
121
|
+
`vault_get` is the authoritative exact-ID read path for follow-up evidence. Call `vault_search` first, then pass the exact returned `id` to `vault_get`.
|
|
122
|
+
|
|
96
123
|
## Tools exposed
|
|
97
124
|
|
|
98
125
|
Current MCP contract:
|
|
@@ -117,6 +144,7 @@ Published packages include:
|
|
|
117
144
|
- `dist/server.js`
|
|
118
145
|
- `src/**/*.ts` for source inspection
|
|
119
146
|
- `README.md`
|
|
147
|
+
- `CHANGELOG.md`
|
|
120
148
|
- `package.json`
|
|
121
149
|
|
|
122
150
|
The bin prefers `dist/server.js`. In a monorepo checkout without `dist`, it falls back to `bun run src/server.ts` so development remains fast without a separate compile step.
|
|
@@ -133,16 +161,16 @@ score(d, q, t) = lexical(q,d)
|
|
|
133
161
|
+ summary-level weight(d)
|
|
134
162
|
```
|
|
135
163
|
|
|
136
|
-
The
|
|
164
|
+
The research papers and figures in the repository root document the memory-causation evidence behind the vault template. The MCP package stays focused on the installable server surface.
|
|
137
165
|
|
|
138
166
|
## Node compatibility status
|
|
139
167
|
|
|
140
|
-
The package is intentionally Bun-native today because the server uses Bun APIs
|
|
168
|
+
The package is intentionally Bun-native today because the server uses Bun APIs. The npm bin is Node-compatible only as a launcher: it locates `dist/server.js` or `src/server.ts`, then delegates execution to `bun`. This keeps package installation convenient while avoiding a misleading claim that the MCP server itself runs under plain Node.js.
|
|
141
169
|
|
|
142
170
|
## License
|
|
143
171
|
|
|
144
|
-
Apache-2.0 for package code. Research artifacts in the
|
|
172
|
+
Apache-2.0 for package code. Research artifacts in the repository root may use separate licenses; check the repository root license files.
|
|
145
173
|
|
|
146
174
|
## Releases
|
|
147
175
|
|
|
148
|
-
See [CHANGELOG.md](./CHANGELOG.md). Current npm release: `1.1.
|
|
176
|
+
See [CHANGELOG.md](./CHANGELOG.md). Current npm release: `1.1.5`.
|
package/dist/server.js
CHANGED
|
@@ -270,6 +270,9 @@ function clamp(value, min, max) {
|
|
|
270
270
|
function lower(value) {
|
|
271
271
|
return (value ?? "").toLowerCase();
|
|
272
272
|
}
|
|
273
|
+
function normalized(value) {
|
|
274
|
+
return lower(value).replace(/[^a-z0-9]+/g, " ").replace(/\s+/g, " ").trim();
|
|
275
|
+
}
|
|
273
276
|
function queryTerms(query) {
|
|
274
277
|
return lower(query).split(/\s+/).map((term) => term.trim()).filter(Boolean);
|
|
275
278
|
}
|
|
@@ -278,12 +281,17 @@ function includesQuery(value, query) {
|
|
|
278
281
|
if (terms.length === 0)
|
|
279
282
|
return false;
|
|
280
283
|
const haystack = lower(value);
|
|
281
|
-
|
|
284
|
+
const normalizedHaystack = normalized(value);
|
|
285
|
+
return terms.some((term) => haystack.includes(term)) || normalized(queryTerms(query).join(" ")).split(/\s+/).some((term) => normalizedHaystack.includes(term));
|
|
286
|
+
}
|
|
287
|
+
function equalsQuery(value, query) {
|
|
288
|
+
return Boolean(query?.trim()) && lower(value).trim() === lower(query).trim();
|
|
282
289
|
}
|
|
283
290
|
function matchedFields(entry, query) {
|
|
284
291
|
if (!query?.trim())
|
|
285
292
|
return [];
|
|
286
293
|
const candidates = [
|
|
294
|
+
["id_exact", equalsQuery(entry.id, query) ? entry.id : undefined],
|
|
287
295
|
["title", entry.title],
|
|
288
296
|
["content", entry.content],
|
|
289
297
|
["id", entry.id],
|
|
@@ -297,6 +305,8 @@ function whyMatched(entry, query, fields) {
|
|
|
297
305
|
if (fields.length === 0)
|
|
298
306
|
return "Result is included after filters; no direct field match was detected.";
|
|
299
307
|
const labels = fields.map((field) => {
|
|
308
|
+
if (field === "id_exact")
|
|
309
|
+
return "exact id";
|
|
300
310
|
if (field === "title")
|
|
301
311
|
return "title";
|
|
302
312
|
if (field === "content")
|
|
@@ -311,21 +321,21 @@ function snippetFromContent(content, query, maxChars = 240) {
|
|
|
311
321
|
const limit = Math.max(0, Math.floor(maxChars));
|
|
312
322
|
if (limit === 0)
|
|
313
323
|
return "";
|
|
314
|
-
const
|
|
315
|
-
if (
|
|
316
|
-
return
|
|
324
|
+
const normalized2 = content.replace(/\s+/g, " ").trim();
|
|
325
|
+
if (normalized2.length <= limit)
|
|
326
|
+
return normalized2;
|
|
317
327
|
const terms = queryTerms(query);
|
|
318
|
-
const lowerContent = lower(
|
|
328
|
+
const lowerContent = lower(normalized2);
|
|
319
329
|
const hitIndex = terms.map((term) => lowerContent.indexOf(term)).filter((index) => index >= 0).sort((a, b) => a - b)[0];
|
|
320
330
|
if (hitIndex === undefined)
|
|
321
|
-
return
|
|
331
|
+
return normalized2.slice(0, limit).trimEnd();
|
|
322
332
|
const halfWindow = Math.floor(limit / 2);
|
|
323
|
-
const start = Math.max(0, Math.min(hitIndex - halfWindow,
|
|
324
|
-
const end = Math.min(
|
|
333
|
+
const start = Math.max(0, Math.min(hitIndex - halfWindow, normalized2.length - limit));
|
|
334
|
+
const end = Math.min(normalized2.length, start + limit);
|
|
325
335
|
const prefix = start > 0 ? "..." : "";
|
|
326
|
-
const suffix = end <
|
|
336
|
+
const suffix = end < normalized2.length ? "..." : "";
|
|
327
337
|
const available = Math.max(0, limit - prefix.length - suffix.length);
|
|
328
|
-
return `${prefix}${
|
|
338
|
+
return `${prefix}${normalized2.slice(start, start + available).trim()}${suffix}`;
|
|
329
339
|
}
|
|
330
340
|
function staleVerdict(lastAnalyzedAt) {
|
|
331
341
|
if (!lastAnalyzedAt) {
|
|
@@ -341,14 +351,46 @@ function staleVerdict(lastAnalyzedAt) {
|
|
|
341
351
|
}
|
|
342
352
|
return { verdict: "PASS", reason: "Analysis is fresh enough for the read surface." };
|
|
343
353
|
}
|
|
354
|
+
function analysisVerdict(lastAnalyzedAt) {
|
|
355
|
+
if (!lastAnalyzedAt) {
|
|
356
|
+
return {
|
|
357
|
+
verdict: "NOT_ANALYZED",
|
|
358
|
+
freshness_verdict: "PASS",
|
|
359
|
+
reason: "No analysis timestamp was provided; content can still be readable."
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
const freshness = staleVerdict(lastAnalyzedAt);
|
|
363
|
+
if (freshness.verdict === "PASS") {
|
|
364
|
+
return {
|
|
365
|
+
verdict: "PASS",
|
|
366
|
+
freshness_verdict: "PASS",
|
|
367
|
+
reason: freshness.reason
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
if (freshness.reason.includes("could not be parsed")) {
|
|
371
|
+
return {
|
|
372
|
+
verdict: "INVALID",
|
|
373
|
+
freshness_verdict: "FLAG",
|
|
374
|
+
reason: freshness.reason
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
verdict: "STALE",
|
|
379
|
+
freshness_verdict: "FLAG",
|
|
380
|
+
reason: freshness.reason
|
|
381
|
+
};
|
|
382
|
+
}
|
|
344
383
|
function itemFreshness(entry) {
|
|
345
384
|
const lastAnalyzedAt = entry.score?.lastAnalyzedAt ?? null;
|
|
346
|
-
const
|
|
385
|
+
const analysis = analysisVerdict(lastAnalyzedAt);
|
|
347
386
|
return {
|
|
348
387
|
last_analyzed_at: lastAnalyzedAt,
|
|
349
388
|
source_mtime: entry.modified || null,
|
|
350
|
-
|
|
351
|
-
|
|
389
|
+
readability_verdict: "PASS",
|
|
390
|
+
index_verdict: "PASS",
|
|
391
|
+
analysis_verdict: analysis.verdict,
|
|
392
|
+
freshness_verdict: analysis.freshness_verdict,
|
|
393
|
+
freshness_reason: analysis.reason
|
|
352
394
|
};
|
|
353
395
|
}
|
|
354
396
|
function queueFreshness(queueItems) {
|
|
@@ -672,9 +714,10 @@ var vaultTools = [
|
|
|
672
714
|
...freshness
|
|
673
715
|
};
|
|
674
716
|
});
|
|
675
|
-
const
|
|
676
|
-
const
|
|
677
|
-
|
|
717
|
+
const hasAnalysisRisk = results.some((result) => ["STALE", "INVALID"].includes(result.analysis_verdict));
|
|
718
|
+
const hasNotAnalyzed = results.some((result) => result.analysis_verdict === "NOT_ANALYZED");
|
|
719
|
+
const envelope = okEnvelope({ query, category, results, total: results.length }, hasAnalysisRisk ? flagGuidance("Search completed, but one or more results have stale or invalid analysis metadata.", "Use source_ref for readonly follow-up and refresh stale analysis metadata in the operator lane if needed.", "vault_get") : passGuidance(hasNotAnalyzed ? "Search completed; one or more readable results have not been analyzed yet." : "Search completed with provenance and freshness metadata.", hasNotAnalyzed ? "Use source_ref or vault_get for bounded follow-up evidence; treat NOT_ANALYZED as an analysis caveat, not missing content." : "Use source_ref or vault_get for bounded follow-up evidence.", "vault_get"), {
|
|
720
|
+
freshness: hasAnalysisRisk ? "Some search results have stale or invalid analysis timestamps." : hasNotAnalyzed ? "Some readable search results do not have analysis timestamps yet." : "Search result analysis metadata is fresh.",
|
|
678
721
|
provenance: "vault_search result source_ref values are vault:// references without local paths."
|
|
679
722
|
});
|
|
680
723
|
return {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syndash/research-vault-mcp",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.5",
|
|
4
|
+
"description": "DASH Research Vault MCP server — local-first semantic search, note persistence, and public-safe knowledge-base tools for MCP-compatible agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"research-vault-mcp": "bin/research-vault-mcp.mjs"
|
package/src/evidence_metadata.ts
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import type { DecayScore, VaultEntry } from './types.ts'
|
|
2
2
|
|
|
3
3
|
export type FreshnessVerdict = 'PASS' | 'FLAG'
|
|
4
|
+
export type ReadabilityVerdict = 'PASS' | 'MISSING'
|
|
5
|
+
export type IndexVerdict = 'PASS' | 'NO_MATCH'
|
|
6
|
+
export type AnalysisVerdict = 'PASS' | 'STALE' | 'NOT_ANALYZED' | 'INVALID'
|
|
4
7
|
|
|
5
8
|
export interface FreshnessShape {
|
|
6
9
|
verdict: FreshnessVerdict
|
|
7
10
|
reason: string
|
|
8
11
|
}
|
|
9
12
|
|
|
13
|
+
export interface AnalysisShape {
|
|
14
|
+
verdict: AnalysisVerdict
|
|
15
|
+
freshness_verdict: FreshnessVerdict
|
|
16
|
+
reason: string
|
|
17
|
+
}
|
|
18
|
+
|
|
10
19
|
const STALE_AFTER_DAYS = 7
|
|
11
20
|
const DAY_MS = 24 * 60 * 60 * 1000
|
|
12
21
|
|
|
@@ -18,6 +27,13 @@ function lower(value: string | undefined | null): string {
|
|
|
18
27
|
return (value ?? '').toLowerCase()
|
|
19
28
|
}
|
|
20
29
|
|
|
30
|
+
function normalized(value: string | undefined | null): string {
|
|
31
|
+
return lower(value)
|
|
32
|
+
.replace(/[^a-z0-9]+/g, ' ')
|
|
33
|
+
.replace(/\s+/g, ' ')
|
|
34
|
+
.trim()
|
|
35
|
+
}
|
|
36
|
+
|
|
21
37
|
function queryTerms(query?: string): string[] {
|
|
22
38
|
return lower(query)
|
|
23
39
|
.split(/\s+/)
|
|
@@ -29,13 +45,20 @@ function includesQuery(value: string | undefined, query?: string): boolean {
|
|
|
29
45
|
const terms = queryTerms(query)
|
|
30
46
|
if (terms.length === 0) return false
|
|
31
47
|
const haystack = lower(value)
|
|
48
|
+
const normalizedHaystack = normalized(value)
|
|
32
49
|
return terms.some(term => haystack.includes(term))
|
|
50
|
+
|| normalized(queryTerms(query).join(' ')).split(/\s+/).some(term => normalizedHaystack.includes(term))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function equalsQuery(value: string | undefined, query?: string): boolean {
|
|
54
|
+
return Boolean(query?.trim()) && lower(value).trim() === lower(query).trim()
|
|
33
55
|
}
|
|
34
56
|
|
|
35
57
|
export function matchedFields(entry: VaultEntry & { content?: string }, query?: string): string[] {
|
|
36
58
|
if (!query?.trim()) return []
|
|
37
59
|
|
|
38
60
|
const candidates: Array<[string, string | undefined]> = [
|
|
61
|
+
['id_exact', equalsQuery(entry.id, query) ? entry.id : undefined],
|
|
39
62
|
['title', entry.title],
|
|
40
63
|
['content', entry.content],
|
|
41
64
|
['id', entry.id],
|
|
@@ -52,6 +75,7 @@ export function whyMatched(entry: VaultEntry & { content?: string }, query: stri
|
|
|
52
75
|
if (fields.length === 0) return 'Result is included after filters; no direct field match was detected.'
|
|
53
76
|
|
|
54
77
|
const labels = fields.map(field => {
|
|
78
|
+
if (field === 'id_exact') return 'exact id'
|
|
55
79
|
if (field === 'title') return 'title'
|
|
56
80
|
if (field === 'content') return 'note content'
|
|
57
81
|
if (field === 'category') return 'category'
|
|
@@ -104,15 +128,51 @@ export function staleVerdict(lastAnalyzedAt?: string | null): FreshnessShape {
|
|
|
104
128
|
return { verdict: 'PASS', reason: 'Analysis is fresh enough for the read surface.' }
|
|
105
129
|
}
|
|
106
130
|
|
|
131
|
+
export function analysisVerdict(lastAnalyzedAt?: string | null): AnalysisShape {
|
|
132
|
+
if (!lastAnalyzedAt) {
|
|
133
|
+
return {
|
|
134
|
+
verdict: 'NOT_ANALYZED',
|
|
135
|
+
freshness_verdict: 'PASS',
|
|
136
|
+
reason: 'No analysis timestamp was provided; content can still be readable.',
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const freshness = staleVerdict(lastAnalyzedAt)
|
|
141
|
+
if (freshness.verdict === 'PASS') {
|
|
142
|
+
return {
|
|
143
|
+
verdict: 'PASS',
|
|
144
|
+
freshness_verdict: 'PASS',
|
|
145
|
+
reason: freshness.reason,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (freshness.reason.includes('could not be parsed')) {
|
|
150
|
+
return {
|
|
151
|
+
verdict: 'INVALID',
|
|
152
|
+
freshness_verdict: 'FLAG',
|
|
153
|
+
reason: freshness.reason,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
verdict: 'STALE',
|
|
159
|
+
freshness_verdict: 'FLAG',
|
|
160
|
+
reason: freshness.reason,
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
107
164
|
export function itemFreshness(entry: VaultEntry & { score?: DecayScore & { lastAnalyzedAt?: string } }) {
|
|
108
165
|
const lastAnalyzedAt = entry.score?.lastAnalyzedAt ?? null
|
|
109
|
-
const
|
|
166
|
+
const analysis = analysisVerdict(lastAnalyzedAt)
|
|
110
167
|
|
|
111
168
|
return {
|
|
112
169
|
last_analyzed_at: lastAnalyzedAt,
|
|
113
170
|
source_mtime: entry.modified || null,
|
|
114
|
-
|
|
115
|
-
|
|
171
|
+
readability_verdict: 'PASS' as ReadabilityVerdict,
|
|
172
|
+
index_verdict: 'PASS' as IndexVerdict,
|
|
173
|
+
analysis_verdict: analysis.verdict,
|
|
174
|
+
freshness_verdict: analysis.freshness_verdict,
|
|
175
|
+
freshness_reason: analysis.reason,
|
|
116
176
|
}
|
|
117
177
|
}
|
|
118
178
|
|
package/src/vault.ts
CHANGED
|
@@ -330,22 +330,31 @@ const vaultTools = [
|
|
|
330
330
|
}
|
|
331
331
|
})
|
|
332
332
|
|
|
333
|
-
const
|
|
333
|
+
const hasAnalysisRisk = results.some(result => ['STALE', 'INVALID'].includes(result.analysis_verdict))
|
|
334
|
+
const hasNotAnalyzed = results.some(result => result.analysis_verdict === 'NOT_ANALYZED')
|
|
334
335
|
const envelope = okEnvelope(
|
|
335
336
|
{ query, category, results, total: results.length },
|
|
336
|
-
|
|
337
|
+
hasAnalysisRisk
|
|
337
338
|
? flagGuidance(
|
|
338
|
-
'Search completed, but one or more results
|
|
339
|
-
'Use source_ref for readonly follow-up and refresh analysis metadata in the operator lane if needed.',
|
|
339
|
+
'Search completed, but one or more results have stale or invalid analysis metadata.',
|
|
340
|
+
'Use source_ref for readonly follow-up and refresh stale analysis metadata in the operator lane if needed.',
|
|
340
341
|
'vault_get',
|
|
341
342
|
)
|
|
342
343
|
: passGuidance(
|
|
343
|
-
|
|
344
|
-
|
|
344
|
+
hasNotAnalyzed
|
|
345
|
+
? 'Search completed; one or more readable results have not been analyzed yet.'
|
|
346
|
+
: 'Search completed with provenance and freshness metadata.',
|
|
347
|
+
hasNotAnalyzed
|
|
348
|
+
? 'Use source_ref or vault_get for bounded follow-up evidence; treat NOT_ANALYZED as an analysis caveat, not missing content.'
|
|
349
|
+
: 'Use source_ref or vault_get for bounded follow-up evidence.',
|
|
345
350
|
'vault_get',
|
|
346
351
|
),
|
|
347
352
|
{
|
|
348
|
-
freshness:
|
|
353
|
+
freshness: hasAnalysisRisk
|
|
354
|
+
? 'Some search results have stale or invalid analysis timestamps.'
|
|
355
|
+
: hasNotAnalyzed
|
|
356
|
+
? 'Some readable search results do not have analysis timestamps yet.'
|
|
357
|
+
: 'Search result analysis metadata is fresh.',
|
|
349
358
|
provenance: 'vault_search result source_ref values are vault:// references without local paths.',
|
|
350
359
|
},
|
|
351
360
|
)
|