@gmickel/gno 1.21.0 → 1.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -2
- package/assets/skill/SKILL.md +15 -0
- package/package.json +2 -1
- package/spec/cli.md +80 -20
- package/spec/evals-agentic.md +83 -0
- package/spec/evals.md +6 -0
- package/spec/mcp.md +18 -0
- package/spec/output-schemas/publish-artifact.schema.json +284 -0
- package/spec/output-schemas/query-diagnose-v1.schema.json +123 -0
- package/spec/output-schemas/query-diagnose.schema.json +89 -2
- package/src/app/context-runtime-types.ts +3 -0
- package/src/app/context-runtime.ts +1 -0
- package/src/app/context-surface.ts +4 -2
- package/src/cli/commands/ask.ts +31 -20
- package/src/cli/commands/context-build.ts +17 -7
- package/src/cli/commands/query.ts +58 -37
- package/src/cli/commands/search.ts +29 -19
- package/src/cli/commands/vsearch.ts +31 -22
- package/src/cli/options.ts +39 -0
- package/src/cli/program.ts +48 -0
- package/src/config/defaults.ts +10 -1
- package/src/config/types.ts +71 -0
- package/src/core/project-affinity-surface.ts +114 -0
- package/src/core/project-affinity.ts +330 -0
- package/src/core/validation.ts +20 -1
- package/src/mcp/tools/ask.ts +10 -1
- package/src/mcp/tools/context.ts +18 -0
- package/src/mcp/tools/index.ts +13 -2
- package/src/mcp/tools/query.ts +12 -0
- package/src/mcp/tools/search.ts +7 -0
- package/src/mcp/tools/vsearch.ts +7 -0
- package/src/pipeline/diagnose.ts +48 -3
- package/src/pipeline/explain.ts +54 -13
- package/src/pipeline/hybrid.ts +100 -59
- package/src/pipeline/project-affinity.ts +162 -0
- package/src/pipeline/search.ts +76 -10
- package/src/pipeline/types.ts +9 -0
- package/src/pipeline/vsearch.ts +117 -91
- package/src/publish/artifact-validation.ts +259 -0
- package/src/publish/artifact.ts +234 -118
- package/src/publish/export-service.ts +5 -9
- package/src/publish/metadata.ts +195 -0
- package/src/sdk/client.ts +80 -20
- package/src/sdk/index.ts +2 -0
- package/src/sdk/types.ts +20 -7
- package/src/serve/context-capsule.ts +18 -1
- package/src/serve/routes/api.ts +69 -0
|
@@ -8,7 +8,7 @@ import type { Collection } from "../config/types";
|
|
|
8
8
|
import type { DocumentRow, StorePort, TagRow } from "../store/types";
|
|
9
9
|
|
|
10
10
|
import { parseRef } from "../core/ref-parser";
|
|
11
|
-
import { parseFrontmatter } from "../ingestion/frontmatter";
|
|
11
|
+
import { parseFrontmatter, stripFrontmatter } from "../ingestion/frontmatter";
|
|
12
12
|
import { getContentBatch } from "../store/content-batch";
|
|
13
13
|
import {
|
|
14
14
|
buildEncryptedPublishArtifact,
|
|
@@ -186,11 +186,11 @@ async function exportCollectionArtifact(
|
|
|
186
186
|
if (isPublishDisabledByFrontmatter(rawMarkdown)) {
|
|
187
187
|
continue;
|
|
188
188
|
}
|
|
189
|
+
const frontmatter = parseFrontmatter(rawMarkdown).metadata;
|
|
189
190
|
const sanitized = sanitizeObsidianMarkdown(rawMarkdown);
|
|
190
191
|
warnings.push(...sanitized.warnings);
|
|
191
|
-
const markdown = sanitized.markdown;
|
|
192
|
+
const markdown = stripFrontmatter(sanitized.markdown);
|
|
192
193
|
const tags = tagsByDocId.get(doc.id) ?? [];
|
|
193
|
-
const frontmatter = parseFrontmatter(markdown).metadata;
|
|
194
194
|
const title = deriveExportedTitle(doc);
|
|
195
195
|
notes.push({
|
|
196
196
|
markdown,
|
|
@@ -240,7 +240,6 @@ async function exportCollectionArtifact(
|
|
|
240
240
|
encryptedPayload: encrypted.encryptedPayload,
|
|
241
241
|
routeSlug,
|
|
242
242
|
secretToken: encrypted.secretToken,
|
|
243
|
-
source: collection.name,
|
|
244
243
|
sourceType: "collection",
|
|
245
244
|
});
|
|
246
245
|
}
|
|
@@ -249,7 +248,6 @@ async function exportCollectionArtifact(
|
|
|
249
248
|
homeNoteSlug: chooseHomeNoteSlug(notes),
|
|
250
249
|
notes,
|
|
251
250
|
routeSlug,
|
|
252
|
-
source: collection.name,
|
|
253
251
|
sourceType: "collection",
|
|
254
252
|
summary,
|
|
255
253
|
title,
|
|
@@ -274,11 +272,11 @@ async function exportDocumentArtifact(
|
|
|
274
272
|
`Refused to export: ${doc.uri} has publish: false in frontmatter`
|
|
275
273
|
);
|
|
276
274
|
}
|
|
275
|
+
const frontmatter = parseFrontmatter(rawMarkdown).metadata;
|
|
277
276
|
const sanitized = sanitizeObsidianMarkdown(rawMarkdown);
|
|
278
277
|
warnings.push(...sanitized.warnings);
|
|
279
|
-
const markdown = sanitized.markdown;
|
|
278
|
+
const markdown = stripFrontmatter(sanitized.markdown);
|
|
280
279
|
const tags = await loadDocumentTags(store, doc);
|
|
281
|
-
const frontmatter = parseFrontmatter(markdown).metadata;
|
|
282
280
|
const title = options.title ?? deriveExportedTitle(doc);
|
|
283
281
|
const summary =
|
|
284
282
|
options.summary ?? deriveExportedSummary(markdown, frontmatter);
|
|
@@ -315,7 +313,6 @@ async function exportDocumentArtifact(
|
|
|
315
313
|
encryptedPayload: encrypted.encryptedPayload,
|
|
316
314
|
routeSlug,
|
|
317
315
|
secretToken: encrypted.secretToken,
|
|
318
|
-
source: doc.uri,
|
|
319
316
|
sourceType: "note",
|
|
320
317
|
});
|
|
321
318
|
}
|
|
@@ -331,7 +328,6 @@ async function exportDocumentArtifact(
|
|
|
331
328
|
},
|
|
332
329
|
],
|
|
333
330
|
routeSlug,
|
|
334
|
-
source: doc.uri,
|
|
335
331
|
sourceType: "note",
|
|
336
332
|
summary,
|
|
337
333
|
title,
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reader-safe publish metadata projection.
|
|
3
|
+
*
|
|
4
|
+
* @module src/publish/metadata
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { DocumentRow, TagRow } from "../store/types";
|
|
8
|
+
|
|
9
|
+
const ALLOWED_FRONTMATTER_METADATA_KEYS = new Set([
|
|
10
|
+
"audience",
|
|
11
|
+
"canonical",
|
|
12
|
+
"canonicalUrl",
|
|
13
|
+
"canonicalURL",
|
|
14
|
+
"coverAlt",
|
|
15
|
+
"coverImage",
|
|
16
|
+
"icon",
|
|
17
|
+
"image",
|
|
18
|
+
"layout",
|
|
19
|
+
"publishedAt",
|
|
20
|
+
"readingTime",
|
|
21
|
+
"series",
|
|
22
|
+
"seriesOrder",
|
|
23
|
+
"status",
|
|
24
|
+
"subtitle",
|
|
25
|
+
"theme",
|
|
26
|
+
"topic",
|
|
27
|
+
"topics",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const PUBLIC_URL_METADATA_KEYS = new Set([
|
|
31
|
+
"canonical",
|
|
32
|
+
"canonicalUrl",
|
|
33
|
+
"canonicalURL",
|
|
34
|
+
"coverImage",
|
|
35
|
+
"image",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const FORBIDDEN_URI_TOKEN_PATTERN =
|
|
39
|
+
/(?:^|[^a-z0-9+.-])(?:file:(?:\/\/)?|gno:\/\/)/iu;
|
|
40
|
+
const LOCAL_PATH_TOKEN_PATTERN =
|
|
41
|
+
/(?:^|[\s([{"'=,:;])(?:~[/\\]|[a-z]:[/\\]|\\\\[^\\/\s]+[/\\]|\/(?:Applications|bin|dev|etc|home|Library|mnt|opt|private|proc|root|srv|sys|System|tmp|Users|usr|var|Volumes)(?:[/\\]|$))/iu;
|
|
42
|
+
const LOCAL_HOSTNAME_SUFFIX_PATTERN =
|
|
43
|
+
/(?:^|\.)(?:home|internal|lan|local|localhost)$/iu;
|
|
44
|
+
|
|
45
|
+
const containsLocalReference = (value: string): boolean =>
|
|
46
|
+
FORBIDDEN_URI_TOKEN_PATTERN.test(value) ||
|
|
47
|
+
LOCAL_PATH_TOKEN_PATTERN.test(value);
|
|
48
|
+
|
|
49
|
+
const isNonPublicIpv4 = (hostname: string): boolean => {
|
|
50
|
+
if (!/^\d{1,3}(?:\.\d{1,3}){3}$/u.test(hostname)) return false;
|
|
51
|
+
const octets = hostname.split(".").map(Number);
|
|
52
|
+
if (octets.some((octet) => octet > 255)) return true;
|
|
53
|
+
const [first = 0, second = 0, third = 0] = octets;
|
|
54
|
+
return (
|
|
55
|
+
first === 0 ||
|
|
56
|
+
first === 10 ||
|
|
57
|
+
first === 127 ||
|
|
58
|
+
first >= 224 ||
|
|
59
|
+
(first === 100 && second >= 64 && second <= 127) ||
|
|
60
|
+
(first === 169 && second === 254) ||
|
|
61
|
+
(first === 172 && second >= 16 && second <= 31) ||
|
|
62
|
+
(first === 192 && second === 0 && third === 0) ||
|
|
63
|
+
(first === 192 && second === 0 && third === 2) ||
|
|
64
|
+
(first === 192 && second === 168) ||
|
|
65
|
+
(first === 198 && (second === 18 || second === 19)) ||
|
|
66
|
+
(first === 198 && second === 51 && third === 100) ||
|
|
67
|
+
(first === 203 && second === 0 && third === 113)
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const isNonPublicIpv6 = (hostname: string): boolean => {
|
|
72
|
+
const normalized = hostname.replace(/^\[|\]$/gu, "").toLowerCase();
|
|
73
|
+
if (!normalized.includes(":")) return false;
|
|
74
|
+
return (
|
|
75
|
+
normalized === "::" ||
|
|
76
|
+
normalized.startsWith("::") ||
|
|
77
|
+
normalized.startsWith("fc") ||
|
|
78
|
+
normalized.startsWith("fd") ||
|
|
79
|
+
/^fe[89ab]/u.test(normalized) ||
|
|
80
|
+
normalized.startsWith("ff") ||
|
|
81
|
+
normalized.startsWith("2001:db8:")
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const isPublicHttpUrl = (value: string): boolean => {
|
|
86
|
+
let url: URL;
|
|
87
|
+
try {
|
|
88
|
+
url = new URL(value);
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (
|
|
94
|
+
(url.protocol !== "http:" && url.protocol !== "https:") ||
|
|
95
|
+
url.username.length > 0 ||
|
|
96
|
+
url.password.length > 0
|
|
97
|
+
) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const hostname = url.hostname.replace(/\.$/u, "").toLowerCase();
|
|
102
|
+
if (
|
|
103
|
+
hostname.length === 0 ||
|
|
104
|
+
LOCAL_HOSTNAME_SUFFIX_PATTERN.test(hostname) ||
|
|
105
|
+
isNonPublicIpv4(hostname) ||
|
|
106
|
+
isNonPublicIpv6(hostname)
|
|
107
|
+
) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const isIpLiteral =
|
|
112
|
+
hostname.includes(":") || /^\d+(?:\.\d+){3}$/u.test(hostname);
|
|
113
|
+
return isIpLiteral || hostname.includes(".");
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const isSafeMetadataValue = (key: string, value: string): boolean => {
|
|
117
|
+
if (containsLocalReference(value)) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
if (
|
|
121
|
+
PUBLIC_URL_METADATA_KEYS.has(key) ||
|
|
122
|
+
(key === "icon" && /^https?:\/\//iu.test(value))
|
|
123
|
+
) {
|
|
124
|
+
return isPublicHttpUrl(value);
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const filterReaderSafeMetadata = (
|
|
130
|
+
metadata: Record<string, string | string[]>
|
|
131
|
+
): Record<string, string | string[]> => {
|
|
132
|
+
const result: Record<string, string | string[]> = {};
|
|
133
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
134
|
+
if (typeof value === "string") {
|
|
135
|
+
if (isSafeMetadataValue(key, value)) {
|
|
136
|
+
result[key] = value;
|
|
137
|
+
}
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const safeValues = value.filter((entry) => isSafeMetadataValue(key, entry));
|
|
142
|
+
if (safeValues.length > 0) {
|
|
143
|
+
result[key] = safeValues;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export const buildExportedMetadata = (
|
|
150
|
+
doc: Pick<
|
|
151
|
+
DocumentRow,
|
|
152
|
+
"author" | "categories" | "contentType" | "frontmatterDate" | "languageHint"
|
|
153
|
+
>,
|
|
154
|
+
parsedFrontmatter: Record<string, unknown>,
|
|
155
|
+
tags: TagRow[]
|
|
156
|
+
): Record<string, string | string[]> => {
|
|
157
|
+
const metadata: Record<string, string | string[]> = {};
|
|
158
|
+
|
|
159
|
+
if (doc.author) metadata.author = doc.author;
|
|
160
|
+
if (doc.contentType) metadata.contentType = doc.contentType;
|
|
161
|
+
if (doc.languageHint) metadata.language = doc.languageHint;
|
|
162
|
+
if (doc.frontmatterDate) metadata.date = doc.frontmatterDate;
|
|
163
|
+
if (doc.categories?.length) metadata.categories = doc.categories;
|
|
164
|
+
|
|
165
|
+
const tagValues = tags.map((tag) => tag.tag);
|
|
166
|
+
if (tagValues.length) metadata.tags = tagValues;
|
|
167
|
+
|
|
168
|
+
for (const [key, value] of Object.entries(parsedFrontmatter)) {
|
|
169
|
+
if (
|
|
170
|
+
key === "tags" ||
|
|
171
|
+
key === "title" ||
|
|
172
|
+
key === "summary" ||
|
|
173
|
+
!ALLOWED_FRONTMATTER_METADATA_KEYS.has(key)
|
|
174
|
+
) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (
|
|
178
|
+
typeof value === "string" &&
|
|
179
|
+
value.trim() &&
|
|
180
|
+
isSafeMetadataValue(key, value.trim())
|
|
181
|
+
) {
|
|
182
|
+
metadata[key] = value.trim();
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (Array.isArray(value)) {
|
|
186
|
+
const cleaned = value
|
|
187
|
+
.filter((entry): entry is string => typeof entry === "string")
|
|
188
|
+
.map((entry) => entry.trim())
|
|
189
|
+
.filter((entry) => entry.length > 0 && isSafeMetadataValue(key, entry));
|
|
190
|
+
if (cleaned.length > 0) metadata[key] = cleaned;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return filterReaderSafeMetadata(metadata);
|
|
195
|
+
};
|
package/src/sdk/client.ts
CHANGED
|
@@ -38,6 +38,7 @@ import type {
|
|
|
38
38
|
GnoQueryOptions,
|
|
39
39
|
GnoRefactorNoteResult,
|
|
40
40
|
GnoRenameNoteOptions,
|
|
41
|
+
GnoSearchOptions,
|
|
41
42
|
GnoUpdateOptions,
|
|
42
43
|
GnoVectorSearchOptions,
|
|
43
44
|
KnowledgeChangesResult,
|
|
@@ -97,6 +98,10 @@ import {
|
|
|
97
98
|
} from "../core/knowledge-delta";
|
|
98
99
|
import { resolveNoteCreatePlan } from "../core/note-creation";
|
|
99
100
|
import { resolveNotePreset } from "../core/note-presets";
|
|
101
|
+
import {
|
|
102
|
+
ProjectAffinityInputError,
|
|
103
|
+
resolveRemoteProjectAffinity,
|
|
104
|
+
} from "../core/project-affinity-surface";
|
|
100
105
|
import { RetrievalTraceManagementService } from "../core/retrieval-trace-management";
|
|
101
106
|
import {
|
|
102
107
|
finishRetrievalTraceAfterError,
|
|
@@ -160,6 +165,20 @@ interface RuntimePorts {
|
|
|
160
165
|
vectorIndex: VectorIndexPort | null;
|
|
161
166
|
}
|
|
162
167
|
|
|
168
|
+
const resolveSdkProjectAffinity = async (
|
|
169
|
+
config: Config,
|
|
170
|
+
projectHints: readonly string[] | undefined
|
|
171
|
+
) => {
|
|
172
|
+
try {
|
|
173
|
+
return await resolveRemoteProjectAffinity(config, projectHints);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (error instanceof ProjectAffinityInputError) {
|
|
176
|
+
throw sdkError("VALIDATION", error.message);
|
|
177
|
+
}
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
163
182
|
function unwrapStore<T>(
|
|
164
183
|
result: StoreResult<T>,
|
|
165
184
|
code: "STORE" | "RUNTIME" = "STORE"
|
|
@@ -471,17 +490,22 @@ class GnoClientImpl implements GnoClient {
|
|
|
471
490
|
|
|
472
491
|
async search(
|
|
473
492
|
query: string,
|
|
474
|
-
options:
|
|
493
|
+
options: GnoSearchOptions = {}
|
|
475
494
|
): Promise<SearchResults> {
|
|
476
495
|
this.assertOpen();
|
|
477
496
|
let traceSession: RetrievalTraceSession | null = null;
|
|
478
497
|
try {
|
|
498
|
+
const { projectHints, ...searchOptions } = options;
|
|
499
|
+
const projectAffinity = await resolveSdkProjectAffinity(
|
|
500
|
+
this.config,
|
|
501
|
+
projectHints
|
|
502
|
+
);
|
|
479
503
|
traceSession = unwrapStore(
|
|
480
504
|
await startRetrievalTraceRequest({
|
|
481
505
|
store: this.store,
|
|
482
506
|
config: this.config,
|
|
483
507
|
query,
|
|
484
|
-
filters: retrievalTraceFilters(
|
|
508
|
+
filters: retrievalTraceFilters(searchOptions),
|
|
485
509
|
pipeline: "bm25",
|
|
486
510
|
indexName: this.indexName,
|
|
487
511
|
})
|
|
@@ -490,7 +514,8 @@ class GnoClientImpl implements GnoClient {
|
|
|
490
514
|
this.decorateSearchResults(
|
|
491
515
|
unwrapStore(
|
|
492
516
|
await searchBm25(this.store, query, {
|
|
493
|
-
...
|
|
517
|
+
...searchOptions,
|
|
518
|
+
projectAffinity,
|
|
494
519
|
traceSession: traceSession ?? undefined,
|
|
495
520
|
})
|
|
496
521
|
)
|
|
@@ -513,6 +538,11 @@ class GnoClientImpl implements GnoClient {
|
|
|
513
538
|
let traceSession: RetrievalTraceSession | null = null;
|
|
514
539
|
|
|
515
540
|
try {
|
|
541
|
+
const { projectHints, ...searchOptions } = options;
|
|
542
|
+
const projectAffinity = await resolveSdkProjectAffinity(
|
|
543
|
+
this.config,
|
|
544
|
+
projectHints
|
|
545
|
+
);
|
|
516
546
|
const embedUri = resolveModelUri(
|
|
517
547
|
this.config,
|
|
518
548
|
"embed",
|
|
@@ -524,7 +554,7 @@ class GnoClientImpl implements GnoClient {
|
|
|
524
554
|
store: this.store,
|
|
525
555
|
config: this.config,
|
|
526
556
|
query,
|
|
527
|
-
filters: retrievalTraceFilters(
|
|
557
|
+
filters: retrievalTraceFilters(searchOptions),
|
|
528
558
|
pipeline: "vector",
|
|
529
559
|
indexName: this.indexName,
|
|
530
560
|
modelUris: [embedUri],
|
|
@@ -564,7 +594,11 @@ class GnoClientImpl implements GnoClient {
|
|
|
564
594
|
},
|
|
565
595
|
query,
|
|
566
596
|
new Float32Array(queryEmbedResult.value),
|
|
567
|
-
{
|
|
597
|
+
{
|
|
598
|
+
...searchOptions,
|
|
599
|
+
projectAffinity,
|
|
600
|
+
traceSession: traceSession ?? undefined,
|
|
601
|
+
}
|
|
568
602
|
)
|
|
569
603
|
)
|
|
570
604
|
),
|
|
@@ -628,12 +662,17 @@ class GnoClientImpl implements GnoClient {
|
|
|
628
662
|
let traceSession: RetrievalTraceSession | null = null;
|
|
629
663
|
|
|
630
664
|
try {
|
|
665
|
+
const { projectHints, ...queryOptions } = options;
|
|
666
|
+
const projectAffinity = await resolveSdkProjectAffinity(
|
|
667
|
+
this.config,
|
|
668
|
+
projectHints
|
|
669
|
+
);
|
|
631
670
|
traceSession = unwrapStore(
|
|
632
671
|
await startRetrievalTraceRequest({
|
|
633
672
|
store: this.store,
|
|
634
673
|
config: this.config,
|
|
635
674
|
query,
|
|
636
|
-
filters: retrievalTraceFilters(
|
|
675
|
+
filters: retrievalTraceFilters(queryOptions),
|
|
637
676
|
pipeline: "hybrid",
|
|
638
677
|
indexName: this.indexName,
|
|
639
678
|
modelUris: [embedUri, expandUri, rerankUri].filter(
|
|
@@ -664,7 +703,11 @@ class GnoClientImpl implements GnoClient {
|
|
|
664
703
|
rerankPort: ports.rerankPort,
|
|
665
704
|
},
|
|
666
705
|
query,
|
|
667
|
-
{
|
|
706
|
+
{
|
|
707
|
+
...queryOptions,
|
|
708
|
+
projectAffinity,
|
|
709
|
+
traceSession: traceSession ?? undefined,
|
|
710
|
+
}
|
|
668
711
|
)
|
|
669
712
|
)
|
|
670
713
|
),
|
|
@@ -739,12 +782,17 @@ class GnoClientImpl implements GnoClient {
|
|
|
739
782
|
let traceSession: RetrievalTraceSession | null = null;
|
|
740
783
|
|
|
741
784
|
try {
|
|
785
|
+
const { projectHints, ...askOptions } = options;
|
|
786
|
+
const projectAffinity = await resolveSdkProjectAffinity(
|
|
787
|
+
this.config,
|
|
788
|
+
projectHints
|
|
789
|
+
);
|
|
742
790
|
traceSession = unwrapStore(
|
|
743
791
|
await startRetrievalTraceRequest({
|
|
744
792
|
store: this.store,
|
|
745
793
|
config: this.config,
|
|
746
794
|
query,
|
|
747
|
-
filters: retrievalTraceFilters(
|
|
795
|
+
filters: retrievalTraceFilters(askOptions),
|
|
748
796
|
pipeline: "ask",
|
|
749
797
|
indexName: this.indexName,
|
|
750
798
|
modelUris: [embedUri, expandUri, answerUri, rerankUri].filter(
|
|
@@ -777,16 +825,21 @@ class GnoClientImpl implements GnoClient {
|
|
|
777
825
|
}
|
|
778
826
|
|
|
779
827
|
if (verificationRequested && ports.answerPort) {
|
|
780
|
-
const verified = await buildVerifiedAsk(
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
828
|
+
const verified = await buildVerifiedAsk(
|
|
829
|
+
query,
|
|
830
|
+
{ ...askOptions, projectAffinity },
|
|
831
|
+
{
|
|
832
|
+
store: this.store,
|
|
833
|
+
config: this.config,
|
|
834
|
+
indexName: this.indexName,
|
|
835
|
+
vectorIndex: ports.vectorIndex,
|
|
836
|
+
embedPort: ports.embedPort,
|
|
837
|
+
rerankPort: ports.rerankPort,
|
|
838
|
+
genPort: ports.answerPort,
|
|
839
|
+
projectAffinity,
|
|
840
|
+
traceSession: traceSession ?? undefined,
|
|
841
|
+
}
|
|
842
|
+
);
|
|
790
843
|
if (traceSession) {
|
|
791
844
|
unwrapStore(
|
|
792
845
|
await traceSession.finish(
|
|
@@ -831,6 +884,7 @@ class GnoClientImpl implements GnoClient {
|
|
|
831
884
|
noRerank: options.noRerank,
|
|
832
885
|
candidateLimit: options.candidateLimit,
|
|
833
886
|
queryLanguageHint: options.queryLanguageHint,
|
|
887
|
+
projectAffinity,
|
|
834
888
|
traceSession: traceSession ?? undefined,
|
|
835
889
|
}
|
|
836
890
|
)
|
|
@@ -918,8 +972,9 @@ class GnoClientImpl implements GnoClient {
|
|
|
918
972
|
|
|
919
973
|
async context(input: GnoContextInput): Promise<GnoContextResult> {
|
|
920
974
|
this.assertOpen();
|
|
975
|
+
const { projectHints, ...contextInput } = input;
|
|
921
976
|
validateContextCapsuleBuildInput(
|
|
922
|
-
{ ...
|
|
977
|
+
{ ...contextInput, indexName: this.indexName },
|
|
923
978
|
this.indexName,
|
|
924
979
|
this.config.collections.map((collection) => collection.name)
|
|
925
980
|
);
|
|
@@ -935,6 +990,10 @@ class GnoClientImpl implements GnoClient {
|
|
|
935
990
|
let ports: RuntimePorts | null = null;
|
|
936
991
|
let traceSession: RetrievalTraceSession | null = null;
|
|
937
992
|
try {
|
|
993
|
+
const projectAffinity = await resolveSdkProjectAffinity(
|
|
994
|
+
this.config,
|
|
995
|
+
projectHints
|
|
996
|
+
);
|
|
938
997
|
traceSession = unwrapStore(
|
|
939
998
|
await startRetrievalTraceRequest({
|
|
940
999
|
store: this.store,
|
|
@@ -968,7 +1027,7 @@ class GnoClientImpl implements GnoClient {
|
|
|
968
1027
|
collection,
|
|
969
1028
|
});
|
|
970
1029
|
const capsule = await buildContextCapsule(
|
|
971
|
-
{ ...
|
|
1030
|
+
{ ...contextInput, indexName: this.indexName },
|
|
972
1031
|
{
|
|
973
1032
|
store: this.store,
|
|
974
1033
|
config: this.config,
|
|
@@ -976,6 +1035,7 @@ class GnoClientImpl implements GnoClient {
|
|
|
976
1035
|
vectorIndex: ports.vectorIndex,
|
|
977
1036
|
embedPort: ports.embedPort,
|
|
978
1037
|
rerankPort: ports.rerankPort,
|
|
1038
|
+
projectAffinity,
|
|
979
1039
|
traceSession: traceSession ?? undefined,
|
|
980
1040
|
}
|
|
981
1041
|
);
|
package/src/sdk/index.ts
CHANGED
package/src/sdk/types.ts
CHANGED
|
@@ -97,13 +97,26 @@ export interface GnoModelOverrides {
|
|
|
97
97
|
rerankModel?: string;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
export
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
export interface GnoProjectHintOptions {
|
|
101
|
+
/** Opaque caller project hints; never resolved against the server filesystem. */
|
|
102
|
+
projectHints?: string[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type GnoSearchOptions = Omit<SearchOptions, "projectAffinity"> &
|
|
106
|
+
GnoProjectHintOptions;
|
|
107
|
+
export type GnoQueryOptions = Omit<HybridSearchOptions, "projectAffinity"> &
|
|
108
|
+
GnoModelOverrides &
|
|
109
|
+
GnoProjectHintOptions;
|
|
110
|
+
export type GnoAskOptions = Omit<AskOptions, "projectAffinity"> &
|
|
111
|
+
GnoModelOverrides &
|
|
112
|
+
GnoProjectHintOptions;
|
|
113
|
+
export type GnoVectorSearchOptions = Omit<SearchOptions, "projectAffinity"> &
|
|
114
|
+
GnoProjectHintOptions & {
|
|
115
|
+
model?: string;
|
|
116
|
+
};
|
|
105
117
|
|
|
106
|
-
export type GnoContextInput = Omit<ContextCapsuleBuildInput, "indexName"
|
|
118
|
+
export type GnoContextInput = Omit<ContextCapsuleBuildInput, "indexName"> &
|
|
119
|
+
GnoProjectHintOptions;
|
|
107
120
|
export type GnoContextResult = ContextCapsuleV1;
|
|
108
121
|
export type GnoContextVerificationResult = ContextCapsuleVerification;
|
|
109
122
|
export type GnoContextErrorCode =
|
|
@@ -227,7 +240,7 @@ export interface GnoClient {
|
|
|
227
240
|
readonly configPath: string | null;
|
|
228
241
|
readonly configSource: "file" | "inline";
|
|
229
242
|
isOpen(): boolean;
|
|
230
|
-
search(query: string, options?:
|
|
243
|
+
search(query: string, options?: GnoSearchOptions): Promise<SearchResults>;
|
|
231
244
|
vsearch(
|
|
232
245
|
query: string,
|
|
233
246
|
options?: GnoVectorSearchOptions
|
|
@@ -21,6 +21,10 @@ import {
|
|
|
21
21
|
parseContextVerifySurfaceInput,
|
|
22
22
|
} from "../app/context-surface";
|
|
23
23
|
import { ContextCapsuleContractError } from "../core/context-capsule";
|
|
24
|
+
import {
|
|
25
|
+
ProjectAffinityInputError,
|
|
26
|
+
resolveRemoteProjectAffinity,
|
|
27
|
+
} from "../core/project-affinity-surface";
|
|
24
28
|
import { startRetrievalTraceRequest } from "../core/retrieval-trace-request";
|
|
25
29
|
import { withRetrievalTraceHeader } from "./retrieval-trace";
|
|
26
30
|
|
|
@@ -85,7 +89,7 @@ export const handleContextBuild = async (
|
|
|
85
89
|
): Promise<Response> => {
|
|
86
90
|
let traceSession: RetrievalTraceSession | null = null;
|
|
87
91
|
try {
|
|
88
|
-
const { input, format } = parseContextBuildSurfaceInput(
|
|
92
|
+
const { input, format, projectHints } = parseContextBuildSurfaceInput(
|
|
89
93
|
await parseJsonBody(request),
|
|
90
94
|
context.indexName
|
|
91
95
|
);
|
|
@@ -94,6 +98,18 @@ export const handleContextBuild = async (
|
|
|
94
98
|
context.indexName,
|
|
95
99
|
context.config.collections.map((collection) => collection.name)
|
|
96
100
|
);
|
|
101
|
+
let projectAffinity;
|
|
102
|
+
try {
|
|
103
|
+
projectAffinity = await resolveRemoteProjectAffinity(
|
|
104
|
+
context.config,
|
|
105
|
+
projectHints
|
|
106
|
+
);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (error instanceof ProjectAffinityInputError) {
|
|
109
|
+
throw new ContextCapsuleContractError("invalid_input", error.message);
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
97
113
|
const started = await startRetrievalTraceRequest({
|
|
98
114
|
store: context.store,
|
|
99
115
|
config: context.config,
|
|
@@ -137,6 +153,7 @@ export const handleContextBuild = async (
|
|
|
137
153
|
vectorIndex: context.vectorIndex,
|
|
138
154
|
embedPort: context.embedPort,
|
|
139
155
|
rerankPort: context.rerankPort,
|
|
156
|
+
projectAffinity,
|
|
140
157
|
traceSession: traceSession ?? undefined,
|
|
141
158
|
});
|
|
142
159
|
const finished = await traceSession?.finish(
|