@gmickel/gno 2.3.2 → 2.4.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 +24 -4
- package/assets/skill/SKILL.md +30 -0
- package/assets/skill/cli-reference.md +12 -0
- package/assets/skill/examples.md +15 -0
- package/assets/skill/mcp-reference.md +16 -0
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.3.2.zip → gno-browser-clipper-v2.4.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.4.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +30 -0
- package/spec/db/schema.sql +2 -0
- package/spec/evals.md +48 -0
- package/spec/mcp.md +12 -0
- package/spec/output-schemas/ask.schema.json +12 -0
- package/spec/output-schemas/context-capsule-v1.schema.json +205 -32
- package/spec/output-schemas/context-capsule-verification.schema.json +32 -9
- package/spec/output-schemas/evidence-fixtures.schema.json +176 -0
- package/spec/output-schemas/evidence-report.schema.json +322 -0
- package/spec/output-schemas/evidence-run.schema.json +277 -0
- package/spec/output-schemas/metadata-predicate.schema.json +460 -0
- package/spec/output-schemas/retrieval-trace-filters.schema.json +13 -4
- package/spec/output-schemas/search-results.schema.json +12 -0
- package/spec/output-schemas/status.schema.json +37 -6
- package/src/app/context-runtime-contract.ts +33 -2
- package/src/app/context-runtime-input.ts +17 -0
- package/src/app/context-runtime-types.ts +2 -0
- package/src/app/context-runtime.ts +27 -3
- package/src/app/context-surface.ts +2 -0
- package/src/app/verified-ask.ts +1 -0
- package/src/cli/commands/ask.ts +1 -0
- package/src/cli/commands/context-build.ts +1 -0
- package/src/cli/commands/search.ts +1 -0
- package/src/cli/commands/status.ts +10 -0
- package/src/cli/options.ts +19 -0
- package/src/cli/program.ts +12 -0
- package/src/converters/types.ts +2 -0
- package/src/core/context-capsule-retrieval-schema.ts +8 -0
- package/src/core/context-capsule-schema.ts +47 -3
- package/src/core/context-capsule-validation.ts +14 -2
- package/src/core/context-capsule-verification.ts +1 -0
- package/src/core/context-capsule.ts +16 -2
- package/src/core/context-compiler.ts +3 -0
- package/src/core/record-metadata.ts +1 -1
- package/src/core/retrieval-replay-candidate.ts +5 -0
- package/src/core/retrieval-trace-filter-normalization.ts +4 -0
- package/src/core/retrieval-trace-request.ts +1 -0
- package/src/core/retrieval-trace-session.ts +5 -1
- package/src/core/retrieval-trace.ts +2 -0
- package/src/core/typed-metadata.ts +224 -0
- package/src/ingestion/record-adapter-canonical.ts +13 -2
- package/src/ingestion/record-container.ts +9 -0
- package/src/ingestion/sync.ts +11 -1
- package/src/ingestion/typed-metadata.ts +55 -0
- package/src/mcp/tools/ask.ts +10 -2
- package/src/mcp/tools/context.ts +1 -0
- package/src/mcp/tools/index.ts +12 -1
- package/src/mcp/tools/query.ts +13 -0
- package/src/mcp/tools/search.ts +9 -0
- package/src/mcp/tools/status.ts +4 -0
- package/src/mcp/tools/vsearch.ts +9 -0
- package/src/pipeline/diagnose.ts +6 -0
- package/src/pipeline/filters.ts +65 -0
- package/src/pipeline/graph-retrieval.ts +9 -1
- package/src/pipeline/hybrid.ts +22 -2
- package/src/pipeline/search.ts +11 -0
- package/src/pipeline/types.ts +7 -1
- package/src/pipeline/vsearch.ts +21 -1
- package/src/sdk/client.ts +29 -1
- package/src/sdk/index.ts +4 -0
- package/src/sdk/types.ts +7 -0
- package/src/serve/context-capsule.ts +1 -0
- package/src/serve/public/app.tsx +1 -1
- package/src/serve/public/components/MetadataFilter.tsx +186 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/lib/metadata-filter.ts +28 -0
- package/src/serve/public/lib/retrieval-filters.ts +3 -0
- package/src/serve/public/pages/Ask.tsx +48 -3
- package/src/serve/public/pages/DocView.tsx +33 -0
- package/src/serve/public/pages/Search.tsx +44 -1
- package/src/serve/routes/api.ts +50 -0
- package/src/store/migrations/030-typed-metadata.ts +13 -0
- package/src/store/migrations/index.ts +2 -0
- package/src/store/sqlite/adapter.ts +55 -4
- package/src/store/sqlite/eligibility.ts +12 -0
- package/src/store/sqlite/metadata-predicate.ts +71 -0
- package/src/store/types.ts +11 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.3.2.zip.sha256 +0 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ZodError } from "zod";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
normalizeMetadataPredicate,
|
|
5
|
+
type MetadataPredicate,
|
|
6
|
+
} from "../../../core/typed-metadata";
|
|
7
|
+
|
|
8
|
+
export function parseMetadataFilter(text: string): {
|
|
9
|
+
filter?: MetadataPredicate;
|
|
10
|
+
error?: string;
|
|
11
|
+
} {
|
|
12
|
+
if (!text.trim()) return {};
|
|
13
|
+
try {
|
|
14
|
+
return { filter: normalizeMetadataPredicate(JSON.parse(text)) };
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error instanceof ZodError) {
|
|
17
|
+
const issue = error.issues[0];
|
|
18
|
+
const path = issue?.path.length ? `.${issue.path.join(".")}` : "";
|
|
19
|
+
return {
|
|
20
|
+
error: `filter${path}: ${issue?.message ?? "Invalid predicate"}. Check the operator, key, and JSON value type.`,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
error:
|
|
25
|
+
"filter: Invalid JSON. Use quoted strings, finite numbers, booleans, or arrays of one type.",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -8,6 +8,7 @@ export interface QueryModeEntry {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export interface RetrievalFiltersState {
|
|
11
|
+
filter?: string;
|
|
11
12
|
collection: string;
|
|
12
13
|
intent: string;
|
|
13
14
|
candidateLimit: string;
|
|
@@ -123,6 +124,7 @@ export function parseFiltersFromSearch(
|
|
|
123
124
|
const tags = parseTagsCsv(tagsAll ?? tagsAny ?? "");
|
|
124
125
|
|
|
125
126
|
return {
|
|
127
|
+
filter: params.get("filter") ?? defaults.filter ?? "",
|
|
126
128
|
collection: params.get("collection") ?? defaults.collection ?? "",
|
|
127
129
|
intent: params.get("intent") ?? defaults.intent ?? "",
|
|
128
130
|
candidateLimit:
|
|
@@ -151,6 +153,7 @@ export function applyFiltersToUrl(
|
|
|
151
153
|
}
|
|
152
154
|
};
|
|
153
155
|
|
|
156
|
+
setOrDelete("filter", filters.filter ?? "");
|
|
154
157
|
setOrDelete("collection", filters.collection);
|
|
155
158
|
setOrDelete("intent", filters.intent);
|
|
156
159
|
setOrDelete("candidateLimit", filters.candidateLimit);
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
type AskVerification,
|
|
26
26
|
AskVerificationPanel,
|
|
27
27
|
} from "../components/AskVerificationPanel";
|
|
28
|
+
import { MetadataFilter } from "../components/MetadataFilter";
|
|
28
29
|
import {
|
|
29
30
|
ThoroughnessSelector,
|
|
30
31
|
type Thoroughness,
|
|
@@ -48,6 +49,7 @@ import {
|
|
|
48
49
|
import { Textarea } from "../components/ui/textarea";
|
|
49
50
|
import { apiFetch } from "../hooks/use-api";
|
|
50
51
|
import { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts";
|
|
52
|
+
import { parseMetadataFilter } from "../lib/metadata-filter";
|
|
51
53
|
import {
|
|
52
54
|
parseTagsCsv,
|
|
53
55
|
type QueryModeEntry,
|
|
@@ -92,6 +94,7 @@ interface AskResponse {
|
|
|
92
94
|
citations?: Citation[];
|
|
93
95
|
results: SearchResult[];
|
|
94
96
|
meta: {
|
|
97
|
+
warnings?: { code: string; message: string }[];
|
|
95
98
|
expanded: boolean;
|
|
96
99
|
reranked: boolean;
|
|
97
100
|
vectorsUsed: boolean;
|
|
@@ -200,14 +203,21 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
200
203
|
const [activePreset, setActivePreset] = useState("slim-tuned");
|
|
201
204
|
const [verify, setVerify] = useState(false);
|
|
202
205
|
|
|
203
|
-
const [showAdvanced, setShowAdvanced] = useState(
|
|
204
|
-
|
|
206
|
+
const [showAdvanced, setShowAdvanced] = useState(
|
|
207
|
+
Boolean(new URLSearchParams(window.location.search).get("filter"))
|
|
208
|
+
);
|
|
209
|
+
const [selectedCollection, setSelectedCollection] = useState(
|
|
210
|
+
new URLSearchParams(window.location.search).get("collection") ?? ""
|
|
211
|
+
);
|
|
205
212
|
const [intent, setIntent] = useState("");
|
|
206
213
|
const [candidateLimit, setCandidateLimit] = useState("");
|
|
207
214
|
const [exclude, setExclude] = useState("");
|
|
208
215
|
const [since, setSince] = useState("");
|
|
209
216
|
const [until, setUntil] = useState("");
|
|
210
217
|
const [category, setCategory] = useState("");
|
|
218
|
+
const [metadataFilter, setMetadataFilter] = useState(
|
|
219
|
+
new URLSearchParams(window.location.search).get("filter") ?? ""
|
|
220
|
+
);
|
|
211
221
|
const [author, setAuthor] = useState("");
|
|
212
222
|
const [tagMode, setTagMode] = useState<TagMode>("any");
|
|
213
223
|
const [tagsInput, setTagsInput] = useState("");
|
|
@@ -255,6 +265,16 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
255
265
|
void bootstrap();
|
|
256
266
|
}, []);
|
|
257
267
|
|
|
268
|
+
useEffect(() => {
|
|
269
|
+
const url = new URL(window.location.href);
|
|
270
|
+
if (selectedCollection.trim())
|
|
271
|
+
url.searchParams.set("collection", selectedCollection.trim());
|
|
272
|
+
else url.searchParams.delete("collection");
|
|
273
|
+
if (metadataFilter) url.searchParams.set("filter", metadataFilter);
|
|
274
|
+
else url.searchParams.delete("filter");
|
|
275
|
+
window.history.replaceState({}, "", url.toString());
|
|
276
|
+
}, [metadataFilter, selectedCollection]);
|
|
277
|
+
|
|
258
278
|
const cycleThoroughness = useCallback(() => {
|
|
259
279
|
setThoroughness((current) => {
|
|
260
280
|
if (!hybridAvailable) {
|
|
@@ -262,7 +282,7 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
262
282
|
}
|
|
263
283
|
const currentIdx = THOROUGHNESS_ORDER.indexOf(current);
|
|
264
284
|
const nextIdx = (currentIdx + 1) % THOROUGHNESS_ORDER.length;
|
|
265
|
-
return THOROUGHNESS_ORDER[nextIdx];
|
|
285
|
+
return THOROUGHNESS_ORDER[nextIdx] ?? current;
|
|
266
286
|
});
|
|
267
287
|
}, [hybridAvailable]);
|
|
268
288
|
|
|
@@ -304,12 +324,19 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
304
324
|
if (!query.trim()) {
|
|
305
325
|
return;
|
|
306
326
|
}
|
|
327
|
+
const parsedFilter = parseMetadataFilter(metadataFilter);
|
|
328
|
+
if (parsedFilter.error) {
|
|
329
|
+
setQueryModeError(parsedFilter.error);
|
|
330
|
+
setShowAdvanced(true);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
307
333
|
if (!structuredQueryState.ok) {
|
|
308
334
|
setQueryModeError(structuredQueryState.error.message);
|
|
309
335
|
setShowAdvanced(true);
|
|
310
336
|
return;
|
|
311
337
|
}
|
|
312
338
|
|
|
339
|
+
setQueryModeError(null);
|
|
313
340
|
const entryId = crypto.randomUUID();
|
|
314
341
|
const currentQuery = query.trim();
|
|
315
342
|
|
|
@@ -334,6 +361,7 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
334
361
|
verify,
|
|
335
362
|
};
|
|
336
363
|
|
|
364
|
+
if (parsedFilter.filter) requestBody.filter = parsedFilter.filter;
|
|
337
365
|
if (selectedCollection) {
|
|
338
366
|
requestBody.collection = selectedCollection;
|
|
339
367
|
}
|
|
@@ -393,6 +421,7 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
393
421
|
);
|
|
394
422
|
},
|
|
395
423
|
[
|
|
424
|
+
metadataFilter,
|
|
396
425
|
author,
|
|
397
426
|
candidateLimit,
|
|
398
427
|
activePreset,
|
|
@@ -428,6 +457,7 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
428
457
|
setUntil("");
|
|
429
458
|
setCategory("");
|
|
430
459
|
setAuthor("");
|
|
460
|
+
setMetadataFilter("");
|
|
431
461
|
setTagsInput("");
|
|
432
462
|
setTagMode("any");
|
|
433
463
|
setQueryModes([]);
|
|
@@ -454,6 +484,7 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
454
484
|
since ? `since:${since}` : null,
|
|
455
485
|
until ? `until:${until}` : null,
|
|
456
486
|
category.trim() ? `category:${category.trim()}` : null,
|
|
487
|
+
metadataFilter.trim() ? "custom metadata" : null,
|
|
457
488
|
author.trim() ? `author:${author.trim()}` : null,
|
|
458
489
|
parseTagsCsv(tagsInput).length > 0
|
|
459
490
|
? `${tagMode}:${parseTagsCsv(tagsInput).join(",")}`
|
|
@@ -687,6 +718,11 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
687
718
|
</div>
|
|
688
719
|
</div>
|
|
689
720
|
|
|
721
|
+
<MetadataFilter
|
|
722
|
+
onChange={setMetadataFilter}
|
|
723
|
+
value={metadataFilter}
|
|
724
|
+
/>
|
|
725
|
+
|
|
690
726
|
<div className="flex flex-wrap items-center gap-2">
|
|
691
727
|
<span className="text-muted-foreground text-xs">
|
|
692
728
|
Tag match:
|
|
@@ -857,6 +893,15 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
857
893
|
|
|
858
894
|
{entry.response && (
|
|
859
895
|
<>
|
|
896
|
+
{entry.response.meta.warnings?.map((warning) => (
|
|
897
|
+
<p
|
|
898
|
+
className="rounded border border-secondary/30 bg-secondary/10 p-3 text-sm"
|
|
899
|
+
key={warning.code}
|
|
900
|
+
role="status"
|
|
901
|
+
>
|
|
902
|
+
{warning.message}
|
|
903
|
+
</p>
|
|
904
|
+
))}
|
|
860
905
|
{entry.response.answer && (
|
|
861
906
|
<div className="prose prose-sm prose-invert max-w-none rounded-lg border border-border/30 bg-card/60 p-5 shadow-[0_0_30px_-10px_hsl(var(--primary)/0.08)]">
|
|
862
907
|
<p className="whitespace-pre-wrap leading-relaxed">
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
useState,
|
|
30
30
|
} from "react";
|
|
31
31
|
|
|
32
|
+
import type { TypedMetadata } from "../../../core/typed-metadata";
|
|
32
33
|
import type { PdfFallbackReason } from "../lib/pdf";
|
|
33
34
|
|
|
34
35
|
import {
|
|
@@ -167,6 +168,8 @@ interface PageProps {
|
|
|
167
168
|
}
|
|
168
169
|
|
|
169
170
|
interface DocData {
|
|
171
|
+
typedMetadata?: TypedMetadata | null;
|
|
172
|
+
metadataError?: string | null;
|
|
170
173
|
docid: string;
|
|
171
174
|
uri: string;
|
|
172
175
|
title: string | null;
|
|
@@ -1326,12 +1329,41 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
1326
1329
|
</nav>
|
|
1327
1330
|
);
|
|
1328
1331
|
|
|
1332
|
+
const storedMetadata =
|
|
1333
|
+
doc?.metadataError ||
|
|
1334
|
+
(doc?.typedMetadata && Object.keys(doc.typedMetadata).length > 0) ? (
|
|
1335
|
+
<section
|
|
1336
|
+
aria-label="Indexed custom metadata"
|
|
1337
|
+
className="space-y-2 border-border/20 border-t px-3 py-3"
|
|
1338
|
+
>
|
|
1339
|
+
<h3 className="font-mono text-muted-foreground text-xs">
|
|
1340
|
+
Indexed custom metadata
|
|
1341
|
+
</h3>
|
|
1342
|
+
{doc.metadataError ? (
|
|
1343
|
+
<p className="break-words text-destructive text-xs" role="alert">
|
|
1344
|
+
{doc.metadataError}. Correct the source and sync again; this
|
|
1345
|
+
document is excluded from custom metadata filters.
|
|
1346
|
+
</p>
|
|
1347
|
+
) : (
|
|
1348
|
+
<dl className="space-y-1 text-xs">
|
|
1349
|
+
{Object.entries(doc.typedMetadata ?? {}).map(([key, value]) => (
|
|
1350
|
+
<div key={key}>
|
|
1351
|
+
<dt className="break-all text-muted-foreground">{key}</dt>
|
|
1352
|
+
<dd className="break-all font-mono">{JSON.stringify(value)}</dd>
|
|
1353
|
+
</div>
|
|
1354
|
+
))}
|
|
1355
|
+
</dl>
|
|
1356
|
+
)}
|
|
1357
|
+
</section>
|
|
1358
|
+
) : null;
|
|
1359
|
+
|
|
1329
1360
|
/** Left rail — metadata + outline */
|
|
1330
1361
|
const renderDocumentFactsRail = () => (
|
|
1331
1362
|
<nav
|
|
1332
1363
|
aria-label="Document facts"
|
|
1333
1364
|
className="w-full min-w-0 max-w-full space-y-0 overflow-x-hidden"
|
|
1334
1365
|
>
|
|
1366
|
+
{storedMetadata}
|
|
1335
1367
|
{/* Frontmatter + tags */}
|
|
1336
1368
|
{(hasFrontmatter || showStandaloneTags) && (
|
|
1337
1369
|
<>
|
|
@@ -1623,6 +1655,7 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
1623
1655
|
</div>
|
|
1624
1656
|
</div>
|
|
1625
1657
|
|
|
1658
|
+
{storedMetadata}
|
|
1626
1659
|
{(hasFrontmatter || showStandaloneTags) && (
|
|
1627
1660
|
<div className="rounded-lg border border-border/40 bg-muted/10 p-2.5">
|
|
1628
1661
|
{hasFrontmatter && (
|
|
@@ -7,11 +7,12 @@ import {
|
|
|
7
7
|
SlidersHorizontal,
|
|
8
8
|
XIcon,
|
|
9
9
|
} from "lucide-react";
|
|
10
|
-
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
10
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
11
11
|
|
|
12
12
|
import { resolveDepthPolicy } from "../../../core/depth-policy";
|
|
13
13
|
import { normalizeStructuredQueryInput } from "../../../core/structured-query";
|
|
14
14
|
import { Loader } from "../components/ai-elements/loader";
|
|
15
|
+
import { MetadataFilter } from "../components/MetadataFilter";
|
|
15
16
|
import {
|
|
16
17
|
ThoroughnessSelector,
|
|
17
18
|
type Thoroughness,
|
|
@@ -37,6 +38,7 @@ import { apiFetch } from "../hooks/use-api";
|
|
|
37
38
|
import { useDocEvents } from "../hooks/use-doc-events";
|
|
38
39
|
import { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts";
|
|
39
40
|
import { buildDocDeepLink } from "../lib/deep-links";
|
|
41
|
+
import { parseMetadataFilter } from "../lib/metadata-filter";
|
|
40
42
|
import {
|
|
41
43
|
applyFiltersToUrl,
|
|
42
44
|
parseFiltersFromSearch,
|
|
@@ -111,6 +113,7 @@ interface SearchResult {
|
|
|
111
113
|
interface SearchResponse {
|
|
112
114
|
results: SearchResult[];
|
|
113
115
|
meta: {
|
|
116
|
+
warnings?: { code: string; message: string }[];
|
|
114
117
|
query: string;
|
|
115
118
|
mode: string;
|
|
116
119
|
totalResults: number;
|
|
@@ -152,6 +155,7 @@ export default function Search({ navigate }: PageProps) {
|
|
|
152
155
|
const [results, setResults] = useState<SearchResult[]>([]);
|
|
153
156
|
const [meta, setMeta] = useState<SearchResponse["meta"] | null>(null);
|
|
154
157
|
const [loading, setLoading] = useState(false);
|
|
158
|
+
const searchRequest = useRef(0);
|
|
155
159
|
const [error, setError] = useState<string | null>(null);
|
|
156
160
|
const [searched, setSearched] = useState(false);
|
|
157
161
|
const [capabilities, setCapabilities] = useState<ServerCapabilities | null>(
|
|
@@ -170,6 +174,7 @@ export default function Search({ navigate }: PageProps) {
|
|
|
170
174
|
initialFilters.until ||
|
|
171
175
|
initialFilters.category ||
|
|
172
176
|
initialFilters.author ||
|
|
177
|
+
initialFilters.filter ||
|
|
173
178
|
initialFilters.queryModes.length > 0
|
|
174
179
|
)
|
|
175
180
|
);
|
|
@@ -187,6 +192,9 @@ export default function Search({ navigate }: PageProps) {
|
|
|
187
192
|
const [since, setSince] = useState(initialFilters.since);
|
|
188
193
|
const [until, setUntil] = useState(initialFilters.until);
|
|
189
194
|
const [category, setCategory] = useState(initialFilters.category);
|
|
195
|
+
const [metadataFilter, setMetadataFilter] = useState(
|
|
196
|
+
initialFilters.filter ?? ""
|
|
197
|
+
);
|
|
190
198
|
const [author, setAuthor] = useState(initialFilters.author);
|
|
191
199
|
const [queryModes, setQueryModes] = useState<QueryModeEntry[]>(
|
|
192
200
|
initialFilters.queryModes
|
|
@@ -217,6 +225,7 @@ export default function Search({ navigate }: PageProps) {
|
|
|
217
225
|
useEffect(() => {
|
|
218
226
|
const url = new URL(window.location.href);
|
|
219
227
|
applyFiltersToUrl(url, {
|
|
228
|
+
filter: metadataFilter,
|
|
220
229
|
collection: selectedCollection,
|
|
221
230
|
intent,
|
|
222
231
|
candidateLimit,
|
|
@@ -232,6 +241,7 @@ export default function Search({ navigate }: PageProps) {
|
|
|
232
241
|
window.history.replaceState({}, "", url.toString());
|
|
233
242
|
}, [
|
|
234
243
|
activeTags,
|
|
244
|
+
metadataFilter,
|
|
235
245
|
author,
|
|
236
246
|
candidateLimit,
|
|
237
247
|
category,
|
|
@@ -319,10 +329,23 @@ export default function Search({ navigate }: PageProps) {
|
|
|
319
329
|
const handleSearch = useCallback(
|
|
320
330
|
async (e?: React.FormEvent) => {
|
|
321
331
|
e?.preventDefault();
|
|
332
|
+
const request = ++searchRequest.current;
|
|
322
333
|
if (!query.trim()) {
|
|
323
334
|
return;
|
|
324
335
|
}
|
|
336
|
+
const parsedFilter = parseMetadataFilter(metadataFilter);
|
|
337
|
+
if (parsedFilter.error) {
|
|
338
|
+
setLoading(false);
|
|
339
|
+
setResults([]);
|
|
340
|
+
setMeta(null);
|
|
341
|
+
setError(parsedFilter.error);
|
|
342
|
+
setShowAdvanced(true);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
325
345
|
if (!structuredQueryState.ok) {
|
|
346
|
+
setLoading(false);
|
|
347
|
+
setResults([]);
|
|
348
|
+
setMeta(null);
|
|
326
349
|
setError(structuredQueryState.error.message);
|
|
327
350
|
return;
|
|
328
351
|
}
|
|
@@ -351,6 +374,7 @@ export default function Search({ navigate }: PageProps) {
|
|
|
351
374
|
limit: 20,
|
|
352
375
|
};
|
|
353
376
|
|
|
377
|
+
if (parsedFilter.filter) body.filter = parsedFilter.filter;
|
|
354
378
|
if (selectedCollection) {
|
|
355
379
|
body.collection = selectedCollection;
|
|
356
380
|
}
|
|
@@ -400,6 +424,7 @@ export default function Search({ navigate }: PageProps) {
|
|
|
400
424
|
}
|
|
401
425
|
);
|
|
402
426
|
|
|
427
|
+
if (request !== searchRequest.current) return;
|
|
403
428
|
setLoading(false);
|
|
404
429
|
if (fetchError) {
|
|
405
430
|
setError(fetchError);
|
|
@@ -412,6 +437,7 @@ export default function Search({ navigate }: PageProps) {
|
|
|
412
437
|
},
|
|
413
438
|
[
|
|
414
439
|
activeTags,
|
|
440
|
+
metadataFilter,
|
|
415
441
|
author,
|
|
416
442
|
candidateLimit,
|
|
417
443
|
activePreset,
|
|
@@ -438,6 +464,7 @@ export default function Search({ navigate }: PageProps) {
|
|
|
438
464
|
}, [
|
|
439
465
|
activeTags,
|
|
440
466
|
activePreset,
|
|
467
|
+
metadataFilter,
|
|
441
468
|
author,
|
|
442
469
|
candidateLimit,
|
|
443
470
|
category,
|
|
@@ -477,6 +504,7 @@ export default function Search({ navigate }: PageProps) {
|
|
|
477
504
|
since ? `since:${since}` : null,
|
|
478
505
|
until ? `until:${until}` : null,
|
|
479
506
|
category.trim() ? `category:${category.trim()}` : null,
|
|
507
|
+
metadataFilter.trim() ? "custom metadata" : null,
|
|
480
508
|
author.trim() ? `author:${author.trim()}` : null,
|
|
481
509
|
queryModes.length > 0 ? `${queryModes.length} query mode(s)` : null,
|
|
482
510
|
].filter((pill): pill is string => Boolean(pill));
|
|
@@ -493,6 +521,7 @@ export default function Search({ navigate }: PageProps) {
|
|
|
493
521
|
setUntil("");
|
|
494
522
|
setCategory("");
|
|
495
523
|
setAuthor("");
|
|
524
|
+
setMetadataFilter("");
|
|
496
525
|
setQueryModes([]);
|
|
497
526
|
setQueryModeText("");
|
|
498
527
|
setQueryModeError(null);
|
|
@@ -773,6 +802,11 @@ export default function Search({ navigate }: PageProps) {
|
|
|
773
802
|
</div>
|
|
774
803
|
</div>
|
|
775
804
|
|
|
805
|
+
<MetadataFilter
|
|
806
|
+
onChange={setMetadataFilter}
|
|
807
|
+
value={metadataFilter}
|
|
808
|
+
/>
|
|
809
|
+
|
|
776
810
|
<div className="flex flex-wrap items-center gap-2">
|
|
777
811
|
<span className="text-muted-foreground text-xs">
|
|
778
812
|
Tag match:
|
|
@@ -954,6 +988,15 @@ export default function Search({ navigate }: PageProps) {
|
|
|
954
988
|
</div>
|
|
955
989
|
)}
|
|
956
990
|
|
|
991
|
+
{meta?.warnings?.map((warning) => (
|
|
992
|
+
<p
|
|
993
|
+
className="mb-3 rounded border border-secondary/30 bg-secondary/10 p-3 text-sm"
|
|
994
|
+
key={warning.code}
|
|
995
|
+
role="status"
|
|
996
|
+
>
|
|
997
|
+
{warning.message}
|
|
998
|
+
</p>
|
|
999
|
+
))}
|
|
957
1000
|
{error && (
|
|
958
1001
|
<Card className="mb-6 border-destructive bg-destructive/10">
|
|
959
1002
|
<CardContent className="py-4 text-destructive">
|
package/src/serve/routes/api.ts
CHANGED
|
@@ -124,6 +124,11 @@ import {
|
|
|
124
124
|
parseAndValidateTagFilter,
|
|
125
125
|
validateTag,
|
|
126
126
|
} from "../../core/tags";
|
|
127
|
+
import {
|
|
128
|
+
metadataPredicateSchema,
|
|
129
|
+
normalizeMetadataPredicate,
|
|
130
|
+
type MetadataPredicate,
|
|
131
|
+
} from "../../core/typed-metadata";
|
|
127
132
|
import { validateRelPath } from "../../core/validation";
|
|
128
133
|
import { writeLeasePath } from "../../core/write-lease";
|
|
129
134
|
import {
|
|
@@ -287,6 +292,7 @@ export interface SearchRequestBody {
|
|
|
287
292
|
/** Comma-separated category filters */
|
|
288
293
|
category?: string;
|
|
289
294
|
author?: string;
|
|
295
|
+
filter?: MetadataPredicate;
|
|
290
296
|
/** Comma-separated tags - filter to docs having ALL (AND) */
|
|
291
297
|
tagsAll?: string;
|
|
292
298
|
/** Comma-separated tags - filter to docs having ANY (OR) */
|
|
@@ -308,6 +314,7 @@ export interface QueryRequestBody {
|
|
|
308
314
|
/** Comma-separated category filters */
|
|
309
315
|
category?: string;
|
|
310
316
|
author?: string;
|
|
317
|
+
filter?: MetadataPredicate;
|
|
311
318
|
queryModes?: QueryModeInput[];
|
|
312
319
|
noExpand?: boolean;
|
|
313
320
|
noRerank?: boolean;
|
|
@@ -339,6 +346,7 @@ export interface AskRequestBody {
|
|
|
339
346
|
/** Comma-separated category filters */
|
|
340
347
|
category?: string;
|
|
341
348
|
author?: string;
|
|
349
|
+
filter?: MetadataPredicate;
|
|
342
350
|
maxAnswerTokens?: number;
|
|
343
351
|
verify?: boolean;
|
|
344
352
|
contextBudgetTokens?: number;
|
|
@@ -369,6 +377,7 @@ const ASK_REQUEST_KEYS = new Set<keyof AskRequestBody>([
|
|
|
369
377
|
"until",
|
|
370
378
|
"category",
|
|
371
379
|
"author",
|
|
380
|
+
"filter",
|
|
372
381
|
"maxAnswerTokens",
|
|
373
382
|
"verify",
|
|
374
383
|
"contextBudgetTokens",
|
|
@@ -586,6 +595,29 @@ function errorResponse(
|
|
|
586
595
|
);
|
|
587
596
|
}
|
|
588
597
|
|
|
598
|
+
function parseRestMetadataFilter(
|
|
599
|
+
value: unknown
|
|
600
|
+
):
|
|
601
|
+
| { ok: true; filter: MetadataPredicate | undefined }
|
|
602
|
+
| { ok: false; response: Response } {
|
|
603
|
+
if (value === undefined) return { ok: true, filter: undefined };
|
|
604
|
+
const parsed = metadataPredicateSchema.safeParse(value);
|
|
605
|
+
if (!parsed.success) {
|
|
606
|
+
const issue = parsed.error.issues[0];
|
|
607
|
+
const path = ["filter", ...(issue?.path ?? [])].join(".");
|
|
608
|
+
return {
|
|
609
|
+
ok: false,
|
|
610
|
+
response: errorResponse(
|
|
611
|
+
"VALIDATION",
|
|
612
|
+
`${path}: ${issue?.message ?? "Invalid predicate"}`,
|
|
613
|
+
400,
|
|
614
|
+
{ field: path }
|
|
615
|
+
),
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
return { ok: true, filter: normalizeMetadataPredicate(parsed.data) };
|
|
619
|
+
}
|
|
620
|
+
|
|
589
621
|
function fileRefactorHttpErrorResponse(error: FileRefactorHttpError): Response {
|
|
590
622
|
return errorResponse(error.code, error.message, error.status, error.details);
|
|
591
623
|
}
|
|
@@ -2090,6 +2122,8 @@ export async function handleDoc(
|
|
|
2090
2122
|
title: doc.title,
|
|
2091
2123
|
content,
|
|
2092
2124
|
contentAvailable,
|
|
2125
|
+
typedMetadata: doc.typedMetadata ?? null,
|
|
2126
|
+
metadataError: doc.metadataError ?? null,
|
|
2093
2127
|
collection: doc.collection,
|
|
2094
2128
|
relPath,
|
|
2095
2129
|
tags,
|
|
@@ -4194,6 +4228,9 @@ export async function handleSearch(
|
|
|
4194
4228
|
? parseCommaSeparatedValues(body.exclude)
|
|
4195
4229
|
: undefined;
|
|
4196
4230
|
const author = body.author?.trim() || undefined;
|
|
4231
|
+
const parsedFilter = parseRestMetadataFilter(body.filter);
|
|
4232
|
+
if (!parsedFilter.ok) return parsedFilter.response;
|
|
4233
|
+
const filter = parsedFilter.filter;
|
|
4197
4234
|
let projectAffinity: ProjectAffinityScoringInput | undefined;
|
|
4198
4235
|
try {
|
|
4199
4236
|
projectAffinity = context
|
|
@@ -4221,6 +4258,7 @@ export async function handleSearch(
|
|
|
4221
4258
|
until: body.until,
|
|
4222
4259
|
categories,
|
|
4223
4260
|
author,
|
|
4261
|
+
filter,
|
|
4224
4262
|
projectAffinity,
|
|
4225
4263
|
contentTypeRules: context
|
|
4226
4264
|
? normalizeContentTypes(context.config.contentTypes ?? []).rules
|
|
@@ -4388,6 +4426,9 @@ export async function handleQuery(
|
|
|
4388
4426
|
? parseCommaSeparatedValues(body.exclude)
|
|
4389
4427
|
: undefined;
|
|
4390
4428
|
const author = body.author?.trim() || undefined;
|
|
4429
|
+
const parsedFilter = parseRestMetadataFilter(body.filter);
|
|
4430
|
+
if (!parsedFilter.ok) return parsedFilter.response;
|
|
4431
|
+
const filter = parsedFilter.filter;
|
|
4391
4432
|
let projectAffinity: ProjectAffinityScoringInput | undefined;
|
|
4392
4433
|
try {
|
|
4393
4434
|
projectAffinity = await resolveRemoteProjectAffinity(
|
|
@@ -4425,6 +4466,7 @@ export async function handleQuery(
|
|
|
4425
4466
|
until: body.until,
|
|
4426
4467
|
categories,
|
|
4427
4468
|
author,
|
|
4469
|
+
filter,
|
|
4428
4470
|
projectAffinity,
|
|
4429
4471
|
explain: body.explain,
|
|
4430
4472
|
};
|
|
@@ -4603,6 +4645,9 @@ export async function handleQueryDiagnose(
|
|
|
4603
4645
|
? parseCommaSeparatedValues(body.exclude)
|
|
4604
4646
|
: undefined;
|
|
4605
4647
|
const author = body.author?.trim() || undefined;
|
|
4648
|
+
const parsedFilter = parseRestMetadataFilter(body.filter);
|
|
4649
|
+
if (!parsedFilter.ok) return parsedFilter.response;
|
|
4650
|
+
const filter = parsedFilter.filter;
|
|
4606
4651
|
const contentTypeRules = normalizeContentTypes(
|
|
4607
4652
|
ctx.config.contentTypes ?? []
|
|
4608
4653
|
).rules;
|
|
@@ -4654,6 +4699,7 @@ export async function handleQueryDiagnose(
|
|
|
4654
4699
|
until: body.until,
|
|
4655
4700
|
categories,
|
|
4656
4701
|
author,
|
|
4702
|
+
filter,
|
|
4657
4703
|
projectAffinity,
|
|
4658
4704
|
contentTypeRules,
|
|
4659
4705
|
contentTypeRulesFingerprint:
|
|
@@ -4847,6 +4893,9 @@ export async function handleAsk(
|
|
|
4847
4893
|
? parseCommaSeparatedValues(body.exclude)
|
|
4848
4894
|
: undefined;
|
|
4849
4895
|
const author = body.author?.trim() || undefined;
|
|
4896
|
+
const parsedFilter = parseRestMetadataFilter(body.filter);
|
|
4897
|
+
if (!parsedFilter.ok) return parsedFilter.response;
|
|
4898
|
+
const filter = parsedFilter.filter;
|
|
4850
4899
|
let projectAffinity: ProjectAffinityScoringInput | undefined;
|
|
4851
4900
|
try {
|
|
4852
4901
|
projectAffinity = await resolveRemoteProjectAffinity(
|
|
@@ -4885,6 +4934,7 @@ export async function handleAsk(
|
|
|
4885
4934
|
until: body.until,
|
|
4886
4935
|
categories,
|
|
4887
4936
|
author,
|
|
4937
|
+
filter,
|
|
4888
4938
|
verify: body.verify,
|
|
4889
4939
|
contextBudgetTokens: body.contextBudgetTokens,
|
|
4890
4940
|
contextBudgetBytes: body.contextBudgetBytes,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Migration } from "./runner";
|
|
2
|
+
|
|
3
|
+
export const migration: Migration = {
|
|
4
|
+
version: 30,
|
|
5
|
+
name: "typed_metadata",
|
|
6
|
+
up(db): void {
|
|
7
|
+
db.exec("ALTER TABLE documents ADD COLUMN typed_metadata TEXT");
|
|
8
|
+
db.exec("ALTER TABLE documents ADD COLUMN metadata_error TEXT");
|
|
9
|
+
},
|
|
10
|
+
down(): void {
|
|
11
|
+
// Derived columns are retained; SQLite rollback must not rewrite source rows.
|
|
12
|
+
},
|
|
13
|
+
};
|
|
@@ -43,6 +43,7 @@ import { migration as m026 } from "./026-file-refactor-recovery-journal";
|
|
|
43
43
|
import { migration as m027 } from "./027-memory-scopes";
|
|
44
44
|
import { migration as m028 } from "./028-vector-variants";
|
|
45
45
|
import { migration as m029 } from "./029-graph-reference-state";
|
|
46
|
+
import { migration as m030 } from "./030-typed-metadata";
|
|
46
47
|
|
|
47
48
|
/** All migrations in order */
|
|
48
49
|
export const migrations = [
|
|
@@ -75,4 +76,5 @@ export const migrations = [
|
|
|
75
76
|
m027,
|
|
76
77
|
m028,
|
|
77
78
|
m029,
|
|
79
|
+
m030,
|
|
78
80
|
];
|