@gmickel/gno 1.25.1 → 1.27.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 (94) hide show
  1. package/README.md +12 -5
  2. package/assets/skill/SKILL.md +37 -17
  3. package/browser-extension/artifacts/{gno-browser-clipper-v1.25.1.zip → gno-browser-clipper-v1.27.0.zip} +0 -0
  4. package/browser-extension/artifacts/gno-browser-clipper-v1.27.0.zip.sha256 +1 -0
  5. package/browser-extension/dist/manifest.json +1 -1
  6. package/package.json +1 -1
  7. package/spec/cli.md +161 -6
  8. package/spec/db/schema.sql +1 -1
  9. package/spec/evals-agentic.md +17 -0
  10. package/spec/mcp.md +25 -6
  11. package/spec/output-schemas/ask.schema.json +3 -0
  12. package/spec/output-schemas/project-profile-apply.schema.json +209 -0
  13. package/spec/output-schemas/project-profile-command.schema.json +160 -0
  14. package/spec/output-schemas/query-diagnose.schema.json +68 -5
  15. package/spec/output-schemas/search-results.schema.json +87 -1
  16. package/spec/output-schemas/setup-profile-result.schema.json +87 -0
  17. package/spec/output-schemas/status.schema.json +24 -0
  18. package/spec/project-profile.schema.json +303 -0
  19. package/src/app/context-runtime-contract.ts +4 -1
  20. package/src/app/context-runtime-types.ts +2 -0
  21. package/src/app/context-runtime.ts +26 -0
  22. package/src/app/verified-ask.ts +6 -1
  23. package/src/cli/commands/ask.ts +8 -1
  24. package/src/cli/commands/collection/add.ts +39 -45
  25. package/src/cli/commands/collection/remove.ts +28 -28
  26. package/src/cli/commands/collection/rename.ts +55 -73
  27. package/src/cli/commands/context/add.ts +37 -26
  28. package/src/cli/commands/context/rm.ts +47 -20
  29. package/src/cli/commands/init.ts +55 -125
  30. package/src/cli/commands/models/use.ts +43 -38
  31. package/src/cli/commands/profile-apply.ts +334 -0
  32. package/src/cli/commands/profile.ts +409 -0
  33. package/src/cli/commands/query.ts +6 -3
  34. package/src/cli/commands/search.ts +6 -1
  35. package/src/cli/commands/setup-activation.ts +205 -54
  36. package/src/cli/commands/setup-profile.ts +223 -0
  37. package/src/cli/commands/setup.ts +3 -0
  38. package/src/cli/commands/status.ts +43 -7
  39. package/src/cli/program.ts +112 -9
  40. package/src/config/content-types.ts +82 -0
  41. package/src/config/index.ts +11 -0
  42. package/src/config/project-profile.ts +374 -0
  43. package/src/config/saver.ts +16 -7
  44. package/src/config/types.ts +53 -2
  45. package/src/core/config-mutation.ts +138 -76
  46. package/src/core/config-write-lock.ts +89 -0
  47. package/src/core/context-compiler.ts +38 -1
  48. package/src/core/context-identity.ts +16 -0
  49. package/src/core/context-resolver.ts +2 -12
  50. package/src/core/folder-setup-planning.ts +6 -21
  51. package/src/core/folder-setup.ts +30 -2
  52. package/src/core/path-rules.ts +53 -0
  53. package/src/core/project-affinity-surface.ts +102 -7
  54. package/src/core/project-profile-apply-state.ts +268 -0
  55. package/src/core/project-profile-apply-validation.ts +95 -0
  56. package/src/core/project-profile-apply.ts +408 -0
  57. package/src/core/project-profile-canonical.ts +71 -0
  58. package/src/core/project-profile-diff.ts +302 -0
  59. package/src/core/project-profile-discovery.ts +519 -0
  60. package/src/core/project-profile-file.ts +37 -0
  61. package/src/core/project-profile-parser.ts +98 -0
  62. package/src/core/project-profile.ts +490 -0
  63. package/src/core/retrieval-replay-candidate.ts +6 -1
  64. package/src/ingestion/sync-options.ts +6 -2
  65. package/src/ingestion/sync.ts +21 -29
  66. package/src/ingestion/types.ts +1 -1
  67. package/src/ingestion/walker.ts +85 -44
  68. package/src/llm/cache.ts +21 -0
  69. package/src/mcp/tools/ask.ts +1 -0
  70. package/src/mcp/tools/index.ts +4 -0
  71. package/src/mcp/tools/query.ts +4 -2
  72. package/src/mcp/tools/search.ts +3 -0
  73. package/src/mcp/tools/status.ts +4 -0
  74. package/src/pipeline/content-type-boost.ts +264 -0
  75. package/src/pipeline/diagnose.ts +46 -19
  76. package/src/pipeline/explain.ts +15 -2
  77. package/src/pipeline/hybrid.ts +170 -74
  78. package/src/pipeline/rerank.ts +45 -15
  79. package/src/pipeline/search.ts +29 -11
  80. package/src/pipeline/types.ts +13 -4
  81. package/src/pipeline/vsearch.ts +30 -10
  82. package/src/sdk/client.ts +19 -3
  83. package/src/sdk/index.ts +1 -0
  84. package/src/sdk/types.ts +21 -5
  85. package/src/serve/config-sync.ts +2 -2
  86. package/src/serve/resident-runtime.ts +1 -0
  87. package/src/serve/routes/api.ts +18 -3
  88. package/src/serve/status-model.ts +2 -0
  89. package/src/serve/status.ts +4 -0
  90. package/src/store/migrations/021-multi-context-identity.ts +37 -0
  91. package/src/store/migrations/index.ts +2 -0
  92. package/src/store/sqlite/adapter.ts +86 -0
  93. package/src/store/types.ts +3 -2
  94. package/browser-extension/artifacts/gno-browser-clipper-v1.25.1.zip.sha256 +0 -1
