@mrsf/cli 0.1.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.
- package/LICENSE +21 -0
- package/README.md +346 -0
- package/dist/bin.d.ts +8 -0
- package/dist/bin.js +76 -0
- package/dist/commands/add.d.ts +6 -0
- package/dist/commands/add.js +82 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.js +52 -0
- package/dist/commands/list.d.ts +6 -0
- package/dist/commands/list.js +95 -0
- package/dist/commands/reanchor.d.ts +6 -0
- package/dist/commands/reanchor.js +127 -0
- package/dist/commands/rename.d.ts +6 -0
- package/dist/commands/rename.js +53 -0
- package/dist/commands/resolve.d.ts +6 -0
- package/dist/commands/resolve.js +35 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.js +156 -0
- package/dist/commands/validate.d.ts +6 -0
- package/dist/commands/validate.js +55 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +25 -0
- package/dist/lib/comments.d.ts +52 -0
- package/dist/lib/comments.js +228 -0
- package/dist/lib/discovery.d.ts +40 -0
- package/dist/lib/discovery.js +125 -0
- package/dist/lib/fuzzy.d.ts +39 -0
- package/dist/lib/fuzzy.js +277 -0
- package/dist/lib/git.d.ts +68 -0
- package/dist/lib/git.js +246 -0
- package/dist/lib/parser.d.ts +19 -0
- package/dist/lib/parser.js +56 -0
- package/dist/lib/reanchor.d.ts +45 -0
- package/dist/lib/reanchor.js +389 -0
- package/dist/lib/resolve-files.d.ts +18 -0
- package/dist/lib/resolve-files.js +42 -0
- package/dist/lib/types.d.ts +151 -0
- package/dist/lib/types.js +5 -0
- package/dist/lib/validator.d.ts +13 -0
- package/dist/lib/validator.js +177 -0
- package/dist/lib/writer.d.ts +30 -0
- package/dist/lib/writer.js +62 -0
- package/package.json +70 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MRSF Comment Operations — CRUD for sidecar comments.
|
|
3
|
+
*/
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { computeHash } from "./writer.js";
|
|
6
|
+
import { getCurrentCommit, isGitAvailable } from "./git.js";
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Add
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
/**
|
|
11
|
+
* Add a new comment to a document. Mutates doc.comments in place.
|
|
12
|
+
* Returns the created comment.
|
|
13
|
+
*/
|
|
14
|
+
export async function addComment(doc, opts, repoRoot) {
|
|
15
|
+
const id = opts.id ?? randomUUID();
|
|
16
|
+
const timestamp = opts.timestamp ?? new Date().toISOString();
|
|
17
|
+
// Auto-detect commit from HEAD when git is available
|
|
18
|
+
let commit = opts.commit;
|
|
19
|
+
if (!commit && repoRoot && (await isGitAvailable())) {
|
|
20
|
+
commit = (await getCurrentCommit(repoRoot)) ?? undefined;
|
|
21
|
+
}
|
|
22
|
+
const comment = {
|
|
23
|
+
id,
|
|
24
|
+
author: opts.author,
|
|
25
|
+
timestamp,
|
|
26
|
+
text: opts.text,
|
|
27
|
+
resolved: false,
|
|
28
|
+
};
|
|
29
|
+
// Optional anchoring fields
|
|
30
|
+
if (opts.line != null)
|
|
31
|
+
comment.line = opts.line;
|
|
32
|
+
if (opts.end_line != null)
|
|
33
|
+
comment.end_line = opts.end_line;
|
|
34
|
+
if (opts.start_column != null)
|
|
35
|
+
comment.start_column = opts.start_column;
|
|
36
|
+
if (opts.end_column != null)
|
|
37
|
+
comment.end_column = opts.end_column;
|
|
38
|
+
if (opts.type)
|
|
39
|
+
comment.type = opts.type;
|
|
40
|
+
if (opts.severity)
|
|
41
|
+
comment.severity = opts.severity;
|
|
42
|
+
if (opts.reply_to)
|
|
43
|
+
comment.reply_to = opts.reply_to;
|
|
44
|
+
if (commit)
|
|
45
|
+
comment.commit = commit;
|
|
46
|
+
doc.comments.push(comment);
|
|
47
|
+
return comment;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Populate selected_text from document lines (reads the file content region).
|
|
51
|
+
* Should be called after addComment if the caller provides line info but not selected_text.
|
|
52
|
+
*/
|
|
53
|
+
export function populateSelectedText(comment, documentLines) {
|
|
54
|
+
if (comment.selected_text)
|
|
55
|
+
return; // already set
|
|
56
|
+
if (comment.line == null)
|
|
57
|
+
return;
|
|
58
|
+
const startIdx = comment.line - 1;
|
|
59
|
+
const endIdx = (comment.end_line ?? comment.line) - 1;
|
|
60
|
+
if (startIdx < 0 || endIdx >= documentLines.length)
|
|
61
|
+
return;
|
|
62
|
+
if (startIdx === endIdx) {
|
|
63
|
+
let line = documentLines[startIdx];
|
|
64
|
+
if (comment.start_column != null && comment.end_column != null) {
|
|
65
|
+
line = line.slice(comment.start_column, comment.end_column);
|
|
66
|
+
}
|
|
67
|
+
comment.selected_text = line;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
const lines = [];
|
|
71
|
+
for (let i = startIdx; i <= endIdx; i++) {
|
|
72
|
+
let l = documentLines[i];
|
|
73
|
+
if (i === startIdx && comment.start_column != null) {
|
|
74
|
+
l = l.slice(comment.start_column);
|
|
75
|
+
}
|
|
76
|
+
if (i === endIdx && comment.end_column != null) {
|
|
77
|
+
l = l.slice(0, comment.end_column);
|
|
78
|
+
}
|
|
79
|
+
lines.push(l);
|
|
80
|
+
}
|
|
81
|
+
comment.selected_text = lines.join("\n");
|
|
82
|
+
}
|
|
83
|
+
// Also set the hash
|
|
84
|
+
if (comment.selected_text) {
|
|
85
|
+
comment.selected_text_hash = computeHash(comment.selected_text);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Resolve / Unresolve
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
/**
|
|
92
|
+
* Resolve a comment by id. Per §9, resolving a parent does NOT
|
|
93
|
+
* automatically resolve its replies.
|
|
94
|
+
*
|
|
95
|
+
* Returns true if the comment was found and updated.
|
|
96
|
+
*/
|
|
97
|
+
export function resolveComment(doc, commentId, cascade = false) {
|
|
98
|
+
const comment = doc.comments.find((c) => c.id === commentId);
|
|
99
|
+
if (!comment)
|
|
100
|
+
return false;
|
|
101
|
+
comment.resolved = true;
|
|
102
|
+
if (cascade) {
|
|
103
|
+
// Cascade to direct replies only
|
|
104
|
+
for (const c of doc.comments) {
|
|
105
|
+
if (c.reply_to === commentId) {
|
|
106
|
+
c.resolved = true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Unresolve a comment by id.
|
|
114
|
+
*/
|
|
115
|
+
export function unresolveComment(doc, commentId) {
|
|
116
|
+
const comment = doc.comments.find((c) => c.id === commentId);
|
|
117
|
+
if (!comment)
|
|
118
|
+
return false;
|
|
119
|
+
comment.resolved = false;
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Remove
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
/**
|
|
126
|
+
* Remove a comment by id. Returns true if found and removed.
|
|
127
|
+
*/
|
|
128
|
+
export function removeComment(doc, commentId) {
|
|
129
|
+
const idx = doc.comments.findIndex((c) => c.id === commentId);
|
|
130
|
+
if (idx === -1)
|
|
131
|
+
return false;
|
|
132
|
+
doc.comments.splice(idx, 1);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// List / Filter
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
/**
|
|
139
|
+
* Filter comments based on criteria.
|
|
140
|
+
*/
|
|
141
|
+
export function filterComments(comments, filter) {
|
|
142
|
+
return comments.filter((c) => {
|
|
143
|
+
if (filter.open === true && c.resolved)
|
|
144
|
+
return false;
|
|
145
|
+
if (filter.resolved === true && !c.resolved)
|
|
146
|
+
return false;
|
|
147
|
+
if (filter.author && c.author !== filter.author)
|
|
148
|
+
return false;
|
|
149
|
+
if (filter.type && c.type !== filter.type)
|
|
150
|
+
return false;
|
|
151
|
+
if (filter.severity && c.severity !== filter.severity)
|
|
152
|
+
return false;
|
|
153
|
+
if (filter.orphaned === true && c.x_reanchor_status !== "orphaned")
|
|
154
|
+
return false;
|
|
155
|
+
if (filter.orphaned === false && c.x_reanchor_status === "orphaned")
|
|
156
|
+
return false;
|
|
157
|
+
return true;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Get a list of threads — groups of comments by their root ID via reply_to.
|
|
162
|
+
* Returns a map of root comment ID → [root, ...replies in order].
|
|
163
|
+
*/
|
|
164
|
+
export function getThreads(comments) {
|
|
165
|
+
const threads = new Map();
|
|
166
|
+
const replyMap = new Map(); // child id → root id
|
|
167
|
+
// First pass: find all roots and build reply chains
|
|
168
|
+
for (const c of comments) {
|
|
169
|
+
if (!c.reply_to) {
|
|
170
|
+
threads.set(c.id, [c]);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
replyMap.set(c.id, c.reply_to);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// Resolve transitive reply_to chains to roots
|
|
177
|
+
function findRoot(id) {
|
|
178
|
+
const parent = replyMap.get(id);
|
|
179
|
+
if (!parent)
|
|
180
|
+
return id;
|
|
181
|
+
return findRoot(parent);
|
|
182
|
+
}
|
|
183
|
+
// Second pass: attach replies to their root thread
|
|
184
|
+
for (const c of comments) {
|
|
185
|
+
if (c.reply_to) {
|
|
186
|
+
const rootId = findRoot(c.id);
|
|
187
|
+
if (!threads.has(rootId)) {
|
|
188
|
+
threads.set(rootId, []);
|
|
189
|
+
}
|
|
190
|
+
threads.get(rootId).push(c);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return threads;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Generate summary statistics for a comment list.
|
|
197
|
+
*/
|
|
198
|
+
export function summarize(comments) {
|
|
199
|
+
const summary = {
|
|
200
|
+
total: comments.length,
|
|
201
|
+
open: 0,
|
|
202
|
+
resolved: 0,
|
|
203
|
+
orphaned: 0,
|
|
204
|
+
threads: 0,
|
|
205
|
+
byType: {},
|
|
206
|
+
bySeverity: {},
|
|
207
|
+
};
|
|
208
|
+
const roots = new Set();
|
|
209
|
+
for (const c of comments) {
|
|
210
|
+
if (c.resolved)
|
|
211
|
+
summary.resolved++;
|
|
212
|
+
else
|
|
213
|
+
summary.open++;
|
|
214
|
+
if (c.x_reanchor_status === "orphaned")
|
|
215
|
+
summary.orphaned++;
|
|
216
|
+
if (c.type) {
|
|
217
|
+
summary.byType[c.type] = (summary.byType[c.type] ?? 0) + 1;
|
|
218
|
+
}
|
|
219
|
+
if (c.severity) {
|
|
220
|
+
summary.bySeverity[c.severity] = (summary.bySeverity[c.severity] ?? 0) + 1;
|
|
221
|
+
}
|
|
222
|
+
if (!c.reply_to)
|
|
223
|
+
roots.add(c.id);
|
|
224
|
+
}
|
|
225
|
+
summary.threads = roots.size;
|
|
226
|
+
return summary;
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=comments.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MRSF Discovery — resolve sidecar file paths per §3.3.
|
|
3
|
+
*
|
|
4
|
+
* Discovery order:
|
|
5
|
+
* 1. Check for .mrsf.yaml at repo/workspace root → use sidecar_root if defined.
|
|
6
|
+
* 2. Otherwise, co-located sidecar next to the Markdown file.
|
|
7
|
+
*/
|
|
8
|
+
import type { MrsfConfig } from "./types.js";
|
|
9
|
+
/**
|
|
10
|
+
* Find the workspace / repo root by walking up from `startDir` looking for
|
|
11
|
+
* `.mrsf.yaml` or `.git`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function findWorkspaceRoot(startDir: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Load and validate .mrsf.yaml config. Returns null if not found.
|
|
16
|
+
*/
|
|
17
|
+
export declare function loadConfig(workspaceRoot: string, configPath?: string): Promise<MrsfConfig | null>;
|
|
18
|
+
/**
|
|
19
|
+
* Given a Markdown document path (relative to workspace root), resolve the
|
|
20
|
+
* sidecar file path according to §3.3 discovery order.
|
|
21
|
+
*
|
|
22
|
+
* Returns an absolute path to the sidecar.
|
|
23
|
+
*/
|
|
24
|
+
export declare function discoverSidecar(documentPath: string, options?: {
|
|
25
|
+
cwd?: string;
|
|
26
|
+
configPath?: string;
|
|
27
|
+
}): Promise<string>;
|
|
28
|
+
/**
|
|
29
|
+
* Given a sidecar file path, resolve the Markdown document path.
|
|
30
|
+
* Strips .review.yaml or .review.json suffix.
|
|
31
|
+
* Returns an absolute path to the document.
|
|
32
|
+
*/
|
|
33
|
+
export declare function sidecarToDocument(sidecarPath: string, options?: {
|
|
34
|
+
cwd?: string;
|
|
35
|
+
}): string;
|
|
36
|
+
/**
|
|
37
|
+
* Discover all sidecar files in a directory (recursive).
|
|
38
|
+
*/
|
|
39
|
+
export declare function discoverAllSidecars(dirPath: string): Promise<string[]>;
|
|
40
|
+
//# sourceMappingURL=discovery.d.ts.map
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MRSF Discovery — resolve sidecar file paths per §3.3.
|
|
3
|
+
*
|
|
4
|
+
* Discovery order:
|
|
5
|
+
* 1. Check for .mrsf.yaml at repo/workspace root → use sidecar_root if defined.
|
|
6
|
+
* 2. Otherwise, co-located sidecar next to the Markdown file.
|
|
7
|
+
*/
|
|
8
|
+
import { readFile } from "node:fs/promises";
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import yaml from "js-yaml";
|
|
12
|
+
const CONFIG_FILENAME = ".mrsf.yaml";
|
|
13
|
+
const SIDECAR_SUFFIX = ".review.yaml";
|
|
14
|
+
const SIDECAR_SUFFIX_JSON = ".review.json";
|
|
15
|
+
/**
|
|
16
|
+
* Find the workspace / repo root by walking up from `startDir` looking for
|
|
17
|
+
* `.mrsf.yaml` or `.git`.
|
|
18
|
+
*/
|
|
19
|
+
export function findWorkspaceRoot(startDir) {
|
|
20
|
+
let dir = path.resolve(startDir);
|
|
21
|
+
const { root } = path.parse(dir);
|
|
22
|
+
while (dir !== root) {
|
|
23
|
+
if (existsSync(path.join(dir, CONFIG_FILENAME)) ||
|
|
24
|
+
existsSync(path.join(dir, ".git"))) {
|
|
25
|
+
return dir;
|
|
26
|
+
}
|
|
27
|
+
dir = path.dirname(dir);
|
|
28
|
+
}
|
|
29
|
+
return path.resolve(startDir);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Load and validate .mrsf.yaml config. Returns null if not found.
|
|
33
|
+
*/
|
|
34
|
+
export async function loadConfig(workspaceRoot, configPath) {
|
|
35
|
+
const cfgPath = configPath
|
|
36
|
+
? path.resolve(configPath)
|
|
37
|
+
: path.join(workspaceRoot, CONFIG_FILENAME);
|
|
38
|
+
if (!existsSync(cfgPath))
|
|
39
|
+
return null;
|
|
40
|
+
const raw = await readFile(cfgPath, "utf-8");
|
|
41
|
+
const parsed = yaml.load(raw);
|
|
42
|
+
if (!parsed || typeof parsed !== "object")
|
|
43
|
+
return null;
|
|
44
|
+
const config = {};
|
|
45
|
+
if (typeof parsed.sidecar_root === "string") {
|
|
46
|
+
const sr = parsed.sidecar_root;
|
|
47
|
+
// Reject absolute paths
|
|
48
|
+
if (path.isAbsolute(sr)) {
|
|
49
|
+
throw new Error(`.mrsf.yaml: sidecar_root must be a relative path (got "${sr}")`);
|
|
50
|
+
}
|
|
51
|
+
// Reject path traversal
|
|
52
|
+
if (sr.includes("..")) {
|
|
53
|
+
throw new Error(`.mrsf.yaml: sidecar_root must not contain ".." (got "${sr}")`);
|
|
54
|
+
}
|
|
55
|
+
config.sidecar_root = sr;
|
|
56
|
+
}
|
|
57
|
+
return config;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Given a Markdown document path (relative to workspace root), resolve the
|
|
61
|
+
* sidecar file path according to §3.3 discovery order.
|
|
62
|
+
*
|
|
63
|
+
* Returns an absolute path to the sidecar.
|
|
64
|
+
*/
|
|
65
|
+
export async function discoverSidecar(documentPath, options = {}) {
|
|
66
|
+
const cwd = options.cwd ?? process.cwd();
|
|
67
|
+
const workspaceRoot = findWorkspaceRoot(cwd);
|
|
68
|
+
const config = await loadConfig(workspaceRoot, options.configPath);
|
|
69
|
+
// Normalize to a workspace-relative path
|
|
70
|
+
const relDoc = path.isAbsolute(documentPath)
|
|
71
|
+
? path.relative(workspaceRoot, documentPath)
|
|
72
|
+
: documentPath;
|
|
73
|
+
if (config?.sidecar_root) {
|
|
74
|
+
// §3.2 — alternate sidecar location
|
|
75
|
+
return path.join(workspaceRoot, config.sidecar_root, relDoc + SIDECAR_SUFFIX);
|
|
76
|
+
}
|
|
77
|
+
// §3.1 — co-located
|
|
78
|
+
return path.join(workspaceRoot, relDoc + SIDECAR_SUFFIX);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Given a sidecar file path, resolve the Markdown document path.
|
|
82
|
+
* Strips .review.yaml or .review.json suffix.
|
|
83
|
+
* Returns an absolute path to the document.
|
|
84
|
+
*/
|
|
85
|
+
export function sidecarToDocument(sidecarPath, options = {}) {
|
|
86
|
+
const abs = path.resolve(sidecarPath);
|
|
87
|
+
if (abs.endsWith(SIDECAR_SUFFIX)) {
|
|
88
|
+
return abs.slice(0, -SIDECAR_SUFFIX.length);
|
|
89
|
+
}
|
|
90
|
+
else if (abs.endsWith(SIDECAR_SUFFIX_JSON)) {
|
|
91
|
+
return abs.slice(0, -SIDECAR_SUFFIX_JSON.length);
|
|
92
|
+
}
|
|
93
|
+
return abs;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Discover all sidecar files in a directory (recursive).
|
|
97
|
+
*/
|
|
98
|
+
export async function discoverAllSidecars(dirPath) {
|
|
99
|
+
const { readdir, stat } = await import("node:fs/promises");
|
|
100
|
+
const results = [];
|
|
101
|
+
async function walk(dir) {
|
|
102
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
103
|
+
for (const entry of entries) {
|
|
104
|
+
const full = path.join(dir, entry.name);
|
|
105
|
+
if (entry.isDirectory()) {
|
|
106
|
+
if (entry.name === "node_modules" || entry.name === ".git")
|
|
107
|
+
continue;
|
|
108
|
+
await walk(full);
|
|
109
|
+
}
|
|
110
|
+
else if (entry.name.endsWith(SIDECAR_SUFFIX) ||
|
|
111
|
+
entry.name.endsWith(SIDECAR_SUFFIX_JSON)) {
|
|
112
|
+
results.push(full);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const s = await stat(dirPath);
|
|
117
|
+
if (s.isFile()) {
|
|
118
|
+
results.push(path.resolve(dirPath));
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
await walk(path.resolve(dirPath));
|
|
122
|
+
}
|
|
123
|
+
return results;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=discovery.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MRSF Fuzzy Matching Engine
|
|
3
|
+
*
|
|
4
|
+
* Provides exact, normalized, token-level LCS, and character-level
|
|
5
|
+
* Levenshtein matching for re-anchoring selected_text.
|
|
6
|
+
*/
|
|
7
|
+
import type { FuzzyCandidate } from "./types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Find all exact occurrences of `needle` in lines (1-based array).
|
|
10
|
+
*/
|
|
11
|
+
export declare function exactMatch(lines: string[], needle: string): FuzzyCandidate[];
|
|
12
|
+
/**
|
|
13
|
+
* Find matches after normalizing whitespace.
|
|
14
|
+
*/
|
|
15
|
+
export declare function normalizedMatch(lines: string[], needle: string): FuzzyCandidate[];
|
|
16
|
+
/**
|
|
17
|
+
* Score two texts using token-level LCS.
|
|
18
|
+
* Returns 0.0–1.0.
|
|
19
|
+
*/
|
|
20
|
+
export declare function tokenLcsScore(a: string, b: string): number;
|
|
21
|
+
/**
|
|
22
|
+
* Normalized Levenshtein similarity 0.0–1.0.
|
|
23
|
+
*/
|
|
24
|
+
export declare function levenshteinScore(a: string, b: string): number;
|
|
25
|
+
/**
|
|
26
|
+
* Compute a combined similarity score between two text fragments.
|
|
27
|
+
* Blends token LCS (structural) and Levenshtein (character-level).
|
|
28
|
+
*/
|
|
29
|
+
export declare function combinedScore(needle: string, candidate: string): number;
|
|
30
|
+
/**
|
|
31
|
+
* Search the document for fuzzy matches of `needle`.
|
|
32
|
+
*
|
|
33
|
+
* @param lines 1-based line array (index 0 unused).
|
|
34
|
+
* @param needle The original selected_text.
|
|
35
|
+
* @param threshold Minimum score to include (0.0–1.0).
|
|
36
|
+
* @param hintLine Optional original line number for proximity scoring.
|
|
37
|
+
*/
|
|
38
|
+
export declare function fuzzySearch(lines: string[], needle: string, threshold?: number, hintLine?: number): FuzzyCandidate[];
|
|
39
|
+
//# sourceMappingURL=fuzzy.d.ts.map
|