@gmickel/gno 1.36.0 → 1.36.1
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/browser-extension/artifacts/{gno-browser-clipper-v1.36.0.zip → gno-browser-clipper-v1.36.1.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.36.1.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/src/core/context-evidence.ts +8 -2
- package/src/pipeline/hybrid.ts +15 -1
- package/src/pipeline/search.ts +15 -6
- package/src/pipeline/snippet.ts +203 -0
- package/src/pipeline/vsearch.ts +15 -6
- package/browser-extension/artifacts/gno-browser-clipper-v1.36.0.zip.sha256 +0 -1
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
8ece31111978af50978a899662a7bd14eddcc90f2c90f6b0f04b67b3b602d980 gno-browser-clipper-v1.36.1.zip
|
package/package.json
CHANGED
|
@@ -413,13 +413,19 @@ export const materializeContextEvidenceCandidates = async (
|
|
|
413
413
|
const content = contentByHash.get(document.mirrorHash);
|
|
414
414
|
const chunk = getChunk(document.mirrorHash, metadata.seq);
|
|
415
415
|
const chunkKey = `${document.mirrorHash}:${metadata.seq}`;
|
|
416
|
+
// Display snippets may omit leading YAML frontmatter, so startLine can sit
|
|
417
|
+
// after the stored chunk start. endLine must still identify the same chunk.
|
|
418
|
+
const displayRangeWithinChunk =
|
|
419
|
+
chunk !== undefined &&
|
|
420
|
+
snippetRange.endLine === chunk.endLine &&
|
|
421
|
+
snippetRange.startLine >= chunk.startLine &&
|
|
422
|
+
snippetRange.startLine <= chunk.endLine;
|
|
416
423
|
if (
|
|
417
424
|
!content ||
|
|
418
425
|
metadata.mirrorHash !== document.mirrorHash ||
|
|
419
426
|
!chunk ||
|
|
420
427
|
chunk.mirrorHash !== document.mirrorHash ||
|
|
421
|
-
|
|
422
|
-
snippetRange.endLine !== chunk.endLine
|
|
428
|
+
!displayRangeWithinChunk
|
|
423
429
|
) {
|
|
424
430
|
throw new ContextEvidenceError(
|
|
425
431
|
"chunk_coordinate_mismatch",
|
package/src/pipeline/hybrid.ts
CHANGED
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
} from "./query-modes";
|
|
59
59
|
import { rerankCandidates } from "./rerank";
|
|
60
60
|
import { attachSearchResultContexts } from "./result-context";
|
|
61
|
+
import { cleanDisplaySnippet } from "./snippet";
|
|
61
62
|
import {
|
|
62
63
|
isWithinTemporalRange,
|
|
63
64
|
resolveRecencyTimestamp,
|
|
@@ -1003,6 +1004,7 @@ export async function searchHybrid(
|
|
|
1003
1004
|
) ?? chunk);
|
|
1004
1005
|
|
|
1005
1006
|
let snippet = snippetChunk.text;
|
|
1007
|
+
let snippetStartLine = snippetChunk.startLine;
|
|
1006
1008
|
let snippetRange: { startLine: number; endLine: number } | undefined = {
|
|
1007
1009
|
startLine: snippetChunk.startLine,
|
|
1008
1010
|
endLine: snippetChunk.endLine,
|
|
@@ -1021,6 +1023,18 @@ export async function searchHybrid(
|
|
|
1021
1023
|
snippetRange = undefined; // Full content has no range
|
|
1022
1024
|
}
|
|
1023
1025
|
// Fallback to chunk text if content unavailable
|
|
1026
|
+
} else {
|
|
1027
|
+
const cleanedSnippet = cleanDisplaySnippet(
|
|
1028
|
+
snippetChunk.text,
|
|
1029
|
+
snippetChunk.text
|
|
1030
|
+
);
|
|
1031
|
+
snippet = cleanedSnippet.text;
|
|
1032
|
+
snippetStartLine =
|
|
1033
|
+
snippetChunk.startLine + cleanedSnippet.startLineOffset;
|
|
1034
|
+
snippetRange = {
|
|
1035
|
+
startLine: snippetStartLine,
|
|
1036
|
+
endLine: snippetChunk.endLine,
|
|
1037
|
+
};
|
|
1024
1038
|
}
|
|
1025
1039
|
|
|
1026
1040
|
for (const doc of candidateDocs) {
|
|
@@ -1050,7 +1064,7 @@ export async function searchHybrid(
|
|
|
1050
1064
|
title: doc.title ?? undefined,
|
|
1051
1065
|
contentType: doc.contentType ?? undefined,
|
|
1052
1066
|
categories: doc.categories ?? undefined,
|
|
1053
|
-
line:
|
|
1067
|
+
line: snippetStartLine,
|
|
1054
1068
|
snippet,
|
|
1055
1069
|
snippetLanguage: chunk.language ?? undefined,
|
|
1056
1070
|
snippetRange,
|
package/src/pipeline/search.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { selectBestChunkForSteering } from "./intent";
|
|
|
30
30
|
import { hasProjectAffinity } from "./project-affinity";
|
|
31
31
|
import { detectQueryLanguage } from "./query-language";
|
|
32
32
|
import { attachSearchResultContexts } from "./result-context";
|
|
33
|
+
import { cleanDisplaySnippet } from "./snippet";
|
|
33
34
|
import {
|
|
34
35
|
resolveRecencyTimestamp,
|
|
35
36
|
resolveTemporalRange,
|
|
@@ -107,6 +108,7 @@ function buildSearchResult(ctx: BuildResultContext): SearchResult {
|
|
|
107
108
|
// Determine snippet content and range
|
|
108
109
|
let snippet: string;
|
|
109
110
|
let snippetRange: { startLine: number; endLine: number } | undefined;
|
|
111
|
+
let line = chunk?.startLine;
|
|
110
112
|
|
|
111
113
|
if (options?.full && fullContent) {
|
|
112
114
|
// --full: use full content, no range (full doc)
|
|
@@ -117,11 +119,18 @@ function buildSearchResult(ctx: BuildResultContext): SearchResult {
|
|
|
117
119
|
snippet = chunk.text;
|
|
118
120
|
snippetRange = { startLine: chunk.startLine, endLine: chunk.endLine };
|
|
119
121
|
} else {
|
|
120
|
-
// Default:
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
// Default: FTS snippet or chunk text, with leading frontmatter stripped
|
|
123
|
+
const cleaned = cleanDisplaySnippet(
|
|
124
|
+
fts.snippet ?? chunk?.text ?? "",
|
|
125
|
+
chunk?.text
|
|
126
|
+
);
|
|
127
|
+
snippet = cleaned.text;
|
|
128
|
+
if (chunk) {
|
|
129
|
+
line = chunk.startLine + cleaned.startLineOffset;
|
|
130
|
+
snippetRange = { startLine: line, endLine: chunk.endLine };
|
|
131
|
+
} else {
|
|
132
|
+
snippetRange = undefined;
|
|
133
|
+
}
|
|
125
134
|
}
|
|
126
135
|
|
|
127
136
|
const result: SearchResult = {
|
|
@@ -131,7 +140,7 @@ function buildSearchResult(ctx: BuildResultContext): SearchResult {
|
|
|
131
140
|
title: fts.title,
|
|
132
141
|
contentType: fts.contentType,
|
|
133
142
|
categories: fts.categories,
|
|
134
|
-
line
|
|
143
|
+
line,
|
|
135
144
|
snippet,
|
|
136
145
|
snippetLanguage: chunk?.language ?? undefined,
|
|
137
146
|
snippetRange,
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display-layer snippet cleaning for search/query results.
|
|
3
|
+
* Strips leading YAML frontmatter so snippets prefer prose. Does not change
|
|
4
|
+
* indexed text, `--full` content, or `--line-numbers` raw chunks.
|
|
5
|
+
*
|
|
6
|
+
* @module src/pipeline/snippet
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { stripFrontmatter } from "../ingestion/frontmatter";
|
|
10
|
+
|
|
11
|
+
/** FTS5 highlight markers from snippet(documents_fts, ..., '<mark>', '</mark>', '...', 32). */
|
|
12
|
+
const MARK_TAG_REGEX = /<\/?mark>/g;
|
|
13
|
+
|
|
14
|
+
/** Leading blank lines after a closed frontmatter fence. */
|
|
15
|
+
const LEADING_BLANK_LINES_REGEX = /^(?:[ \t]*\r?\n)+/;
|
|
16
|
+
|
|
17
|
+
/** YAML mapping line (`key: value` or `key:`). */
|
|
18
|
+
const YAML_MAPPING_LINE_REGEX = /^[\w./-]+\s*:/;
|
|
19
|
+
|
|
20
|
+
/** YAML sequence item. */
|
|
21
|
+
const YAML_SEQUENCE_LINE_REGEX = /^- /;
|
|
22
|
+
|
|
23
|
+
export interface DisplaySnippet {
|
|
24
|
+
text: string;
|
|
25
|
+
/**
|
|
26
|
+
* Lines to add to the chunk's startLine when the emitted text is derived
|
|
27
|
+
* from stripped chunk prose (not a kept FTS window).
|
|
28
|
+
*/
|
|
29
|
+
startLineOffset: number;
|
|
30
|
+
/** True when a frontmatter-dominated FTS snippet was replaced by chunk prose. */
|
|
31
|
+
usedChunkFallback: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Clean a default-path snippet: strip a leading closed YAML fence, or replace
|
|
36
|
+
* an FTS window that is only frontmatter with stripped chunk prose.
|
|
37
|
+
* Never returns an empty string when the original text had content.
|
|
38
|
+
*/
|
|
39
|
+
export function cleanDisplaySnippet(
|
|
40
|
+
snippet: string,
|
|
41
|
+
chunkText?: string
|
|
42
|
+
): DisplaySnippet {
|
|
43
|
+
const strippedSnippet = stripLeadingFrontmatterBlock(snippet);
|
|
44
|
+
if (strippedSnippet.didStrip) {
|
|
45
|
+
return {
|
|
46
|
+
text: strippedSnippet.text,
|
|
47
|
+
startLineOffset: strippedSnippet.lineCount,
|
|
48
|
+
usedChunkFallback: false,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const afterEmbeddedFence = proseAfterEmbeddedFrontmatterFence(snippet);
|
|
53
|
+
if (afterEmbeddedFence !== undefined) {
|
|
54
|
+
const cleanedChunk =
|
|
55
|
+
chunkText === undefined
|
|
56
|
+
? undefined
|
|
57
|
+
: stripLeadingFrontmatterBlock(chunkText);
|
|
58
|
+
return {
|
|
59
|
+
text: afterEmbeddedFence,
|
|
60
|
+
startLineOffset: cleanedChunk?.didStrip ? cleanedChunk.lineCount : 0,
|
|
61
|
+
usedChunkFallback: false,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const canFallback =
|
|
66
|
+
chunkText !== undefined &&
|
|
67
|
+
chunkText !== snippet &&
|
|
68
|
+
isFrontmatterDominatedSnippet(snippet);
|
|
69
|
+
if (canFallback) {
|
|
70
|
+
const cleanedChunk = stripLeadingFrontmatterBlock(chunkText);
|
|
71
|
+
if (cleanedChunk.text.length > 0) {
|
|
72
|
+
return {
|
|
73
|
+
text: cleanedChunk.text,
|
|
74
|
+
startLineOffset: cleanedChunk.didStrip ? cleanedChunk.lineCount : 0,
|
|
75
|
+
usedChunkFallback: true,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
text: snippet,
|
|
82
|
+
startLineOffset: 0,
|
|
83
|
+
usedChunkFallback: false,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** True for FTS-style snippets that are only (or start as) YAML frontmatter. */
|
|
88
|
+
export function isFrontmatterDominatedSnippet(text: string): boolean {
|
|
89
|
+
const unmarked = text.replace(MARK_TAG_REGEX, "");
|
|
90
|
+
const trimmed = unmarked.trimStart();
|
|
91
|
+
const withoutLeadingEllipsis = trimmed.startsWith("...")
|
|
92
|
+
? trimmed.slice(3).trimStart()
|
|
93
|
+
: trimmed;
|
|
94
|
+
if (withoutLeadingEllipsis.startsWith("---")) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const contentLines = unmarked.split(/\r?\n/).filter((line) => {
|
|
99
|
+
const trimmedLine = line.trim();
|
|
100
|
+
return trimmedLine.length > 0 && trimmedLine !== "...";
|
|
101
|
+
});
|
|
102
|
+
if (contentLines.length === 0) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
if (contentLines.every(isYamlFrontmatterLine)) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
return proseAfterEmbeddedFrontmatterFence(text) !== undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* FTS windows often straddle the closing fence (`...yaml\n---\n# Heading`).
|
|
113
|
+
* Keep the prose after that fence when the prefix looks like YAML.
|
|
114
|
+
*/
|
|
115
|
+
function proseAfterEmbeddedFrontmatterFence(text: string): string | undefined {
|
|
116
|
+
const lines = text.split(/\r?\n/);
|
|
117
|
+
for (let i = 1; i < lines.length; i++) {
|
|
118
|
+
const line = lines[i];
|
|
119
|
+
if (line === undefined) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (line.replace(MARK_TAG_REGEX, "").trim() !== "---") {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const prefixLines = lines.slice(0, i);
|
|
126
|
+
if (!prefixLooksLikeFrontmatter(prefixLines)) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const after = lines
|
|
130
|
+
.slice(i + 1)
|
|
131
|
+
.join("\n")
|
|
132
|
+
.replace(LEADING_BLANK_LINES_REGEX, "");
|
|
133
|
+
if (after.trim().length === 0) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
return after;
|
|
137
|
+
}
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function prefixLooksLikeFrontmatter(lines: string[]): boolean {
|
|
142
|
+
const content = lines.filter((line) => {
|
|
143
|
+
const trimmed = line.replace(MARK_TAG_REGEX, "").trim();
|
|
144
|
+
return trimmed.length > 0 && trimmed !== "...";
|
|
145
|
+
});
|
|
146
|
+
if (content.length === 0) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
return content.every((line, index) => {
|
|
150
|
+
const trimmed = line.replace(MARK_TAG_REGEX, "").trim();
|
|
151
|
+
if (index === 0 && trimmed.startsWith("...")) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
return isYamlFrontmatterLine(trimmed);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function stripLeadingFrontmatterBlock(text: string): {
|
|
159
|
+
text: string;
|
|
160
|
+
didStrip: boolean;
|
|
161
|
+
lineCount: number;
|
|
162
|
+
} {
|
|
163
|
+
const afterFence = stripFrontmatter(text);
|
|
164
|
+
if (afterFence === text) {
|
|
165
|
+
return { text, didStrip: false, lineCount: 0 };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const withoutBlanks = afterFence.replace(LEADING_BLANK_LINES_REGEX, "");
|
|
169
|
+
if (withoutBlanks.trim().length === 0) {
|
|
170
|
+
return { text, didStrip: false, lineCount: 0 };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const prefix = text.slice(0, text.length - withoutBlanks.length);
|
|
174
|
+
return {
|
|
175
|
+
text: withoutBlanks,
|
|
176
|
+
didStrip: true,
|
|
177
|
+
lineCount: countConsumedLines(prefix),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function isYamlFrontmatterLine(line: string): boolean {
|
|
182
|
+
const trimmed = line.trim();
|
|
183
|
+
if (trimmed === "---") {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
if (YAML_SEQUENCE_LINE_REGEX.test(trimmed)) {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
return YAML_MAPPING_LINE_REGEX.test(trimmed);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function countConsumedLines(prefix: string): number {
|
|
193
|
+
if (prefix.length === 0) {
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
let newlineCount = 0;
|
|
197
|
+
for (const char of prefix) {
|
|
198
|
+
if (char === "\n") {
|
|
199
|
+
newlineCount += 1;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return prefix.endsWith("\n") ? newlineCount : newlineCount + 1;
|
|
203
|
+
}
|
package/src/pipeline/vsearch.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { selectBestChunkForSteering } from "./intent";
|
|
|
28
28
|
import { hasProjectAffinity } from "./project-affinity";
|
|
29
29
|
import { detectQueryLanguage } from "./query-language";
|
|
30
30
|
import { attachSearchResultContexts } from "./result-context";
|
|
31
|
+
import { cleanDisplaySnippet } from "./snippet";
|
|
31
32
|
import {
|
|
32
33
|
resolveRecencyTimestamp,
|
|
33
34
|
isWithinTemporalRange,
|
|
@@ -236,6 +237,8 @@ export async function searchVectorWithEmbedding(
|
|
|
236
237
|
continue;
|
|
237
238
|
}
|
|
238
239
|
|
|
240
|
+
const cleanedSnippet = cleanDisplaySnippet(chunk.text, chunk.text);
|
|
241
|
+
const snippetStartLine = chunk.startLine + cleanedSnippet.startLineOffset;
|
|
239
242
|
const scoredResult = applyContentTypeBoost(
|
|
240
243
|
{
|
|
241
244
|
docid: doc.docid,
|
|
@@ -244,11 +247,11 @@ export async function searchVectorWithEmbedding(
|
|
|
244
247
|
title: doc.title ?? undefined,
|
|
245
248
|
contentType: doc.contentType ?? undefined,
|
|
246
249
|
categories: doc.categories ?? undefined,
|
|
247
|
-
line:
|
|
248
|
-
snippet:
|
|
250
|
+
line: snippetStartLine,
|
|
251
|
+
snippet: cleanedSnippet.text,
|
|
249
252
|
snippetLanguage: chunk.language ?? undefined,
|
|
250
253
|
snippetRange: {
|
|
251
|
-
startLine:
|
|
254
|
+
startLine: snippetStartLine,
|
|
252
255
|
endLine: chunk.endLine,
|
|
253
256
|
},
|
|
254
257
|
source: {
|
|
@@ -338,6 +341,12 @@ export async function searchVectorWithEmbedding(
|
|
|
338
341
|
|
|
339
342
|
const collectionPath = collectionPaths.get(doc.collection);
|
|
340
343
|
const sourceRelPath = doc.recordSourcePath ?? doc.relPath;
|
|
344
|
+
const cleanedChunk = fullContent
|
|
345
|
+
? undefined
|
|
346
|
+
: cleanDisplaySnippet(chunk.text, chunk.text);
|
|
347
|
+
const snippetStartLine = cleanedChunk
|
|
348
|
+
? chunk.startLine + cleanedChunk.startLineOffset
|
|
349
|
+
: chunk.startLine;
|
|
341
350
|
|
|
342
351
|
const result = applyContentTypeBoost(
|
|
343
352
|
{
|
|
@@ -347,13 +356,13 @@ export async function searchVectorWithEmbedding(
|
|
|
347
356
|
title: doc.title ?? undefined,
|
|
348
357
|
contentType: doc.contentType ?? undefined,
|
|
349
358
|
categories: doc.categories ?? undefined,
|
|
350
|
-
line:
|
|
351
|
-
snippet: fullContent ?? chunk.text,
|
|
359
|
+
line: snippetStartLine,
|
|
360
|
+
snippet: fullContent ?? cleanedChunk?.text ?? chunk.text,
|
|
352
361
|
snippetLanguage: chunk.language ?? undefined,
|
|
353
362
|
// --full: no snippetRange (full doc content)
|
|
354
363
|
snippetRange: fullContent
|
|
355
364
|
? undefined
|
|
356
|
-
: { startLine:
|
|
365
|
+
: { startLine: snippetStartLine, endLine: chunk.endLine },
|
|
357
366
|
source: {
|
|
358
367
|
relPath: sourceRelPath,
|
|
359
368
|
absPath: collectionPath
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
8d8b73a55081409e3dbba4e6927d864839329cbcdb98a8f78274c29688edeb9e gno-browser-clipper-v1.36.0.zip
|