@gmickel/gno 2.3.3 → 2.5.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.
Files changed (117) hide show
  1. package/README.md +22 -3
  2. package/assets/skill/SKILL.md +53 -0
  3. package/assets/skill/cli-reference.md +29 -0
  4. package/assets/skill/examples.md +30 -0
  5. package/assets/skill/mcp-reference.md +26 -0
  6. package/assets/spa-production.json.gz +0 -0
  7. package/browser-extension/artifacts/{gno-browser-clipper-v2.3.3.zip → gno-browser-clipper-v2.5.1.zip} +0 -0
  8. package/browser-extension/artifacts/gno-browser-clipper-v2.5.1.zip.sha256 +1 -0
  9. package/browser-extension/dist/manifest.json +1 -1
  10. package/package.json +1 -1
  11. package/spec/cli.md +41 -0
  12. package/spec/compiled-context.md +68 -0
  13. package/spec/db/schema.sql +2 -0
  14. package/spec/evals.md +48 -0
  15. package/spec/mcp.md +37 -0
  16. package/spec/output-schemas/ask.schema.json +12 -0
  17. package/spec/output-schemas/compiled-context-check.schema.json +44 -0
  18. package/spec/output-schemas/compiled-context-file.schema.json +165 -0
  19. package/spec/output-schemas/compiled-context-preview.schema.json +142 -0
  20. package/spec/output-schemas/context-capsule-v1.schema.json +205 -32
  21. package/spec/output-schemas/context-capsule-verification.schema.json +32 -9
  22. package/spec/output-schemas/evidence-fixtures.schema.json +176 -0
  23. package/spec/output-schemas/evidence-report.schema.json +322 -0
  24. package/spec/output-schemas/evidence-run.schema.json +277 -0
  25. package/spec/output-schemas/metadata-predicate.schema.json +460 -0
  26. package/spec/output-schemas/retrieval-trace-filters.schema.json +13 -4
  27. package/spec/output-schemas/search-results.schema.json +12 -0
  28. package/spec/output-schemas/status.schema.json +37 -6
  29. package/src/app/compiled-context-files.ts +361 -0
  30. package/src/app/compiled-context.ts +240 -0
  31. package/src/app/context-runtime-contract.ts +33 -2
  32. package/src/app/context-runtime-input.ts +17 -0
  33. package/src/app/context-runtime-types.ts +2 -0
  34. package/src/app/context-runtime.ts +27 -3
  35. package/src/app/context-surface.ts +7 -0
  36. package/src/app/verified-ask.ts +1 -0
  37. package/src/cli/commands/ask.ts +1 -0
  38. package/src/cli/commands/audit.ts +4 -0
  39. package/src/cli/commands/context-build.ts +1 -0
  40. package/src/cli/commands/context-compiled.ts +136 -0
  41. package/src/cli/commands/search.ts +1 -0
  42. package/src/cli/commands/status.ts +10 -0
  43. package/src/cli/errors.ts +10 -3
  44. package/src/cli/options.ts +19 -0
  45. package/src/cli/program.ts +82 -0
  46. package/src/converters/types.ts +2 -0
  47. package/src/core/compiled-context.ts +254 -0
  48. package/src/core/context-budget.ts +2 -10
  49. package/src/core/context-capsule-retrieval-schema.ts +8 -0
  50. package/src/core/context-capsule-schema.ts +47 -3
  51. package/src/core/context-capsule-validation.ts +14 -2
  52. package/src/core/context-capsule-verification.ts +1 -0
  53. package/src/core/context-capsule.ts +16 -2
  54. package/src/core/context-compiler.ts +3 -0
  55. package/src/core/file-lock.ts +22 -5
  56. package/src/core/folder-setup-planning.ts +2 -1
  57. package/src/core/network-boundary-inventory.ts +8 -0
  58. package/src/core/record-metadata.ts +1 -1
  59. package/src/core/retrieval-replay-candidate.ts +5 -0
  60. package/src/core/retrieval-trace-filter-normalization.ts +4 -0
  61. package/src/core/retrieval-trace-request.ts +1 -0
  62. package/src/core/retrieval-trace-session.ts +5 -1
  63. package/src/core/retrieval-trace.ts +2 -0
  64. package/src/core/setup-receipt.ts +27 -20
  65. package/src/core/typed-metadata.ts +228 -0
  66. package/src/core/validation.ts +9 -2
  67. package/src/core/windows-private-path.ts +96 -0
  68. package/src/index.ts +2 -2
  69. package/src/ingestion/compiled-context.ts +15 -0
  70. package/src/ingestion/record-adapter-canonical.ts +13 -2
  71. package/src/ingestion/record-container.ts +9 -0
  72. package/src/ingestion/sync.ts +51 -9
  73. package/src/ingestion/typed-metadata.ts +55 -0
  74. package/src/ingestion/walker.ts +5 -4
  75. package/src/llm/nodeLlamaCpp/simulator-install.ts +6 -2
  76. package/src/mcp/http-egress.ts +11 -3
  77. package/src/mcp/retrieval-warnings.ts +24 -0
  78. package/src/mcp/tools/ask.ts +23 -2
  79. package/src/mcp/tools/context.ts +47 -2
  80. package/src/mcp/tools/index.ts +58 -1
  81. package/src/mcp/tools/query.ts +16 -1
  82. package/src/mcp/tools/search.ts +12 -1
  83. package/src/mcp/tools/status.ts +4 -0
  84. package/src/mcp/tools/vsearch.ts +12 -1
  85. package/src/pipeline/diagnose.ts +6 -0
  86. package/src/pipeline/filters.ts +65 -0
  87. package/src/pipeline/graph-retrieval.ts +9 -1
  88. package/src/pipeline/hybrid.ts +22 -2
  89. package/src/pipeline/search.ts +11 -0
  90. package/src/pipeline/types.ts +7 -1
  91. package/src/pipeline/vsearch.ts +21 -1
  92. package/src/sdk/client.ts +98 -3
  93. package/src/sdk/index.ts +11 -0
  94. package/src/sdk/types.ts +35 -1
  95. package/src/serve/compiled-context.ts +84 -0
  96. package/src/serve/context-capsule.ts +1 -0
  97. package/src/serve/public/app.tsx +12 -1
  98. package/src/serve/public/components/MetadataFilter.tsx +186 -0
  99. package/src/serve/public/globals.built.css +1 -1
  100. package/src/serve/public/lib/metadata-filter.ts +28 -0
  101. package/src/serve/public/lib/retrieval-filters.ts +3 -0
  102. package/src/serve/public/lib/workspace-tabs.ts +2 -0
  103. package/src/serve/public/pages/Ask.tsx +48 -3
  104. package/src/serve/public/pages/CompiledContext.tsx +364 -0
  105. package/src/serve/public/pages/Dashboard.tsx +7 -0
  106. package/src/serve/public/pages/DocView.tsx +33 -0
  107. package/src/serve/public/pages/Search.tsx +44 -1
  108. package/src/serve/routes/api.ts +50 -0
  109. package/src/serve/server.ts +43 -2
  110. package/src/serve/spa-production-build.ts +6 -5
  111. package/src/store/migrations/030-typed-metadata.ts +13 -0
  112. package/src/store/migrations/index.ts +2 -0
  113. package/src/store/sqlite/adapter.ts +69 -5
  114. package/src/store/sqlite/eligibility.ts +12 -0
  115. package/src/store/sqlite/metadata-predicate.ts +71 -0
  116. package/src/store/types.ts +11 -0
  117. package/browser-extension/artifacts/gno-browser-clipper-v2.3.3.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);
