@gmickel/gno 1.9.0 → 1.10.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 (37) hide show
  1. package/assets/skill/SKILL.md +28 -16
  2. package/assets/skill/cli-reference.md +30 -0
  3. package/assets/skill/examples.md +20 -0
  4. package/assets/skill/mcp-reference.md +7 -1
  5. package/package.json +1 -1
  6. package/src/cli/commands/graph.ts +89 -2
  7. package/src/cli/commands/links.ts +237 -54
  8. package/src/cli/commands/query.ts +212 -0
  9. package/src/cli/commands/ref-parser.ts +8 -103
  10. package/src/cli/options.ts +4 -0
  11. package/src/cli/program.ts +176 -9
  12. package/src/config/content-types.ts +14 -0
  13. package/src/core/file-lock.ts +38 -3
  14. package/src/core/graph-query.ts +137 -0
  15. package/src/core/graph-resolver.ts +117 -0
  16. package/src/core/ref-parser.ts +145 -0
  17. package/src/ingestion/frontmatter.ts +95 -2
  18. package/src/ingestion/sync.ts +281 -1
  19. package/src/mcp/tools/get.ts +1 -1
  20. package/src/mcp/tools/index.ts +89 -1
  21. package/src/mcp/tools/links.ts +83 -1
  22. package/src/mcp/tools/multi-get.ts +1 -1
  23. package/src/mcp/tools/query.ts +207 -0
  24. package/src/pipeline/diagnose.ts +302 -0
  25. package/src/pipeline/filters.ts +119 -0
  26. package/src/pipeline/hybrid.ts +90 -17
  27. package/src/pipeline/types.ts +32 -0
  28. package/src/publish/export-service.ts +1 -1
  29. package/src/sdk/documents.ts +2 -2
  30. package/src/serve/routes/api.ts +194 -0
  31. package/src/serve/routes/graph.ts +173 -1
  32. package/src/serve/server.ts +24 -1
  33. package/src/store/migrations/010-typed-edges.ts +67 -0
  34. package/src/store/migrations/011-doc-edge-traversal-indexes.ts +30 -0
  35. package/src/store/migrations/index.ts +4 -0
  36. package/src/store/sqlite/adapter.ts +826 -102
  37. package/src/store/types.ts +141 -0
@@ -13,8 +13,8 @@ import type {
13
13
  MultiGetResponse,
14
14
  SkippedDoc,
15
15
  } from "../cli/commands/multi-get";
16
- import type { ParsedRef } from "../cli/commands/ref-parser";
17
16
  import type { Config } from "../config/types";
17
+ import type { ParsedRef } from "../core/ref-parser";
18
18
  import type { DocumentRow, StorePort } from "../store/types";