@@ -5,6 +5,7 @@
5
5
  * @module src/pipeline/types
6
6
  */
7
7
 
8
+ import type { NormalizedContentTypeRule } from "../config/content-types";
8
9
  import type {
9
10
  ContextCapsuleV1,
10
11
  ContextCapsuleVerification,
@@ -13,6 +14,7 @@ import type { RetrievalTraceSession } from "../core/retrieval-trace-session";
13
14
  import type { StoreResult } from "../store/types";
14
15
  import type { ClaimVerificationResult } from "./claim-verification";
15
16
  import type { SemanticVerificationCapability } from "./claim-verifier";
17
+ import type { ContentTypeBoostScoreMetadata } from "./content-type-boost";
16
18
  import type {
17
19
  ProjectAffinityScoreMetadata,
18
20
  ProjectAffinityScoringInput,
@@ -132,10 +134,7 @@ export interface SearchMeta {
132
134
  /** Explicit exclusion terms applied */
133
135
  exclude?: string[];
134
136
  /** Explain data (when --explain is used) */
135
- explain?: {
136
- lines: ExplainLine[];
137
- results: ExplainResult[];
138
- };
137
+ explain?: SearchExplain;
139
138
  /** Internal diagnose trace, only populated when diagnoseTrace is enabled */
140
139
  trace?: QueryDiagnoseTrace;
141
140
  }
@@ -172,6 +171,8 @@ export interface SearchOptions {
172
171
  traceSession?: RetrievalTraceSession;
173
172
  /** Trusted, already-resolved project affinity; never accepts raw roots. */
174
173
  projectAffinity?: ProjectAffinityScoringInput;
174
+ /** Internal normalized rules used by bounded content-type scoring. */
175
+ contentTypeRules?: NormalizedContentTypeRule[];
175
176
  /** Max results */
176
177
  limit?: number;
177
178
  /** Min score threshold (0-1) */
@@ -459,6 +460,8 @@ export interface AskMeta {
459
460
  answerGenerated?: boolean;
460
461
  totalResults?: number;
461
462
  answerContext?: AnswerContextExplain;
463
+ /** Optional retrieval scoring explanation; absent from normal output. */
464
+ explain?: SearchExplain;
462
465
  verificationRequested?: boolean;
463
466
  abstained?: boolean;
464
467
  }
@@ -545,4 +548,10 @@ export interface ExplainResult {
545
548
  vecScore?: number;
546
549
  rerankScore?: number;
547
550
  projectAffinity?: ProjectAffinityScoreMetadata;
551
+ contentTypeBoost?: ContentTypeBoostScoreMetadata;
552
+ }
553
+
554
+ export interface SearchExplain {
555
+ lines: ExplainLine[];
556
+ results: ExplainResult[];
548
557
  }
@@ -11,13 +11,19 @@ import type { StorePort } from "../store/types";
11
11
  import type { VectorIndexPort } from "../store/vector/types";
12
12
  import type { SearchOptions, SearchResult, SearchResults } from "./types";
13
13
 
14
+ import { normalizeContentTypes } from "../config/content-types";
14
15
  import { getContentBatch } from "../store/content-batch";
15
16
  import { err, ok } from "../store/types";
16
17
  import { createChunkLookup } from "./chunk-lookup";
18
+ import {
19
+ applyContentTypeBoost,
20
+ hasAuxiliaryRanking,
21
+ sortByFinalScoreStable,
22
+ } from "./content-type-boost";
17
23
  import { formatQueryForEmbedding } from "./contextual";
18
24
  import { matchesExcludedChunks, matchesExcludedText } from "./exclude";
19
25
  import { selectBestChunkForSteering } from "./intent";
20
- import { applyProjectAffinity, hasProjectAffinity } from "./project-affinity";
26
+ import { hasProjectAffinity } from "./project-affinity";
21
27
  import { detectQueryLanguage } from "./query-language";
22
28
  import { attachSearchResultContexts } from "./result-context";
23
29
  import {
@@ -83,9 +89,17 @@ export async function searchVectorWithEmbedding(
83
89
  const { store, vectorIndex } = deps;
84
90
  const limit = options.limit ?? 20;
85
91
  const minScore = options.minScore ?? 0;
86
- const affinityActive = hasProjectAffinity(options.projectAffinity);
92
+ const contentTypeRules =
93
+ options.contentTypeRules ??
94
+ normalizeContentTypes(deps.config.contentTypes ?? []).rules;
95
+ const auxiliaryRankingActive = hasAuxiliaryRanking(
96
+ options.projectAffinity,
97
+ contentTypeRules
98
+ );
99
+ const projectAffinityActive = hasProjectAffinity(options.projectAffinity);
87
100
  const recencySort = shouldSortByRecency(query);
88
- const retrievalLimit = recencySort || affinityActive ? limit * 3 : limit;
101
+ const retrievalLimit =
102
+ recencySort || projectAffinityActive ? limit * 3 : limit;
89
103
  const temporalRange = resolveTemporalRange(
90
104
  query,
91
105
  options.since,
@@ -106,7 +120,7 @@ export async function searchVectorWithEmbedding(
106
120
  queryEmbedding,
107
121
  retrievalLimit,
108
122
  {
109
- minScore: affinityActive ? undefined : minScore,
123
+ minScore: projectAffinityActive ? undefined : minScore,
110
124
  allowedMirrorHashes: options.retrievalScope?.allowedMirrorHashes,
111
125
  }
112
126
  );
@@ -165,7 +179,7 @@ export async function searchVectorWithEmbedding(
165
179
 
166
180
  for (const vec of vecResults) {
167
181
  const baseScore = normalizeVectorScore(vec.distance);
168
- if (!affinityActive && baseScore < minScore) {
182
+ if (!projectAffinityActive && baseScore < minScore) {
169
183
  continue;
170
184
  }
171
185
 
@@ -196,7 +210,7 @@ export async function searchVectorWithEmbedding(
196
210
  if (!matchingDocs || matchingDocs.length === 0) {
197
211
  continue;
198
212
  }
199
- const docs = affinityActive ? matchingDocs : [matchingDocs.at(-1)!];
213
+ const docs = auxiliaryRankingActive ? matchingDocs : [matchingDocs.at(-1)!];
200
214
  for (const doc of docs) {
201
215
  const collectionPath = collectionPaths.get(doc.collection);
202
216
  const excluded =
@@ -218,7 +232,7 @@ export async function searchVectorWithEmbedding(
218
232
  continue;
219
233
  }
220
234
 
221
- const scoredResult = applyProjectAffinity(
235
+ const scoredResult = applyContentTypeBoost(
222
236
  {
223
237
  docid: doc.docid,
224
238
  score: baseScore,
@@ -254,7 +268,9 @@ export async function searchVectorWithEmbedding(
254
268
  : undefined,
255
269
  },
256
270
  doc.collection,
271
+ contentTypeRules,
257
272
  options.projectAffinity,
273
+ doc.contentTypeSource,
258
274
  { kind: "vector_distance", score: vec.distance }
259
275
  );
260
276
  if (scoredResult.score < minScore) continue;
@@ -317,7 +333,7 @@ export async function searchVectorWithEmbedding(
317
333
 
318
334
  const collectionPath = collectionPaths.get(doc.collection);
319
335
 
320
- const result = applyProjectAffinity(
336
+ const result = applyContentTypeBoost(
321
337
  {
322
338
  docid: doc.docid,
323
339
  score,
@@ -353,7 +369,9 @@ export async function searchVectorWithEmbedding(
353
369
  : undefined,
354
370
  },
355
371
  doc.collection,
372
+ contentTypeRules,
356
373
  options.projectAffinity,
374
+ doc.contentTypeSource,
357
375
  { kind: "vector_distance", score: rawDistance }
358
376
  );
359
377
  results.push(
@@ -390,8 +408,8 @@ export async function searchVectorWithEmbedding(
390
408
  }
391
409
  return b.score - a.score;
392
410
  });
393
- } else if (affinityActive) {
394
- results.sort((a, b) => b.score - a.score);
411
+ } else if (auxiliaryRankingActive) {
412
+ sortByFinalScoreStable(results);
395
413
  }
396
414
 
397
415
  const finalResults = results.slice(0, limit);
@@ -485,6 +503,7 @@ interface DocumentInfo {
485
503
  relPath: string;
486
504
  author: string | null;
487
505
  contentType: string | null;
506
+ contentTypeSource: string | null;
488
507
  categories: string[] | null;
489
508
  sourceHash: string;
490
509
  sourceMime: string;
@@ -625,6 +644,7 @@ async function buildDocumentMap(
625
644
  relPath: doc.relPath,
626
645
  author: doc.author ?? null,
627
646
  contentType: doc.contentType ?? null,
647
+ contentTypeSource: doc.contentTypeSource ?? null,
628
648
  categories: doc.categories ?? null,
629
649
  sourceHash: doc.sourceHash,
630
650
  sourceMime: doc.sourceMime,
package/src/sdk/client.ts CHANGED
@@ -11,7 +11,7 @@ import type { Config } from "../config/types";
11
11
  import type { DownloadPolicy } from "../llm/policy";
12
12
  import type { EmbeddingPort, GenerationPort, RerankPort } from "../llm/types";
13
13
  import type { AskResult, SearchResults } from "../pipeline/types";
14
- import type { IndexStatus, StoreResult } from "../store/types";
14
+ import type { StoreResult } from "../store/types";
15
15
  import type { VectorIndexPort } from "../store/vector";
16
16
  import type {
17
17
  GnoAskOptions,
@@ -32,6 +32,7 @@ import type {
32
32
  GnoGetOptions,
33
33
  GnoIndexOptions,
34
34
  GnoIndexResult,
35
+ GnoIndexStatus,
35
36
  GnoListOptions,
36
37
  GnoMoveNoteOptions,
37
38
  GnoMultiGetOptions,
@@ -65,9 +66,11 @@ import {
65
66
  } from "../app/index-name";
66
67
  import { buildVerifiedAsk } from "../app/verified-ask";
67
68
  import {
69
+ buildContentTypeBoostStatus,
68
70
  ConfigSchema,
69
71
  loadConfig,
70
72
  normalizeConfigContentTypes,
73
+ normalizeContentTypes,
71
74
  } from "../config";
72
75
  import {
73
76
  buildCaptureReceipt,
@@ -516,6 +519,9 @@ class GnoClientImpl implements GnoClient {
516
519
  await searchBm25(this.store, query, {
517
520
  ...searchOptions,
518
521
  projectAffinity,
522
+ contentTypeRules: normalizeContentTypes(
523
+ this.config.contentTypes ?? []
524
+ ).rules,
519
525
  traceSession: traceSession ?? undefined,
520
526
  })
521
527
  )
@@ -883,6 +889,7 @@ class GnoClientImpl implements GnoClient {
883
889
  noExpand: options.noExpand,
884
890
  noRerank: options.noRerank,
885
891
  candidateLimit: options.candidateLimit,
892
+ explain: options.explain,
886
893
  queryLanguageHint: options.queryLanguageHint,
887
894
  projectAffinity,
888
895
  traceSession: traceSession ?? undefined,
@@ -945,6 +952,9 @@ class GnoClientImpl implements GnoClient {
945
952
  answerGenerated,
946
953
  totalResults: searchResult.results.length,
947
954
  answerContext,
955
+ ...(options.explain && searchResult.meta.explain
956
+ ? { explain: searchResult.meta.explain }
957
+ : {}),
948
958
  },
949
959
  };
950
960
  if (answerRequested && traceSession) {
@@ -1169,13 +1179,19 @@ class GnoClientImpl implements GnoClient {
1169
1179
  );
1170
1180
  }
1171
1181
 
1172
- async status(): Promise<IndexStatus> {
1182
+ async status(): Promise<GnoIndexStatus> {
1173
1183
  this.assertOpen();
1174
- return unwrapStore(
1184
+ const status = unwrapStore(
1175
1185
  await this.store.getStatus({
1176
1186
  embedModel: resolveModelUri(this.config, "embed"),
1177
1187
  })
1178
1188
  );
1189
+ return {
1190
+ ...status,
1191
+ contentTypeBoost: buildContentTypeBoostStatus(
1192
+ this.config.contentTypes ?? []
1193
+ ),
1194
+ };
1179
1195
  }
1180
1196
 
1181
1197
  async listRetrievalTraces(
package/src/sdk/index.ts CHANGED
@@ -43,6 +43,7 @@ export type {
43
43
  GnoGetResult,
44
44
  GnoIndexOptions,
45
45
  GnoIndexResult,
46
+ GnoIndexStatus,
46
47
  GnoListDocument,
47
48
  GnoListOptions,
48
49
  GnoListResult,
package/src/sdk/types.ts CHANGED
@@ -8,6 +8,7 @@ import type {
8
8
  ContextCapsuleBuildInput,
9
9
  ContextRuntimeErrorCode,
10
10
  } from "../app/context-runtime";
11
+ import type { ContentTypeBoostStatus } from "../config/content-types";
11
12
  import type { Config } from "../config/types";
12
13
  import type { CaptureInput, CaptureReceipt } from "../core/capture";
13
14
  import type {
@@ -102,15 +103,27 @@ export interface GnoProjectHintOptions {
102
103
  projectHints?: string[];
103
104
  }
104
105
 
105
- export type GnoSearchOptions = Omit<SearchOptions, "projectAffinity"> &
106
+ export type GnoSearchOptions = Omit<
107
+ SearchOptions,
108
+ "contentTypeRules" | "projectAffinity"
109
+ > &
106
110
  GnoProjectHintOptions;
107
- export type GnoQueryOptions = Omit<HybridSearchOptions, "projectAffinity"> &
111
+ export type GnoQueryOptions = Omit<
112
+ HybridSearchOptions,
113
+ "contentTypeRules" | "projectAffinity"
114
+ > &
108
115
  GnoModelOverrides &
109
116
  GnoProjectHintOptions;
110
- export type GnoAskOptions = Omit<AskOptions, "projectAffinity"> &
117
+ export type GnoAskOptions = Omit<
118
+ AskOptions,
119
+ "contentTypeRules" | "projectAffinity"
120
+ > &
111
121
  GnoModelOverrides &
112
122
  GnoProjectHintOptions;
113
- export type GnoVectorSearchOptions = Omit<SearchOptions, "projectAffinity"> &
123
+ export type GnoVectorSearchOptions = Omit<
124
+ SearchOptions,
125
+ "contentTypeRules" | "projectAffinity"
126
+ > &
114
127
  GnoProjectHintOptions & {
115
128
  model?: string;
116
129
  };
@@ -118,6 +131,9 @@ export type GnoVectorSearchOptions = Omit<SearchOptions, "projectAffinity"> &
118
131
  export type GnoContextInput = Omit<ContextCapsuleBuildInput, "indexName"> &
119
132
  GnoProjectHintOptions;
120
133
  export type GnoContextResult = ContextCapsuleV1;
134
+ export type GnoIndexStatus = IndexStatus & {
135
+ contentTypeBoost: ContentTypeBoostStatus;
136
+ };
121
137
  export type GnoContextVerificationResult = ContextCapsuleVerification;
122
138
  export type GnoContextErrorCode =
123
139
  | ContextRuntimeErrorCode
@@ -268,7 +284,7 @@ export interface GnoClient {
268
284
  ref: string,
269
285
  options?: KnowledgeImpactInput
270
286
  ): Promise<KnowledgeImpactResult>;
271
- status(): Promise<IndexStatus>;
287
+ status(): Promise<GnoIndexStatus>;
272
288
  listRetrievalTraces(
273
289
  options?: RetrievalTraceListRequest
274
290
  ): Promise<RetrievalTraceListResult>;
@@ -44,7 +44,7 @@ export async function applyConfigChange(
44
44
  const result = await applyConfigChangeCore(
45
45
  {
46
46
  store,
47
- configPath,
47
+ configPath: configPath ?? ctxHolder.actualConfigPath,
48
48
  onConfigUpdated: (config) => {
49
49
  ctxHolder.config = config;
50
50
  ctxHolder.current = { ...ctxHolder.current, config };
@@ -72,7 +72,7 @@ export async function applyConfigChangeTyped<T>(
72
72
  const result = await applyConfigChangeCore(
73
73
  {
74
74
  store,
75
- configPath,
75
+ configPath: configPath ?? ctxHolder.actualConfigPath,
76
76
  onConfigUpdated: (config) => {
77
77
  ctxHolder.config = config;
78
78
  ctxHolder.current = { ...ctxHolder.current, config };
@@ -240,6 +240,7 @@ export async function startResidentRuntime(
240
240
  const ctxHolder: ContextHolder = {
241
241
  current: ctx,
242
242
  config: initialConfig,
243
+ actualConfigPath,
243
244
  scheduler: null,
244
245
  eventBus: options.eventBus ?? null,
245
246
  watchService: null,
@@ -45,7 +45,7 @@ import {
45
45
  updateCollection,
46
46
  } from "../../collection";
47
47
  import {
48
- fingerprintContentTypeRules,
48
+ fingerprintContentTypeMetadataRules,
49
49
  normalizeContentTypes,
50
50
  } from "../../config";
51
51
  import { type PublicCaptureInput } from "../../core/capture";
@@ -168,6 +168,7 @@ import { handleChanges, handleDiff, handleImpact } from "./changes";
168
168
  export interface ContextHolder {
169
169
  current: ServerContext;
170
170
  config: Config;
171
+ actualConfigPath?: string;
171
172
  scheduler: EmbedScheduler | null;
172
173
  eventBus: DocumentEventBus | null;
173
174
  watchService: CollectionWatchService | null;
@@ -271,6 +272,7 @@ export interface QueryRequestBody {
271
272
  noRerank?: boolean;
272
273
  noGraph?: boolean;
273
274
  graph?: boolean;
275
+ explain?: boolean;
274
276
  /** Comma-separated tags - filter to docs having ALL (AND) */
275
277
  tagsAll?: string;
276
278
  /** Comma-separated tags - filter to docs having ANY (OR) */
@@ -305,6 +307,7 @@ export interface AskRequestBody {
305
307
  noRerank?: boolean;
306
308
  graph?: boolean;
307
309
  noGraph?: boolean;
310
+ explain?: boolean;
308
311
  /** Comma-separated tags - filter to docs having ALL (AND) */
309
312
  tagsAll?: string;
310
313
  /** Comma-separated tags - filter to docs having ANY (OR) */
@@ -334,6 +337,7 @@ const ASK_REQUEST_KEYS = new Set<keyof AskRequestBody>([
334
337
  "noRerank",
335
338
  "graph",
336
339
  "noGraph",
340
+ "explain",
337
341
  "tagsAll",
338
342
  "tagsAny",
339
343
  ]);
@@ -3509,7 +3513,6 @@ export async function handleSearch(
3509
3513
  if (body.author !== undefined && typeof body.author !== "string") {
3510
3514
  return errorResponse("VALIDATION", "author must be a string");
3511
3515
  }
3512
-
3513
3516
  // Parse tag filters
3514
3517
  let tagsAll: string[] | undefined;
3515
3518
  let tagsAny: string[] | undefined;
@@ -3571,6 +3574,9 @@ export async function handleSearch(
3571
3574
  categories,
3572
3575
  author,
3573
3576
  projectAffinity,
3577
+ contentTypeRules: context
3578
+ ? normalizeContentTypes(context.config.contentTypes ?? []).rules
3579
+ : undefined,
3574
3580
  };
3575
3581
 
3576
3582
  const trace = context
@@ -3678,6 +3684,9 @@ export async function handleQuery(
3678
3684
  if (body.author !== undefined && typeof body.author !== "string") {
3679
3685
  return errorResponse("VALIDATION", "author must be a string");
3680
3686
  }
3687
+ if (body.explain !== undefined && typeof body.explain !== "boolean") {
3688
+ return errorResponse("VALIDATION", "explain must be a boolean");
3689
+ }
3681
3690
 
3682
3691
  const { queryModes, error: queryModesError } = parseQueryModesInput(
3683
3692
  body.queryModes
@@ -3767,6 +3776,7 @@ export async function handleQuery(
3767
3776
  categories,
3768
3777
  author,
3769
3778
  projectAffinity,
3779
+ explain: body.explain,
3770
3780
  };
3771
3781
  const trace = await startRestTrace(ctx, {
3772
3782
  query: normalizedQuery,
@@ -3997,7 +4007,7 @@ export async function handleQueryDiagnose(
3997
4007
  projectAffinity,
3998
4008
  contentTypeRules,
3999
4009
  contentTypeRulesFingerprint:
4000
- fingerprintContentTypeRules(contentTypeRules),
4010
+ fingerprintContentTypeMetadataRules(contentTypeRules),
4001
4011
  }
4002
4012
  );
4003
4013
 
@@ -4046,6 +4056,7 @@ export async function handleAsk(
4046
4056
  "noRerank",
4047
4057
  "graph",
4048
4058
  "noGraph",
4059
+ "explain",
4049
4060
  ] as const) {
4050
4061
  if (body[field] !== undefined && typeof body[field] !== "boolean") {
4051
4062
  return errorResponse("VALIDATION", `${field} must be a boolean`);
@@ -4229,6 +4240,7 @@ export async function handleAsk(
4229
4240
  contextBudgetBytes: body.contextBudgetBytes,
4230
4241
  maxAnswerTokens: body.maxAnswerTokens,
4231
4242
  projectAffinity,
4243
+ explain: body.explain,
4232
4244
  };
4233
4245
  const trace = await startRestTrace(ctx, {
4234
4246
  query: normalizedQuery,
@@ -4445,6 +4457,9 @@ export async function handleAsk(
4445
4457
  answerGenerated,
4446
4458
  totalResults: results.length,
4447
4459
  answerContext,
4460
+ ...(body.explain && searchResult.value.meta.explain
4461
+ ? { explain: searchResult.value.meta.explain }
4462
+ : {}),
4448
4463
  },
4449
4464
  };
4450
4465
 
@@ -1,3 +1,4 @@
1
+ import type { ContentTypeBoostStatus } from "../config/content-types";
1
2
  import type { ActivationStatus } from "../core/activation-status";
2
3
 
3
4
  export type HealthCheckStatus = "ok" | "warn" | "error";
@@ -196,6 +197,7 @@ export interface AppStatusResponse {
196
197
  hybrid: boolean;
197
198
  answer: boolean;
198
199
  };
200
+ contentTypeBoost: ContentTypeBoostStatus;
199
201
  activation: ActivationStatus;
200
202
  onboarding: OnboardingState;
201
203
  health: HealthCenterState;
@@ -13,6 +13,7 @@ import type {
13
13
  } from "./status-model";
14
14
 
15
15
  import { getModelsCachePath } from "../app/constants";
16
+ import { buildContentTypeBoostStatus } from "../config/content-types";
16
17
  import {
17
18
  type ActivationStatus,
18
19
  buildActivationStatus,
@@ -740,6 +741,9 @@ export async function buildAppStatus(
740
741
  name: preset.name,
741
742
  },
742
743
  capabilities: ctx.capabilities,
744
+ contentTypeBoost: buildContentTypeBoostStatus(
745
+ ctx.config.contentTypes ?? []
746
+ ),
743
747
  activation,
744
748
  onboarding: buildOnboarding(
745
749
  status,
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Migration: allow multiple distinct context records at one logical scope.
3
+ *
4
+ * Context resolution already treats text as part of deterministic identity.
5
+ * The original two-column key accidentally limited each scope to one record.
6
+ *
7
+ * @module src/store/migrations/021-multi-context-identity
8
+ */
9
+
10
+ import type { Database } from "bun:sqlite";
11
+
12
+ import type { Migration } from "./runner";
13
+
14
+ export const migration: Migration = {
15
+ version: 21,
16
+ name: "multi_context_identity",
17
+
18
+ up(db: Database): void {
19
+ db.exec(`
20
+ CREATE TABLE contexts_v21 (
21
+ scope_type TEXT NOT NULL
22
+ CHECK (scope_type IN ('global', 'collection', 'prefix')),
23
+ scope_key TEXT NOT NULL,
24
+ text TEXT NOT NULL,
25
+ synced_at TEXT NOT NULL DEFAULT (datetime('now')),
26
+ PRIMARY KEY (scope_type, scope_key, text)
27
+ );
28
+
29
+ INSERT INTO contexts_v21 (scope_type, scope_key, text, synced_at)
30
+ SELECT scope_type, scope_key, text, synced_at
31
+ FROM contexts;
32
+
33
+ DROP TABLE contexts;
34
+ ALTER TABLE contexts_v21 RENAME TO contexts;
35
+ `);
36
+ },
37
+ };
@@ -34,6 +34,7 @@ import { migration as m017 } from "./017-document-change-retention-counters";
34
34
  import { migration as m018 } from "./018-saved-capsule-registration-epoch";
35
35
  import { migration as m019 } from "./019-saved-capsule-registration-generation";
36
36
  import { migration as m020 } from "./020-browser-clipper-security";
37
+ import { migration as m021 } from "./021-multi-context-identity";
37
38
 
38
39
  /** All migrations in order */
39
40
  export const migrations = [
@@ -57,4 +58,5 @@ export const migrations = [
57
58
  m018,
58
59
  m019,
59
60
  m020,
61
+ m021,
60
62
  ];
@@ -625,6 +625,57 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
625
625
  }
626
626
  }
627
627
 
628
+ async upsertCollections(
629
+ collections: Collection[]
630
+ ): Promise<StoreResult<void>> {
631
+ try {
632
+ const db = this.ensureOpen();
633
+ const stmt = db.prepare(`
634
+ INSERT INTO collections (name, path, pattern, include, exclude, update_cmd, language_hint, synced_at)
635
+ VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))
636
+ ON CONFLICT(name) DO UPDATE SET
637
+ path = excluded.path,
638
+ pattern = excluded.pattern,
639
+ include = excluded.include,
640
+ exclude = excluded.exclude,
641
+ update_cmd = excluded.update_cmd,
642
+ language_hint = excluded.language_hint,
643
+ synced_at = datetime('now')
644
+ WHERE collections.path IS NOT excluded.path
645
+ OR collections.pattern IS NOT excluded.pattern
646
+ OR collections.include IS NOT excluded.include
647
+ OR collections.exclude IS NOT excluded.exclude
648
+ OR collections.update_cmd IS NOT excluded.update_cmd
649
+ OR collections.language_hint IS NOT excluded.language_hint
650
+ `);
651
+ const transaction = db.transaction(() => {
652
+ for (const collection of collections) {
653
+ stmt.run(
654
+ collection.name,
655
+ collection.path,
656
+ collection.pattern,
657
+ collection.include.length > 0
658
+ ? JSON.stringify(collection.include)
659
+ : null,
660
+ collection.exclude.length > 0
661
+ ? JSON.stringify(collection.exclude)
662
+ : null,
663
+ collection.updateCmd ?? null,
664
+ collection.languageHint ?? null
665
+ );
666
+ }
667
+ });
668
+ transaction();
669
+ return ok(undefined);
670
+ } catch (cause) {
671
+ return err(
672
+ "QUERY_FAILED",
673
+ cause instanceof Error ? cause.message : "Failed to upsert collections",
674
+ cause
675
+ );
676
+ }
677
+ }
678
+
628
679
  async syncContexts(contexts: Context[]): Promise<StoreResult<void>> {
629
680
  try {
630
681
  const db = this.ensureOpen();
@@ -636,6 +687,8 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
636
687
  const stmt = db.prepare(`
637
688
  INSERT INTO contexts (scope_type, scope_key, text, synced_at)
638
689
  VALUES (?, ?, ?, datetime('now'))
690
+ ON CONFLICT(scope_type, scope_key, text) DO UPDATE SET
691
+ synced_at = excluded.synced_at
639
692
  `);
640
693
 
641
694
  for (const c of contexts) {
@@ -655,6 +708,36 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
655
708
  }
656
709
  }
657
710
 
711
+ async upsertContexts(contexts: Context[]): Promise<StoreResult<void>> {
712
+ try {
713
+ const db = this.ensureOpen();
714
+ const stmt = db.prepare(`
715
+ INSERT INTO contexts (scope_type, scope_key, text, synced_at)
716
+ VALUES (?, ?, ?, datetime('now'))
717
+ ON CONFLICT(scope_type, scope_key, text) DO NOTHING
718
+ `);
719
+ let inserted = 0;
720
+ const transaction = db.transaction(() => {
721
+ for (const context of contexts) {
722
+ inserted += stmt.run(
723
+ context.scopeType,
724
+ context.scopeKey,
725
+ context.text
726
+ ).changes;
727
+ }
728
+ });
729
+ transaction();
730
+ if (inserted > 0) this.contextGeneration += 1;
731
+ return ok(undefined);
732
+ } catch (cause) {
733
+ return err(
734
+ "QUERY_FAILED",
735
+ cause instanceof Error ? cause.message : "Failed to upsert contexts",
736
+ cause
737
+ );
738
+ }
739
+ }
740
+
658
741
  async getCollections(): Promise<StoreResult<CollectionRow[]>> {
659
742
  try {
660
743
  const db = this.ensureOpen();
@@ -2103,6 +2186,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
2103
2186
  d.source_size,
2104
2187
  d.source_hash,
2105
2188
  d.content_type,
2189
+ d.content_type_source,
2106
2190
  d.categories
2107
2191
  FROM fts_matches fm
2108
2192
  JOIN documents d ON d.id = fm.rowid AND d.active = 1
@@ -2129,6 +2213,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
2129
2213
  source_size: number | null;
2130
2214
  source_hash: string | null;
2131
2215
  content_type: string | null;
2216
+ content_type_source: string | null;
2132
2217
  categories: string | null;
2133
2218
  }
2134
2219
 
@@ -2167,6 +2252,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
2167
2252
  sourceSize: r.source_size ?? undefined,
2168
2253
  sourceHash: r.source_hash ?? undefined,
2169
2254
  contentType: r.content_type ?? undefined,
2255
+ contentTypeSource: r.content_type_source ?? undefined,
2170
2256
  categories: parseCategoriesJson(r.categories) ?? undefined,
2171
2257
  }))
2172
2258
  );