@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.
Files changed (88) hide show
  1. package/README.md +24 -4
  2. package/assets/skill/SKILL.md +30 -0
  3. package/assets/skill/cli-reference.md +12 -0
  4. package/assets/skill/examples.md +15 -0
  5. package/assets/skill/mcp-reference.md +16 -0
  6. package/assets/spa-production.json.gz +0 -0
  7. package/browser-extension/artifacts/{gno-browser-clipper-v2.3.2.zip → gno-browser-clipper-v2.4.0.zip} +0 -0
  8. package/browser-extension/artifacts/gno-browser-clipper-v2.4.0.zip.sha256 +1 -0
  9. package/browser-extension/dist/manifest.json +1 -1
  10. package/package.json +1 -1
  11. package/spec/cli.md +30 -0
  12. package/spec/db/schema.sql +2 -0
  13. package/spec/evals.md +48 -0
  14. package/spec/mcp.md +12 -0
  15. package/spec/output-schemas/ask.schema.json +12 -0
  16. package/spec/output-schemas/context-capsule-v1.schema.json +205 -32
  17. package/spec/output-schemas/context-capsule-verification.schema.json +32 -9
  18. package/spec/output-schemas/evidence-fixtures.schema.json +176 -0
  19. package/spec/output-schemas/evidence-report.schema.json +322 -0
  20. package/spec/output-schemas/evidence-run.schema.json +277 -0
  21. package/spec/output-schemas/metadata-predicate.schema.json +460 -0
  22. package/spec/output-schemas/retrieval-trace-filters.schema.json +13 -4
  23. package/spec/output-schemas/search-results.schema.json +12 -0
  24. package/spec/output-schemas/status.schema.json +37 -6
  25. package/src/app/context-runtime-contract.ts +33 -2
  26. package/src/app/context-runtime-input.ts +17 -0
  27. package/src/app/context-runtime-types.ts +2 -0
  28. package/src/app/context-runtime.ts +27 -3
  29. package/src/app/context-surface.ts +2 -0
  30. package/src/app/verified-ask.ts +1 -0
  31. package/src/cli/commands/ask.ts +1 -0
  32. package/src/cli/commands/context-build.ts +1 -0
  33. package/src/cli/commands/search.ts +1 -0
  34. package/src/cli/commands/status.ts +10 -0
  35. package/src/cli/options.ts +19 -0
  36. package/src/cli/program.ts +12 -0
  37. package/src/converters/types.ts +2 -0
  38. package/src/core/context-capsule-retrieval-schema.ts +8 -0
  39. package/src/core/context-capsule-schema.ts +47 -3
  40. package/src/core/context-capsule-validation.ts +14 -2
  41. package/src/core/context-capsule-verification.ts +1 -0
  42. package/src/core/context-capsule.ts +16 -2
  43. package/src/core/context-compiler.ts +3 -0
  44. package/src/core/record-metadata.ts +1 -1
  45. package/src/core/retrieval-replay-candidate.ts +5 -0
  46. package/src/core/retrieval-trace-filter-normalization.ts +4 -0
  47. package/src/core/retrieval-trace-request.ts +1 -0
  48. package/src/core/retrieval-trace-session.ts +5 -1
  49. package/src/core/retrieval-trace.ts +2 -0
  50. package/src/core/typed-metadata.ts +224 -0
  51. package/src/ingestion/record-adapter-canonical.ts +13 -2
  52. package/src/ingestion/record-container.ts +9 -0
  53. package/src/ingestion/sync.ts +11 -1
  54. package/src/ingestion/typed-metadata.ts +55 -0
  55. package/src/mcp/tools/ask.ts +10 -2
  56. package/src/mcp/tools/context.ts +1 -0
  57. package/src/mcp/tools/index.ts +12 -1
  58. package/src/mcp/tools/query.ts +13 -0
  59. package/src/mcp/tools/search.ts +9 -0
  60. package/src/mcp/tools/status.ts +4 -0
  61. package/src/mcp/tools/vsearch.ts +9 -0
  62. package/src/pipeline/diagnose.ts +6 -0
  63. package/src/pipeline/filters.ts +65 -0
  64. package/src/pipeline/graph-retrieval.ts +9 -1
  65. package/src/pipeline/hybrid.ts +22 -2
  66. package/src/pipeline/search.ts +11 -0
  67. package/src/pipeline/types.ts +7 -1
  68. package/src/pipeline/vsearch.ts +21 -1
  69. package/src/sdk/client.ts +29 -1
  70. package/src/sdk/index.ts +4 -0
  71. package/src/sdk/types.ts +7 -0
  72. package/src/serve/context-capsule.ts +1 -0
  73. package/src/serve/public/app.tsx +1 -1
  74. package/src/serve/public/components/MetadataFilter.tsx +186 -0
  75. package/src/serve/public/globals.built.css +1 -1
  76. package/src/serve/public/lib/metadata-filter.ts +28 -0
  77. package/src/serve/public/lib/retrieval-filters.ts +3 -0
  78. package/src/serve/public/pages/Ask.tsx +48 -3
  79. package/src/serve/public/pages/DocView.tsx +33 -0
  80. package/src/serve/public/pages/Search.tsx +44 -1
  81. package/src/serve/routes/api.ts +50 -0
  82. package/src/store/migrations/030-typed-metadata.ts +13 -0
  83. package/src/store/migrations/index.ts +2 -0
  84. package/src/store/sqlite/adapter.ts +55 -4
  85. package/src/store/sqlite/eligibility.ts +12 -0
  86. package/src/store/sqlite/metadata-predicate.ts +71 -0
  87. package/src/store/types.ts +11 -0
  88. package/browser-extension/artifacts/gno-browser-clipper-v2.3.2.zip.sha256 +0 -1