19
19
  import type {
20
20
  GnoGetOptions,
@@ -22,8 +22,8 @@ import type {
22
22
  GnoMultiGetOptions,
23
23
  } from "./types";
24
24
 
25
- import { isGlobPattern, parseRef, splitRefs } from "../cli/commands/ref-parser";
26
25
  import { getDocumentCapabilities } from "../core/document-capabilities";
26
+ import { isGlobPattern, parseRef, splitRefs } from "../core/ref-parser";
27
27
  import { sdkError } from "./errors";
28
28
 
29
29
  const URI_PREFIX_PATTERN = /^gno:\/\/[^/]+\//;
@@ -35,6 +35,10 @@ import {
35
35
  removeCollection,
36
36
  updateCollection,
37
37
  } from "../../collection";
38
+ import {
39
+ fingerprintContentTypeRules,
40
+ normalizeContentTypes,
41
+ } from "../../config";
38
42
  import {
39
43
  buildCaptureReceipt,
40
44
  listCaptureDiskRelPaths,
@@ -100,6 +104,7 @@ import {
100
104
  generateGroundedAnswer,
101
105
  processAnswerResult,
102
106
  } from "../../pipeline/answer";
107
+ import { diagnoseQueryTarget } from "../../pipeline/diagnose";
103
108
  import { searchHybrid } from "../../pipeline/hybrid";
104
109
  import { validateQueryModes } from "../../pipeline/query-modes";
105
110
  import { searchBm25 } from "../../pipeline/search";
@@ -192,6 +197,10 @@ export interface QueryRequestBody {
192
197
  tagsAny?: string;
193
198
  }
194
199
 
200
+ export interface QueryDiagnoseRequestBody extends QueryRequestBody {
201
+ target?: string;
202
+ }
203
+
195
204
  export interface AskRequestBody {
196
205
  query: string;
197
206
  limit?: number;
@@ -3517,6 +3526,191 @@ export async function handleQuery(
3517
3526
  return jsonResponse(result.value);
3518
3527
  }
3519
3528
 
3529
+ /**
3530
+ * POST /api/query/diagnose
3531
+ * Body: { query, target, limit?, minScore?, collection?, lang?, queryModes?, noExpand?, noRerank?, graph? }
3532
+ * Returns targeted retrieval diagnostics for one document.
3533
+ */
3534
+ export async function handleQueryDiagnose(
3535
+ ctx: ServerContext,
3536
+ req: Request
3537
+ ): Promise<Response> {
3538
+ let body: QueryDiagnoseRequestBody;
3539
+ try {
3540
+ body = (await req.json()) as QueryDiagnoseRequestBody;
3541
+ } catch {
3542
+ return errorResponse("VALIDATION", "Invalid JSON body");
3543
+ }
3544
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
3545
+ return errorResponse("VALIDATION", "JSON body must be an object");
3546
+ }
3547
+
3548
+ if (!body.query || typeof body.query !== "string") {
3549
+ return errorResponse("VALIDATION", "Missing or invalid query");
3550
+ }
3551
+ if (!body.target || typeof body.target !== "string") {
3552
+ return errorResponse("VALIDATION", "Missing or invalid target");
3553
+ }
3554
+
3555
+ const rawQuery = body.query.trim();
3556
+ const target = body.target.trim();
3557
+ if (!rawQuery) {
3558
+ return errorResponse("VALIDATION", "Query cannot be empty");
3559
+ }
3560
+ if (!target) {
3561
+ return errorResponse("VALIDATION", "target cannot be empty");
3562
+ }
3563
+
3564
+ if (
3565
+ body.limit !== undefined &&
3566
+ (typeof body.limit !== "number" || body.limit < 1)
3567
+ ) {
3568
+ return errorResponse("VALIDATION", "limit must be a positive integer");
3569
+ }
3570
+ if (
3571
+ body.minScore !== undefined &&
3572
+ (typeof body.minScore !== "number" ||
3573
+ body.minScore < 0 ||
3574
+ body.minScore > 1)
3575
+ ) {
3576
+ return errorResponse(
3577
+ "VALIDATION",
3578
+ "minScore must be a number between 0 and 1"
3579
+ );
3580
+ }
3581
+ if (body.since !== undefined && typeof body.since !== "string") {
3582
+ return errorResponse("VALIDATION", "since must be a string");
3583
+ }
3584
+ if (body.until !== undefined && typeof body.until !== "string") {
3585
+ return errorResponse("VALIDATION", "until must be a string");
3586
+ }
3587
+ if (body.intent !== undefined && typeof body.intent !== "string") {
3588
+ return errorResponse("VALIDATION", "intent must be a string");
3589
+ }
3590
+ if (body.exclude !== undefined && typeof body.exclude !== "string") {
3591
+ return errorResponse(
3592
+ "VALIDATION",
3593
+ "exclude must be a comma-separated string"
3594
+ );
3595
+ }
3596
+ if (
3597
+ body.candidateLimit !== undefined &&
3598
+ (typeof body.candidateLimit !== "number" || body.candidateLimit < 1)
3599
+ ) {
3600
+ return errorResponse(
3601
+ "VALIDATION",
3602
+ "candidateLimit must be a positive integer"
3603
+ );
3604
+ }
3605
+ if (body.category !== undefined && typeof body.category !== "string") {
3606
+ return errorResponse(
3607
+ "VALIDATION",
3608
+ "category must be a comma-separated string"
3609
+ );
3610
+ }
3611
+ if (body.author !== undefined && typeof body.author !== "string") {
3612
+ return errorResponse("VALIDATION", "author must be a string");
3613
+ }
3614
+
3615
+ const { queryModes, error: queryModesError } = parseQueryModesInput(
3616
+ body.queryModes
3617
+ );
3618
+ if (queryModesError) {
3619
+ return queryModesError;
3620
+ }
3621
+
3622
+ const {
3623
+ query,
3624
+ queryModes: normalizedQueryModes,
3625
+ error: structuredQueryError,
3626
+ } = normalizeStructuredQueryBody(rawQuery, queryModes);
3627
+ if (structuredQueryError) {
3628
+ return structuredQueryError;
3629
+ }
3630
+ const normalizedQuery = query ?? rawQuery;
3631
+
3632
+ let tagsAll: string[] | undefined;
3633
+ let tagsAny: string[] | undefined;
3634
+
3635
+ if (body.tagsAll) {
3636
+ try {
3637
+ tagsAll = parseAndValidateTagFilter(body.tagsAll);
3638
+ } catch (e) {
3639
+ return errorResponse(
3640
+ "VALIDATION",
3641
+ e instanceof Error ? e.message : "Invalid tagsAll"
3642
+ );
3643
+ }
3644
+ }
3645
+
3646
+ if (body.tagsAny) {
3647
+ try {
3648
+ tagsAny = parseAndValidateTagFilter(body.tagsAny);
3649
+ } catch (e) {
3650
+ return errorResponse(
3651
+ "VALIDATION",
3652
+ e instanceof Error ? e.message : "Invalid tagsAny"
3653
+ );
3654
+ }
3655
+ }
3656
+
3657
+ const categories = body.category
3658
+ ? parseCommaSeparatedValues(body.category)
3659
+ : undefined;
3660
+ const exclude = body.exclude
3661
+ ? parseCommaSeparatedValues(body.exclude)
3662
+ : undefined;
3663
+ const author = body.author?.trim() || undefined;
3664
+ const contentTypeRules = normalizeContentTypes(
3665
+ ctx.config.contentTypes ?? []
3666
+ ).rules;
3667
+
3668
+ const result = await diagnoseQueryTarget(
3669
+ {
3670
+ store: ctx.store,
3671
+ config: ctx.config,
3672
+ vectorIndex: ctx.vectorIndex,
3673
+ embedPort: ctx.embedPort,
3674
+ expandPort: ctx.expandPort,
3675
+ rerankPort: ctx.rerankPort,
3676
+ },
3677
+ normalizedQuery,
3678
+ {
3679
+ target,
3680
+ limit: Math.min(body.limit ?? 20, 50),
3681
+ minScore: body.minScore,
3682
+ collection: body.collection,
3683
+ lang: body.lang,
3684
+ intent: body.intent?.trim() || undefined,
3685
+ candidateLimit:
3686
+ body.candidateLimit !== undefined
3687
+ ? Math.min(body.candidateLimit, 100)
3688
+ : undefined,
3689
+ exclude,
3690
+ queryModes: normalizedQueryModes,
3691
+ noExpand: body.noExpand,
3692
+ noRerank: body.noRerank,
3693
+ graph: body.graph === true,
3694
+ noGraph: body.noGraph,
3695
+ tagsAll,
3696
+ tagsAny,
3697
+ since: body.since,
3698
+ until: body.until,
3699
+ categories,
3700
+ author,
3701
+ contentTypeRules,
3702
+ contentTypeRulesFingerprint:
3703
+ fingerprintContentTypeRules(contentTypeRules),
3704
+ }
3705
+ );
3706
+
3707
+ if (!result.ok) {
3708
+ return errorResponse("RUNTIME", result.error.message, 500);
3709
+ }
3710
+
3711
+ return jsonResponse(result.value);
3712
+ }
3713
+
3520
3714
  /**
3521
3715
  * POST /api/ask
3522
3716
  * Body: { query, limit?, collection?, lang?, maxAnswerTokens? }
@@ -4,8 +4,17 @@
4
4
  * @module src/serve/routes/graph
5
5
  */
6
6
 
7
+ import type { Config } from "../../config/types";
7
8
  import type { SqliteAdapter } from "../../store/sqlite/adapter";
8
- import type { GetGraphOptions, GraphResult } from "../../store/types";
9
+ import type {
10
+ GetGraphOptions,
11
+ GraphQueryDirection,
12
+ GraphQueryResult,
13
+ GraphResult,
14
+ } from "../../store/types";
15
+
16
+ import { normalizeContentTypes } from "../../config";
17
+ import { diagnoseGraphQuery } from "../../core/graph-query";
9
18
 
10
19
  // ─────────────────────────────────────────────────────────────────────────────
11
20
  // Types
@@ -13,6 +22,21 @@ import type { GetGraphOptions, GraphResult } from "../../store/types";
13
22
 
14
23
  export interface GraphResponse extends GraphResult {}
15
24
 
25
+ export interface GraphQueryResponse extends GraphQueryResult {}
26
+
27
+ interface GraphQueryRequestBody {
28
+ doc?: string;
29
+ root?: string;
30
+ direction?: GraphQueryDirection;
31
+ edgeType?: string;
32
+ relation?: string;
33
+ maxDepth?: number;
34
+ depth?: number;
35
+ maxNodes?: number;
36
+ frontierLimit?: number;
37
+ visitedLimit?: number;
38
+ }
39
+
16
40
  // ─────────────────────────────────────────────────────────────────────────────
17
41
  // Helpers
18
42
  // ─────────────────────────────────────────────────────────────────────────────
@@ -67,6 +91,29 @@ function parseThreshold(
67
91
  return { ok: true, value: parsed };
68
92
  }
69
93
 
94
+ function parseBodyPositiveInt(
95
+ name: string,
96
+ value: number | undefined,
97
+ defaultValue: number,
98
+ min: number,
99
+ max: number
100
+ ): ParseResult {
101
+ if (value === undefined) return { ok: true, value: defaultValue };
102
+ if (!Number.isFinite(value) || !Number.isInteger(value)) {
103
+ return {
104
+ ok: false,
105
+ message: `${name} must be an integer between ${min} and ${max}`,
106
+ };
107
+ }
108
+ if (value < min || value > max) {
109
+ return {
110
+ ok: false,
111
+ message: `${name} must be between ${min} and ${max}`,
112
+ };
113
+ }
114
+ return { ok: true, value };
115
+ }
116
+
70
117
  /**
71
118
  * Parse boolean query param (true if "true" or "1", false otherwise).
72
119
  */
@@ -160,3 +207,128 @@ export async function handleGraph(
160
207
 
161
208
  return jsonResponse(result.value satisfies GraphResponse);
162
209
  }
210
+
211
+ /**
212
+ * POST /api/graph/query
213
+ * Returns bounded typed-edge traversal from one resolved root document.
214
+ */
215
+ export async function handleGraphQuery(
216
+ store: SqliteAdapter,
217
+ config: Config,
218
+ req: Request
219
+ ): Promise<Response> {
220
+ let body: GraphQueryRequestBody;
221
+ try {
222
+ body = (await req.json()) as GraphQueryRequestBody;
223
+ } catch {
224
+ return errorResponse("VALIDATION", "Invalid JSON body", 400);
225
+ }
226
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
227
+ return errorResponse("VALIDATION", "JSON body must be an object", 400);
228
+ }
229
+
230
+ const docRef = body.doc ?? body.root;
231
+ if (!docRef || typeof docRef !== "string") {
232
+ return errorResponse("VALIDATION", "Missing or invalid doc", 400);
233
+ }
234
+ const rootRef = docRef.trim();
235
+ if (!rootRef) {
236
+ return errorResponse("VALIDATION", "doc cannot be empty", 400);
237
+ }
238
+
239
+ const direction = body.direction ?? "both";
240
+ if (!["out", "in", "both"].includes(direction)) {
241
+ return errorResponse(
242
+ "VALIDATION",
243
+ "direction must be 'out', 'in', or 'both'",
244
+ 400
245
+ );
246
+ }
247
+
248
+ if (body.edgeType !== undefined && typeof body.edgeType !== "string") {
249
+ return errorResponse("VALIDATION", "edgeType must be a string", 400);
250
+ }
251
+ if (body.relation !== undefined && typeof body.relation !== "string") {
252
+ return errorResponse("VALIDATION", "relation must be a string", 400);
253
+ }
254
+ const edgeTypeValue = body.edgeType?.trim();
255
+ const relationValue = body.relation?.trim();
256
+ const edgeType = edgeTypeValue || relationValue || undefined;
257
+ if (
258
+ (body.edgeType !== undefined || body.relation !== undefined) &&
259
+ !edgeType
260
+ ) {
261
+ return errorResponse(
262
+ "VALIDATION",
263
+ "edgeType/relation cannot be empty",
264
+ 400
265
+ );
266
+ }
267
+ if (edgeTypeValue && relationValue && edgeTypeValue !== relationValue) {
268
+ return errorResponse(
269
+ "VALIDATION",
270
+ "edgeType and relation are aliases and must match when both are provided",
271
+ 400
272
+ );
273
+ }
274
+
275
+ const maxDepthResult = parseBodyPositiveInt(
276
+ "maxDepth",
277
+ body.maxDepth ?? body.depth,
278
+ 2,
279
+ 1,
280
+ 6
281
+ );
282
+ if (!maxDepthResult.ok) {
283
+ return errorResponse("VALIDATION", maxDepthResult.message, 400);
284
+ }
285
+ const maxNodesResult = parseBodyPositiveInt(
286
+ "maxNodes",
287
+ body.maxNodes,
288
+ 100,
289
+ 1,
290
+ 1000
291
+ );
292
+ if (!maxNodesResult.ok) {
293
+ return errorResponse("VALIDATION", maxNodesResult.message, 400);
294
+ }
295
+ const frontierLimitResult = parseBodyPositiveInt(
296
+ "frontierLimit",
297
+ body.frontierLimit,
298
+ 100,
299
+ 1,
300
+ 1000
301
+ );
302
+ if (!frontierLimitResult.ok) {
303
+ return errorResponse("VALIDATION", frontierLimitResult.message, 400);
304
+ }
305
+ const visitedLimitResult = parseBodyPositiveInt(
306
+ "visitedLimit",
307
+ body.visitedLimit,
308
+ 500,
309
+ 1,
310
+ 5000
311
+ );
312
+ if (!visitedLimitResult.ok) {
313
+ return errorResponse("VALIDATION", visitedLimitResult.message, 400);
314
+ }
315
+
316
+ const result = await diagnoseGraphQuery(store, rootRef, {
317
+ direction,
318
+ edgeType,
319
+ maxDepth: maxDepthResult.value,
320
+ maxNodes: maxNodesResult.value,
321
+ frontierLimit: frontierLimitResult.value,
322
+ visitedLimit: visitedLimitResult.value,
323
+ contentTypeRules: normalizeContentTypes(config.contentTypes ?? []).rules,
324
+ });
325
+ if (!result.success) {
326
+ return errorResponse(
327
+ result.isValidation ? "VALIDATION" : "RUNTIME",
328
+ result.error,
329
+ result.isValidation ? 400 : 500
330
+ );
331
+ }
332
+
333
+ return jsonResponse(result.data satisfies GraphQueryResponse);
334
+ }
@@ -46,6 +46,7 @@ import {
46
46
  handlePublishExport,
47
47
  handlePresets,
48
48
  handleQuery,
49
+ handleQueryDiagnose,
49
50
  handleRefactorPlan,
50
51
  handleRenameDoc,
51
52
  handleRevealDoc,
@@ -58,7 +59,7 @@ import {
58
59
  handleUpdateCollection,
59
60
  handleUpdateDoc,
60
61
  } from "./routes/api";
61
- import { handleGraph } from "./routes/graph";
62
+ import { handleGraph, handleGraphQuery } from "./routes/graph";
62
63
  import {
63
64
  handleDocBacklinks,
64
65
  handleDocLinks,
@@ -489,6 +490,17 @@ export async function startServer(
489
490
  );
490
491
  },
491
492
  },
493
+ "/api/query/diagnose": {
494
+ POST: async (req: Request) => {
495
+ if (!isRequestAllowed(req, port)) {
496
+ return withSecurityHeaders(forbiddenResponse(), isDev);
497
+ }
498
+ return withSecurityHeaders(
499
+ await handleQueryDiagnose(ctxHolder.current, req),
500
+ isDev
501
+ );
502
+ },
503
+ },
492
504
  "/api/ask": {
493
505
  POST: async (req: Request) => {
494
506
  if (!isRequestAllowed(req, port)) {
@@ -653,6 +665,17 @@ export async function startServer(
653
665
  return withSecurityHeaders(await handleGraph(store, url), isDev);
654
666
  },
655
667
  },
668
+ "/api/graph/query": {
669
+ POST: async (req: Request) => {
670
+ if (!isRequestAllowed(req, port)) {
671
+ return withSecurityHeaders(forbiddenResponse(), isDev);
672
+ }
673
+ return withSecurityHeaders(
674
+ await handleGraphQuery(store, ctxHolder.config, req),
675
+ isDev
676
+ );
677
+ },
678
+ },
656
679
  },
657
680
  });
