pi-readseek 0.7.6 → 0.7.8

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 (60) hide show
  1. package/README.md +28 -46
  2. package/dist/index.js +7333 -0
  3. package/package.json +8 -7
  4. package/index.ts +0 -147
  5. package/src/binary-detect.ts +0 -22
  6. package/src/check.ts +0 -92
  7. package/src/coerce-obvious-int.ts +0 -41
  8. package/src/confusable-hyphens.ts +0 -6
  9. package/src/def.ts +0 -143
  10. package/src/diff-data.ts +0 -302
  11. package/src/edit-classify.ts +0 -58
  12. package/src/edit-diff.ts +0 -359
  13. package/src/edit-output.ts +0 -110
  14. package/src/edit-render-helpers.ts +0 -141
  15. package/src/edit-syntax-validate.ts +0 -65
  16. package/src/edit.ts +0 -686
  17. package/src/file-map.ts +0 -17
  18. package/src/fs-error.ts +0 -33
  19. package/src/grep-budget.ts +0 -35
  20. package/src/grep-output.ts +0 -181
  21. package/src/grep-render-helpers.ts +0 -77
  22. package/src/grep-symbol-scope.ts +0 -184
  23. package/src/grep.ts +0 -608
  24. package/src/hashline.ts +0 -736
  25. package/src/hover.ts +0 -140
  26. package/src/path-utils.ts +0 -24
  27. package/src/pending-diff-preview.ts +0 -269
  28. package/src/read-local-bundle.ts +0 -75
  29. package/src/read-output.ts +0 -186
  30. package/src/read-render-helpers.ts +0 -104
  31. package/src/read.ts +0 -661
  32. package/src/readseek/constants.ts +0 -17
  33. package/src/readseek/enums.ts +0 -38
  34. package/src/readseek/formatter.ts +0 -350
  35. package/src/readseek/symbol-error-format.ts +0 -18
  36. package/src/readseek/symbol-lookup.ts +0 -240
  37. package/src/readseek/symbol-tree.ts +0 -21
  38. package/src/readseek/types.ts +0 -49
  39. package/src/readseek-client.ts +0 -1250
  40. package/src/readseek-params.ts +0 -39
  41. package/src/readseek-settings.ts +0 -263
  42. package/src/readseek-value.ts +0 -184
  43. package/src/refs-output.ts +0 -47
  44. package/src/refs.ts +0 -161
  45. package/src/register-tool.ts +0 -29
  46. package/src/rename.ts +0 -182
  47. package/src/replace-symbol.ts +0 -77
  48. package/src/runtime.ts +0 -3
  49. package/src/search-output.ts +0 -50
  50. package/src/search.ts +0 -214
  51. package/src/session-anchors.ts +0 -34
  52. package/src/stat-search-path.ts +0 -29
  53. package/src/syntax-validate-mode.ts +0 -15
  54. package/src/tool-prompt-metadata.ts +0 -107
  55. package/src/tool-types.ts +0 -12
  56. package/src/tui-diff-component.ts +0 -108
  57. package/src/tui-diff-renderer.ts +0 -92
  58. package/src/tui-render-utils.ts +0 -286
  59. package/src/view.ts +0 -158
  60. package/src/write.ts +0 -357