@@ -141,6 +141,10 @@ import {
141
141
  import { buildWikiBestMatchSubquery } from "../../core/graph-resolver";
142
142
  import { buildContentPrefilterNeedles } from "../../core/link-relevance";
143
143
  import { normalizeWikiName, stripWikiMdExt } from "../../core/links";
144
+ import {
145
+ TYPED_METADATA_INGEST_VERSION,
146
+ typedMetadataSchema,
147
+ } from "../../core/typed-metadata";
144
148
  import {
145
149
  parseActivationReceipt,
146
150
  serializeActivationReceipt,
@@ -1427,6 +1431,30 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
1427
1431
  // Documents
1428
1432
  // ─────────────────────────────────────────────────────────────────────────
1429
1433
 
1434
+ async getTypedMetadataCoverage(
1435
+ options: import("../types").DocumentEligibilityOptions
1436
+ ): Promise<StoreResult<{ pending: number; invalid: number }>> {
1437
+ try {
1438
+ const db = this.ensureOpen();
1439
+ const eligible = buildEligibleDocumentQuery(
1440
+ { ...options, filter: undefined },
1441
+ db
1442
+ );
1443
+ const row = db
1444
+ .query<{ pending: number; invalid: number }, (string | number)[]>(`
1445
+ SELECT COALESCE(SUM(CASE WHEN d.ingest_version IS NULL OR d.ingest_version < ${TYPED_METADATA_INGEST_VERSION} OR (d.typed_metadata IS NULL AND d.metadata_error IS NULL) THEN 1 ELSE 0 END),0) AS pending,
1446
+ COALESCE(SUM(CASE WHEN d.metadata_error IS NOT NULL THEN 1 ELSE 0 END),0) AS invalid
1447
+ FROM documents d WHERE d.mirror_hash IS NOT NULL AND d.id IN (SELECT id FROM (${eligible.sql}))
1448
+ `)
1449
+ .get(...eligible.params);
1450
+ return ok(row ?? { pending: 0, invalid: 0 });
1451
+ } catch (cause) {
1452
+ return err("QUERY_FAILED", "Cannot determine typed metadata coverage", {
1453
+ cause,
1454
+ });
1455
+ }
1456
+ }
1457
+
1430
1458
  async upsertDocument(
1431
1459
  doc: DocumentInput
1432
1460
  ): Promise<StoreResult<UpsertDocumentResult>> {
@@ -1457,13 +1485,13 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
1457
1485
  collection, rel_path, source_hash, source_mime, source_ext,
1458
1486
  source_size, source_mtime, source_ctime, docid, uri, title, mirror_hash,
1459
1487
  converter_id, converter_version, language_hint, content_type, categories,
1460
- content_type_source, author, frontmatter_date, date_fields,
1488
+ content_type_source, author, frontmatter_date, date_fields, typed_metadata, metadata_error,
1461
1489
  record_key, record_source_path, record_source_locator, record_metadata, record_anchors,
1462
1490
  record_adapter_fingerprint,
1463
1491
  content_type_rules_fingerprint,
1464
1492
  active, indexed_at, last_error_code, last_error_message, last_error_at,
1465
1493
  ingest_version, updated_at
1466
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, datetime('now'), ?, ?, ?, ?, datetime('now'))
1494
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, datetime('now'), ?, ?, ?, ?, datetime('now'))
1467
1495
  ON CONFLICT(collection, rel_path) DO UPDATE SET
1468
1496
  source_hash = excluded.source_hash,
1469
1497
  source_mime = excluded.source_mime,
@@ -1484,6 +1512,8 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
1484
1512
  author = excluded.author,
1485
1513
  frontmatter_date = excluded.frontmatter_date,
1486
1514
  date_fields = excluded.date_fields,
1515
+ typed_metadata = excluded.typed_metadata,
1516
+ metadata_error = excluded.metadata_error,
1487
1517
  record_key = excluded.record_key,
1488
1518
  record_source_path = excluded.record_source_path,
1489
1519
  record_source_locator = excluded.record_source_locator,
@@ -1521,6 +1551,10 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
1521
1551
  doc.author ?? null,
1522
1552
  doc.frontmatterDate ?? null,
1523
1553
  doc.dateFields ? JSON.stringify(doc.dateFields) : null,
1554
+ doc.typedMetadata
1555
+ ? JSON.stringify(typedMetadataSchema.parse(doc.typedMetadata))
1556
+ : null,
1557
+ doc.metadataError ?? null,
1524
1558
  doc.recordKey ?? null,
1525
1559
  doc.recordSourcePath ?? null,
1526
1560
  doc.recordSourceLocator ?? null,
@@ -2109,6 +2143,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
2109
2143
  options: {
2110
2144
  collection?: string;
2111
2145
  activeOnly?: boolean;
2146
+ eligibility?: import("../types").DocumentEligibilityOptions;
2112
2147
  } = {}
2113
2148
  ): Promise<StoreResult<DocumentRow[]>> {
2114
2149
  try {
@@ -2131,7 +2166,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
2131
2166
  const batch = uniqueDocids.slice(i, i + SQL_PARAM_LIMIT);
2132
2167
  const placeholders = batch.map(() => "?").join(",");
2133
2168
  const clauses = [`docid IN (${placeholders})`];
2134
- const params: string[] = [...batch];
2169
+ const params: (string | number)[] = [...batch];
2135
2170
 
2136
2171
  if (options.activeOnly ?? true) {
2137
2172
  clauses.push("active = 1");
@@ -2141,8 +2176,15 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
2141
2176
  params.push(options.collection);
2142
2177
  }
2143
2178
 
2179
+ if (options.eligibility) {
2180
+ const eligible = buildEligibleDocumentQuery(options.eligibility, db);
2181
+ clauses.push(`id IN (SELECT id FROM (${eligible.sql}))`);
2182
+ params.push(...eligible.params);
2183
+ }
2144
2184
  const sql = `SELECT * FROM documents WHERE ${clauses.join(" AND ")} ORDER BY id`;
2145
- rows.push(...db.query<DbDocumentRow, string[]>(sql).all(...params));
2185
+ rows.push(
2186
+ ...db.query<DbDocumentRow, (string | number)[]>(sql).all(...params)
2187
+ );
2146
2188
  }
2147
2189
 
2148
2190
  return ok(rows.map(mapDocumentRow));
@@ -5960,7 +6002,10 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
5960
6002
  const recentErrors = recentErrorsRow?.count ?? 0;
5961
6003
  const healthy = recentErrors === 0;
5962
6004
 
6005
+ const metadataCoverage = await this.getTypedMetadataCoverage({});
6006
+ if (!metadataCoverage.ok) return metadataCoverage;
5963
6007
  return ok({
6008
+ typedMetadata: metadataCoverage.value,
5964
6009
  version,
5965
6010
  indexName,
5966
6011
  configPath: this.configPath,
@@ -6286,6 +6331,8 @@ interface DbDocumentRow {
6286
6331
  author: string | null;
6287
6332
  frontmatter_date: string | null;
6288
6333
  date_fields: string | null;
6334
+ typed_metadata: string | null;
6335
+ metadata_error: string | null;
6289
6336
  record_key: string | null;
6290
6337
  record_source_path: string | null;
6291
6338
  record_source_locator: string | null;
@@ -6454,6 +6501,10 @@ function mapDocumentRow(row: DbDocumentRow): DocumentRow {
6454
6501
  author: row.author,
6455
6502
  frontmatterDate: row.frontmatter_date,
6456
6503
  dateFields,
6504
+ typedMetadata: row.typed_metadata
6505
+ ? typedMetadataSchema.parse(JSON.parse(row.typed_metadata))
6506
+ : null,
6507
+ metadataError: row.metadata_error,
6457
6508
  recordKey: row.record_key,
6458
6509
  recordSourcePath: row.record_source_path,
6459
6510
  recordSourceLocator: row.record_source_locator,
@@ -2,7 +2,9 @@ import type { Database } from "bun:sqlite";
2
2
 
3
3
  import type { DocumentEligibilityOptions } from "../types";
4
4
 
5
+ import { TYPED_METADATA_INGEST_VERSION } from "../../core/typed-metadata";
5
6
  import { matchesExcludedText } from "../../pipeline/exclude";
7
+ import { compileMetadataPredicate } from "./metadata-predicate";
6
8
 
7
9
  /** Unbounded owner selection; compose with FTS rowids or chunk mirror ownership
8
10
  * before ranking/LIMIT. EXISTS keeps multiple tags/scopes from duplicating owners.
@@ -14,6 +16,16 @@ export function buildEligibleDocumentQuery(
14
16
  // Build tag filter conditions using EXISTS subqueries
15
17
  const conditions: string[] = ["d.active = 1"];
16
18
  const params: (string | number)[] = [];
19
+ if (options.filter) {
20
+ const predicate = compileMetadataPredicate(options.filter);
21
+ conditions.push(
22
+ "d.ingest_version >= ?",
23
+ "d.metadata_error IS NULL",
24
+ "d.typed_metadata IS NOT NULL",
25
+ predicate.sql
26
+ );
27
+ params.push(TYPED_METADATA_INGEST_VERSION, ...predicate.params);
28
+ }
17
29
  if (options.chunkLanguage) {
18
30
  conditions.push(
19
31
  "EXISTS (SELECT 1 FROM content_chunks lc WHERE lc.mirror_hash = d.mirror_hash AND lc.language = ?)"
@@ -0,0 +1,71 @@
1
+ import type {
2
+ MetadataPredicate,
3
+ MetadataScalar,
4
+ } from "../../core/typed-metadata";
5
+
6
+ import { normalizeMetadataPredicate } from "../../core/typed-metadata";
7
+
8
+ /** JSON keys and values are bound parameters, never paths or SQL fragments. */
9
+ export function compileMetadataPredicate(input: MetadataPredicate): {
10
+ sql: string;
11
+ params: (string | number)[];
12
+ } {
13
+ const params: (string | number)[] = [];
14
+ const bind = (value: string | number): string => {
15
+ params.push(value);
16
+ return "?";
17
+ };
18
+ const scalar = (alias: string, value: MetadataScalar): string => {
19
+ const type =
20
+ typeof value === "number"
21
+ ? `${alias}.type IN ('integer','real')`
22
+ : typeof value === "boolean"
23
+ ? `${alias}.type IN ('true','false')`
24
+ : `${alias}.type = 'text'`;
25
+ return `(${type} AND ${alias}.value = ${bind(typeof value === "boolean" ? Number(value) : value)})`;
26
+ };
27
+ const compile = (node: MetadataPredicate): string => {
28
+ if (node.op === "and" || node.op === "or")
29
+ return `(${node.predicates.map(compile).join(node.op === "and" ? " AND " : " OR ")})`;
30
+ if (node.op === "not") return `NOT (${compile(node.predicate)})`;
31
+ if (!("key" in node)) throw new Error("Invalid metadata predicate");
32
+ const key = bind(node.key);
33
+ const select = `SELECT 1 FROM json_each(d.typed_metadata) mf WHERE mf.key = ${key}`;
34
+ if (node.op === "exists")
35
+ return `${node.value ? "" : "NOT "}EXISTS (${select})`;
36
+ if ("values" in node) {
37
+ if (node.op === "all") {
38
+ const conditions = node.values.map(
39
+ (value) =>
40
+ `EXISTS (SELECT 1 FROM json_each(mf.value) mv WHERE ${scalar("mv", value)})`
41
+ );
42
+ return `EXISTS (${select} AND CASE WHEN mf.type = 'array' THEN (${conditions.join(" AND ")}) ELSE 0 END)`;
43
+ }
44
+ const arrayMatches = node.values
45
+ .map((value) => scalar("mv", value))
46
+ .join(" OR ");
47
+ const scalarMatches = node.values
48
+ .map((value) => scalar("mf", value))
49
+ .join(" OR ");
50
+ const match = `CASE WHEN mf.type = 'array' THEN EXISTS (SELECT 1 FROM json_each(mf.value) mv WHERE ${arrayMatches}) ELSE (${scalarMatches}) END`;
51
+ return `EXISTS (${select} AND ${node.op === "nin" ? "NOT " : ""}(${match}))`;
52
+ }
53
+ if (node.op === "eq")
54
+ return `EXISTS (${select} AND ${scalar("mf", node.value)})`;
55
+ if (node.op === "ne") {
56
+ const equal = scalar("mf", node.value);
57
+ const type =
58
+ typeof node.value === "number"
59
+ ? "mf.type IN ('integer','real')"
60
+ : typeof node.value === "boolean"
61
+ ? "mf.type IN ('true','false')"
62
+ : "mf.type = 'text'";
63
+ return `EXISTS (${select} AND ${type} AND NOT ${equal})`;
64
+ }
65
+ if (typeof node.value !== "number")
66
+ throw new Error("Ordering requires a number");
67
+ const operators = { gt: ">", gte: ">=", lt: "<", lte: "<=" } as const;
68
+ return `EXISTS (${select} AND mf.type IN ('integer','real') AND mf.value ${operators[node.op]} ${bind(node.value)})`;
69
+ };
70
+ return { sql: compile(normalizeMetadataPredicate(input)), params };
71
+ }
@@ -20,6 +20,7 @@ import type {
20
20
  FileRefactorRecoveryReceipt,
21
21
  FileRefactorRecoveryReceiptDraft,
22
22
  } from "../core/file-refactor-journal";
23
+ import type { MetadataPredicate, TypedMetadata } from "../core/typed-metadata";
23
24
  import type {
24
25
  ChunkingPolicyToken,
25
26
  ChunkingStatus,
@@ -119,6 +120,8 @@ export interface ContextRow {
119
120
 
120
121
  /** Document row from DB */
121
122
  export interface DocumentRow {
123
+ typedMetadata?: TypedMetadata | null;
124
+ metadataError?: string | null;
122
125
  id: number;
123
126
  collection: string;
124
127
  relPath: string;
@@ -377,6 +380,8 @@ export interface DocEdgeInput {
377
380
 
378
381
  /** Input for upserting a document */
379
382
  export interface DocumentInput {
383
+ typedMetadata?: TypedMetadata;
384
+ metadataError?: string;
380
385
  collection: string;
381
386
  relPath: string;
382
387
  sourceHash: string;
@@ -667,6 +672,7 @@ export interface GraphReferenceStore {
667
672
  }
668
673
 
669
674
  export interface DocumentEligibilityOptions {
675
+ filter?: MetadataPredicate;
670
676
  /** Internal owner eligibility; public lexical language remains reserved. */
671
677
  chunkLanguage?: string;
672
678
  /** Include author, content type and categories in whole-owner exclusion. */
@@ -793,6 +799,7 @@ export interface CollectionStatus {
793
799
 
794
800
  /** Index-level status */
795
801
  export interface IndexStatus {
802
+ typedMetadata?: { pending: number; invalid: number };
796
803
  /** Config version string */
797
804
  version: string;
798
805
  /** Index name (from dbPath) */
@@ -1522,6 +1529,9 @@ export type WithTransaction = <T>(
1522
1529
  * Implementations: SQLite adapter (src/store/sqlite/adapter.ts)
1523
1530
  */
1524
1531
  export interface StorePort {
1532
+ getTypedMetadataCoverage?(
1533
+ options: DocumentEligibilityOptions
1534
+ ): Promise<StoreResult<{ pending: number; invalid: number }>>;
1525
1535
  // ─────────────────────────────────────────────────────────────────────────
1526
1536
  // Lifecycle
1527
1537
  // ─────────────────────────────────────────────────────────────────────────
@@ -1869,6 +1879,7 @@ export interface StorePort {
1869
1879
  options?: {
1870
1880
  collection?: string;
1871
1881
  activeOnly?: boolean;
1882
+ eligibility?: DocumentEligibilityOptions;
1872
1883
  }
1873
1884
  ): Promise<StoreResult<DocumentRow[]>>;
1874
1885
 
@@ -1 +0,0 @@
1
- 293a8a52bc9368e6755cc9a23970f83fc6c0344a6832132c93a65b9c73bc3e28 gno-browser-clipper-v2.3.2.zip