658
681
  } catch (e) {
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Migration: typed semantic document edges.
3
+ *
4
+ * @module src/store/migrations/010-typed-edges
5
+ */
6
+
7
+ import type { Database } from "bun:sqlite";
8
+
9
+ import type { Migration } from "./runner";
10
+
11
+ const TABLE_INFO_DOCUMENTS = "PRAGMA table_info(documents)";
12
+
13
+ interface TableInfoRow {
14
+ name: string;
15
+ }
16
+
17
+ function getDocumentColumns(db: Database): Set<string> {
18
+ const rows = db.query<TableInfoRow, []>(TABLE_INFO_DOCUMENTS).all();
19
+ return new Set(rows.map((row) => row.name));
20
+ }
21
+
22
+ export const migration: Migration = {
23
+ version: 10,
24
+ name: "typed_edges",
25
+
26
+ up(db: Database): void {
27
+ const columns = getDocumentColumns(db);
28
+ if (!columns.has("content_type_source")) {
29
+ db.exec("ALTER TABLE documents ADD COLUMN content_type_source TEXT");
30
+ }
31
+
32
+ db.exec(`
33
+ CREATE TABLE IF NOT EXISTS doc_edges (
34
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
35
+ src_doc_id INTEGER NOT NULL,
36
+ dst_doc_id INTEGER NOT NULL,
37
+ edge_type TEXT NOT NULL,
38
+ confidence TEXT NOT NULL CHECK (confidence IN ('parsed', 'configured', 'manual', 'inferred')),
39
+ source TEXT NOT NULL CHECK (source IN ('wikilink', 'markdown-link', 'frontmatter-relation')),
40
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
41
+ UNIQUE(src_doc_id, dst_doc_id, edge_type, source),
42
+ FOREIGN KEY (src_doc_id) REFERENCES documents(id) ON DELETE CASCADE,
43
+ FOREIGN KEY (dst_doc_id) REFERENCES documents(id) ON DELETE CASCADE
44
+ )
45
+ `);
46
+
47
+ db.exec(`
48
+ CREATE INDEX IF NOT EXISTS idx_doc_edges_src_type
49
+ ON doc_edges(src_doc_id, edge_type)
50
+ `);
51
+ db.exec(`
52
+ CREATE INDEX IF NOT EXISTS idx_doc_edges_dst_type
53
+ ON doc_edges(dst_doc_id, edge_type)
54
+ `);
55
+ db.exec(`
56
+ CREATE INDEX IF NOT EXISTS idx_documents_content_type_source
57
+ ON documents(content_type_source)
58
+ `);
59
+ },
60
+
61
+ down(db: Database): void {
62
+ db.exec("DROP INDEX IF EXISTS idx_documents_content_type_source");
63
+ db.exec("DROP INDEX IF EXISTS idx_doc_edges_dst_type");
64
+ db.exec("DROP INDEX IF EXISTS idx_doc_edges_src_type");
65
+ // Keep table/column on rollback; SQLite column drop requires rebuild.
66
+ },
67
+ };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Migration: covering indexes for bounded typed-edge traversal.
3
+ *
4
+ * @module src/store/migrations/011-doc-edge-traversal-indexes
5
+ */
6
+
7
+ import type { Database } from "bun:sqlite";
8
+
9
+ import type { Migration } from "./runner";
10
+
11
+ export const migration: Migration = {
12
+ version: 11,
13
+ name: "doc_edge_traversal_indexes",
14
+
15
+ up(db: Database): void {
16
+ db.exec(`
17
+ CREATE INDEX IF NOT EXISTS idx_doc_edges_src_type_dst_id
18
+ ON doc_edges(src_doc_id, edge_type, dst_doc_id, id)
19
+ `);
20
+ db.exec(`
21
+ CREATE INDEX IF NOT EXISTS idx_doc_edges_dst_type_src_id
22
+ ON doc_edges(dst_doc_id, edge_type, src_doc_id, id)
23
+ `);
24
+ },
25
+
26
+ down(db: Database): void {
27
+ db.exec("DROP INDEX IF EXISTS idx_doc_edges_dst_type_src_id");
28
+ db.exec("DROP INDEX IF EXISTS idx_doc_edges_src_type_dst_id");
29
+ },
30
+ };
@@ -23,6 +23,8 @@ import { migration as m006 } from "./006-document-metadata";
23
23
  import { migration as m007 } from "./007-document-date-fields";
24
24
  import { migration as m008 } from "./008-vector-fingerprints";
25
25
  import { migration as m009 } from "./009-content-type-rule-fingerprint";
26
+ import { migration as m010 } from "./010-typed-edges";
27
+ import { migration as m011 } from "./011-doc-edge-traversal-indexes";
26
28
 
27
29
  /** All migrations in order */
28
30
  export const migrations = [
@@ -35,4 +37,6 @@ export const migrations = [
35
37
  m007,
36
38
  m008,
37
39
  m009,
40
+ m010,
41
+ m011,
38
42
  ];