@gmickel/gno 1.22.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.
Files changed (42) hide show
  1. package/README.md +16 -1
  2. package/assets/skill/SKILL.md +15 -0
  3. package/package.json +1 -1
  4. package/spec/cli.md +36 -20
  5. package/spec/evals-agentic.md +35 -0
  6. package/spec/evals.md +6 -0
  7. package/spec/mcp.md +18 -0
  8. package/spec/output-schemas/query-diagnose-v1.schema.json +123 -0
  9. package/spec/output-schemas/query-diagnose.schema.json +89 -2
  10. package/src/app/context-runtime-types.ts +3 -0
  11. package/src/app/context-runtime.ts +1 -0
  12. package/src/app/context-surface.ts +4 -2
  13. package/src/cli/commands/ask.ts +31 -20
  14. package/src/cli/commands/context-build.ts +17 -7
  15. package/src/cli/commands/query.ts +58 -37
  16. package/src/cli/commands/search.ts +29 -19
  17. package/src/cli/commands/vsearch.ts +31 -22
  18. package/src/cli/options.ts +39 -0
  19. package/src/cli/program.ts +48 -0
  20. package/src/config/defaults.ts +10 -1
  21. package/src/config/types.ts +71 -0
  22. package/src/core/project-affinity-surface.ts +114 -0
  23. package/src/core/project-affinity.ts +330 -0
  24. package/src/core/validation.ts +20 -1
  25. package/src/mcp/tools/ask.ts +10 -1
  26. package/src/mcp/tools/context.ts +18 -0
  27. package/src/mcp/tools/index.ts +13 -2
  28. package/src/mcp/tools/query.ts +12 -0
  29. package/src/mcp/tools/search.ts +7 -0
  30. package/src/mcp/tools/vsearch.ts +7 -0
  31. package/src/pipeline/diagnose.ts +48 -3
  32. package/src/pipeline/explain.ts +54 -13
  33. package/src/pipeline/hybrid.ts +100 -59
  34. package/src/pipeline/project-affinity.ts +162 -0
  35. package/src/pipeline/search.ts +76 -10
  36. package/src/pipeline/types.ts +9 -0
  37. package/src/pipeline/vsearch.ts +117 -91
  38. package/src/sdk/client.ts +80 -20
  39. package/src/sdk/index.ts +2 -0
  40. package/src/sdk/types.ts +20 -7
  41. package/src/serve/context-capsule.ts +18 -1
  42. package/src/serve/routes/api.ts +69 -0
@@ -17,6 +17,7 @@ import { createChunkLookup } from "./chunk-lookup";
17
17
  import { formatQueryForEmbedding } from "./contextual";
18
18
  import { matchesExcludedChunks, matchesExcludedText } from "./exclude";
19
19
  import { selectBestChunkForSteering } from "./intent";
20
+ import { applyProjectAffinity, hasProjectAffinity } from "./project-affinity";
20
21
  import { detectQueryLanguage } from "./query-language";
21
22
  import { attachSearchResultContexts } from "./result-context";