@@ -65,6 +65,8 @@ function getLocationLabel(location: string): string {
65
65
  return "Connectors";
66
66
  case "/graph":
67
67
  return "Graph";
68
+ case "/context/compiled":
69
+ return "Compiled context";
68
70
  case "/traces":
69
71
  return "Trace history";
70
72
  case "/doc":
@@ -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(false);
204
- const [selectedCollection, setSelectedCollection] = useState("");
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">
@@ -0,0 +1,364 @@
1
+ import { ArrowLeftIcon, DownloadIcon } from "lucide-react";
2
+ import { useState } from "react";
3
+
4
+ import type {
5
+ CompiledContextPreview as Preview,
6
+ CompiledContextCheck as Check,
7
+ } from "../../../core/compiled-context";
8
+
9
+ import { Button } from "../components/ui/button";
10
+ import {
11
+ Card,
12
+ CardContent,
13
+ CardHeader,
14
+ CardTitle,
15
+ } from "../components/ui/card";
16
+ import { Input } from "../components/ui/input";
17
+ import { Textarea } from "../components/ui/textarea";
18
+ import { apiFetch } from "../hooks/use-api";
19
+
20
+ const MAX_BYTES = 4 * 1024 * 1024;
21
+ const byteLength = (value: string): number =>
22
+ new TextEncoder().encode(value).length;
23
+
24
+ export default function CompiledContext({
25
+ navigate,
26
+ }: {
27
+ navigate: (to: string | number) => void;
28
+ }) {
29
+ const [capsule, setCapsule] = useState("");
30
+ const [markdown, setMarkdown] = useState("");
31
+ const [tokens, setTokens] = useState("12000");
32
+ const [bytes, setBytes] = useState("");
33
+ const [preview, setPreview] = useState<Preview | null>(null);
34
+ const [check, setCheck] = useState<Check | null>(null);
35
+ const [busy, setBusy] = useState(false);
36
+ const [error, setError] = useState<string | null>(null);
37
+
38
+ const invalidate = () => {
39
+ setPreview(null);
40
+ setCheck(null);
41
+ setError(null);
42
+ };
43
+ const upload = async (
44
+ file: File | undefined,
45
+ kind: "capsule" | "markdown"
46
+ ) => {
47
+ if (!file) return;
48
+ invalidate();
49
+ if (kind === "capsule") setCapsule("");
50
+ else setMarkdown("");
51
+ if (file.size > MAX_BYTES) {
52
+ setError("Files must be 4 MiB or smaller.");
53
+ return;
54
+ }
55
+ setBusy(true);
56
+ try {
57
+ const text = await file.text();
58
+ if (kind === "capsule") setCapsule(text);
59
+ else setMarkdown(text);
60
+ } catch {
61
+ setError("Could not read this file.");
62
+ } finally {
63
+ setBusy(false);
64
+ }
65
+ };
66
+ const run = async (kind: "preview" | "check") => {
67
+ invalidate();
68
+ setBusy(true);
69
+ try {
70
+ if (byteLength(capsule) > MAX_BYTES || byteLength(markdown) > MAX_BYTES)
71
+ throw new Error("Inputs must be 4 MiB or smaller.");
72
+ const parsed: unknown = JSON.parse(capsule);
73
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
74
+ throw new Error("Supply a Capsule JSON object.");
75
+ if (kind === "preview") {
76
+ const budgetTokens = Number(tokens);
77
+ const budgetBytes = bytes === "" ? undefined : Number(bytes);
78
+ if (
79
+ !Number.isInteger(budgetTokens) ||
80
+ budgetTokens < 1 ||
81
+ budgetTokens > 1_000_000 ||
82
+ (budgetBytes !== undefined &&
83
+ (!Number.isInteger(budgetBytes) ||
84
+ budgetBytes < 1 ||
85
+ budgetBytes > MAX_BYTES))
86
+ )
87
+ throw new Error(
88
+ "Choose a positive budget: at most 1,000,000 tokens and 4 MiB."
89
+ );
90
+ const result = await apiFetch<Preview>(
91
+ "/api/context/compiled/preview",
92
+ {
93
+ method: "POST",
94
+ body: JSON.stringify({
95
+ capsule: parsed,
96
+ budgetTokens,
97
+ budgetBytes,
98
+ }),
99
+ }
100
+ );
101
+ if (result.error || !result.data)
102
+ throw new Error(result.error ?? "No preview returned.");
103
+ setPreview(result.data);
104
+ } else {
105
+ if (!markdown.trim())
106
+ throw new Error("Supply the compiled Markdown to check.");
107
+ const result = await apiFetch<Check>("/api/context/compiled/check", {
108
+ method: "POST",
109
+ body: JSON.stringify({ capsule: parsed, markdown }),
110
+ });
111
+ if (result.error || !result.data)
112
+ throw new Error(result.error ?? "No check returned.");
113
+ setCheck(result.data);
114
+ }
115
+ } catch (cause) {
116
+ setError(cause instanceof Error ? cause.message : "Request failed.");
117
+ } finally {
118
+ setBusy(false);
119
+ }
120
+ };
121
+ const download = () => {
122
+ if (!preview) return;
123
+ const url = URL.createObjectURL(
124
+ new Blob([preview.markdown], { type: "text/markdown;charset=utf-8" })
125
+ );
126
+ const link = document.createElement("a");
127
+ link.href = url;
128
+ link.download = "project.gno-context.md";
129
+ link.click();
130
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
131
+ };
132
+
133
+ return (
134
+ <main className="container mx-auto max-w-5xl space-y-6 px-4 py-8">
135
+ <Button onClick={() => navigate("/")} variant="ghost">
136
+ <ArrowLeftIcon className="size-4" /> Dashboard
137
+ </Button>
138
+ <header className="space-y-2">
139
+ <h1 className="font-serif text-3xl">Compiled project context</h1>
140
+ <p className="text-muted-foreground">
141
+ Turn a verified Context Capsule into a compact, cited Markdown file.
142
+ Source passages remain untrusted evidence, not agent instructions.
143
+ </p>
144
+ </header>
145
+ <form
146
+ onSubmit={(event) => {
147
+ event.preventDefault();
148
+ void run("preview");
149
+ }}
150
+ >
151
+ <fieldset className="min-w-0 space-y-4" disabled={busy}>
152
+ <legend className="sr-only">Compile a Context Capsule</legend>
153
+ <Card>
154
+ <CardHeader>
155
+ <CardTitle>1. Supply a Capsule</CardTitle>
156
+ </CardHeader>
157
+ <CardContent className="space-y-3">
158
+ <p className="text-sm text-muted-foreground">
159
+ Use a Capsule from this GNO instance. Verification checks
160
+ current sources and access before returning evidence. Inputs are
161
+ limited to 4 MiB each.
162
+ </p>
163
+ <label className="block space-y-2" htmlFor="capsule-file">
164
+ <span>Upload Capsule JSON</span>
165
+ <Input
166
+ accept=".json,application/json"
167
+ id="capsule-file"
168
+ onChange={(event) =>
169
+ void upload(event.target.files?.[0], "capsule")
170
+ }
171
+ type="file"
172
+ />
173
+ </label>
174
+ <label className="block space-y-2" htmlFor="capsule-json">
175
+ <span>Or paste Capsule JSON</span>
176
+ <Textarea
177
+ className="h-40 min-w-0 field-sizing-fixed font-mono text-xs"
178
+ id="capsule-json"
179
+ onChange={(event) => {
180
+ invalidate();
181
+ setCapsule(event.target.value);
182
+ }}
183
+ spellCheck={false}
184
+ value={capsule}
185
+ />
186
+ </label>
187
+ <div className="grid gap-4 sm:grid-cols-2">
188
+ <label className="space-y-2" htmlFor="context-tokens">
189
+ <span>Token budget</span>
190
+ <Input
191
+ id="context-tokens"
192
+ max={1_000_000}
193
+ min={1}
194
+ onChange={(event) => {
195
+ invalidate();
196
+ setTokens(event.target.value);
197
+ }}
198
+ required
199
+ type="number"
200
+ value={tokens}
201
+ />
202
+ </label>
203
+ <label className="space-y-2" htmlFor="context-bytes">
204
+ <span>Byte budget (optional)</span>
205
+ <Input
206
+ id="context-bytes"
207
+ max={MAX_BYTES}
208
+ min={1}
209
+ onChange={(event) => {
210
+ invalidate();
211
+ setBytes(event.target.value);
212
+ }}
213
+ type="number"
214
+ value={bytes}
215
+ />
216
+ </label>
217
+ </div>
218
+ <Button type="submit">
219
+ {busy ? "Verifying…" : "Preview verified context"}
220
+ </Button>
221
+ </CardContent>
222
+ </Card>
223
+ <Card>
224
+ <CardHeader>
225
+ <CardTitle>2. Check an existing artifact</CardTitle>
226
+ </CardHeader>
227
+ <CardContent className="space-y-3">
228
+ <label className="block space-y-2" htmlFor="context-file">
229
+ <span>Upload compiled Markdown</span>
230
+ <Input
231
+ accept=".md,text/markdown"
232
+ id="context-file"
233
+ onChange={(event) =>
234
+ void upload(event.target.files?.[0], "markdown")
235
+ }
236
+ type="file"
237
+ />
238
+ </label>
239
+ <label className="block space-y-2" htmlFor="context-markdown">
240
+ <span>Or paste compiled Markdown</span>
241
+ <Textarea
242
+ className="h-32 min-w-0 field-sizing-fixed font-mono text-xs"
243
+ id="context-markdown"
244
+ onChange={(event) => {
245
+ invalidate();
246
+ setMarkdown(event.target.value);
247
+ }}
248
+ spellCheck={false}
249
+ value={markdown}
250
+ />
251
+ </label>
252
+ <Button
253
+ onClick={() => void run("check")}
254
+ type="button"
255
+ variant="outline"
256
+ >
257
+ Check freshness
258
+ </Button>
259
+ </CardContent>
260
+ </Card>
261
+ </fieldset>
262
+ </form>
263
+ {error && (
264
+ <p className="break-words text-destructive" role="alert">
265
+ {error}
266
+ </p>
267
+ )}
268
+ {check && (
269
+ <Card>
270
+ <CardHeader>
271
+ <CardTitle>Artifact: {check.status}</CardTitle>
272
+ </CardHeader>
273
+ <CardContent aria-live="polite">
274
+ <ul className="list-inside list-disc">
275
+ {check.reasons.map((reason) => (
276
+ <li key={reason}>{reason}</li>
277
+ ))}
278
+ </ul>
279
+ <p className="mt-2 text-sm text-muted-foreground">
280
+ Check is read-only. Conflicts require inspecting manual edits;
281
+ stale evidence requires an explicit local refresh.
282
+ </p>
283
+ </CardContent>
284
+ </Card>
285
+ )}
286
+ {preview && (
287
+ <Card>
288
+ <CardHeader>
289
+ <CardTitle>Verified preview</CardTitle>
290
+ </CardHeader>
291
+ <CardContent className="min-w-0 space-y-4 break-words">
292
+ <p>
293
+ {preview.budget.usedTokens.toLocaleString()} tokens ·{" "}
294
+ {preview.budget.usedBytes.toLocaleString()} bytes ·{" "}
295
+ {preview.budget.estimator}
296
+ </p>
297
+ <p>
298
+ {preview.coverage.complete
299
+ ? "Required facets covered"
300
+ : "Incomplete coverage"}
301
+ </p>
302
+ <p>
303
+ Covered facets:{" "}
304
+ {preview.coverage.coveredFacets.join(", ") || "None"}
305
+ </p>
306
+ <p>
307
+ Unresolved facets:{" "}
308
+ {preview.coverage.unresolvedFacets.join(", ") || "None"}
309
+ </p>
310
+ <details>
311
+ <summary className="cursor-pointer">
312
+ Evidence and verification
313
+ </summary>
314
+ <div className="space-y-2 break-all font-mono text-xs">
315
+ <p>Output digest: {preview.digest}</p>
316
+ <p>Verification digest: {preview.verificationDigest}</p>
317
+ <p>Evidence: {preview.evidenceIds.join(", ") || "None"}</p>
318
+ <ul>
319
+ {preview.omissions.map((item) => (
320
+ <li key={item.evidenceId}>
321
+ {item.evidenceId}: {item.reason}
322
+ </li>
323
+ ))}
324
+ </ul>
325
+ </div>
326
+ </details>
327
+ <pre
328
+ aria-label="Compiled Markdown preview"
329
+ className="max-h-[32rem] overflow-auto whitespace-pre-wrap break-all rounded-md border bg-muted/30 p-4 font-mono text-xs"
330
+ >
331
+ {preview.markdown}
332
+ </pre>
333
+ <Button onClick={download}>
334
+ <DownloadIcon className="size-4" /> Download Markdown
335
+ </Button>
336
+ <p className="text-sm text-muted-foreground">
337
+ Download contains exactly the preview bytes. It does not create a
338
+ local refresh sidecar or update your agent instructions.
339
+ </p>
340
+ </CardContent>
341
+ </Card>
342
+ )}
343
+ <Card>
344
+ <CardHeader>
345
+ <CardTitle>Refresh local files explicitly</CardTitle>
346
+ </CardHeader>
347
+ <CardContent className="space-y-3">
348
+ <p className="text-sm text-muted-foreground">
349
+ For managed refresh, first compile locally with an explicit Capsule
350
+ file. Run gno update after source edits: checks use indexed state.
351
+ Choose a fresh Capsule output filename for each stale refresh.
352
+ Refresh preserves hand edits by reporting a conflict. Nothing
353
+ refreshes in the background.
354
+ </p>
355
+ <pre className="overflow-auto whitespace-pre-wrap break-all rounded-md bg-muted/30 p-4 text-xs">
356
+ {
357
+ "gno context compiled compile --capsule capsule.json --budget 12000 --output project.gno-context.md\ngno context compiled check project.gno-context.md\ngno context compiled refresh project.gno-context.md --capsule-output project.gno-context.capsule.json"
358
+ }
359
+ </pre>
360
+ </CardContent>
361
+ </Card>
362
+ </main>
363
+ );
364
+ }
@@ -540,6 +540,13 @@ export default function Dashboard({ navigate }: PageProps) {
540
540
  <HistoryIcon className="size-4" />
541
541
  Trace history
542
542
  </Button>
543
+ <Button
544
+ onClick={() => navigate("/context/compiled")}
545
+ size="lg"
546
+ variant="outline"
547
+ >
548
+ Compiled context
549
+ </Button>
543
550
  </nav>
544
551
 
545
552
  {error && (
@@ -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 && (