@gmickel/gno 1.26.0 → 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 (54) hide show
  1. package/README.md +2 -1
  2. package/assets/skill/SKILL.md +8 -0
  3. package/browser-extension/artifacts/{gno-browser-clipper-v1.26.0.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 +15 -2
  8. package/spec/evals-agentic.md +17 -0
  9. package/spec/mcp.md +25 -6
  10. package/spec/output-schemas/ask.schema.json +3 -0
  11. package/spec/output-schemas/query-diagnose.schema.json +61 -4
  12. package/spec/output-schemas/search-results.schema.json +87 -1
  13. package/spec/output-schemas/status.schema.json +24 -0
  14. package/spec/project-profile.schema.json +3 -1
  15. package/src/app/context-runtime-contract.ts +4 -1
  16. package/src/app/context-runtime-types.ts +2 -0
  17. package/src/app/context-runtime.ts +26 -0
  18. package/src/app/verified-ask.ts +6 -1
  19. package/src/cli/commands/ask.ts +8 -1
  20. package/src/cli/commands/query.ts +6 -3
  21. package/src/cli/commands/search.ts +6 -1
  22. package/src/cli/commands/status.ts +43 -7
  23. package/src/cli/program.ts +2 -0
  24. package/src/config/content-types.ts +82 -0
  25. package/src/config/index.ts +8 -0
  26. package/src/config/project-profile.ts +8 -1
  27. package/src/config/types.ts +11 -2
  28. package/src/core/context-compiler.ts +38 -1
  29. package/src/core/retrieval-replay-candidate.ts +6 -1
  30. package/src/ingestion/sync-options.ts +6 -2
  31. package/src/ingestion/sync.ts +21 -29
  32. package/src/ingestion/types.ts +1 -1
  33. package/src/mcp/tools/ask.ts +1 -0
  34. package/src/mcp/tools/index.ts +4 -0
  35. package/src/mcp/tools/query.ts +4 -2
  36. package/src/mcp/tools/search.ts +3 -0
  37. package/src/mcp/tools/status.ts +4 -0
  38. package/src/pipeline/content-type-boost.ts +264 -0
  39. package/src/pipeline/diagnose.ts +46 -19
  40. package/src/pipeline/explain.ts +15 -2
  41. package/src/pipeline/hybrid.ts +170 -74
  42. package/src/pipeline/rerank.ts +45 -15
  43. package/src/pipeline/search.ts +29 -11
  44. package/src/pipeline/types.ts +13 -4
  45. package/src/pipeline/vsearch.ts +30 -10
  46. package/src/sdk/client.ts +19 -3
  47. package/src/sdk/index.ts +1 -0
  48. package/src/sdk/types.ts +21 -5
  49. package/src/serve/routes/api.ts +17 -3
  50. package/src/serve/status-model.ts +2 -0
  51. package/src/serve/status.ts +4 -0
  52. package/src/store/sqlite/adapter.ts +3 -0
  53. package/src/store/types.ts +3 -2
  54. package/browser-extension/artifacts/gno-browser-clipper-v1.26.0.zip.sha256 +0 -1
@@ -7,6 +7,7 @@
7
7
  import type { IndexStatus } from "../../store/types";
8
8
  import type { ToolContext } from "../server";
9
9
 
10
+ import { buildContentTypeBoostStatus } from "../../config/content-types";
10
11
  import { resolveModelUri } from "../../llm/registry";
11
12
  import { createStandaloneResidentStatus } from "../../serve/resident-status";
12
13
  import { runTool, type ToolResult } from "./index";
@@ -87,6 +88,9 @@ export function handleStatus(
87
88
  return {
88
89
  ...result.value,
89
90
  configPath: ctx.actualConfigPath,
91
+ contentTypeBoost: buildContentTypeBoostStatus(
92
+ ctx.config.contentTypes ?? []
93
+ ),
90
94
  resident:
91
95
  ctx.getResidentStatus?.() ?? createStandaloneResidentStatus("stdio"),
92
96
  };
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Bounded content-type scoring composed with trusted project affinity.
3
+ *
4
+ * @module src/pipeline/content-type-boost
5
+ */
6
+
7
+ import type { NormalizedContentTypeRule } from "../config/content-types";
8
+ import type { ProjectAffinityScoringInput } from "./project-affinity";
9
+ import type { SearchResult } from "./types";
10
+
11
+ import {
12
+ fingerprintContentTypeRules,
13
+ resolveContentTypeRule,
14
+ } from "../config/content-types";
15
+ import {
16
+ CONTENT_TYPE_SEARCH_BOOST_MAX,
17
+ CONTENT_TYPE_SEARCH_BOOST_MIN,
18
+ CONTENT_TYPE_SEARCH_BOOST_NEUTRAL,
19
+ } from "../config/types";
20
+ import {
21
+ applyAuxiliaryScore,
22
+ hasProjectAffinity,
23
+ SEARCH_RESULT_AFFINITY_METADATA,
24
+ scoreProjectAffinity,
25
+ type ProjectAffinityScoreMetadata,
26
+ } from "./project-affinity";
27
+
28
+ export const CONTENT_TYPE_MAX_CONTRIBUTION = 0.05;
29
+
30
+ export interface ContentTypeBoostScoreMetadata {
31
+ baseScore: number;
32
+ cappedContribution: number;
33
+ combinedAuxiliaryApplied: number;
34
+ combinedAuxiliaryCap: number;
35
+ combinedAuxiliaryRequested: number;
36
+ configuredFactor: number;
37
+ contentType: string;
38
+ finalScore: number;
39
+ rawContribution: number;
40
+ rawScore: number;
41
+ rawScoreKind: ProjectAffinityScoreMetadata["rawScoreKind"];
42
+ ruleSource: "configured-id" | "prefix";
43
+ rulesFingerprint: string;
44
+ }
45
+
46
+ export interface AuxiliaryScoreResult {
47
+ contentTypeBoost?: ContentTypeBoostScoreMetadata;
48
+ projectAffinity: ProjectAffinityScoreMetadata;
49
+ }
50
+
51
+ export const SEARCH_RESULT_CONTENT_TYPE_BOOST_METADATA = Symbol(
52
+ "gno.searchResultContentTypeBoostMetadata"
53
+ );
54
+
55
+ const clamp = (value: number, min: number, max: number): number =>
56
+ Math.min(max, Math.max(min, value));
57
+
58
+ const rankingFingerprints = new WeakMap<
59
+ readonly NormalizedContentTypeRule[],
60
+ string
61
+ >();
62
+
63
+ const rankingFingerprint = (
64
+ rules: readonly NormalizedContentTypeRule[] | undefined
65
+ ): string => {
66
+ if (!rules) return fingerprintContentTypeRules([]);
67
+ const cached = rankingFingerprints.get(rules);
68
+ if (cached) return cached;
69
+ const fingerprint = fingerprintContentTypeRules([...rules]);
70
+ rankingFingerprints.set(rules, fingerprint);
71
+ return fingerprint;
72
+ };
73
+
74
+ /** Map the supported factor range continuously onto the contribution range. */
75
+ export function contentTypeBoostContribution(factor: number): {
76
+ raw: number;
77
+ capped: number;
78
+ } {
79
+ const raw =
80
+ factor >= CONTENT_TYPE_SEARCH_BOOST_NEUTRAL
81
+ ? ((factor - CONTENT_TYPE_SEARCH_BOOST_NEUTRAL) /
82
+ (CONTENT_TYPE_SEARCH_BOOST_MAX - CONTENT_TYPE_SEARCH_BOOST_NEUTRAL)) *
83
+ CONTENT_TYPE_MAX_CONTRIBUTION
84
+ : ((factor - CONTENT_TYPE_SEARCH_BOOST_NEUTRAL) /
85
+ (CONTENT_TYPE_SEARCH_BOOST_NEUTRAL - CONTENT_TYPE_SEARCH_BOOST_MIN)) *
86
+ CONTENT_TYPE_MAX_CONTRIBUTION;
87
+ return {
88
+ raw,
89
+ capped: clamp(
90
+ raw,
91
+ -CONTENT_TYPE_MAX_CONTRIBUTION,
92
+ CONTENT_TYPE_MAX_CONTRIBUTION
93
+ ),
94
+ };
95
+ }
96
+
97
+ export function hasContentTypeBoost(
98
+ rules: readonly NormalizedContentTypeRule[] | undefined
99
+ ): boolean {
100
+ return Boolean(
101
+ rules?.some(
102
+ (rule) => rule.searchBoost !== CONTENT_TYPE_SEARCH_BOOST_NEUTRAL
103
+ )
104
+ );
105
+ }
106
+
107
+ export function hasAuxiliaryRanking(
108
+ projectAffinity: ProjectAffinityScoringInput | undefined,
109
+ rules: readonly NormalizedContentTypeRule[] | undefined
110
+ ): boolean {
111
+ return hasProjectAffinity(projectAffinity) || hasContentTypeBoost(rules);
112
+ }
113
+
114
+ export function scoreContentTypeBoost(
115
+ baseScore: number,
116
+ contentType: string | undefined,
117
+ contentTypeSource: string | null | undefined,
118
+ relativePath: string,
119
+ collection: string,
120
+ rules: readonly NormalizedContentTypeRule[] | undefined,
121
+ projectAffinity: ProjectAffinityScoringInput | undefined,
122
+ raw: {
123
+ kind: ProjectAffinityScoreMetadata["rawScoreKind"];
124
+ score: number;
125
+ } = { kind: "normalized", score: baseScore }
126
+ ): AuxiliaryScoreResult {
127
+ const configuredId =
128
+ contentTypeSource === "frontmatter-type" ||
129
+ contentTypeSource === "frontmatter"
130
+ ? contentType
131
+ : undefined;
132
+ const resolution = resolveContentTypeRule(
133
+ configuredId,
134
+ relativePath,
135
+ rules ? [...rules] : []
136
+ );
137
+ const factor =
138
+ resolution?.rule.searchBoost ?? CONTENT_TYPE_SEARCH_BOOST_NEUTRAL;
139
+ const contribution = contentTypeBoostContribution(factor);
140
+ const projectScore = scoreProjectAffinity(
141
+ baseScore,
142
+ collection,
143
+ projectAffinity,
144
+ raw
145
+ );
146
+ const combined = applyAuxiliaryScore(baseScore, [
147
+ projectScore.affinityRequested,
148
+ contribution.capped,
149
+ ]);
150
+ const compositeProjectScore: ProjectAffinityScoreMetadata = {
151
+ ...projectScore,
152
+ combinedAuxiliaryApplied: combined.applied,
153
+ combinedAuxiliaryRequested: combined.requested,
154
+ finalBlendedScore: combined.finalScore,
155
+ finalScore: combined.finalScore,
156
+ };
157
+
158
+ if (!resolution || factor === CONTENT_TYPE_SEARCH_BOOST_NEUTRAL) {
159
+ return { projectAffinity: compositeProjectScore };
160
+ }
161
+
162
+ return {
163
+ projectAffinity: compositeProjectScore,
164
+ contentTypeBoost: {
165
+ baseScore,
166
+ cappedContribution: contribution.capped,
167
+ combinedAuxiliaryApplied: compositeProjectScore.combinedAuxiliaryApplied,
168
+ combinedAuxiliaryCap: compositeProjectScore.combinedAuxiliaryCap,
169
+ combinedAuxiliaryRequested:
170
+ compositeProjectScore.combinedAuxiliaryRequested,
171
+ configuredFactor: factor,
172
+ contentType: resolution.rule.id,
173
+ finalScore: compositeProjectScore.finalScore,
174
+ rawContribution: contribution.raw,
175
+ rawScore: raw.score,
176
+ rawScoreKind: raw.kind,
177
+ ruleSource: resolution.source,
178
+ rulesFingerprint: rankingFingerprint(rules),
179
+ },
180
+ };
181
+ }
182
+
183
+ export function attachAuxiliaryScoreMetadata(
184
+ result: SearchResult,
185
+ scored: AuxiliaryScoreResult,
186
+ finalScore: number,
187
+ includeProjectAffinity = false
188
+ ): SearchResult {
189
+ const projectAffinity = {
190
+ ...scored.projectAffinity,
191
+ finalBlendedScore: finalScore,
192
+ finalScore,
193
+ };
194
+ result.score = finalScore;
195
+ if (includeProjectAffinity) {
196
+ Object.defineProperty(result, SEARCH_RESULT_AFFINITY_METADATA, {
197
+ configurable: true,
198
+ enumerable: false,
199
+ value: projectAffinity,
200
+ writable: true,
201
+ });
202
+ }
203
+ if (scored.contentTypeBoost) {
204
+ Object.defineProperty(result, SEARCH_RESULT_CONTENT_TYPE_BOOST_METADATA, {
205
+ configurable: true,
206
+ enumerable: false,
207
+ value: { ...scored.contentTypeBoost, finalScore },
208
+ writable: true,
209
+ });
210
+ }
211
+ return result;
212
+ }
213
+
214
+ export function applyContentTypeBoost(
215
+ result: SearchResult,
216
+ collection: string,
217
+ rules: readonly NormalizedContentTypeRule[] | undefined,
218
+ projectAffinity: ProjectAffinityScoringInput | undefined,
219
+ contentTypeSource?: string | null,
220
+ raw?: {
221
+ kind: ProjectAffinityScoreMetadata["rawScoreKind"];
222
+ score: number;
223
+ }
224
+ ): SearchResult {
225
+ const scored = scoreContentTypeBoost(
226
+ result.score,
227
+ result.contentType,
228
+ contentTypeSource,
229
+ result.source.relPath,
230
+ collection,
231
+ rules,
232
+ projectAffinity,
233
+ raw
234
+ );
235
+ const affinityActive = hasProjectAffinity(projectAffinity);
236
+ if (!(scored.contentTypeBoost || affinityActive)) return result;
237
+ return attachAuxiliaryScoreMetadata(
238
+ result,
239
+ scored,
240
+ scored.projectAffinity.finalScore,
241
+ affinityActive
242
+ );
243
+ }
244
+
245
+ export function getContentTypeBoostMetadata(
246
+ result: SearchResult
247
+ ): ContentTypeBoostScoreMetadata | undefined {
248
+ return (
249
+ result as SearchResult & {
250
+ [SEARCH_RESULT_CONTENT_TYPE_BOOST_METADATA]?: ContentTypeBoostScoreMetadata;
251
+ }
252
+ )[SEARCH_RESULT_CONTENT_TYPE_BOOST_METADATA];
253
+ }
254
+
255
+ export function sortByFinalScoreStable(results: SearchResult[]): void {
256
+ const originalRank = new Map(
257
+ results.map((result, index) => [result, index] as const)
258
+ );
259
+ results.sort(
260
+ (left, right) =>
261
+ right.score - left.score ||
262
+ (originalRank.get(left) ?? 0) - (originalRank.get(right) ?? 0)
263
+ );
264
+ }
@@ -14,16 +14,23 @@ import type {
14
14
  QueryDiagnoseTraceCandidate,
15
15
  } from "./types";
16
16
 
17
- import { fingerprintContentTypeRules } from "../config";
17
+ import {
18
+ fingerprintContentTypeMetadataRules,
19
+ normalizeContentTypes,
20
+ } from "../config";
18
21
  import { resolveDocRef } from "../core/ref-parser";
19
22
  import { err, ok } from "../store/types";
23
+ import {
24
+ getContentTypeBoostMetadata,
25
+ scoreContentTypeBoost,
26
+ type ContentTypeBoostScoreMetadata,
27
+ } from "./content-type-boost";
20
28
  import { evaluateQueryTargetFilters } from "./filters";
21
29
  import { searchHybrid } from "./hybrid";
22
30
  import {
23
31
  getProjectAffinityMetadata,
24
32
  type ProjectAffinityScoringInput,
25
33
  type ProjectAffinityScoreMetadata,
26
- scoreProjectAffinity,
27
34
  } from "./project-affinity";
28
35
 
29
36
  export type QueryDiagnoseTargetStatus =
@@ -52,7 +59,7 @@ export interface QueryDiagnoseStage {
52
59
  }
53
60
 
54
61
  export interface QueryDiagnoseResult {
55
- schemaVersion: "1.0" | "1.1";
62
+ schemaVersion: "1.0" | "1.1" | "1.2";
56
63
  query: string;
57
64
  target: {
58
65
  ref: string;
@@ -72,6 +79,7 @@ export interface QueryDiagnoseResult {
72
79
  };
73
80
  stages: QueryDiagnoseStage[];
74
81
  affinity?: ProjectAffinityScoreMetadata;
82
+ contentTypeBoost?: ContentTypeBoostScoreMetadata;
75
83
  chunk: {
76
84
  seq: number | null;
77
85
  startLine: number | null;
@@ -182,9 +190,12 @@ export async function diagnoseQueryTarget(
182
190
  }
183
191
 
184
192
  const doc = resolved.doc;
185
- const rules = options.contentTypeRules ?? [];
193
+ const rules =
194
+ options.contentTypeRules ??
195
+ normalizeContentTypes(deps.config.contentTypes ?? []).rules;
186
196
  const expectedFingerprint =
187
- options.contentTypeRulesFingerprint ?? fingerprintContentTypeRules(rules);
197
+ options.contentTypeRulesFingerprint ??
198
+ fingerprintContentTypeMetadataRules(rules);
188
199
  const fingerprintMatches = doc.contentTypeRulesFingerprint
189
200
  ? doc.contentTypeRulesFingerprint === expectedFingerprint
190
201
  : null;
@@ -302,16 +313,24 @@ export async function diagnoseQueryTarget(
302
313
  (candidate) =>
303
314
  candidate.mirrorHash === doc.mirrorHash && targetSeqs.has(candidate.seq)
304
315
  );
316
+ const fallbackScore = lastMatched
317
+ ? scoreContentTypeBoost(
318
+ lastMatched.score,
319
+ doc.contentType ?? undefined,
320
+ doc.contentTypeSource,
321
+ doc.relPath,
322
+ doc.collection,
323
+ rules,
324
+ options.projectAffinity,
325
+ { kind: "hybrid_blended", score: lastMatched.score }
326
+ )
327
+ : undefined;
305
328
  const affinity =
306
329
  (targetResult ? getProjectAffinityMetadata(targetResult) : undefined) ??
307
- (lastMatched && options.projectAffinity
308
- ? scoreProjectAffinity(
309
- lastMatched.score,
310
- doc.collection,
311
- options.projectAffinity,
312
- { kind: "hybrid_blended", score: lastMatched.score }
313
- )
314
- : null);
330
+ (options.projectAffinity ? fallbackScore?.projectAffinity : undefined);
331
+ const contentTypeBoost =
332
+ (targetResult ? getContentTypeBoostMetadata(targetResult) : undefined) ??
333
+ fallbackScore?.contentTypeBoost;
315
334
 
316
335
  const baseResult: QueryDiagnoseResult = {
317
336
  ...buildBaseResult(query, options.target, "diagnosed", doc, {
@@ -335,13 +354,21 @@ export async function diagnoseQueryTarget(
335
354
  queryModes: searchResult.value.meta.queryModes,
336
355
  },
337
356
  };
338
- return ok(
357
+ const trustedAffinity =
339
358
  affinity && hasTrustedProjectAffinityInput(options.projectAffinity)
340
- ? {
341
- ...baseResult,
342
- schemaVersion: "1.1",
343
- affinity,
344
- }
359
+ ? affinity
360
+ : undefined;
361
+ if (contentTypeBoost) {
362
+ return ok({
363
+ ...baseResult,
364
+ schemaVersion: "1.2",
365
+ ...(trustedAffinity ? { affinity: trustedAffinity } : {}),
366
+ contentTypeBoost,
367
+ });
368
+ }
369
+ return ok(
370
+ trustedAffinity
371
+ ? { ...baseResult, schemaVersion: "1.1", affinity: trustedAffinity }
345
372
  : baseResult
346
373
  );
347
374
  }
@@ -14,6 +14,8 @@ import type {
14
14
  SearchResult,
15
15
  } from "./types";
16
16
 
17
+ import { getContentTypeBoostMetadata } from "./content-type-boost";
18
+ import { getProjectAffinityMetadata } from "./project-affinity";
17
19
  import { SEARCH_RESULT_PLANNER_METADATA } from "./types";
18
20
 
19
21
  // ─────────────────────────────────────────────────────────────────────────────
@@ -39,7 +41,8 @@ export function formatResultExplain(results: ExplainResult[]): string {
39
41
  r.bm25Score !== undefined ||
40
42
  r.vecScore !== undefined ||
41
43
  r.rerankScore !== undefined ||
42
- r.projectAffinity !== undefined
44
+ r.projectAffinity !== undefined ||
45
+ r.contentTypeBoost !== undefined
43
46
  ) {
44
47
  msg += " (";
45
48
  if (r.fusionScore !== undefined) {
@@ -69,6 +72,12 @@ export function formatResultExplain(results: ExplainResult[]): string {
69
72
  }
70
73
  msg += `raw=${r.projectAffinity.rawScoreKind}:${r.projectAffinity.rawScore.toFixed(3)}, base=${r.projectAffinity.baseScore.toFixed(3)}, affinity=${r.projectAffinity.affinityApplied.toFixed(3)}/${r.projectAffinity.affinityRequested.toFixed(3)}, auxiliary=${r.projectAffinity.combinedAuxiliaryApplied.toFixed(3)}/${r.projectAffinity.combinedAuxiliaryCap.toFixed(3)}, collection=${r.projectAffinity.collectionAlias}, root=${r.projectAffinity.rootAlias}, source=${r.projectAffinity.source}, final=${r.projectAffinity.finalScore.toFixed(3)}`;
71
74
  }
75
+ if (r.contentTypeBoost) {
76
+ if (msg.at(-1) !== "(") {
77
+ msg += ", ";
78
+ }
79
+ msg += `contentType=${r.contentTypeBoost.contentType}, factor=${r.contentTypeBoost.configuredFactor.toFixed(3)}, rawBoost=${r.contentTypeBoost.rawContribution.toFixed(3)}, cappedBoost=${r.contentTypeBoost.cappedContribution.toFixed(3)}, auxiliary=${r.contentTypeBoost.combinedAuxiliaryApplied.toFixed(3)}/${r.contentTypeBoost.combinedAuxiliaryCap.toFixed(3)}, source=${r.contentTypeBoost.ruleSource}, final=${r.contentTypeBoost.finalScore.toFixed(3)}`;
80
+ }
72
81
  msg += ")";
73
82
  }
74
83
  lines.push(`[explain] result ${r.rank}: ${r.docid} ${msg}`);
@@ -227,7 +236,11 @@ export function buildExplainResults(
227
236
  entry.mirrorHash === result.conversion?.mirrorHash &&
228
237
  entry.seq === (planner?.retrievalSeq ?? planner?.seq)
229
238
  );
230
- return buildExplainResult(result.docid, result.score, index, candidate);
239
+ return {
240
+ ...buildExplainResult(result.docid, result.score, index, candidate),
241
+ projectAffinity: getProjectAffinityMetadata(result),
242
+ contentTypeBoost: getContentTypeBoostMetadata(result),
243
+ };
231
244
  });
232
245
  }
233
246
  return candidates.slice(0, 20).map((candidate, index) => {