22
23
  import {
@@ -82,8 +83,9 @@ export async function searchVectorWithEmbedding(
82
83
  const { store, vectorIndex } = deps;
83
84
  const limit = options.limit ?? 20;
84
85
  const minScore = options.minScore ?? 0;
86
+ const affinityActive = hasProjectAffinity(options.projectAffinity);
85
87
  const recencySort = shouldSortByRecency(query);
86
- const retrievalLimit = recencySort ? limit * 3 : limit;
88
+ const retrievalLimit = recencySort || affinityActive ? limit * 3 : limit;
87
89
  const temporalRange = resolveTemporalRange(
88
90
  query,
89
91
  options.since,
@@ -104,7 +106,7 @@ export async function searchVectorWithEmbedding(
104
106
  queryEmbedding,
105
107
  retrievalLimit,
106
108
  {
107
- minScore,
109
+ minScore: affinityActive ? undefined : minScore,
108
110
  allowedMirrorHashes: options.retrievalScope?.allowedMirrorHashes,
109
111
  }
110
112
  );
@@ -126,7 +128,7 @@ export async function searchVectorWithEmbedding(
126
128
  }
127
129
 
128
130
  // Cache docs to avoid N+1 queries (filtered by collection and tags)
129
- const docByMirrorHash = await buildDocumentMap(store, {
131
+ const docsByMirrorHash = await buildDocumentMap(store, {
130
132
  collection: options.collection,
131
133
  relPathPrefix: options.retrievalScope?.relPathPrefix,
132
134
  tagsAll: options.tagsAll,
@@ -152,12 +154,18 @@ export async function searchVectorWithEmbedding(
152
154
  // For --full, track best score per docid to de-dupe
153
155
  const bestByDocid = new Map<
154
156
  string,
155
- { doc: DocumentInfo; chunk: ChunkInfo; score: number }
157
+ {
158
+ doc: DocumentInfo;
159
+ chunk: ChunkInfo;
160
+ rankingScore: number;
161
+ rawDistance: number;
162
+ score: number;
163
+ }
156
164
  >();
157
165
 
158
166
  for (const vec of vecResults) {
159
- const score = normalizeVectorScore(vec.distance);
160
- if (score < minScore) {
167
+ const baseScore = normalizeVectorScore(vec.distance);
168
+ if (!affinityActive && baseScore < minScore) {
161
169
  continue;
162
170
  }
163
171
 
@@ -184,56 +192,36 @@ export async function searchVectorWithEmbedding(
184
192
  }
185
193
 
186
194
  // Get document (cached)
187
- const doc = docByMirrorHash.get(vec.mirrorHash);
188
- if (!doc) {
189
- continue;
190
- }
191
-
192
- const excluded =
193
- matchesExcludedText(
194
- [
195
- doc.title ?? "",
196
- doc.relPath,
197
- doc.author ?? "",
198
- doc.contentType ?? "",
199
- ...(doc.categories ?? []),
200
- ],
201
- options.exclude
202
- ) ||
203
- matchesExcludedChunks(
204
- chunksMap.get(vec.mirrorHash) ?? [],
205
- options.exclude
206
- );
207
- if (excluded) {
195
+ const matchingDocs = docsByMirrorHash.get(vec.mirrorHash);
196
+ if (!matchingDocs || matchingDocs.length === 0) {
208
197
  continue;
209
198
  }
210
-
211
- // For --full, de-dupe by docid (keep best scoring chunk per doc)
212
- if (options.full) {
213
- const existing = bestByDocid.get(doc.docid);
214
- if (!existing || score > existing.score) {
215
- bestByDocid.set(doc.docid, {
216
- doc,
217
- chunk: {
218
- text: chunk.text,
219
- language: chunk.language,
220
- startLine: chunk.startLine,
221
- endLine: chunk.endLine,
222
- seq: chunk.seq,
223
- },
224
- score,
225
- });
199
+ const docs = affinityActive ? matchingDocs : [matchingDocs.at(-1)!];
200
+ for (const doc of docs) {
201
+ const collectionPath = collectionPaths.get(doc.collection);
202
+ const excluded =
203
+ matchesExcludedText(
204
+ [
205
+ doc.title ?? "",
206
+ doc.relPath,
207
+ doc.author ?? "",
208
+ doc.contentType ?? "",
209
+ ...(doc.categories ?? []),
210
+ ],
211
+ options.exclude
212
+ ) ||
213
+ matchesExcludedChunks(
214
+ chunksMap.get(vec.mirrorHash) ?? [],
215
+ options.exclude
216
+ );
217
+ if (excluded) {
218
+ continue;
226
219
  }
227
- continue;
228
- }
229
220
 
230
- const collectionPath = collectionPaths.get(doc.collection);
231
-
232
- results.push(
233
- attachSearchResultPlannerMetadata(
221
+ const scoredResult = applyProjectAffinity(
234
222
  {
235
223
  docid: doc.docid,
236
- score,
224
+ score: baseScore,
237
225
  uri: doc.uri,
238
226
  title: doc.title ?? undefined,
239
227
  contentType: doc.contentType ?? undefined,
@@ -265,7 +253,35 @@ export async function searchVectorWithEmbedding(
265
253
  }
266
254
  : undefined,
267
255
  },
268
- {
256
+ doc.collection,
257
+ options.projectAffinity,
258
+ { kind: "vector_distance", score: vec.distance }
259
+ );
260
+ if (scoredResult.score < minScore) continue;
261
+
262
+ // For --full, de-dupe by docid (keep best scoring chunk per doc)
263
+ if (options.full) {
264
+ const existing = bestByDocid.get(doc.docid);
265
+ if (!existing || scoredResult.score > existing.rankingScore) {
266
+ bestByDocid.set(doc.docid, {
267
+ doc,
268
+ chunk: {
269
+ text: chunk.text,
270
+ language: chunk.language,
271
+ startLine: chunk.startLine,
272
+ endLine: chunk.endLine,
273
+ seq: chunk.seq,
274
+ },
275
+ rankingScore: scoredResult.score,
276
+ rawDistance: vec.distance,
277
+ score: baseScore,
278
+ });
279
+ }
280
+ continue;
281
+ }
282
+
283
+ results.push(
284
+ attachSearchResultPlannerMetadata(scoredResult, {
269
285
  retrievalRank: 0,
270
286
  mirrorHash: vec.mirrorHash,
271
287
  seq: chunk.seq,
@@ -276,9 +292,9 @@ export async function searchVectorWithEmbedding(
276
292
  passageHash: new Bun.CryptoHasher("sha256")
277
293
  .update(chunk.text)
278
294
  .digest("hex"),
279
- }
280
- )
281
- );
295
+ })
296
+ );
297
+ }
282
298
  }
283
299
 
284
300
  // For --full, fetch full content and build results
@@ -294,47 +310,52 @@ export async function searchVectorWithEmbedding(
294
310
  }
295
311
  const fullContentByHash = fullContentResult.value;
296
312
 
297
- for (const { doc, chunk, score } of bestByDocid.values()) {
313
+ for (const { doc, chunk, rawDistance, score } of bestByDocid.values()) {
298
314
  const fullContent = doc.mirrorHash
299
315
  ? fullContentByHash.get(doc.mirrorHash)
300
316
  : undefined;
301
317
 
302
318
  const collectionPath = collectionPaths.get(doc.collection);
303
319
 
304
- const result: SearchResult = {
305
- docid: doc.docid,
306
- score,
307
- uri: doc.uri,
308
- title: doc.title ?? undefined,
309
- contentType: doc.contentType ?? undefined,
310
- categories: doc.categories ?? undefined,
311
- line: chunk.startLine,
312
- snippet: fullContent ?? chunk.text,
313
- snippetLanguage: chunk.language ?? undefined,
314
- // --full: no snippetRange (full doc content)
315
- snippetRange: fullContent
316
- ? undefined
317
- : { startLine: chunk.startLine, endLine: chunk.endLine },
318
- source: {
319
- relPath: doc.relPath,
320
- absPath: collectionPath
321
- ? `${collectionPath}/${doc.relPath}`
320
+ const result = applyProjectAffinity(
321
+ {
322
+ docid: doc.docid,
323
+ score,
324
+ uri: doc.uri,
325
+ title: doc.title ?? undefined,
326
+ contentType: doc.contentType ?? undefined,
327
+ categories: doc.categories ?? undefined,
328
+ line: chunk.startLine,
329
+ snippet: fullContent ?? chunk.text,
330
+ snippetLanguage: chunk.language ?? undefined,
331
+ // --full: no snippetRange (full doc content)
332
+ snippetRange: fullContent
333
+ ? undefined
334
+ : { startLine: chunk.startLine, endLine: chunk.endLine },
335
+ source: {
336
+ relPath: doc.relPath,
337
+ absPath: collectionPath
338
+ ? `${collectionPath}/${doc.relPath}`
339
+ : undefined,
340
+ mime: doc.sourceMime,
341
+ ext: doc.sourceExt,
342
+ modifiedAt: doc.sourceMtime,
343
+ documentDate: doc.frontmatterDate ?? undefined,
344
+ sizeBytes: doc.sourceSize,
345
+ sourceHash: doc.sourceHash,
346
+ },
347
+ conversion: doc.mirrorHash
348
+ ? {
349
+ mirrorHash: doc.mirrorHash,
350
+ converterId: doc.converterId ?? undefined,
351
+ converterVersion: doc.converterVersion ?? undefined,
352
+ }
322
353
  : undefined,
323
- mime: doc.sourceMime,
324
- ext: doc.sourceExt,
325
- modifiedAt: doc.sourceMtime,
326
- documentDate: doc.frontmatterDate ?? undefined,
327
- sizeBytes: doc.sourceSize,
328
- sourceHash: doc.sourceHash,
329
354
  },
330
- conversion: doc.mirrorHash
331
- ? {
332
- mirrorHash: doc.mirrorHash,
333
- converterId: doc.converterId ?? undefined,
334
- converterVersion: doc.converterVersion ?? undefined,
335
- }
336
- : undefined,
337
- };
355
+ doc.collection,
356
+ options.projectAffinity,
357
+ { kind: "vector_distance", score: rawDistance }
358
+ );
338
359
  results.push(
339
360
  doc.mirrorHash
340
361
  ? attachSearchResultPlannerMetadata(result, {
@@ -369,6 +390,8 @@ export async function searchVectorWithEmbedding(
369
390
  }
370
391
  return b.score - a.score;
371
392
  });
393
+ } else if (affinityActive) {
394
+ results.sort((a, b) => b.score - a.score);
372
395
  }
373
396
 
374
397
  const finalResults = results.slice(0, limit);
@@ -507,8 +530,8 @@ function matchesCategoryFilter(
507
530
  async function buildDocumentMap(
508
531
  store: StorePort,
509
532
  options: DocumentMapOptions = {}
510
- ): Promise<Map<string, DocumentInfo>> {
511
- const result = new Map<string, DocumentInfo>();
533
+ ): Promise<Map<string, DocumentInfo[]>> {
534
+ const result = new Map<string, DocumentInfo[]>();
512
535
 
513
536
  if (options.mirrorHashes && options.mirrorHashes.length === 0) {
514
537
  return result;
@@ -594,7 +617,7 @@ async function buildDocumentMap(
594
617
  continue;
595
618
  }
596
619
 
597
- result.set(doc.mirrorHash!, {
620
+ const documentInfo: DocumentInfo = {
598
621
  docid: doc.docid,
599
622
  uri: doc.uri,
600
623
  title: doc.title,
@@ -612,7 +635,10 @@ async function buildDocumentMap(
612
635
  mirrorHash: doc.mirrorHash,
613
636
  converterId: doc.converterId,
614
637
  converterVersion: doc.converterVersion,
615
- });
638
+ };
639
+ const matchingDocuments = result.get(doc.mirrorHash!) ?? [];
640
+ matchingDocuments.push(documentInfo);
641
+ result.set(doc.mirrorHash!, matchingDocuments);
616
642
  }
617
643
 
618
644
  return result;
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: import("../pipeline/types").SearchOptions = {}
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(options),
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
- ...options,
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(options),
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
- { ...options, traceSession: traceSession ?? undefined }
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(options),
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
- { ...options, traceSession: traceSession ?? undefined }
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(options),
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(query, options, {
781
- store: this.store,
782
- config: this.config,
783
- indexName: this.indexName,
784
- vectorIndex: ports.vectorIndex,
785
- embedPort: ports.embedPort,
786
- rerankPort: ports.rerankPort,
787
- genPort: ports.answerPort,
788
- traceSession: traceSession ?? undefined,
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
- { ...input, indexName: this.indexName },
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
- { ...input, indexName: this.indexName },
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
@@ -51,6 +51,8 @@ export type {
51
51
  GnoMultiGetOptions,
52
52
  GnoMultiGetResult,
53
53
  GnoQueryOptions,
54
+ GnoProjectHintOptions,
55
+ GnoSearchOptions,
54
56
  GnoSkippedDocument,
55
57
  GnoUpdateOptions,
56
58
  GnoVectorSearchOptions,
package/src/sdk/types.ts CHANGED
@@ -97,13 +97,26 @@ export interface GnoModelOverrides {
97
97
  rerankModel?: string;
98
98
  }
99
99
 
100
- export type GnoQueryOptions = HybridSearchOptions & GnoModelOverrides;
101
- export type GnoAskOptions = AskOptions & GnoModelOverrides;
102
- export type GnoVectorSearchOptions = SearchOptions & {
103
- model?: string;
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?: SearchOptions): Promise<SearchResults>;
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(