package/src/file-map.ts DELETED
@@ -1,17 +0,0 @@
1
- import { stat } from "node:fs/promises";
2
-
3
- import type { FileMap } from "./readseek/types.js";
4
- import { readSeekMap } from "./readseek-client.js";
5
-
6
- /**
7
- * Fetch a structural file map from readseek, which maintains its own on-disk
8
- * map cache keyed by file hash. Returns null on any failure — never throws.
9
- */
10
- export async function getOrGenerateMap(absPath: string): Promise<FileMap | null> {
11
- try {
12
- const { size } = await stat(absPath);
13
- return await readSeekMap(absPath, size);
14
- } catch {
15
- return null;
16
- }
17
- }
package/src/fs-error.ts DELETED
@@ -1,33 +0,0 @@
1
- const SYSLOG_PATH_SUFFIX = /,\s+[a-z]+(\s+'.*')?$/;
2
-
3
- /**
4
- * Derive the canonical OS strerror from a Node {@link err.message} by
5
- * stripping the trailing syscall and optional path suffix so the result
6
- * is portable and never duplicates information already carried in the
7
- * error-envelope `path` field.
8
- */
9
- function strerror(err: { message?: unknown }): string {
10
- const raw = String(err?.message ?? String(err));
11
- return raw.replace(SYSLOG_PATH_SUFFIX, "");
12
- }
13
-
14
- /**
15
- * Build a readseek error from a Node `fs` errno exception.
16
- *
17
- * Every errno maps to its own canonical code automatically — no
18
- * hand-curated switch, no `"fs-error"` catch-all.
19
- *
20
- * @param err The caught exception (carries `err.code` and `err.message`).
21
- * @param domain Tool-level prefix for the message: `"read-error"`,
22
- * `"edit-error"`, `"write-error"`, `"stat-error"`.
23
- */
24
- export function formatFsError(
25
- err: { code?: unknown; message?: string },
26
- domain: string,
27
- ): { code: string; message: string } {
28
- const errno = typeof err.code === "string" ? err.code : "EIO";
29
- return {
30
- code: errno,
31
- message: `${domain}: ${errno}: ${strerror(err)}`,
32
- };
33
- }
@@ -1,35 +0,0 @@
1
- import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@earendil-works/pi-coding-agent";
2
- import { resolveReadSeekJsonSettings } from "./readseek-settings.js";
3
-
4
- export interface GrepOutputBudget {
5
- maxLines: number;
6
- maxBytes: number;
7
- }
8
-
9
- /**
10
- * Effective grep-output ceilings used as clamp upper bounds and as the
11
- * fallback defaults when the settings are unset.
12
- *
13
- * The bytes ceiling is the already-tightened 50 KiB used by `buildGrepOutput`
14
- * today, NOT the unclamped `DEFAULT_MAX_BYTES`.
15
- */
16
- export const GREP_OUTPUT_DEFAULT_MAX_LINES = DEFAULT_MAX_LINES;
17
- export const GREP_OUTPUT_DEFAULT_MAX_BYTES = Math.min(DEFAULT_MAX_BYTES, 50 * 1024);
18
-
19
- function resolveDimension(jsonValue: number | undefined, ceiling: number): number {
20
- if (jsonValue !== undefined) return Math.min(jsonValue, ceiling);
21
- return ceiling;
22
- }
23
-
24
- /**
25
- * Resolve the effective grep visible-output budget from the readseek
26
- * settings. Below-default values are used as-is; above-default values clamp
27
- * to the current defaults.
28
- */
29
- export function resolveGrepOutputBudget(): GrepOutputBudget {
30
- const settings = resolveReadSeekJsonSettings().settings.grep;
31
- return {
32
- maxLines: resolveDimension(settings?.maxLines, GREP_OUTPUT_DEFAULT_MAX_LINES),
33
- maxBytes: resolveDimension(settings?.maxBytes, GREP_OUTPUT_DEFAULT_MAX_BYTES),
34
- };
35
- }
@@ -1,181 +0,0 @@
1
- import type { ReadSeekLine, ReadSeekWarning } from "./readseek-value.js";
2
- import {
3
- formatSize,
4
- truncateHead,
5
- } from "@earendil-works/pi-coding-agent";
6
- import { resolveGrepOutputBudget } from "./grep-budget.js";
7
-
8
- export interface GrepOutputRecord extends ReadSeekLine {
9
- path: string;
10
- kind: "match" | "context";
11
- }
12
-
13
- interface GrepOutputReadSeekRecord {
14
- path: string;
15
- line: number;
16
- anchor: string;
17
- kind: "match" | "context";
18
- }
19
-
20
- export type GrepOutputEntry =
21
- | { kind: "match" | "context"; line: ReadSeekLine }
22
- | { kind: "separator"; text: string };
23
-
24
- export interface GrepOutputScopeSymbol {
25
- name: string;
26
- kind: string;
27
- startLine: number;
28
- endLine: number;
29
- parentName?: string;
30
- }
31
-
32
- export interface GrepScopeWarning extends ReadSeekWarning {
33
- path?: string;
34
- line?: number;
35
- }
36
-
37
- export interface GrepOutputGroup {
38
- displayPath: string;
39
- absolutePath: string;
40
- matchCount: number;
41
- entries: GrepOutputEntry[];
42
- scope?: {
43
- mode: "symbol";
44
- symbol: GrepOutputScopeSymbol;
45
- matchLines: number[];
46
- contextLines?: number;
47
- };
48
- }
49
-
50
- type ScopedGrepOutputGroup = GrepOutputGroup & {
51
- scope: NonNullable<GrepOutputGroup["scope"]>;
52
- };
53
-
54
- function hasScope(group: GrepOutputGroup): group is ScopedGrepOutputGroup {
55
- return group.scope !== undefined;
56
- }
57
-
58
- interface BuildGrepOutputInput {
59
- summary: boolean;
60
- totalMatches: number;
61
- groups: GrepOutputGroup[];
62
- limit?: number;
63
- records: GrepOutputRecord[];
64
- scopeMode?: "symbol";
65
- scopeWarnings?: GrepScopeWarning[];
66
- passthroughLines?: string[];
67
- }
68
-
69
- interface GrepOutputResult {
70
- text: string;
71
- readSeekValue: {
72
- tool: "grep";
73
- summary: boolean;
74
- totalMatches: number;
75
- records: GrepOutputReadSeekRecord[];
76
- scopes?: {
77
- mode: "symbol";
78
- groups: Array<{
79
- path: string;
80
- displayPath: string;
81
- symbol: GrepOutputScopeSymbol;
82
- matchCount: number;
83
- matchLines: number[];
84
- lineAnchors: string[];
85
- }>;
86
- warnings: GrepScopeWarning[];
87
- };
88
- };
89
- }
90
-
91
- function renderEntry(displayPath: string, entry: GrepOutputEntry): string {
92
- if (entry.kind === "separator") return entry.text;
93
- const marker = entry.kind === "match" ? ">>" : " ";
94
- return `${displayPath}:${marker}${entry.line.anchor}|${entry.line.display}`;
95
- }
96
-
97
- function renderGroupHeader(group: GrepOutputGroup): string {
98
- if (!group.scope) {
99
- return `--- ${group.displayPath} (${group.matchCount} matches) ---`;
100
- }
101
-
102
- const parent = group.scope.symbol.parentName ? ` in ${group.scope.symbol.parentName}` : "";
103
- const suffix = group.scope.contextLines !== undefined ? `, scoped to ±${group.scope.contextLines} lines` : "";
104
- return `--- ${group.displayPath} :: ${group.scope.symbol.kind} ${group.scope.symbol.name}${parent} (${group.scope.symbol.startLine}-${group.scope.symbol.endLine}, ${group.matchCount} matches${suffix}) ---`;
105
- }
106
-
107
- function buildScopeMetadata(groups: GrepOutputGroup[], warnings: GrepScopeWarning[]) {
108
- return {
109
- mode: "symbol" as const,
110
- groups: groups
111
- .filter(hasScope)
112
- .map((group) => ({
113
- path: group.absolutePath,
114
- displayPath: group.displayPath,
115
- symbol: group.scope.symbol,
116
- matchCount: group.matchCount,
117
- matchLines: [...group.scope.matchLines],
118
- lineAnchors: group.entries.flatMap((entry) => (entry.kind === "separator" ? [] : [entry.line.anchor])),
119
- })),
120
- warnings: [...warnings],
121
- };
122
- }
123
-
124
- export function buildGrepOutput(input: BuildGrepOutputInput): GrepOutputResult {
125
- const fileCount = new Set(input.groups.map((group) => group.absolutePath)).size;
126
- const header = `[${input.totalMatches} matches in ${fileCount} files]`;
127
- let text: string;
128
- if (input.summary) {
129
- const fileLines = [...input.groups]
130
- .sort((a, b) => b.matchCount - a.matchCount)
131
- .map((group) => `${group.absolutePath}: ${group.matchCount} matches`);
132
- text = [header, ...fileLines].join("\n");
133
- } else {
134
- const blocks: string[] = [header];
135
- for (const group of input.groups) {
136
- blocks.push(renderGroupHeader(group));
137
- for (const entry of group.entries) {
138
- blocks.push(renderEntry(group.displayPath, entry));
139
- }
140
- }
141
- text = blocks.join("\n");
142
- }
143
- const passthroughLines = input.passthroughLines ?? [];
144
- if (passthroughLines.length > 0) {
145
- text += `\n\n${passthroughLines.join("\n")}`;
146
- }
147
- if (input.limit !== undefined && input.totalMatches === input.limit) {
148
- text += `\n\n[Results truncated at ${input.limit} matches — refine pattern or increase limit]`;
149
- }
150
- const scopeWarnings = input.scopeWarnings ?? [];
151
- if (!input.summary && input.scopeMode === "symbol" && scopeWarnings.length > 0) {
152
- text = `${scopeWarnings.map((warning) => warning.message).join("\n\n")}\n\n${text}`;
153
- }
154
- const budget = resolveGrepOutputBudget();
155
- const truncated = truncateHead(text, {
156
- maxLines: budget.maxLines,
157
- maxBytes: budget.maxBytes,
158
- });
159
- if (truncated.truncated) {
160
- text = `${truncated.content}\n\n[Output truncated: showing ${truncated.outputLines} of ${truncated.totalLines} lines (${formatSize(truncated.outputBytes)} of ${formatSize(truncated.totalBytes)}). Refine pattern or increase limit.]`;
161
- }
162
- const readSeekValue: GrepOutputResult["readSeekValue"] = {
163
- tool: "grep",
164
- summary: input.summary,
165
- totalMatches: input.totalMatches,
166
- records: input.records.map((record) => ({
167
- path: record.path,
168
- line: record.line,
169
- anchor: record.anchor,
170
- kind: record.kind,
171
- })),
172
- };
173
- if (!input.summary && input.scopeMode === "symbol") {
174
- readSeekValue.scopes = buildScopeMetadata(input.groups, scopeWarnings);
175
- }
176
-
177
- return {
178
- text,
179
- readSeekValue,
180
- };
181
- }
@@ -1,77 +0,0 @@
1
- interface GrepCallTextResult {
2
- pattern: string;
3
- suffix: string | undefined;
4
- }
5
-
6
- export function formatGrepCallText(
7
- args: Record<string, unknown> | undefined,
8
- ): GrepCallTextResult {
9
- const pattern = typeof args?.pattern === "string" ? args.pattern : "";
10
-
11
- const parts: string[] = [];
12
- if (typeof args?.path === "string" && args.path !== ".") {
13
- parts.push(args.path as string);
14
- }
15
- if (typeof args?.glob === "string") {
16
- parts.push(args.glob as string);
17
- }
18
-
19
- return {
20
- pattern,
21
- suffix: parts.length > 0 ? parts.join(" ") : undefined,
22
- };
23
- }
24
-
25
- export const GREP_TRUNCATION_THRESHOLD = 50;
26
-
27
- interface GrepResultTextInput {
28
- totalMatches: number;
29
- summary: boolean;
30
- records: unknown[];
31
- fileCount: number;
32
- hasBinaryWarning?: boolean;
33
- isError?: boolean;
34
- errorText?: string;
35
- }
36
-
37
- interface GrepResultTextOutput {
38
- summary: string;
39
- badges: string[];
40
- noMatches: boolean;
41
- truncated: boolean;
42
- errorText: string | undefined;
43
- }
44
-
45
- export function formatGrepResultText(input: GrepResultTextInput): GrepResultTextOutput {
46
- // Error case
47
- if (input.isError && input.errorText) {
48
- return {
49
- summary: "",
50
- badges: [],
51
- noMatches: false,
52
- truncated: false,
53
- errorText: input.errorText,
54
- };
55
- }
56
-
57
- const { totalMatches, summary, fileCount } = input;
58
- const noMatches = totalMatches === 0;
59
- const truncated = totalMatches > GREP_TRUNCATION_THRESHOLD;
60
-
61
- const matchWord = totalMatches === 1 ? "match" : "matches";
62
- const fileWord = fileCount === 1 ? "file" : "files";
63
- const summaryText = noMatches ? "" : `\u2713 ${totalMatches} ${matchWord} in ${fileCount} ${fileWord}`;
64
-
65
- const badges: string[] = [];
66
- if (truncated) badges.push("10/file cap");
67
- if (summary) badges.push("summary");
68
- if (input.hasBinaryWarning) badges.push("\u26a0 binary");
69
-
70
- return {
71
- summary: summaryText,
72
- badges,
73
- noMatches,
74
- truncated,
75
- errorText: undefined,
76
- };
77
- }
@@ -1,184 +0,0 @@
1
- import { buildReadSeekLine } from "./readseek-value.js";
2
- import type { GrepOutputEntry, GrepOutputGroup, GrepOutputScopeSymbol, GrepScopeWarning } from "./grep-output.js";
3
- import type { FileMap } from "./readseek/types.js";
4
- import { traverseSymbolTree } from "./readseek/symbol-tree.js";
5
-
6
- function findEnclosingSymbol(map: FileMap, lineNumber: number): GrepOutputScopeSymbol | null {
7
- const candidates = traverseSymbolTree(map.symbols, (symbol, parentName): GrepOutputScopeSymbol => ({
8
- name: symbol.name,
9
- kind: symbol.kind,
10
- startLine: symbol.startLine,
11
- endLine: symbol.endLine,
12
- parentName,
13
- }))
14
- .filter((s) => lineNumber >= s.startLine && lineNumber <= s.endLine)
15
- .sort((a, b) => {
16
- const rangeA = a.endLine - a.startLine;
17
- const rangeB = b.endLine - b.startLine;
18
- if (rangeA !== rangeB) return rangeA - rangeB;
19
- if (a.startLine !== b.startLine) return a.startLine - b.startLine;
20
- return a.name.localeCompare(b.name);
21
- });
22
- return candidates.length ? candidates[0] : null;
23
- }
24
-
25
- function firstLineNumber(group: GrepOutputGroup): number {
26
- const first = group.entries.find((e) => e.kind !== "separator");
27
- return first ? first.line.line : Number.MAX_SAFE_INTEGER;
28
- }
29
-
30
- function buildSymbolEntries(
31
- fileLines: string[],
32
- symbol: GrepOutputScopeSymbol,
33
- matchLines: Set<number>,
34
- scopeContext: number | undefined,
35
- ): GrepOutputEntry[] {
36
- if (scopeContext === undefined) {
37
- const entries: GrepOutputEntry[] = [];
38
- for (let lineNumber = symbol.startLine; lineNumber <= symbol.endLine; lineNumber++) {
39
- const built = buildReadSeekLine(lineNumber, fileLines[lineNumber - 1] ?? "");
40
- entries.push({ kind: matchLines.has(lineNumber) ? "match" : "context", line: built });
41
- }
42
- return entries;
43
- }
44
- // Windowed path: ±scopeContext lines, clipped, merged, with '--' separators between non-overlapping ranges.
45
- const ranges = [...matchLines].sort((a, b) => a - b).map((ln) => ({
46
- startLine: Math.max(symbol.startLine, ln - scopeContext),
47
- endLine: Math.min(symbol.endLine, ln + scopeContext),
48
- }));
49
- const merged: Array<{ startLine: number; endLine: number }> = [];
50
- for (const r of ranges) {
51
- const last = merged[merged.length - 1];
52
- if (last && r.startLine <= last.endLine + 1) {
53
- last.endLine = Math.max(last.endLine, r.endLine);
54
- } else {
55
- merged.push({ ...r });
56
- }
57
- }
58
- const entries: GrepOutputEntry[] = [];
59
- for (let i = 0; i < merged.length; i++) {
60
- if (i > 0) entries.push({ kind: "separator", text: "--" });
61
- const range = merged[i];
62
- for (let ln = range.startLine; ln <= range.endLine; ln++) {
63
- const built = buildReadSeekLine(ln, fileLines[ln - 1] ?? "");
64
- entries.push({ kind: matchLines.has(ln) ? "match" : "context", line: built });
65
- }
66
- }
67
- return entries;
68
- }
69
-
70
- function buildFallbackEntries(fileLines: string[], matchLines: number[], contextLines: number): GrepOutputEntry[] {
71
- const lineMap = new Map<number, GrepOutputEntry>();
72
- for (const matchLine of matchLines) {
73
- const start = Math.max(1, matchLine - contextLines);
74
- const end = Math.min(fileLines.length, matchLine + contextLines);
75
- for (let lineNumber = start; lineNumber <= end; lineNumber++) {
76
- const built = buildReadSeekLine(lineNumber, fileLines[lineNumber - 1] ?? "");
77
- const candidate: GrepOutputEntry = { kind: lineNumber === matchLine ? "match" : "context", line: built };
78
- const existing = lineMap.get(lineNumber);
79
- if (!existing || (existing.kind === "context" && candidate.kind === "match")) {
80
- lineMap.set(lineNumber, candidate);
81
- }
82
- }
83
- }
84
- const ordered = [...lineMap.entries()].sort(([a], [b]) => a - b);
85
- const entries: GrepOutputEntry[] = [];
86
- for (let i = 0; i < ordered.length; i++) {
87
- if (i > 0 && ordered[i][0] > ordered[i - 1][0] + 1) entries.push({ kind: "separator", text: "--" });
88
- entries.push(ordered[i][1]);
89
- }
90
- return entries;
91
- }
92
-
93
- export function scopeGrepGroupsToSymbols(input: {
94
- groups: GrepOutputGroup[];
95
- fileLinesByPath: Map<string, string[]>;
96
- fileMapsByPath: Map<string, FileMap | null>;
97
- contextLines: number;
98
- scopeContext?: number;
99
- }): { groups: GrepOutputGroup[]; warnings: GrepScopeWarning[] } {
100
- const warnings: GrepScopeWarning[] = [];
101
- const rendered: Array<{ order: number; group: GrepOutputGroup }> = [];
102
-
103
- for (const group of input.groups) {
104
- const fileLines = input.fileLinesByPath.get(group.absolutePath);
105
- const fileMap = input.fileMapsByPath.get(group.absolutePath) ?? null;
106
-
107
- if (!fileLines || !fileMap) {
108
- warnings.push({
109
- code: "unmappable-file",
110
- message: `[Warning: symbol scoping unavailable for ${group.absolutePath} — showing normal grep lines for this file]`,
111
- path: group.absolutePath,
112
- });
113
- rendered.push({ order: firstLineNumber(group), group });
114
- continue;
115
- }
116
-
117
- const symbolBuckets = new Map<string, { symbol: GrepOutputScopeSymbol; matchLines: Set<number> }>();
118
- const fallbackMatchLines: number[] = [];
119
-
120
- for (const entry of group.entries) {
121
- if (entry.kind !== "match") continue;
122
- const symbol = findEnclosingSymbol(fileMap, entry.line.line);
123
- if (!symbol) {
124
- fallbackMatchLines.push(entry.line.line);
125
- warnings.push({
126
- code: "no-enclosing-symbol",
127
- message: `[Warning: no enclosing symbol for ${group.absolutePath}:${entry.line.line} — showing normal grep lines for this match]`,
128
- path: group.absolutePath,
129
- line: entry.line.line,
130
- });
131
- continue;
132
- }
133
-
134
- const key = `${symbol.startLine}:${symbol.endLine}:${symbol.parentName ?? ""}:${symbol.name}`;
135
- const bucket = symbolBuckets.get(key) ?? { symbol, matchLines: new Set<number>() };
136
- bucket.matchLines.add(entry.line.line);
137
- symbolBuckets.set(key, bucket);
138
- }
139
-
140
- const scopedGroups = [...symbolBuckets.values()]
141
- .sort((a, b) => {
142
- if (a.symbol.startLine !== b.symbol.startLine) return a.symbol.startLine - b.symbol.startLine;
143
- return a.symbol.name.localeCompare(b.symbol.name);
144
- })
145
- .map(({ symbol, matchLines }) => ({
146
- displayPath: group.displayPath,
147
- absolutePath: group.absolutePath,
148
- matchCount: matchLines.size,
149
- scope: {
150
- mode: "symbol" as const,
151
- symbol,
152
- matchLines: [...matchLines].sort((a, b) => a - b),
153
- ...(input.scopeContext !== undefined ? { contextLines: input.scopeContext } : {}),
154
- },
155
- entries: buildSymbolEntries(fileLines, symbol, matchLines, input.scopeContext),
156
- }));
157
-
158
- for (const scopedGroup of scopedGroups) rendered.push({ order: scopedGroup.scope!.symbol.startLine, group: scopedGroup });
159
-
160
- if (fallbackMatchLines.length > 0) {
161
- rendered.push({
162
- order: Math.min(...fallbackMatchLines),
163
- group: {
164
- displayPath: group.displayPath,
165
- absolutePath: group.absolutePath,
166
- matchCount: fallbackMatchLines.length,
167
- entries: buildFallbackEntries(fileLines, fallbackMatchLines, input.contextLines),
168
- },
169
- });
170
- }
171
-
172
- if (scopedGroups.length === 0 && fallbackMatchLines.length === 0) {
173
- rendered.push({ order: firstLineNumber(group), group });
174
- }
175
- }
176
-
177
- rendered.sort((a, b) => {
178
- if (a.order !== b.order) return a.order - b.order;
179
- if (a.group.absolutePath !== b.group.absolutePath) return a.group.absolutePath.localeCompare(b.group.absolutePath);
180
- return a.group.displayPath.localeCompare(b.group.displayPath);
181
- });
182
-
183
- return { groups: rendered.map((item) => item.group), warnings };
184
- }