@gmickel/gno 1.9.0 → 1.10.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/assets/skill/SKILL.md +28 -16
- package/assets/skill/cli-reference.md +30 -0
- package/assets/skill/examples.md +20 -0
- package/assets/skill/mcp-reference.md +7 -1
- package/package.json +1 -1
- package/src/cli/commands/graph.ts +89 -2
- package/src/cli/commands/links.ts +237 -54
- package/src/cli/commands/query.ts +212 -0
- package/src/cli/commands/ref-parser.ts +8 -103
- package/src/cli/options.ts +4 -0
- package/src/cli/program.ts +176 -9
- package/src/config/content-types.ts +14 -0
- package/src/core/graph-query.ts +137 -0
- package/src/core/graph-resolver.ts +117 -0
- package/src/core/ref-parser.ts +145 -0
- package/src/ingestion/frontmatter.ts +95 -2
- package/src/ingestion/sync.ts +281 -1
- package/src/mcp/tools/get.ts +1 -1
- package/src/mcp/tools/index.ts +89 -1
- package/src/mcp/tools/links.ts +83 -1
- package/src/mcp/tools/multi-get.ts +1 -1
- package/src/mcp/tools/query.ts +207 -0
- package/src/pipeline/diagnose.ts +302 -0
- package/src/pipeline/filters.ts +119 -0
- package/src/pipeline/hybrid.ts +90 -17
- package/src/pipeline/types.ts +32 -0
- package/src/publish/export-service.ts +1 -1
- package/src/sdk/documents.ts +2 -2
- package/src/serve/routes/api.ts +194 -0
- package/src/serve/routes/graph.ts +173 -1
- package/src/serve/server.ts +24 -1
- package/src/store/migrations/010-typed-edges.ts +67 -0
- package/src/store/migrations/011-doc-edge-traversal-indexes.ts +30 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +826 -102
- package/src/store/types.ts +141 -0
package/src/sdk/documents.ts
CHANGED
|
@@ -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:\/\/[^/]+\//;
|
package/src/serve/routes/api.ts
CHANGED
|
@@ -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 {
|
|
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
|
+
}
|
package/src/serve/server.ts
CHANGED
|
@@ -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
|
];
|