@gmickel/gno 1.35.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.
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Shared peek snapshot builder for CLI and later MCP.
3
+ * Metadata-only: never opens the model cache, resolves model URIs, or activates.
4
+ *
5
+ * @module src/core/peek
6
+ */
7
+
8
+ // node:path — no Bun path utils
9
+ import { join as pathJoin } from "node:path";
10
+
11
+ import type { DocumentRow, IndexStatus } from "../store/types";
12
+
13
+ import { DEFAULT_INDEX_NAME, VERSION, getIndexDbPath } from "../app/constants";
14
+ import {
15
+ isProcessAlive,
16
+ readPidFile,
17
+ resolveProcessPaths,
18
+ } from "../cli/detach";
19
+ import { CliError } from "../cli/errors";
20
+ import { isInitialized, loadConfig } from "../config";
21
+ import { SqliteAdapter } from "../store/sqlite/adapter";
22
+
23
+ export const PEEK_SCHEMA_VERSION = "peek@1.0" as const;
24
+ export const PEEK_RECENT_LIMIT = 10;
25
+
26
+ export interface PeekCounts {
27
+ documents: number;
28
+ collections: number;
29
+ }
30
+
31
+ export interface PeekBacklog {
32
+ pending: number;
33
+ failed: number;
34
+ }
35
+
36
+ export interface PeekRecentItem {
37
+ docid: string;
38
+ uri: string;
39
+ title: string | null;
40
+ collection: string;
41
+ absPath: string;
42
+ modifiedAt: string;
43
+ }
44
+
45
+ export interface PeekServe {
46
+ running: boolean;
47
+ url: string | null;
48
+ }
49
+
50
+ export interface PeekSnapshot {
51
+ schemaVersion: typeof PEEK_SCHEMA_VERSION;
52
+ gnoVersion: string;
53
+ generatedAt: string;
54
+ initialized: boolean;
55
+ indexName: string;
56
+ counts: PeekCounts | null;
57
+ backlog: PeekBacklog | null;
58
+ lastIndexedAt: string | null;
59
+ recent: PeekRecentItem[];
60
+ serve: PeekServe;
61
+ }
62
+
63
+ export interface BuildPeekOptions {
64
+ configPath?: string;
65
+ indexName?: string;
66
+ }
67
+
68
+ function asRuntimeError(error: unknown, fallback: string): CliError {
69
+ if (error instanceof CliError) {
70
+ return error;
71
+ }
72
+ return new CliError(
73
+ "RUNTIME",
74
+ error instanceof Error ? error.message : fallback
75
+ );
76
+ }
77
+
78
+ async function readServeLiveness(): Promise<PeekServe> {
79
+ try {
80
+ const { pidFile } = resolveProcessPaths("serve");
81
+ const payload = await readPidFile(pidFile);
82
+ if (!payload || !isProcessAlive(payload.pid) || payload.port == null) {
83
+ return { running: false, url: null };
84
+ }
85
+ return {
86
+ running: true,
87
+ url: `http://localhost:${payload.port}`,
88
+ };
89
+ } catch (error) {
90
+ throw asRuntimeError(error, "Failed to read serve process state");
91
+ }
92
+ }
93
+
94
+ function mapRecent(
95
+ documents: DocumentRow[],
96
+ status: IndexStatus
97
+ ): PeekRecentItem[] {
98
+ const collectionPaths = new Map(
99
+ status.collections.map((collection) => [collection.name, collection.path])
100
+ );
101
+ return documents.slice(0, PEEK_RECENT_LIMIT).map((doc) => {
102
+ const sourceRelPath = doc.recordSourcePath ?? doc.relPath;
103
+ const collectionPath = collectionPaths.get(doc.collection) ?? "";
104
+ return {
105
+ docid: doc.docid,
106
+ uri: doc.uri,
107
+ title: doc.title,
108
+ collection: doc.collection,
109
+ absPath: pathJoin(collectionPath, sourceRelPath),
110
+ modifiedAt: doc.sourceMtime,
111
+ };
112
+ });
113
+ }
114
+
115
+ function emptySnapshot(
116
+ indexName: string,
117
+ generatedAt: string,
118
+ serve: PeekServe
119
+ ): PeekSnapshot {
120
+ return {
121
+ schemaVersion: PEEK_SCHEMA_VERSION,
122
+ gnoVersion: VERSION,
123
+ generatedAt,
124
+ initialized: false,
125
+ indexName,
126
+ counts: null,
127
+ backlog: null,
128
+ lastIndexedAt: null,
129
+ recent: [],
130
+ serve,
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Build a peek@1.0 snapshot. Throws CliError("RUNTIME") on any subquery
136
+ * failure so callers never emit a half-filled payload.
137
+ */
138
+ export async function buildPeekSnapshot(
139
+ options: BuildPeekOptions = {}
140
+ ): Promise<PeekSnapshot> {
141
+ const generatedAt = new Date().toISOString();
142
+ const requestedIndex = options.indexName ?? DEFAULT_INDEX_NAME;
143
+ const serve = await readServeLiveness();
144
+
145
+ const initialized = await isInitialized(options.configPath);
146
+ if (!initialized) {
147
+ return emptySnapshot(requestedIndex, generatedAt, serve);
148
+ }
149
+
150
+ const configResult = await loadConfig(options.configPath);
151
+ if (!configResult.ok) {
152
+ throw new CliError("RUNTIME", configResult.error.message);
153
+ }
154
+
155
+ const store = new SqliteAdapter();
156
+ const openResult = await store.open(
157
+ getIndexDbPath(options.indexName),
158
+ configResult.value.ftsTokenizer
159
+ );
160
+ if (!openResult.ok) {
161
+ throw new CliError("RUNTIME", openResult.error.message);
162
+ }
163
+
164
+ try {
165
+ const statusResult = await store.getStatus();
166
+ if (!statusResult.ok) {
167
+ throw new CliError("RUNTIME", statusResult.error.message);
168
+ }
169
+
170
+ const recentResult = await store.listDocumentsPaginated({
171
+ limit: PEEK_RECENT_LIMIT,
172
+ offset: 0,
173
+ });
174
+ if (!recentResult.ok) {
175
+ throw new CliError("RUNTIME", recentResult.error.message);
176
+ }
177
+
178
+ const status = statusResult.value;
179
+ return {
180
+ schemaVersion: PEEK_SCHEMA_VERSION,
181
+ gnoVersion: VERSION,
182
+ generatedAt,
183
+ initialized: true,
184
+ indexName: status.indexName,
185
+ counts: {
186
+ documents: status.activeDocuments,
187
+ collections: status.collections.length,
188
+ },
189
+ backlog: {
190
+ pending: status.embeddingBacklog,
191
+ failed: status.recentErrors,
192
+ },
193
+ lastIndexedAt: status.lastUpdatedAt,
194
+ recent: mapRecent(recentResult.value.documents, status),
195
+ serve,
196
+ };
197
+ } catch (error) {
198
+ throw asRuntimeError(error, "Failed to build peek snapshot");
199
+ } finally {
200
+ await store.close();
201
+ }
202
+ }
@@ -56,6 +56,7 @@ export const MCP_HTTP_EGRESS_TOOLS = {
56
56
  gno_search: "snippet",
57
57
  gno_section: "metadata",
58
58
  gno_similar: "snippet",
59
+ gno_peek: "metadata",
59
60
  gno_status: "metadata",
60
61
  gno_sync: "metadata",
61
62
  gno_trace_delete: "retrieval_trace",
@@ -65,6 +65,7 @@ import {
65
65
  import { handleListJobs } from "./list-jobs";
66
66
  import { handleListTags } from "./list-tags";
67
67
  import { handleMultiGet } from "./multi-get";
68
+ import { handlePeek, PEEK_MCP_ANNOTATIONS } from "./peek";
68
69
  import { handleQuery, handleQueryDiagnose } from "./query";
69
70
  import { handleRemoveCollection } from "./remove-collection";
70
71
  import { handleSearch } from "./search";
@@ -129,6 +130,7 @@ export const MCP_TOOL_DESCRIPTIONS = {
129
130
  "Retrieve multiple documents by refs array or glob pattern. Use after gno_search/gno_query to batch top result URIs/docids; set maxBytes and lineNumbers to control context size.",
130
131
  section:
131
132
  "Create or resolve a durable SectionTargetV1 against one indexed document. action=create needs ref plus exactly one of anchor|line; action=resolve needs ref plus target. Exact/recovered include citation (uri, anchor, title, inclusive lines, fingerprint); ambiguous/stale/missing omit citation and are not safe to navigate or cite. Read-only — does not write or persist targets. Follow navigable ranges with gno_get fromLine/lineCount.",
133
+ peek: "Cheap peek@1.0 snapshot: initialized flag, document/collection counts, embedding backlog, recent files, and serve liveness. Model-free — never initializes embeddings or models. Use for counts/backlog/recent/serve questions; use gno_status for full health and activation.",
132
134
  status:
133
135
  "Get index health: collection count, document count, chunk count, embedding backlog, and per-collection stats. Check first when vector/hybrid results look stale or unavailable.",
134
136
  audit:
@@ -664,6 +666,8 @@ const multiGetInputSchema = z.object({
664
666
  .describe("Include line numbers in output"),
665
667
  });
666
668
 
669
+ const peekInputSchema = z.object({});
670
+
667
671
  const statusInputSchema = z.object({});
668
672
 
669
673
  const jobStatusInputSchema = z.object({
@@ -1086,6 +1090,16 @@ export function registerTools(server: McpServer, ctx: ToolContext): void {
1086
1090
  (args) => handleMultiGet(args, ctx)
1087
1091
  );
1088
1092
 
1093
+ server.registerTool(
1094
+ "gno_peek",
1095
+ {
1096
+ description: MCP_TOOL_DESCRIPTIONS.peek,
1097
+ inputSchema: peekInputSchema,
1098
+ annotations: PEEK_MCP_ANNOTATIONS,
1099
+ },
1100
+ (args) => handlePeek(args, ctx)
1101
+ );
1102
+
1089
1103
  server.tool(
1090
1104
  "gno_status",
1091
1105
  MCP_TOOL_DESCRIPTIONS.status,
@@ -0,0 +1,78 @@
1
+ /**
2
+ * MCP gno_peek tool — cheap peek@1.0 snapshot, model-free.
3
+ *
4
+ * @module src/mcp/tools/peek
5
+ */
6
+
7
+ import type { PeekSnapshot } from "../../core/peek";
8
+ import type { ToolContext } from "../server";
9
+
10
+ import { buildPeekSnapshot } from "../../core/peek";
11
+ import { runTool, type ToolResult } from "./index";
12
+
13
+ type PeekInput = Record<string, never>;
14
+
15
+ export const PEEK_MCP_ANNOTATIONS = {
16
+ readOnlyHint: true,
17
+ destructiveHint: false,
18
+ idempotentHint: true,
19
+ openWorldHint: false,
20
+ } as const;
21
+
22
+ function formatPeek(snapshot: PeekSnapshot): string {
23
+ const lines = [
24
+ `schema: ${snapshot.schemaVersion}`,
25
+ `gno: ${snapshot.gnoVersion}`,
26
+ `index: ${snapshot.indexName}`,
27
+ `initialized: ${snapshot.initialized ? "yes" : "no"}`,
28
+ ];
29
+ if (snapshot.counts) {
30
+ lines.push(
31
+ `documents: ${snapshot.counts.documents}`,
32
+ `collections: ${snapshot.counts.collections}`
33
+ );
34
+ }
35
+ if (snapshot.backlog) {
36
+ lines.push(
37
+ `backlog: ${snapshot.backlog.pending} pending, ${snapshot.backlog.failed} failed`
38
+ );
39
+ }
40
+ if (snapshot.lastIndexedAt) {
41
+ lines.push(`lastIndexedAt: ${snapshot.lastIndexedAt}`);
42
+ }
43
+ lines.push(
44
+ snapshot.serve.running && snapshot.serve.url
45
+ ? `serve: ${snapshot.serve.url}`
46
+ : "serve: down"
47
+ );
48
+ if (snapshot.recent.length > 0) {
49
+ lines.push("recent:");
50
+ for (const item of snapshot.recent) {
51
+ const label = item.title ?? item.uri;
52
+ lines.push(` ${item.docid} ${label}`);
53
+ }
54
+ }
55
+ return lines.join("\n");
56
+ }
57
+
58
+ /**
59
+ * Handle gno_peek tool call.
60
+ *
61
+ * Uninitialized (no config / no store) is a success payload, not an error.
62
+ * Never probes serve over HTTP; the shared builder uses pid-file liveness.
63
+ */
64
+ export function handlePeek(
65
+ _args: PeekInput,
66
+ ctx: ToolContext
67
+ ): Promise<ToolResult> {
68
+ return runTool(
69
+ ctx,
70
+ "gno_peek",
71
+ async () =>
72
+ buildPeekSnapshot({
73
+ configPath: ctx.actualConfigPath,
74
+ indexName: ctx.indexName,
75
+ }),
76
+ formatPeek
77
+ );
78
+ }
@@ -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: snippetChunk.startLine,
1067
+ line: snippetStartLine,
1054
1068
  snippet,
1055
1069
  snippetLanguage: chunk.language ?? undefined,
1056
1070
  snippetRange,
@@ -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: use FTS snippet or chunk text
121
- snippet = fts.snippet ?? chunk?.text ?? "";
122
- snippetRange = chunk
123
- ? { startLine: chunk.startLine, endLine: chunk.endLine }
124
- : undefined;
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: chunk?.startLine,
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
+ }
@@ -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: chunk.startLine,
248
- snippet: chunk.text,
250
+ line: snippetStartLine,
251
+ snippet: cleanedSnippet.text,
249
252
  snippetLanguage: chunk.language ?? undefined,
250
253
  snippetRange: {
251
- startLine: chunk.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: chunk.startLine,
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: chunk.startLine, endLine: chunk.endLine },
365
+ : { startLine: snippetStartLine, endLine: chunk.endLine },
357
366
  source: {
358
367
  relPath: sourceRelPath,
359
368
  absPath: collectionPath