@mrsf/cli 0.4.3 → 0.6.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/README.md +29 -0
- package/dist/browser.cjs +1046 -0
- package/dist/browser.cjs.map +7 -0
- package/dist/browser.d.ts +17 -1
- package/dist/browser.js +15 -0
- package/dist/commands/reanchor.js +3 -0
- package/dist/index.cjs +2184 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.js +10 -0
- package/dist/lib/comments.js +2 -2
- package/dist/lib/fuzzy.js +15 -152
- package/dist/lib/identity.d.ts +12 -0
- package/dist/lib/identity.js +23 -0
- package/dist/lib/parser.d.ts +4 -19
- package/dist/lib/parser.js +4 -140
- package/dist/lib/reanchor-core.d.ts +17 -1
- package/dist/lib/reanchor-core.js +141 -18
- package/dist/lib/reanchor.d.ts +1 -1
- package/dist/lib/reanchor.js +5 -3
- package/dist/lib/schema.d.ts +106 -0
- package/dist/lib/schema.js +3 -0
- package/dist/lib/serialize.d.ts +37 -0
- package/dist/lib/serialize.js +170 -0
- package/dist/lib/types.d.ts +25 -0
- package/dist/lib/validate-core.d.ts +6 -0
- package/dist/lib/validate-core.js +128 -0
- package/dist/lib/validator.js +4 -112
- package/dist/lib/writer.d.ts +1 -8
- package/dist/lib/writer.js +5 -14
- package/package.json +14 -6
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2184 @@
|
|
|
1
|
+
const importMetaUrl = require("node:url").pathToFileURL(__filename).href;
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
var __copyProps = (to, from, except, desc) => {
|
|
17
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
18
|
+
for (let key of __getOwnPropNames(from))
|
|
19
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
20
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
25
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
26
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
27
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
28
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
29
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
30
|
+
mod
|
|
31
|
+
));
|
|
32
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
33
|
+
|
|
34
|
+
// src/lib/serialize.ts
|
|
35
|
+
function parseSidecarContent(content, filenameHint) {
|
|
36
|
+
const trimmed = content.trim();
|
|
37
|
+
let parsed;
|
|
38
|
+
const isJson = trimmed.startsWith("{") || filenameHint && filenameHint.endsWith(".review.json");
|
|
39
|
+
if (isJson) {
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(trimmed);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
throw new Error(`Failed to parse JSON: ${e.message}`);
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
try {
|
|
47
|
+
parsed = import_js_yaml2.default.load(trimmed, { schema: import_js_yaml2.default.JSON_SCHEMA });
|
|
48
|
+
} catch (e) {
|
|
49
|
+
throw new Error(`Failed to parse YAML: ${e.message}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
53
|
+
throw new Error("MRSF sidecar must be a YAML/JSON object");
|
|
54
|
+
}
|
|
55
|
+
return parsed;
|
|
56
|
+
}
|
|
57
|
+
function parseSidecarContentLenient(content, filenameHint) {
|
|
58
|
+
const trimmed = content.trim();
|
|
59
|
+
if (!trimmed) {
|
|
60
|
+
return { doc: null, error: "File is empty" };
|
|
61
|
+
}
|
|
62
|
+
const isJson = trimmed.startsWith("{") || filenameHint && filenameHint.endsWith(".review.json");
|
|
63
|
+
let parsed;
|
|
64
|
+
try {
|
|
65
|
+
parsed = isJson ? JSON.parse(trimmed) : import_js_yaml2.default.load(trimmed, { schema: import_js_yaml2.default.JSON_SCHEMA });
|
|
66
|
+
} catch (e) {
|
|
67
|
+
if (!isJson) {
|
|
68
|
+
return salvageYaml(trimmed);
|
|
69
|
+
}
|
|
70
|
+
return { doc: null, error: `Failed to parse JSON: ${e.message}` };
|
|
71
|
+
}
|
|
72
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
73
|
+
return { doc: null, error: "MRSF sidecar must be a YAML/JSON object" };
|
|
74
|
+
}
|
|
75
|
+
const obj = parsed;
|
|
76
|
+
const doc = {
|
|
77
|
+
mrsf_version: typeof obj.mrsf_version === "string" ? obj.mrsf_version : "1.0",
|
|
78
|
+
document: typeof obj.document === "string" ? obj.document : "unknown",
|
|
79
|
+
comments: []
|
|
80
|
+
};
|
|
81
|
+
if (!Array.isArray(obj.comments)) {
|
|
82
|
+
return {
|
|
83
|
+
doc,
|
|
84
|
+
error: "comments field is not an array \u2014 file may be corrupted"
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const good = [];
|
|
88
|
+
const bad = [];
|
|
89
|
+
for (let i = 0; i < obj.comments.length; i++) {
|
|
90
|
+
const c = obj.comments[i];
|
|
91
|
+
if (c && typeof c === "object" && !Array.isArray(c) && typeof c.id === "string") {
|
|
92
|
+
good.push(c);
|
|
93
|
+
} else {
|
|
94
|
+
bad.push(i);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
doc.comments = good;
|
|
98
|
+
if (bad.length > 0) {
|
|
99
|
+
return {
|
|
100
|
+
doc,
|
|
101
|
+
error: `${bad.length} comment(s) at indices [${bad.join(", ")}] were malformed and skipped`,
|
|
102
|
+
partialComments: good
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return { doc };
|
|
106
|
+
}
|
|
107
|
+
function salvageYaml(content) {
|
|
108
|
+
const salvaged = [];
|
|
109
|
+
let mrsf_version = "1.0";
|
|
110
|
+
let document = "unknown";
|
|
111
|
+
const versionMatch = content.match(/^mrsf_version:\s*["']?([^"'\n]+)/m);
|
|
112
|
+
if (versionMatch) mrsf_version = versionMatch[1].trim();
|
|
113
|
+
const docMatch = content.match(/^document:\s*["']?([^"'\n]+)/m);
|
|
114
|
+
if (docMatch) document = docMatch[1].trim();
|
|
115
|
+
const blocks = content.split(/(?=^ - id:\s)/m);
|
|
116
|
+
for (const block of blocks) {
|
|
117
|
+
const trimmed = block.trim();
|
|
118
|
+
if (!trimmed.startsWith("- id:")) continue;
|
|
119
|
+
try {
|
|
120
|
+
const parsed = import_js_yaml2.default.load(trimmed, { schema: import_js_yaml2.default.JSON_SCHEMA });
|
|
121
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
122
|
+
const c = parsed[0];
|
|
123
|
+
if (c && typeof c === "object" && typeof c.id === "string") {
|
|
124
|
+
salvaged.push(c);
|
|
125
|
+
}
|
|
126
|
+
} else if (parsed && typeof parsed === "object" && typeof parsed.id === "string") {
|
|
127
|
+
salvaged.push(parsed);
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const doc = {
|
|
133
|
+
mrsf_version,
|
|
134
|
+
document,
|
|
135
|
+
comments: salvaged
|
|
136
|
+
};
|
|
137
|
+
return {
|
|
138
|
+
doc: salvaged.length > 0 ? doc : null,
|
|
139
|
+
error: `YAML parse failed. Salvaged ${salvaged.length} comment(s) from raw content.`,
|
|
140
|
+
partialComments: salvaged.length > 0 ? salvaged : void 0
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function toYaml(doc) {
|
|
144
|
+
const yamlDoc = new import_yaml.Document(doc);
|
|
145
|
+
return yamlDoc.toString({ lineWidth: 0 });
|
|
146
|
+
}
|
|
147
|
+
function toJson(doc) {
|
|
148
|
+
return JSON.stringify(doc, null, 2) + "\n";
|
|
149
|
+
}
|
|
150
|
+
var import_js_yaml2, import_yaml;
|
|
151
|
+
var init_serialize = __esm({
|
|
152
|
+
"src/lib/serialize.ts"() {
|
|
153
|
+
"use strict";
|
|
154
|
+
import_js_yaml2 = __toESM(require("js-yaml"), 1);
|
|
155
|
+
import_yaml = require("yaml");
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// src/lib/parser.ts
|
|
160
|
+
var parser_exports = {};
|
|
161
|
+
__export(parser_exports, {
|
|
162
|
+
parseSidecar: () => parseSidecar,
|
|
163
|
+
parseSidecarContent: () => parseSidecarContent,
|
|
164
|
+
parseSidecarContentLenient: () => parseSidecarContentLenient,
|
|
165
|
+
parseSidecarLenient: () => parseSidecarLenient,
|
|
166
|
+
readDocumentLines: () => readDocumentLines
|
|
167
|
+
});
|
|
168
|
+
async function parseSidecar(filePath) {
|
|
169
|
+
const abs = import_node_path3.default.resolve(filePath);
|
|
170
|
+
const content = await (0, import_promises2.readFile)(abs, "utf-8");
|
|
171
|
+
return parseSidecarContent(content, abs);
|
|
172
|
+
}
|
|
173
|
+
async function parseSidecarLenient(filePath) {
|
|
174
|
+
const abs = import_node_path3.default.resolve(filePath);
|
|
175
|
+
let content;
|
|
176
|
+
try {
|
|
177
|
+
content = await (0, import_promises2.readFile)(abs, "utf-8");
|
|
178
|
+
} catch (e) {
|
|
179
|
+
return { doc: null, error: `Cannot read file: ${e.message}` };
|
|
180
|
+
}
|
|
181
|
+
return parseSidecarContentLenient(content, abs);
|
|
182
|
+
}
|
|
183
|
+
async function readDocumentLines(filePath) {
|
|
184
|
+
const content = await (0, import_promises2.readFile)(import_node_path3.default.resolve(filePath), "utf-8");
|
|
185
|
+
const lines = content.replace(/\r\n?/g, "\n").split("\n");
|
|
186
|
+
return ["", ...lines];
|
|
187
|
+
}
|
|
188
|
+
var import_promises2, import_node_path3;
|
|
189
|
+
var init_parser = __esm({
|
|
190
|
+
"src/lib/parser.ts"() {
|
|
191
|
+
"use strict";
|
|
192
|
+
import_promises2 = require("node:fs/promises");
|
|
193
|
+
import_node_path3 = __toESM(require("node:path"), 1);
|
|
194
|
+
init_serialize();
|
|
195
|
+
init_serialize();
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// src/index.ts
|
|
200
|
+
var index_exports = {};
|
|
201
|
+
__export(index_exports, {
|
|
202
|
+
DEFAULT_THRESHOLD: () => DEFAULT_THRESHOLD2,
|
|
203
|
+
HIGH_THRESHOLD: () => HIGH_THRESHOLD2,
|
|
204
|
+
addComment: () => addComment2,
|
|
205
|
+
applyReanchorResults: () => applyReanchorResults2,
|
|
206
|
+
combinedScore: () => combinedScore2,
|
|
207
|
+
computeHash: () => computeHash2,
|
|
208
|
+
detectRenames: () => detectRenames2,
|
|
209
|
+
discoverAllSidecars: () => discoverAllSidecars2,
|
|
210
|
+
discoverSidecar: () => discoverSidecar3,
|
|
211
|
+
editComment: () => editComment2,
|
|
212
|
+
exactMatch: () => exactMatch2,
|
|
213
|
+
filterComments: () => filterComments2,
|
|
214
|
+
findRepoRoot: () => findRepoRoot3,
|
|
215
|
+
findWorkspaceRoot: () => findWorkspaceRoot2,
|
|
216
|
+
formatAuthor: () => formatAuthor2,
|
|
217
|
+
fuzzySearch: () => fuzzySearch2,
|
|
218
|
+
getCurrentCommit: () => getCurrentCommit2,
|
|
219
|
+
getDiff: () => getDiff2,
|
|
220
|
+
getFileAtCommit: () => getFileAtCommit3,
|
|
221
|
+
getLineShift: () => getLineShift4,
|
|
222
|
+
getStagedFiles: () => getStagedFiles2,
|
|
223
|
+
getThreads: () => getThreads2,
|
|
224
|
+
isGitAvailable: () => isGitAvailable2,
|
|
225
|
+
isStale: () => isStale2,
|
|
226
|
+
loadConfig: () => loadConfig2,
|
|
227
|
+
mrsfSchema: () => mrsfSchema,
|
|
228
|
+
newCommentId: () => newCommentId2,
|
|
229
|
+
normalizeCommentExtensions: () => normalizeCommentExtensions2,
|
|
230
|
+
normalizedMatch: () => normalizedMatch2,
|
|
231
|
+
parseAuthor: () => parseAuthor2,
|
|
232
|
+
parseDiffHunks: () => parseDiffHunks3,
|
|
233
|
+
parseSidecar: () => parseSidecar2,
|
|
234
|
+
parseSidecarContent: () => parseSidecarContent2,
|
|
235
|
+
parseSidecarContentLenient: () => parseSidecarContentLenient2,
|
|
236
|
+
parseSidecarLenient: () => parseSidecarLenient2,
|
|
237
|
+
populateSelectedText: () => populateSelectedText2,
|
|
238
|
+
readDocumentLines: () => readDocumentLines2,
|
|
239
|
+
reanchorComment: () => reanchorComment2,
|
|
240
|
+
reanchorDocument: () => reanchorDocument2,
|
|
241
|
+
reanchorDocumentLines: () => reanchorDocumentLines2,
|
|
242
|
+
reanchorDocumentText: () => reanchorDocumentText2,
|
|
243
|
+
reanchorFile: () => reanchorFile2,
|
|
244
|
+
removeComment: () => removeComment2,
|
|
245
|
+
resolveAnchor: () => resolveAnchor2,
|
|
246
|
+
resolveComment: () => resolveComment2,
|
|
247
|
+
resolveSidecarPaths: () => resolveSidecarPaths2,
|
|
248
|
+
sidecarToDocument: () => sidecarToDocument2,
|
|
249
|
+
summarize: () => summarize2,
|
|
250
|
+
syncHash: () => syncHash2,
|
|
251
|
+
toJson: () => toJson2,
|
|
252
|
+
toReanchorLines: () => toReanchorLines2,
|
|
253
|
+
toYaml: () => toYaml2,
|
|
254
|
+
unresolveComment: () => unresolveComment2,
|
|
255
|
+
validate: () => validate2,
|
|
256
|
+
validateDocument: () => validateDocument2,
|
|
257
|
+
validateFile: () => validateFile2,
|
|
258
|
+
writeSidecar: () => writeSidecar2
|
|
259
|
+
});
|
|
260
|
+
module.exports = __toCommonJS(index_exports);
|
|
261
|
+
|
|
262
|
+
// src/lib/discovery.ts
|
|
263
|
+
var import_promises = require("node:fs/promises");
|
|
264
|
+
var import_node_fs = require("node:fs");
|
|
265
|
+
var import_node_path = __toESM(require("node:path"), 1);
|
|
266
|
+
var import_js_yaml = __toESM(require("js-yaml"), 1);
|
|
267
|
+
var CONFIG_FILENAME = ".mrsf.yaml";
|
|
268
|
+
var SIDECAR_SUFFIX = ".review.yaml";
|
|
269
|
+
var SIDECAR_SUFFIX_JSON = ".review.json";
|
|
270
|
+
function findWorkspaceRoot(startDir) {
|
|
271
|
+
let dir = import_node_path.default.resolve(startDir);
|
|
272
|
+
const { root } = import_node_path.default.parse(dir);
|
|
273
|
+
while (dir !== root) {
|
|
274
|
+
if ((0, import_node_fs.existsSync)(import_node_path.default.join(dir, CONFIG_FILENAME)) || (0, import_node_fs.existsSync)(import_node_path.default.join(dir, ".git"))) {
|
|
275
|
+
return dir;
|
|
276
|
+
}
|
|
277
|
+
dir = import_node_path.default.dirname(dir);
|
|
278
|
+
}
|
|
279
|
+
return import_node_path.default.resolve(startDir);
|
|
280
|
+
}
|
|
281
|
+
async function loadConfig(workspaceRoot, configPath) {
|
|
282
|
+
const cfgPath = configPath ? import_node_path.default.resolve(configPath) : import_node_path.default.join(workspaceRoot, CONFIG_FILENAME);
|
|
283
|
+
if (!(0, import_node_fs.existsSync)(cfgPath)) return null;
|
|
284
|
+
const raw = await (0, import_promises.readFile)(cfgPath, "utf-8");
|
|
285
|
+
const parsed = import_js_yaml.default.load(raw, { schema: import_js_yaml.default.JSON_SCHEMA });
|
|
286
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
287
|
+
const config = {};
|
|
288
|
+
if (typeof parsed.sidecar_root === "string") {
|
|
289
|
+
const sr = parsed.sidecar_root;
|
|
290
|
+
if (import_node_path.default.isAbsolute(sr)) {
|
|
291
|
+
throw new Error(
|
|
292
|
+
`.mrsf.yaml: sidecar_root must be a relative path (got "${sr}")`
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
if (sr.includes("..")) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
`.mrsf.yaml: sidecar_root must not contain ".." (got "${sr}")`
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
config.sidecar_root = sr;
|
|
301
|
+
}
|
|
302
|
+
return config;
|
|
303
|
+
}
|
|
304
|
+
async function discoverSidecar(documentPath, options = {}) {
|
|
305
|
+
const cwd = options.cwd ?? process.cwd();
|
|
306
|
+
const workspaceRoot = findWorkspaceRoot(cwd);
|
|
307
|
+
const config = await loadConfig(workspaceRoot, options.configPath);
|
|
308
|
+
const relDoc = import_node_path.default.isAbsolute(documentPath) ? import_node_path.default.relative(workspaceRoot, documentPath) : documentPath;
|
|
309
|
+
if (config?.sidecar_root) {
|
|
310
|
+
return import_node_path.default.join(workspaceRoot, config.sidecar_root, relDoc + SIDECAR_SUFFIX);
|
|
311
|
+
}
|
|
312
|
+
return import_node_path.default.join(workspaceRoot, relDoc + SIDECAR_SUFFIX);
|
|
313
|
+
}
|
|
314
|
+
function sidecarToDocument(sidecarPath, options = {}) {
|
|
315
|
+
const abs = import_node_path.default.resolve(sidecarPath);
|
|
316
|
+
if (abs.endsWith(SIDECAR_SUFFIX)) {
|
|
317
|
+
return abs.slice(0, -SIDECAR_SUFFIX.length);
|
|
318
|
+
} else if (abs.endsWith(SIDECAR_SUFFIX_JSON)) {
|
|
319
|
+
return abs.slice(0, -SIDECAR_SUFFIX_JSON.length);
|
|
320
|
+
}
|
|
321
|
+
return abs;
|
|
322
|
+
}
|
|
323
|
+
async function discoverAllSidecars(dirPath) {
|
|
324
|
+
const { readdir, stat } = await import("node:fs/promises");
|
|
325
|
+
const results = [];
|
|
326
|
+
async function walk(dir) {
|
|
327
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
328
|
+
for (const entry of entries) {
|
|
329
|
+
const full = import_node_path.default.join(dir, entry.name);
|
|
330
|
+
if (entry.isDirectory()) {
|
|
331
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
332
|
+
await walk(full);
|
|
333
|
+
} else if (entry.name.endsWith(SIDECAR_SUFFIX) || entry.name.endsWith(SIDECAR_SUFFIX_JSON)) {
|
|
334
|
+
results.push(full);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const s = await stat(dirPath);
|
|
339
|
+
if (s.isFile()) {
|
|
340
|
+
results.push(import_node_path.default.resolve(dirPath));
|
|
341
|
+
} else {
|
|
342
|
+
await walk(import_node_path.default.resolve(dirPath));
|
|
343
|
+
}
|
|
344
|
+
return results;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/lib/resolve-files.ts
|
|
348
|
+
var import_node_path2 = __toESM(require("node:path"), 1);
|
|
349
|
+
var SIDECAR_EXTENSIONS = [".review.yaml", ".review.json"];
|
|
350
|
+
function isSidecarPath(file) {
|
|
351
|
+
return SIDECAR_EXTENSIONS.some((ext) => file.endsWith(ext));
|
|
352
|
+
}
|
|
353
|
+
async function resolveSidecarPaths(files, cwd) {
|
|
354
|
+
if (files.length === 0) {
|
|
355
|
+
const root = findWorkspaceRoot(cwd);
|
|
356
|
+
return discoverAllSidecars(root ?? cwd);
|
|
357
|
+
}
|
|
358
|
+
const resolved = [];
|
|
359
|
+
for (const f of files) {
|
|
360
|
+
const abs = import_node_path2.default.resolve(cwd, f);
|
|
361
|
+
if (isSidecarPath(abs)) {
|
|
362
|
+
resolved.push(abs);
|
|
363
|
+
} else {
|
|
364
|
+
const sidecar = await discoverSidecar(abs, { cwd });
|
|
365
|
+
resolved.push(sidecar);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return resolved;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// src/index.ts
|
|
372
|
+
init_parser();
|
|
373
|
+
|
|
374
|
+
// src/lib/writer.ts
|
|
375
|
+
var import_promises3 = require("node:fs/promises");
|
|
376
|
+
var import_node_fs2 = require("node:fs");
|
|
377
|
+
var import_node_crypto = require("node:crypto");
|
|
378
|
+
var import_node_path4 = __toESM(require("node:path"), 1);
|
|
379
|
+
var import_yaml2 = require("yaml");
|
|
380
|
+
init_serialize();
|
|
381
|
+
init_serialize();
|
|
382
|
+
var writeQueue = /* @__PURE__ */ new Map();
|
|
383
|
+
function enqueueWrite(abs, fn) {
|
|
384
|
+
const prev = writeQueue.get(abs) ?? Promise.resolve();
|
|
385
|
+
const next = prev.then(fn, fn);
|
|
386
|
+
writeQueue.set(abs, next);
|
|
387
|
+
next.then(() => {
|
|
388
|
+
if (writeQueue.get(abs) === next) writeQueue.delete(abs);
|
|
389
|
+
}, () => {
|
|
390
|
+
if (writeQueue.get(abs) === next) writeQueue.delete(abs);
|
|
391
|
+
});
|
|
392
|
+
return next;
|
|
393
|
+
}
|
|
394
|
+
async function atomicWriteFile(filePath, content) {
|
|
395
|
+
const tmp = filePath + "." + (0, import_node_crypto.randomBytes)(6).toString("hex") + ".tmp";
|
|
396
|
+
try {
|
|
397
|
+
await (0, import_promises3.writeFile)(tmp, content, "utf-8");
|
|
398
|
+
await (0, import_promises3.rename)(tmp, filePath);
|
|
399
|
+
} catch (err) {
|
|
400
|
+
try {
|
|
401
|
+
await (0, import_promises3.unlink)(tmp);
|
|
402
|
+
} catch {
|
|
403
|
+
}
|
|
404
|
+
throw err;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function computeHash(text) {
|
|
408
|
+
return (0, import_node_crypto.createHash)("sha256").update(text, "utf-8").digest("hex");
|
|
409
|
+
}
|
|
410
|
+
function syncHash(comment) {
|
|
411
|
+
if (comment.selected_text != null && comment.selected_text.length > 0) {
|
|
412
|
+
comment.selected_text_hash = computeHash(comment.selected_text);
|
|
413
|
+
} else {
|
|
414
|
+
delete comment.selected_text_hash;
|
|
415
|
+
}
|
|
416
|
+
return comment;
|
|
417
|
+
}
|
|
418
|
+
var COMMENT_KEY_ORDER = [
|
|
419
|
+
"id",
|
|
420
|
+
"author",
|
|
421
|
+
"timestamp",
|
|
422
|
+
"text",
|
|
423
|
+
"type",
|
|
424
|
+
"severity",
|
|
425
|
+
"resolved",
|
|
426
|
+
"reply_to",
|
|
427
|
+
"line",
|
|
428
|
+
"end_line",
|
|
429
|
+
"start_column",
|
|
430
|
+
"end_column",
|
|
431
|
+
"selected_text",
|
|
432
|
+
"selected_text_hash",
|
|
433
|
+
"anchored_text",
|
|
434
|
+
"commit"
|
|
435
|
+
];
|
|
436
|
+
function valueToSource(v) {
|
|
437
|
+
if (v instanceof Date) return JSON.stringify(v.toISOString());
|
|
438
|
+
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
439
|
+
if (typeof v === "string") {
|
|
440
|
+
if (v === "") return JSON.stringify(v);
|
|
441
|
+
if (v === "true" || v === "false" || v === "null" || v === "True" || v === "False" || v === "Null" || v === "TRUE" || v === "FALSE" || v === "NULL" || v === "yes" || v === "no" || v === "on" || v === "off" || v === "Yes" || v === "No" || v === "On" || v === "Off" || v === "YES" || v === "NO" || v === "ON" || v === "OFF" || v === "~" || v === ".inf" || v === "-.inf" || v === ".nan") {
|
|
442
|
+
return JSON.stringify(v);
|
|
443
|
+
}
|
|
444
|
+
if (/^\d/.test(v)) return JSON.stringify(v);
|
|
445
|
+
if (/^[A-Za-z_][A-Za-z0-9 _./()\-@+]*$/.test(v) && !v.includes(" #") && !/\s$/.test(v)) {
|
|
446
|
+
return v;
|
|
447
|
+
}
|
|
448
|
+
return JSON.stringify(v);
|
|
449
|
+
}
|
|
450
|
+
return String(v);
|
|
451
|
+
}
|
|
452
|
+
function valueType(v) {
|
|
453
|
+
return valueToSource(v).startsWith('"') ? "double-quoted-scalar" : "scalar";
|
|
454
|
+
}
|
|
455
|
+
function isStructuredValue(value) {
|
|
456
|
+
if (value == null) return false;
|
|
457
|
+
if (Array.isArray(value)) return true;
|
|
458
|
+
return typeof value === "object" && !(value instanceof Date);
|
|
459
|
+
}
|
|
460
|
+
function hasStructuredCommentExtensions(doc) {
|
|
461
|
+
return doc.comments.some(
|
|
462
|
+
(comment) => Object.entries(comment).some(
|
|
463
|
+
([key, value]) => !COMMENT_KEY_ORDER.includes(key) && value !== void 0 && isStructuredValue(value)
|
|
464
|
+
)
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
var NL = "\n";
|
|
468
|
+
function makeCstMapItem(key, value, indent, isFirst) {
|
|
469
|
+
const start = isFirst ? [] : [{ type: "space", offset: 0, indent: 0, source: " ".repeat(indent) }];
|
|
470
|
+
return {
|
|
471
|
+
start,
|
|
472
|
+
key: { type: "scalar", offset: 0, indent, source: key },
|
|
473
|
+
sep: [
|
|
474
|
+
{ type: "map-value-ind", offset: 0, indent, source: ":" },
|
|
475
|
+
{ type: "space", offset: 0, indent, source: " " }
|
|
476
|
+
],
|
|
477
|
+
value: {
|
|
478
|
+
type: valueType(value),
|
|
479
|
+
offset: 0,
|
|
480
|
+
indent,
|
|
481
|
+
source: valueToSource(value),
|
|
482
|
+
end: [{ type: "newline", offset: 0, indent, source: NL }]
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
function makeCstSeqItem(comment, seqIndent) {
|
|
487
|
+
const mapIndent = seqIndent + 2;
|
|
488
|
+
const items = [];
|
|
489
|
+
for (const key of COMMENT_KEY_ORDER) {
|
|
490
|
+
const val = comment[key];
|
|
491
|
+
if (val !== void 0) {
|
|
492
|
+
items.push(makeCstMapItem(key, val, mapIndent, items.length === 0));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
for (const key of Object.keys(comment)) {
|
|
496
|
+
if (!COMMENT_KEY_ORDER.includes(key) && comment[key] !== void 0) {
|
|
497
|
+
items.push(
|
|
498
|
+
makeCstMapItem(
|
|
499
|
+
key,
|
|
500
|
+
comment[key],
|
|
501
|
+
mapIndent,
|
|
502
|
+
items.length === 0
|
|
503
|
+
)
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return {
|
|
508
|
+
start: [
|
|
509
|
+
{ type: "newline", offset: 0, indent: 0, source: NL },
|
|
510
|
+
{ type: "space", offset: 0, indent: 0, source: " ".repeat(seqIndent) },
|
|
511
|
+
{ type: "seq-item-ind", offset: 0, indent: seqIndent, source: "-" },
|
|
512
|
+
{ type: "space", offset: 0, indent: seqIndent, source: " " }
|
|
513
|
+
],
|
|
514
|
+
value: { type: "block-map", offset: 0, indent: mapIndent, items }
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
function findCstDocument(tokens) {
|
|
518
|
+
return tokens.find((t) => t.type === "document") ?? null;
|
|
519
|
+
}
|
|
520
|
+
function findCstMapEntry(blockMap, keyName) {
|
|
521
|
+
return blockMap.items?.find(
|
|
522
|
+
(item) => item.key?.source === keyName
|
|
523
|
+
) ?? null;
|
|
524
|
+
}
|
|
525
|
+
function findCstMapIndex(blockMap, keyName) {
|
|
526
|
+
return blockMap.items?.findIndex(
|
|
527
|
+
(item) => item.key?.source === keyName
|
|
528
|
+
) ?? -1;
|
|
529
|
+
}
|
|
530
|
+
function cstCommentId(blockMap) {
|
|
531
|
+
const entry = findCstMapEntry(blockMap, "id");
|
|
532
|
+
if (!entry?.value?.source) return null;
|
|
533
|
+
const src = entry.value.source;
|
|
534
|
+
if (src.startsWith('"') && src.endsWith('"')) {
|
|
535
|
+
return src.slice(1, -1);
|
|
536
|
+
}
|
|
537
|
+
return src;
|
|
538
|
+
}
|
|
539
|
+
function updateCstScalar(entry, newValue) {
|
|
540
|
+
const newSrc = valueToSource(newValue);
|
|
541
|
+
if (entry.value?.source === newSrc) return false;
|
|
542
|
+
entry.value.source = newSrc;
|
|
543
|
+
entry.value.type = valueType(newValue);
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
function syncCommentToCst(blockMap, comment, currentValues) {
|
|
547
|
+
const commentRec = comment;
|
|
548
|
+
const existingKeys = /* @__PURE__ */ new Set();
|
|
549
|
+
for (const item of blockMap.items ?? []) {
|
|
550
|
+
if (item.key?.source) existingKeys.add(item.key.source);
|
|
551
|
+
}
|
|
552
|
+
const allKeys = /* @__PURE__ */ new Set([...existingKeys, ...Object.keys(comment)]);
|
|
553
|
+
const indent = blockMap.indent ?? 4;
|
|
554
|
+
for (const key of allKeys) {
|
|
555
|
+
const newVal = commentRec[key];
|
|
556
|
+
if (newVal === void 0) {
|
|
557
|
+
if (existingKeys.has(key)) {
|
|
558
|
+
const idx = findCstMapIndex(blockMap, key);
|
|
559
|
+
if (idx >= 0) blockMap.items.splice(idx, 1);
|
|
560
|
+
}
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
const entry = findCstMapEntry(blockMap, key);
|
|
564
|
+
if (entry) {
|
|
565
|
+
const currentVal = currentValues[key];
|
|
566
|
+
if (deepEqual(currentVal, newVal)) {
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
updateCstScalar(entry, newVal);
|
|
570
|
+
} else {
|
|
571
|
+
blockMap.items.push(makeCstMapItem(key, newVal, indent, false));
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
function deepEqual(a, b) {
|
|
576
|
+
if (a === b) return true;
|
|
577
|
+
if (typeof a !== typeof b) return false;
|
|
578
|
+
if (typeof a === "number" && typeof b === "number") return a === b;
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
async function writeSidecar(filePath, doc) {
|
|
582
|
+
const abs = import_node_path4.default.resolve(filePath);
|
|
583
|
+
return enqueueWrite(abs, () => writeSidecarInternal(abs, doc));
|
|
584
|
+
}
|
|
585
|
+
async function writeSidecarInternal(abs, doc) {
|
|
586
|
+
const isJson = abs.endsWith(".review.json");
|
|
587
|
+
for (const comment of doc.comments) syncHash(comment);
|
|
588
|
+
if (isJson) {
|
|
589
|
+
await atomicWriteFile(abs, toJson(doc));
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (hasStructuredCommentExtensions(doc)) {
|
|
593
|
+
await atomicWriteFile(abs, toYaml(doc));
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (!(0, import_node_fs2.existsSync)(abs)) {
|
|
597
|
+
await atomicWriteFile(abs, toYaml(doc));
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
let raw;
|
|
601
|
+
try {
|
|
602
|
+
raw = await (0, import_promises3.readFile)(abs, "utf-8");
|
|
603
|
+
} catch {
|
|
604
|
+
await atomicWriteFile(abs, toYaml(doc));
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
let tokens;
|
|
608
|
+
try {
|
|
609
|
+
tokens = [...new import_yaml2.Parser().parse(raw)];
|
|
610
|
+
} catch {
|
|
611
|
+
await atomicWriteFile(abs, toYaml(doc));
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
let currentDoc = null;
|
|
615
|
+
try {
|
|
616
|
+
currentDoc = (0, import_yaml2.parse)(raw);
|
|
617
|
+
} catch {
|
|
618
|
+
}
|
|
619
|
+
if (currentDoc && !Array.isArray(currentDoc.comments)) {
|
|
620
|
+
await atomicWriteFile(abs, toYaml(doc));
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
const currentById = /* @__PURE__ */ new Map();
|
|
624
|
+
if (currentDoc?.comments) {
|
|
625
|
+
for (const c of currentDoc.comments) {
|
|
626
|
+
if (c.id) currentById.set(c.id, c);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
for (const comment of doc.comments) {
|
|
630
|
+
const cur = currentById.get(comment.id);
|
|
631
|
+
if (!cur) {
|
|
632
|
+
syncHash(comment);
|
|
633
|
+
} else {
|
|
634
|
+
const textChanged = !deepEqual(cur.selected_text, comment.selected_text);
|
|
635
|
+
const hadHash = cur.selected_text_hash !== void 0;
|
|
636
|
+
if (textChanged || hadHash) {
|
|
637
|
+
syncHash(comment);
|
|
638
|
+
} else {
|
|
639
|
+
delete comment.selected_text_hash;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
const docToken = findCstDocument(tokens);
|
|
644
|
+
if (!docToken?.value || docToken.value.type !== "block-map") {
|
|
645
|
+
await atomicWriteFile(abs, toYaml(doc));
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const body = docToken.value;
|
|
649
|
+
const versionEntry = findCstMapEntry(body, "mrsf_version");
|
|
650
|
+
if (versionEntry && !deepEqual(currentDoc?.mrsf_version, doc.mrsf_version)) {
|
|
651
|
+
updateCstScalar(versionEntry, doc.mrsf_version);
|
|
652
|
+
}
|
|
653
|
+
const documentEntry = findCstMapEntry(body, "document");
|
|
654
|
+
if (documentEntry && !deepEqual(currentDoc?.document, doc.document)) {
|
|
655
|
+
updateCstScalar(documentEntry, doc.document);
|
|
656
|
+
}
|
|
657
|
+
const commentsEntry = findCstMapEntry(body, "comments");
|
|
658
|
+
if (!commentsEntry?.value || commentsEntry.value.type !== "block-seq") {
|
|
659
|
+
await atomicWriteFile(abs, toYaml(doc));
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
const seq = commentsEntry.value;
|
|
663
|
+
const seqIndent = seq.indent ?? 2;
|
|
664
|
+
const existingById = /* @__PURE__ */ new Map();
|
|
665
|
+
for (let i = 0; i < seq.items.length; i++) {
|
|
666
|
+
const item = seq.items[i];
|
|
667
|
+
const map = item.value;
|
|
668
|
+
if (map?.type === "block-map") {
|
|
669
|
+
const id = cstCommentId(map);
|
|
670
|
+
if (id) existingById.set(id, { index: i, map });
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
const idsInDoc = new Set(doc.comments.map((c) => c.id));
|
|
674
|
+
const newSeqItems = [];
|
|
675
|
+
for (const comment of doc.comments) {
|
|
676
|
+
const existing = existingById.get(comment.id);
|
|
677
|
+
if (existing) {
|
|
678
|
+
const currentValues = currentById.get(comment.id) ?? {};
|
|
679
|
+
syncCommentToCst(existing.map, comment, currentValues);
|
|
680
|
+
newSeqItems.push(seq.items[existing.index]);
|
|
681
|
+
} else {
|
|
682
|
+
newSeqItems.push(makeCstSeqItem(comment, seqIndent));
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
seq.items = newSeqItems;
|
|
686
|
+
if (newSeqItems.length > 0) {
|
|
687
|
+
const originalFirstIdx = seq.items === newSeqItems ? 0 : -1;
|
|
688
|
+
const origFirstItem = existingById.size > 0 ? [...existingById.values()].find((e) => e.index === 0) : void 0;
|
|
689
|
+
const newFirst = newSeqItems[0];
|
|
690
|
+
const isOriginalFirst = origFirstItem && seq.items[0]?.value === origFirstItem.map;
|
|
691
|
+
if (!isOriginalFirst && newFirst.start) {
|
|
692
|
+
const dashIdx = newFirst.start.findIndex(
|
|
693
|
+
(t) => t.type === "seq-item-ind"
|
|
694
|
+
);
|
|
695
|
+
if (dashIdx > 0) {
|
|
696
|
+
const toRemove = newFirst.start.slice(0, dashIdx).every((t) => t.type === "space" || t.type === "newline");
|
|
697
|
+
if (toRemove) {
|
|
698
|
+
newFirst.start.splice(0, dashIdx);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
const result = tokens.map((t) => import_yaml2.CST.stringify(t)).join("");
|
|
704
|
+
await atomicWriteFile(abs, result);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/lib/validator.ts
|
|
708
|
+
var import_promises4 = require("node:fs/promises");
|
|
709
|
+
var import_node_path5 = __toESM(require("node:path"), 1);
|
|
710
|
+
var import_node_url = require("node:url");
|
|
711
|
+
|
|
712
|
+
// src/lib/validate-core.ts
|
|
713
|
+
var import_ajv = __toESM(require("ajv"), 1);
|
|
714
|
+
var import_ajv_formats = __toESM(require("ajv-formats"), 1);
|
|
715
|
+
|
|
716
|
+
// mrsf.schema.json
|
|
717
|
+
var mrsf_schema_default = {
|
|
718
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
719
|
+
$id: "https://github.com/wictorwilen/MRSF/raw/main/mrsf.schema.json",
|
|
720
|
+
title: "Markdown Review Sidecar Format (MRSF) v1.0",
|
|
721
|
+
description: "Schema for MRSF review sidecar files. See MRSF-v1.0.md for the full specification.",
|
|
722
|
+
type: "object",
|
|
723
|
+
required: ["mrsf_version", "document", "comments"],
|
|
724
|
+
additionalProperties: true,
|
|
725
|
+
properties: {
|
|
726
|
+
mrsf_version: {
|
|
727
|
+
type: "string",
|
|
728
|
+
pattern: "^1\\.\\d+$",
|
|
729
|
+
description: "MRSF format version. MUST be a supported major.minor version (e.g., 1.0)."
|
|
730
|
+
},
|
|
731
|
+
document: {
|
|
732
|
+
type: "string",
|
|
733
|
+
description: "Relative path to the Markdown document being reviewed."
|
|
734
|
+
},
|
|
735
|
+
comments: {
|
|
736
|
+
type: "array",
|
|
737
|
+
items: {
|
|
738
|
+
type: "object",
|
|
739
|
+
required: ["id", "author", "timestamp", "text", "resolved"],
|
|
740
|
+
additionalProperties: true,
|
|
741
|
+
properties: {
|
|
742
|
+
id: {
|
|
743
|
+
type: "string",
|
|
744
|
+
description: "Globally unique, opaque, collision-resistant identifier for the comment."
|
|
745
|
+
},
|
|
746
|
+
author: {
|
|
747
|
+
type: "string",
|
|
748
|
+
description: "Creator of the comment. SHOULD follow the convention 'Display Name (identifier)'."
|
|
749
|
+
},
|
|
750
|
+
timestamp: {
|
|
751
|
+
type: "string",
|
|
752
|
+
format: "date-time",
|
|
753
|
+
description: "ISO 8601 / RFC 3339 timestamp of comment creation; SHOULD include timezone offset."
|
|
754
|
+
},
|
|
755
|
+
text: {
|
|
756
|
+
type: "string",
|
|
757
|
+
maxLength: 16384,
|
|
758
|
+
description: "The content of the review comment. MUST be plain text."
|
|
759
|
+
},
|
|
760
|
+
resolved: {
|
|
761
|
+
type: "boolean",
|
|
762
|
+
description: "Whether the comment has been resolved."
|
|
763
|
+
},
|
|
764
|
+
commit: {
|
|
765
|
+
type: "string",
|
|
766
|
+
description: "Git commit hash associated with the comment. SHOULD be the full (long) SHA."
|
|
767
|
+
},
|
|
768
|
+
type: {
|
|
769
|
+
type: "string",
|
|
770
|
+
description: "Categorization of the comment. Recommended values listed in examples.",
|
|
771
|
+
examples: [
|
|
772
|
+
"suggestion",
|
|
773
|
+
"issue",
|
|
774
|
+
"question",
|
|
775
|
+
"accuracy",
|
|
776
|
+
"style",
|
|
777
|
+
"clarity"
|
|
778
|
+
]
|
|
779
|
+
},
|
|
780
|
+
severity: {
|
|
781
|
+
type: "string",
|
|
782
|
+
description: "Importance level of the comment.",
|
|
783
|
+
enum: ["low", "medium", "high"]
|
|
784
|
+
},
|
|
785
|
+
reply_to: {
|
|
786
|
+
type: "string",
|
|
787
|
+
description: "ID of another comment in the same file that this comment replies to."
|
|
788
|
+
},
|
|
789
|
+
line: {
|
|
790
|
+
type: "integer",
|
|
791
|
+
minimum: 1,
|
|
792
|
+
description: "Starting line number (1-based) in the target document."
|
|
793
|
+
},
|
|
794
|
+
end_line: {
|
|
795
|
+
type: "integer",
|
|
796
|
+
minimum: 1,
|
|
797
|
+
description: "Ending line number (inclusive, 1-based). MUST be >= line."
|
|
798
|
+
},
|
|
799
|
+
start_column: {
|
|
800
|
+
type: "integer",
|
|
801
|
+
minimum: 0,
|
|
802
|
+
description: "Starting column index (0-based) within the starting line."
|
|
803
|
+
},
|
|
804
|
+
end_column: {
|
|
805
|
+
type: "integer",
|
|
806
|
+
minimum: 0,
|
|
807
|
+
description: "Ending column index. MUST be >= start_column when on the same line."
|
|
808
|
+
},
|
|
809
|
+
selected_text: {
|
|
810
|
+
type: "string",
|
|
811
|
+
maxLength: 4096,
|
|
812
|
+
description: "Exact text selected by the reviewer. SHOULD NOT be modified by re-anchoring tools. SHOULD NOT exceed 4096 characters."
|
|
813
|
+
},
|
|
814
|
+
anchored_text: {
|
|
815
|
+
type: "string",
|
|
816
|
+
maxLength: 4096,
|
|
817
|
+
description: "Text currently found at the resolved anchor position. Populated by re-anchoring tools when the document text differs from selected_text. SHOULD be omitted when identical to selected_text."
|
|
818
|
+
},
|
|
819
|
+
selected_text_hash: {
|
|
820
|
+
type: "string",
|
|
821
|
+
pattern: "^[a-f0-9]{64}$",
|
|
822
|
+
description: "Hex-encoded SHA-256 hash of selected_text. Immutable after creation; used for fast exact-match detection during re-anchoring, integrity verification, and staleness checks. SHOULD NOT be modified by re-anchoring tools unless selected_text is also replaced (opt-in behaviour), in which case it MUST be recomputed."
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
// src/lib/schema.ts
|
|
831
|
+
var mrsfSchema = mrsf_schema_default;
|
|
832
|
+
|
|
833
|
+
// src/lib/validate-core.ts
|
|
834
|
+
var Ajv = import_ajv.default.default ?? import_ajv.default;
|
|
835
|
+
var addFormats = import_ajv_formats.default.default ?? import_ajv_formats.default;
|
|
836
|
+
function validateDocument(doc, schema = mrsfSchema) {
|
|
837
|
+
const errors = [];
|
|
838
|
+
const warnings = [];
|
|
839
|
+
validateSchema(doc, schema, errors);
|
|
840
|
+
validateCrossFields(doc, errors, warnings);
|
|
841
|
+
return {
|
|
842
|
+
valid: errors.length === 0,
|
|
843
|
+
errors,
|
|
844
|
+
warnings
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
function validateSchema(doc, rawSchema, errors) {
|
|
848
|
+
const { $schema, ...schema } = rawSchema;
|
|
849
|
+
void $schema;
|
|
850
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
851
|
+
addFormats(ajv);
|
|
852
|
+
const ajvValidate = ajv.compile(schema);
|
|
853
|
+
const schemaValid = ajvValidate(doc);
|
|
854
|
+
if (!schemaValid && ajvValidate.errors) {
|
|
855
|
+
for (const err of ajvValidate.errors) {
|
|
856
|
+
errors.push({
|
|
857
|
+
severity: "error",
|
|
858
|
+
code: "schema-violation",
|
|
859
|
+
message: `${err.instancePath || "/"}: ${err.message ?? "schema error"}`,
|
|
860
|
+
path: err.instancePath || "/"
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
function validateCrossFields(doc, errors, warnings, hash) {
|
|
866
|
+
if (!Array.isArray(doc.comments)) return;
|
|
867
|
+
const ids = /* @__PURE__ */ new Set();
|
|
868
|
+
const allIds = doc.comments.map((x) => x.id);
|
|
869
|
+
for (let i = 0; i < doc.comments.length; i++) {
|
|
870
|
+
const c = doc.comments[i];
|
|
871
|
+
const prefix = `/comments/${i}`;
|
|
872
|
+
if (c.id) {
|
|
873
|
+
if (ids.has(c.id)) {
|
|
874
|
+
errors.push({
|
|
875
|
+
severity: "error",
|
|
876
|
+
code: "duplicate-id",
|
|
877
|
+
message: `Duplicate comment id "${c.id}"`,
|
|
878
|
+
path: `${prefix}/id`,
|
|
879
|
+
commentId: c.id
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
ids.add(c.id);
|
|
883
|
+
}
|
|
884
|
+
if (c.line != null && c.end_line != null && c.end_line < c.line) {
|
|
885
|
+
errors.push({
|
|
886
|
+
severity: "error",
|
|
887
|
+
code: "end-line-before-line",
|
|
888
|
+
message: `end_line (${c.end_line}) must be \u2265 line (${c.line})`,
|
|
889
|
+
path: `${prefix}/end_line`,
|
|
890
|
+
commentId: c.id
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
if (c.start_column != null && c.end_column != null && (c.line == null || c.end_line == null || c.line === c.end_line) && c.end_column < c.start_column) {
|
|
894
|
+
errors.push({
|
|
895
|
+
severity: "error",
|
|
896
|
+
code: "end-column-before-start-column",
|
|
897
|
+
message: `end_column (${c.end_column}) must be \u2265 start_column (${c.start_column}) on the same line`,
|
|
898
|
+
path: `${prefix}/end_column`,
|
|
899
|
+
commentId: c.id
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
if (c.selected_text && c.selected_text.length > 4096) {
|
|
903
|
+
errors.push({
|
|
904
|
+
severity: "error",
|
|
905
|
+
code: "selected-text-too-long",
|
|
906
|
+
message: `selected_text exceeds 4096 characters (${c.selected_text.length})`,
|
|
907
|
+
path: `${prefix}/selected_text`,
|
|
908
|
+
commentId: c.id
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
if (c.text && c.text.length > 16384) {
|
|
912
|
+
warnings.push({
|
|
913
|
+
severity: "warning",
|
|
914
|
+
code: "text-too-long",
|
|
915
|
+
message: `text exceeds recommended 16384 characters (${c.text.length})`,
|
|
916
|
+
path: `${prefix}/text`,
|
|
917
|
+
commentId: c.id
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
if (hash && c.selected_text && c.selected_text_hash) {
|
|
921
|
+
const expected = hash(c.selected_text);
|
|
922
|
+
if (c.selected_text_hash !== expected) {
|
|
923
|
+
warnings.push({
|
|
924
|
+
severity: "warning",
|
|
925
|
+
code: "hash-mismatch",
|
|
926
|
+
message: `selected_text_hash mismatch (expected ${expected.slice(0, 12)}\u2026, got ${c.selected_text_hash.slice(0, 12)}\u2026)`,
|
|
927
|
+
path: `${prefix}/selected_text_hash`,
|
|
928
|
+
commentId: c.id
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
if (c.reply_to && !ids.has(c.reply_to) && !allIds.includes(c.reply_to)) {
|
|
933
|
+
warnings.push({
|
|
934
|
+
severity: "warning",
|
|
935
|
+
code: "unresolved-reply-to",
|
|
936
|
+
message: `reply_to "${c.reply_to}" does not resolve to any comment id in this file`,
|
|
937
|
+
path: `${prefix}/reply_to`,
|
|
938
|
+
commentId: c.id
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
if (c.line != null && !c.selected_text) {
|
|
942
|
+
warnings.push({
|
|
943
|
+
severity: "warning",
|
|
944
|
+
code: "missing-selected-text",
|
|
945
|
+
message: "Comment has line anchors but no selected_text \u2014 anchoring will be fragile across edits",
|
|
946
|
+
path: `${prefix}/selected_text`,
|
|
947
|
+
commentId: c.id
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// src/lib/validator.ts
|
|
954
|
+
var __dirname = import_node_path5.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
955
|
+
var _schemaCache = null;
|
|
956
|
+
async function loadSchema() {
|
|
957
|
+
if (_schemaCache) return _schemaCache;
|
|
958
|
+
const candidates = [
|
|
959
|
+
import_node_path5.default.resolve(__dirname, "mrsf.schema.json"),
|
|
960
|
+
// same dir (esbuild bundle, e.g. MCP server)
|
|
961
|
+
import_node_path5.default.resolve(__dirname, "../../mrsf.schema.json"),
|
|
962
|
+
// from dist/lib/ → cli/ (installed package)
|
|
963
|
+
import_node_path5.default.resolve(__dirname, "../../../mrsf.schema.json"),
|
|
964
|
+
// from dist/lib/ → repo root (dev)
|
|
965
|
+
import_node_path5.default.resolve(__dirname, "../../../../mrsf.schema.json"),
|
|
966
|
+
// fallback
|
|
967
|
+
import_node_path5.default.resolve(process.cwd(), "mrsf.schema.json")
|
|
968
|
+
// cwd fallback
|
|
969
|
+
];
|
|
970
|
+
for (const candidate of candidates) {
|
|
971
|
+
try {
|
|
972
|
+
const raw = await (0, import_promises4.readFile)(candidate, "utf-8");
|
|
973
|
+
_schemaCache = JSON.parse(raw);
|
|
974
|
+
return _schemaCache;
|
|
975
|
+
} catch {
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
throw new Error("Could not locate mrsf.schema.json");
|
|
979
|
+
}
|
|
980
|
+
async function validate(doc, options = {}) {
|
|
981
|
+
const errors = [];
|
|
982
|
+
const warnings = [];
|
|
983
|
+
const rawSchema = await loadSchema();
|
|
984
|
+
validateSchema(doc, rawSchema, errors);
|
|
985
|
+
validateCrossFields(doc, errors, warnings, computeHash);
|
|
986
|
+
const valid = errors.length === 0 && (!options.strict || warnings.length === 0);
|
|
987
|
+
return { valid, errors, warnings };
|
|
988
|
+
}
|
|
989
|
+
async function validateFile(filePath, options = {}) {
|
|
990
|
+
const { parseSidecar: parseSidecar3 } = await Promise.resolve().then(() => (init_parser(), parser_exports));
|
|
991
|
+
try {
|
|
992
|
+
const doc = await parseSidecar3(filePath);
|
|
993
|
+
return validate(doc, options);
|
|
994
|
+
} catch (e) {
|
|
995
|
+
return {
|
|
996
|
+
valid: false,
|
|
997
|
+
errors: [
|
|
998
|
+
{
|
|
999
|
+
severity: "error",
|
|
1000
|
+
code: "parse-error",
|
|
1001
|
+
message: `Failed to parse: ${e.message}`
|
|
1002
|
+
}
|
|
1003
|
+
],
|
|
1004
|
+
warnings: []
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// src/lib/fuzzy.ts
|
|
1010
|
+
var import_fastest_levenshtein = require("fastest-levenshtein");
|
|
1011
|
+
function exactMatch(lines, needle) {
|
|
1012
|
+
if (!needle) return [];
|
|
1013
|
+
const results = [];
|
|
1014
|
+
const needleLines = needle.split("\n");
|
|
1015
|
+
const needleLineCount = needleLines.length;
|
|
1016
|
+
for (let startLine = 1; startLine <= lines.length - needleLineCount; startLine++) {
|
|
1017
|
+
const windowLines = lines.slice(startLine, startLine + needleLineCount);
|
|
1018
|
+
const windowText = windowLines.join("\n");
|
|
1019
|
+
if (needleLineCount === 1) {
|
|
1020
|
+
let col = 0;
|
|
1021
|
+
const line = windowLines[0];
|
|
1022
|
+
while (col < line.length) {
|
|
1023
|
+
const idx = line.indexOf(needle, col);
|
|
1024
|
+
if (idx === -1) break;
|
|
1025
|
+
results.push({
|
|
1026
|
+
text: needle,
|
|
1027
|
+
line: startLine,
|
|
1028
|
+
endLine: startLine,
|
|
1029
|
+
startColumn: idx,
|
|
1030
|
+
endColumn: idx + needle.length,
|
|
1031
|
+
score: 1
|
|
1032
|
+
});
|
|
1033
|
+
col = idx + 1;
|
|
1034
|
+
}
|
|
1035
|
+
} else {
|
|
1036
|
+
const idx = windowText.indexOf(needle);
|
|
1037
|
+
if (idx !== -1) {
|
|
1038
|
+
const beforeMatch = windowText.slice(0, idx);
|
|
1039
|
+
const linesBeforeEnd = beforeMatch.split("\n");
|
|
1040
|
+
const startCol = linesBeforeEnd[linesBeforeEnd.length - 1].length;
|
|
1041
|
+
const afterMatch = needle.split("\n");
|
|
1042
|
+
const endCol = afterMatch[afterMatch.length - 1].length;
|
|
1043
|
+
if (startCol === 0 || linesBeforeEnd.length === 1) {
|
|
1044
|
+
results.push({
|
|
1045
|
+
text: needle,
|
|
1046
|
+
line: startLine + linesBeforeEnd.length - 1,
|
|
1047
|
+
endLine: startLine + linesBeforeEnd.length - 1 + afterMatch.length - 1,
|
|
1048
|
+
startColumn: startCol,
|
|
1049
|
+
endColumn: endCol,
|
|
1050
|
+
score: 1
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return results;
|
|
1057
|
+
}
|
|
1058
|
+
function normalize(text) {
|
|
1059
|
+
return text.replace(/\s+/g, " ").trim();
|
|
1060
|
+
}
|
|
1061
|
+
function normalizedMatch(lines, needle) {
|
|
1062
|
+
const normNeedle = normalize(needle);
|
|
1063
|
+
if (!normNeedle) return [];
|
|
1064
|
+
const results = [];
|
|
1065
|
+
const needleLineEstimate = needle.split("\n").length;
|
|
1066
|
+
const minWindow = Math.max(1, needleLineEstimate - 1);
|
|
1067
|
+
const maxWindow = Math.min(lines.length - 1, needleLineEstimate + 2);
|
|
1068
|
+
for (let winSize = minWindow; winSize <= maxWindow; winSize++) {
|
|
1069
|
+
for (let startLine = 1; startLine + winSize - 1 < lines.length; startLine++) {
|
|
1070
|
+
const windowLines = lines.slice(startLine, startLine + winSize);
|
|
1071
|
+
const windowText = windowLines.join("\n");
|
|
1072
|
+
const normWindow = normalize(windowText);
|
|
1073
|
+
if (normWindow.includes(normNeedle)) {
|
|
1074
|
+
results.push({
|
|
1075
|
+
text: windowText,
|
|
1076
|
+
line: startLine,
|
|
1077
|
+
endLine: startLine + winSize - 1,
|
|
1078
|
+
startColumn: 0,
|
|
1079
|
+
endColumn: windowLines[windowLines.length - 1].length,
|
|
1080
|
+
score: 0.95
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
return deduplicateCandidates(results);
|
|
1086
|
+
}
|
|
1087
|
+
function tokenize(text) {
|
|
1088
|
+
return text.split(/\s+/).filter((t) => t.length > 0);
|
|
1089
|
+
}
|
|
1090
|
+
function lcsLength(a, b) {
|
|
1091
|
+
const m = a.length;
|
|
1092
|
+
const n = b.length;
|
|
1093
|
+
let prev = new Array(n + 1).fill(0);
|
|
1094
|
+
let curr = new Array(n + 1).fill(0);
|
|
1095
|
+
for (let i = 1; i <= m; i++) {
|
|
1096
|
+
for (let j = 1; j <= n; j++) {
|
|
1097
|
+
if (a[i - 1] === b[j - 1]) {
|
|
1098
|
+
curr[j] = prev[j - 1] + 1;
|
|
1099
|
+
} else {
|
|
1100
|
+
curr[j] = Math.max(prev[j], curr[j - 1]);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
[prev, curr] = [curr, prev];
|
|
1104
|
+
curr.fill(0);
|
|
1105
|
+
}
|
|
1106
|
+
return prev[n];
|
|
1107
|
+
}
|
|
1108
|
+
function tokenLcsScore(a, b) {
|
|
1109
|
+
const tokA = tokenize(a);
|
|
1110
|
+
const tokB = tokenize(b);
|
|
1111
|
+
if (tokA.length === 0 && tokB.length === 0) return 1;
|
|
1112
|
+
if (tokA.length === 0 || tokB.length === 0) return 0;
|
|
1113
|
+
const lcs = lcsLength(tokA, tokB);
|
|
1114
|
+
return lcs / Math.max(tokA.length, tokB.length);
|
|
1115
|
+
}
|
|
1116
|
+
function levenshteinScore(a, b) {
|
|
1117
|
+
if (a.length === 0 && b.length === 0) return 1;
|
|
1118
|
+
const maxLen = Math.max(a.length, b.length);
|
|
1119
|
+
if (maxLen === 0) return 1;
|
|
1120
|
+
const dist = (0, import_fastest_levenshtein.distance)(a, b);
|
|
1121
|
+
return 1 - dist / maxLen;
|
|
1122
|
+
}
|
|
1123
|
+
function combinedScore(needle, candidate) {
|
|
1124
|
+
const tScore = tokenLcsScore(needle, candidate);
|
|
1125
|
+
let lScore;
|
|
1126
|
+
if (needle.length < 500 && candidate.length < 500) {
|
|
1127
|
+
lScore = levenshteinScore(needle, candidate);
|
|
1128
|
+
} else {
|
|
1129
|
+
lScore = tScore;
|
|
1130
|
+
}
|
|
1131
|
+
return tScore * 0.6 + lScore * 0.4;
|
|
1132
|
+
}
|
|
1133
|
+
function fuzzySearch(lines, needle, threshold = 0.6, hintLine) {
|
|
1134
|
+
if (!needle) return [];
|
|
1135
|
+
const needleLines = needle.split("\n");
|
|
1136
|
+
const needleLineCount = needleLines.length;
|
|
1137
|
+
const candidates = [];
|
|
1138
|
+
const minWindow = Math.max(1, Math.floor(needleLineCount * 0.7));
|
|
1139
|
+
const maxWindow = Math.min(
|
|
1140
|
+
lines.length - 1,
|
|
1141
|
+
Math.ceil(needleLineCount * 1.3) + 1
|
|
1142
|
+
);
|
|
1143
|
+
for (let winSize = minWindow; winSize <= maxWindow; winSize++) {
|
|
1144
|
+
for (let startLine = 1; startLine + winSize - 1 < lines.length; startLine++) {
|
|
1145
|
+
const windowLines = lines.slice(startLine, startLine + winSize);
|
|
1146
|
+
const windowText = windowLines.join("\n");
|
|
1147
|
+
const score = combinedScore(needle, windowText);
|
|
1148
|
+
if (score >= threshold) {
|
|
1149
|
+
candidates.push({
|
|
1150
|
+
text: windowText,
|
|
1151
|
+
line: startLine,
|
|
1152
|
+
endLine: startLine + winSize - 1,
|
|
1153
|
+
startColumn: 0,
|
|
1154
|
+
endColumn: windowLines[windowLines.length - 1].length,
|
|
1155
|
+
score
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
if (needleLineCount === 1 && needle.length < 200) {
|
|
1161
|
+
for (let lineNum = 1; lineNum < lines.length; lineNum++) {
|
|
1162
|
+
const line = lines[lineNum];
|
|
1163
|
+
if (!line) continue;
|
|
1164
|
+
const winLen = needle.length;
|
|
1165
|
+
const minWinLen = Math.max(3, Math.floor(winLen * 0.7));
|
|
1166
|
+
const maxWinLen = Math.min(line.length, Math.ceil(winLen * 1.3));
|
|
1167
|
+
for (let len = minWinLen; len <= maxWinLen; len++) {
|
|
1168
|
+
for (let col = 0; col + len <= line.length; col++) {
|
|
1169
|
+
const sub = line.substring(col, col + len);
|
|
1170
|
+
const score = combinedScore(needle, sub);
|
|
1171
|
+
if (score >= threshold) {
|
|
1172
|
+
candidates.push({
|
|
1173
|
+
text: sub,
|
|
1174
|
+
line: lineNum,
|
|
1175
|
+
endLine: lineNum,
|
|
1176
|
+
startColumn: col,
|
|
1177
|
+
endColumn: col + len,
|
|
1178
|
+
score
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
let deduped = deduplicateCandidates(candidates);
|
|
1186
|
+
if (hintLine != null) {
|
|
1187
|
+
deduped = deduped.map((c) => {
|
|
1188
|
+
const dist = Math.abs(c.line - hintLine);
|
|
1189
|
+
const proximityBonus = 0.1 * Math.max(0, 1 - dist / 50);
|
|
1190
|
+
return { ...c, score: Math.min(1, c.score + proximityBonus) };
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
return deduped.sort((a, b) => b.score - a.score);
|
|
1194
|
+
}
|
|
1195
|
+
function deduplicateCandidates(candidates) {
|
|
1196
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1197
|
+
for (const c of candidates) {
|
|
1198
|
+
const key = `${c.line}:${c.startColumn}:${c.endLine}:${c.endColumn}`;
|
|
1199
|
+
const existing = seen.get(key);
|
|
1200
|
+
if (!existing || c.score > existing.score) {
|
|
1201
|
+
seen.set(key, c);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
return Array.from(seen.values());
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// src/lib/git.ts
|
|
1208
|
+
var import_node_child_process = require("node:child_process");
|
|
1209
|
+
var import_node_util = require("node:util");
|
|
1210
|
+
var execFile = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
1211
|
+
var GIT_TIMEOUT = 1e4;
|
|
1212
|
+
var _gitAvailable = null;
|
|
1213
|
+
async function isGitAvailable() {
|
|
1214
|
+
if (_gitAvailable != null) return _gitAvailable;
|
|
1215
|
+
try {
|
|
1216
|
+
await execFile("git", ["--version"], { timeout: GIT_TIMEOUT });
|
|
1217
|
+
_gitAvailable = true;
|
|
1218
|
+
} catch {
|
|
1219
|
+
_gitAvailable = false;
|
|
1220
|
+
}
|
|
1221
|
+
return _gitAvailable;
|
|
1222
|
+
}
|
|
1223
|
+
async function findRepoRoot(cwd) {
|
|
1224
|
+
if (!await isGitAvailable()) return null;
|
|
1225
|
+
try {
|
|
1226
|
+
const { stdout } = await execFile(
|
|
1227
|
+
"git",
|
|
1228
|
+
["rev-parse", "--show-toplevel"],
|
|
1229
|
+
{ cwd: cwd ?? process.cwd(), timeout: GIT_TIMEOUT }
|
|
1230
|
+
);
|
|
1231
|
+
return stdout.trim();
|
|
1232
|
+
} catch {
|
|
1233
|
+
return null;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
async function getCurrentCommit(repoRoot) {
|
|
1237
|
+
if (!await isGitAvailable()) return null;
|
|
1238
|
+
try {
|
|
1239
|
+
const { stdout } = await execFile(
|
|
1240
|
+
"git",
|
|
1241
|
+
["rev-parse", "HEAD"],
|
|
1242
|
+
{ cwd: repoRoot, timeout: GIT_TIMEOUT }
|
|
1243
|
+
);
|
|
1244
|
+
return stdout.trim();
|
|
1245
|
+
} catch {
|
|
1246
|
+
return null;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
async function isStale(commentCommit, repoRoot) {
|
|
1250
|
+
const head = await getCurrentCommit(repoRoot);
|
|
1251
|
+
if (!head) return false;
|
|
1252
|
+
const minLen = Math.min(commentCommit.length, head.length);
|
|
1253
|
+
return commentCommit.slice(0, minLen) !== head.slice(0, minLen);
|
|
1254
|
+
}
|
|
1255
|
+
function parseDiffHunks(diffOutput) {
|
|
1256
|
+
const hunks = [];
|
|
1257
|
+
const lines = diffOutput.split("\n");
|
|
1258
|
+
let current = null;
|
|
1259
|
+
for (const line of lines) {
|
|
1260
|
+
const hunkMatch = line.match(
|
|
1261
|
+
/^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@/
|
|
1262
|
+
);
|
|
1263
|
+
if (hunkMatch) {
|
|
1264
|
+
current = {
|
|
1265
|
+
oldStart: parseInt(hunkMatch[1], 10),
|
|
1266
|
+
oldCount: hunkMatch[2] != null ? parseInt(hunkMatch[2], 10) : 1,
|
|
1267
|
+
newStart: parseInt(hunkMatch[3], 10),
|
|
1268
|
+
newCount: hunkMatch[4] != null ? parseInt(hunkMatch[4], 10) : 1,
|
|
1269
|
+
lines: []
|
|
1270
|
+
};
|
|
1271
|
+
hunks.push(current);
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
if (current && (line.startsWith("+") || line.startsWith("-") || line.startsWith(" "))) {
|
|
1275
|
+
current.lines.push(line);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
return hunks;
|
|
1279
|
+
}
|
|
1280
|
+
async function getDiff(fromCommit, toCommit, filePath, repoRoot) {
|
|
1281
|
+
if (!await isGitAvailable()) return [];
|
|
1282
|
+
try {
|
|
1283
|
+
const { stdout } = await execFile(
|
|
1284
|
+
"git",
|
|
1285
|
+
[
|
|
1286
|
+
"diff",
|
|
1287
|
+
`${fromCommit}..${toCommit}`,
|
|
1288
|
+
"--unified=0",
|
|
1289
|
+
"--no-color",
|
|
1290
|
+
"--",
|
|
1291
|
+
filePath
|
|
1292
|
+
],
|
|
1293
|
+
{ cwd: repoRoot, timeout: GIT_TIMEOUT }
|
|
1294
|
+
);
|
|
1295
|
+
return parseDiffHunks(stdout);
|
|
1296
|
+
} catch {
|
|
1297
|
+
return [];
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
function getLineShift(hunks, originalLine) {
|
|
1301
|
+
let cumulativeShift = 0;
|
|
1302
|
+
for (const hunk of hunks) {
|
|
1303
|
+
const oldEnd = hunk.oldStart + hunk.oldCount - 1;
|
|
1304
|
+
if (hunk.oldStart > originalLine) break;
|
|
1305
|
+
if (originalLine >= hunk.oldStart && originalLine <= oldEnd) {
|
|
1306
|
+
return { shift: cumulativeShift, modified: true };
|
|
1307
|
+
}
|
|
1308
|
+
if (oldEnd < originalLine) {
|
|
1309
|
+
cumulativeShift += hunk.newCount - hunk.oldCount;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
return { shift: cumulativeShift, modified: false };
|
|
1313
|
+
}
|
|
1314
|
+
async function getFileAtCommit(commit, filePath, repoRoot) {
|
|
1315
|
+
if (!await isGitAvailable()) return null;
|
|
1316
|
+
try {
|
|
1317
|
+
const { stdout } = await execFile(
|
|
1318
|
+
"git",
|
|
1319
|
+
["show", `${commit}:${filePath}`],
|
|
1320
|
+
{ cwd: repoRoot, timeout: GIT_TIMEOUT }
|
|
1321
|
+
);
|
|
1322
|
+
return stdout;
|
|
1323
|
+
} catch {
|
|
1324
|
+
return null;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
async function getStagedFiles(repoRoot, pattern) {
|
|
1328
|
+
if (!await isGitAvailable()) return [];
|
|
1329
|
+
try {
|
|
1330
|
+
const args = ["diff", "--cached", "--name-only", "--diff-filter=d"];
|
|
1331
|
+
if (pattern) args.push("--", pattern);
|
|
1332
|
+
const { stdout } = await execFile("git", args, {
|
|
1333
|
+
cwd: repoRoot,
|
|
1334
|
+
timeout: GIT_TIMEOUT
|
|
1335
|
+
});
|
|
1336
|
+
return stdout.trim().split("\n").filter((f) => f.length > 0);
|
|
1337
|
+
} catch {
|
|
1338
|
+
return [];
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
async function detectRenames(fromCommit, toCommit, repoRoot) {
|
|
1342
|
+
if (!await isGitAvailable()) return /* @__PURE__ */ new Map();
|
|
1343
|
+
try {
|
|
1344
|
+
const { stdout } = await execFile(
|
|
1345
|
+
"git",
|
|
1346
|
+
["diff", "--name-status", "-M", `${fromCommit}..${toCommit}`],
|
|
1347
|
+
{ cwd: repoRoot, timeout: GIT_TIMEOUT }
|
|
1348
|
+
);
|
|
1349
|
+
const renames = /* @__PURE__ */ new Map();
|
|
1350
|
+
for (const line of stdout.trim().split("\n")) {
|
|
1351
|
+
const match = line.match(/^R\d*\t(.+)\t(.+)$/);
|
|
1352
|
+
if (match) {
|
|
1353
|
+
renames.set(match[1], match[2]);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
return renames;
|
|
1357
|
+
} catch {
|
|
1358
|
+
return /* @__PURE__ */ new Map();
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// src/lib/reanchor-core.ts
|
|
1363
|
+
var HIGH_THRESHOLD = 0.8;
|
|
1364
|
+
var DEFAULT_THRESHOLD = 0.6;
|
|
1365
|
+
var DEFAULT_PROXIMITY_WINDOW = 5;
|
|
1366
|
+
function toReanchorLines(documentText) {
|
|
1367
|
+
return ["", ...documentText.replace(/\r\n/g, "\n").split("\n")];
|
|
1368
|
+
}
|
|
1369
|
+
function reanchorComment(comment, documentLines, opts = {}) {
|
|
1370
|
+
const threshold = opts.threshold ?? DEFAULT_THRESHOLD;
|
|
1371
|
+
const proximityWindow = opts.proximityWindow ?? DEFAULT_PROXIMITY_WINDOW;
|
|
1372
|
+
const commentId = comment.id;
|
|
1373
|
+
const selectedText = comment.selected_text;
|
|
1374
|
+
if (!selectedText && comment.line == null) {
|
|
1375
|
+
return {
|
|
1376
|
+
commentId,
|
|
1377
|
+
status: "anchored",
|
|
1378
|
+
score: 1,
|
|
1379
|
+
reason: "Document-level comment (no anchor needed)."
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
if (comment.line != null && opts.diffHunks?.length) {
|
|
1383
|
+
const { shift, modified } = getLineShift2(opts.diffHunks, comment.line);
|
|
1384
|
+
if (!selectedText) {
|
|
1385
|
+
const shiftedLine = comment.line + shift;
|
|
1386
|
+
const lineSpan = comment.end_line != null ? comment.end_line - comment.line : 0;
|
|
1387
|
+
const shiftedEndLine = comment.end_line != null ? shiftedLine + lineSpan : void 0;
|
|
1388
|
+
return {
|
|
1389
|
+
commentId,
|
|
1390
|
+
status: shift === 0 ? "anchored" : "shifted",
|
|
1391
|
+
score: 1,
|
|
1392
|
+
newLine: shiftedLine,
|
|
1393
|
+
newEndLine: shiftedEndLine,
|
|
1394
|
+
reason: shift === 0 ? "Line-only comment unchanged (diff confirms position)." : `Line-only comment shifted by ${shift > 0 ? "+" : ""}${shift} line(s) via diff.`
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
if (!modified) {
|
|
1398
|
+
const shiftedLine = comment.line + shift;
|
|
1399
|
+
const lineSpan = comment.end_line != null ? comment.end_line - comment.line : 0;
|
|
1400
|
+
const shiftedEndLine = comment.end_line != null ? shiftedLine + lineSpan : void 0;
|
|
1401
|
+
const textAtShifted = extractText(
|
|
1402
|
+
documentLines,
|
|
1403
|
+
shiftedLine,
|
|
1404
|
+
shiftedEndLine,
|
|
1405
|
+
comment.start_column,
|
|
1406
|
+
comment.end_column
|
|
1407
|
+
);
|
|
1408
|
+
if (textAtShifted === selectedText) {
|
|
1409
|
+
return {
|
|
1410
|
+
commentId,
|
|
1411
|
+
status: shift === 0 ? "anchored" : "shifted",
|
|
1412
|
+
score: 1,
|
|
1413
|
+
newLine: shiftedLine,
|
|
1414
|
+
newEndLine: shiftedEndLine,
|
|
1415
|
+
reason: shift === 0 ? "Diff confirms text unchanged at original position." : `Diff shifted by ${shift > 0 ? "+" : ""}${shift} line(s).`
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
if (selectedText) {
|
|
1421
|
+
const exactCandidates = exactMatch(documentLines, selectedText);
|
|
1422
|
+
let chosen;
|
|
1423
|
+
let chosenReason = "";
|
|
1424
|
+
if (exactCandidates.length === 1) {
|
|
1425
|
+
chosen = exactCandidates[0];
|
|
1426
|
+
chosenReason = "Exact text match (unique).";
|
|
1427
|
+
} else if (exactCandidates.length > 1 && comment.line != null) {
|
|
1428
|
+
chosen = closestToLine(exactCandidates, comment.line);
|
|
1429
|
+
chosenReason = `Exact text match (${exactCandidates.length} occurrences; chose nearest to original line ${comment.line}).`;
|
|
1430
|
+
}
|
|
1431
|
+
if (chosen) {
|
|
1432
|
+
if (isImplausibleExactRelocation(comment, chosen, documentLines, proximityWindow)) {
|
|
1433
|
+
const textAtOrigin = extractText(
|
|
1434
|
+
documentLines,
|
|
1435
|
+
comment.line,
|
|
1436
|
+
comment.end_line,
|
|
1437
|
+
comment.start_column,
|
|
1438
|
+
comment.end_column
|
|
1439
|
+
);
|
|
1440
|
+
return {
|
|
1441
|
+
commentId,
|
|
1442
|
+
status: "fuzzy",
|
|
1443
|
+
score: 0.5,
|
|
1444
|
+
newLine: comment.line,
|
|
1445
|
+
newEndLine: comment.end_line,
|
|
1446
|
+
newStartColumn: comment.start_column,
|
|
1447
|
+
newEndColumn: comment.end_column,
|
|
1448
|
+
anchoredText: textAtOrigin ?? void 0,
|
|
1449
|
+
previousSelectedText: selectedText,
|
|
1450
|
+
reason: `Lone exact match at line ${chosen.line} is beyond the proximity window (\xB1${proximityWindow}) of original line ${comment.line} and the text at the original position changed; kept at original position, needs re-anchoring.`
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
return {
|
|
1454
|
+
commentId,
|
|
1455
|
+
status: "anchored",
|
|
1456
|
+
score: 1,
|
|
1457
|
+
newLine: chosen.line,
|
|
1458
|
+
newEndLine: chosen.endLine,
|
|
1459
|
+
newStartColumn: chosen.startColumn,
|
|
1460
|
+
newEndColumn: chosen.endColumn,
|
|
1461
|
+
reason: chosenReason
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
const normCandidates = normalizedMatch(documentLines, selectedText);
|
|
1465
|
+
if (normCandidates.length === 1) {
|
|
1466
|
+
const candidate = normCandidates[0];
|
|
1467
|
+
return {
|
|
1468
|
+
commentId,
|
|
1469
|
+
status: "fuzzy",
|
|
1470
|
+
score: candidate.score,
|
|
1471
|
+
newLine: candidate.line,
|
|
1472
|
+
newEndLine: candidate.endLine,
|
|
1473
|
+
newStartColumn: candidate.startColumn,
|
|
1474
|
+
newEndColumn: candidate.endColumn,
|
|
1475
|
+
anchoredText: candidate.text,
|
|
1476
|
+
previousSelectedText: selectedText,
|
|
1477
|
+
reason: "Normalized whitespace match."
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
const fuzzyCandidates = fuzzySearch(
|
|
1481
|
+
documentLines,
|
|
1482
|
+
selectedText,
|
|
1483
|
+
HIGH_THRESHOLD,
|
|
1484
|
+
comment.line
|
|
1485
|
+
);
|
|
1486
|
+
if (fuzzyCandidates.length === 1 || fuzzyCandidates.length > 0 && fuzzyCandidates[0].score >= HIGH_THRESHOLD) {
|
|
1487
|
+
const best = fuzzyCandidates.length === 1 ? fuzzyCandidates[0] : closestToLine(fuzzyCandidates, comment.line ?? 1);
|
|
1488
|
+
return {
|
|
1489
|
+
commentId,
|
|
1490
|
+
status: "fuzzy",
|
|
1491
|
+
score: best.score,
|
|
1492
|
+
newLine: best.line,
|
|
1493
|
+
newEndLine: best.endLine,
|
|
1494
|
+
newStartColumn: best.startColumn,
|
|
1495
|
+
newEndColumn: best.endColumn,
|
|
1496
|
+
anchoredText: best.text,
|
|
1497
|
+
previousSelectedText: selectedText,
|
|
1498
|
+
reason: `High-confidence fuzzy match (score ${best.score.toFixed(3)}).`
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
if (comment.line != null) {
|
|
1503
|
+
const lineIdx = comment.line;
|
|
1504
|
+
if (lineIdx > 0 && lineIdx < documentLines.length) {
|
|
1505
|
+
const qualifier = opts.commitIsStale ? " (commit is stale \u2014 line may have shifted)" : "";
|
|
1506
|
+
if (selectedText) {
|
|
1507
|
+
const lineText = documentLines[lineIdx];
|
|
1508
|
+
const candidates = fuzzySearch(["", lineText], selectedText, DEFAULT_THRESHOLD);
|
|
1509
|
+
if (candidates.length > 0) {
|
|
1510
|
+
return {
|
|
1511
|
+
commentId,
|
|
1512
|
+
status: "fuzzy",
|
|
1513
|
+
score: candidates[0].score,
|
|
1514
|
+
newLine: comment.line,
|
|
1515
|
+
newEndLine: comment.end_line,
|
|
1516
|
+
anchoredText: candidates[0].text,
|
|
1517
|
+
previousSelectedText: selectedText,
|
|
1518
|
+
reason: `Line-fallback with fuzzy text match (score ${candidates[0].score.toFixed(3)})${qualifier}.`
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
const isLineOnly = !selectedText;
|
|
1523
|
+
return {
|
|
1524
|
+
commentId,
|
|
1525
|
+
status: isLineOnly ? "anchored" : opts.commitIsStale ? "ambiguous" : "anchored",
|
|
1526
|
+
score: isLineOnly ? 1 : opts.commitIsStale ? 0.5 : 0.8,
|
|
1527
|
+
newLine: comment.line,
|
|
1528
|
+
newEndLine: comment.end_line,
|
|
1529
|
+
reason: isLineOnly ? "Line-only comment (no selected_text to verify)." : `Line/column fallback${qualifier}.`
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
if (selectedText) {
|
|
1534
|
+
const lowCandidates = fuzzySearch(
|
|
1535
|
+
documentLines,
|
|
1536
|
+
selectedText,
|
|
1537
|
+
threshold,
|
|
1538
|
+
comment.line
|
|
1539
|
+
);
|
|
1540
|
+
if (lowCandidates.length === 1) {
|
|
1541
|
+
const candidate = lowCandidates[0];
|
|
1542
|
+
return {
|
|
1543
|
+
commentId,
|
|
1544
|
+
status: "fuzzy",
|
|
1545
|
+
score: candidate.score,
|
|
1546
|
+
newLine: candidate.line,
|
|
1547
|
+
newEndLine: candidate.endLine,
|
|
1548
|
+
newStartColumn: candidate.startColumn,
|
|
1549
|
+
newEndColumn: candidate.endColumn,
|
|
1550
|
+
anchoredText: candidate.text,
|
|
1551
|
+
previousSelectedText: selectedText,
|
|
1552
|
+
reason: `Low-threshold fuzzy match (score ${candidate.score.toFixed(3)}).`
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
if (lowCandidates.length > 1) {
|
|
1556
|
+
const best = lowCandidates[0];
|
|
1557
|
+
return {
|
|
1558
|
+
commentId,
|
|
1559
|
+
status: "ambiguous",
|
|
1560
|
+
score: best.score,
|
|
1561
|
+
newLine: best.line,
|
|
1562
|
+
newEndLine: best.endLine,
|
|
1563
|
+
reason: `Ambiguous: ${lowCandidates.length} fuzzy matches (best score ${best.score.toFixed(3)}).`
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
return {
|
|
1568
|
+
commentId,
|
|
1569
|
+
status: "orphaned",
|
|
1570
|
+
score: 0,
|
|
1571
|
+
reason: "No match found. Comment is orphaned."
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
function reanchorDocumentLines(doc, documentLines, opts = {}) {
|
|
1575
|
+
return doc.comments.map((comment) => reanchorComment(comment, documentLines, opts));
|
|
1576
|
+
}
|
|
1577
|
+
function reanchorDocumentText(doc, documentText, opts = {}) {
|
|
1578
|
+
return reanchorDocumentLines(doc, toReanchorLines(documentText), opts);
|
|
1579
|
+
}
|
|
1580
|
+
function resolveAnchor(comment, documentText, opts = {}) {
|
|
1581
|
+
const normalizedText = documentText.replace(/\r\n/g, "\n");
|
|
1582
|
+
const documentLines = toReanchorLines(documentText);
|
|
1583
|
+
const result = reanchorComment(comment, documentLines, opts);
|
|
1584
|
+
if (result.status === "orphaned") {
|
|
1585
|
+
return {
|
|
1586
|
+
status: "orphaned",
|
|
1587
|
+
score: result.score,
|
|
1588
|
+
reason: result.reason
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
const line = result.newLine ?? comment.line;
|
|
1592
|
+
if (line == null) {
|
|
1593
|
+
return {
|
|
1594
|
+
status: result.status,
|
|
1595
|
+
score: result.score,
|
|
1596
|
+
reason: result.reason
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
const rawLines = normalizedText.split("\n");
|
|
1600
|
+
const lineStarts = computeLineStarts(rawLines);
|
|
1601
|
+
const endLine = result.newEndLine ?? comment.end_line ?? line;
|
|
1602
|
+
const startColumn = result.newStartColumn ?? comment.start_column ?? 0;
|
|
1603
|
+
const endColumn = result.newEndColumn ?? comment.end_column;
|
|
1604
|
+
const from = offsetFor(lineStarts, rawLines, line, startColumn);
|
|
1605
|
+
const selectedText = comment.selected_text?.replace(/\r\n/g, "\n");
|
|
1606
|
+
const to = endColumn != null ? offsetFor(lineStarts, rawLines, endLine, endColumn) : selectedText ? from + selectedText.length : offsetFor(lineStarts, rawLines, endLine, rawLines[endLine - 1]?.length ?? 0);
|
|
1607
|
+
return {
|
|
1608
|
+
status: result.status,
|
|
1609
|
+
score: result.score,
|
|
1610
|
+
from,
|
|
1611
|
+
to,
|
|
1612
|
+
line,
|
|
1613
|
+
endLine,
|
|
1614
|
+
startColumn,
|
|
1615
|
+
endColumn: endColumn ?? columnForOffset(lineStarts, rawLines, endLine, to),
|
|
1616
|
+
reason: result.reason
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
function computeLineStarts(lines) {
|
|
1620
|
+
const starts = [0];
|
|
1621
|
+
let offset = 0;
|
|
1622
|
+
for (const line of lines) {
|
|
1623
|
+
starts.push(offset);
|
|
1624
|
+
offset += line.length + 1;
|
|
1625
|
+
}
|
|
1626
|
+
return starts;
|
|
1627
|
+
}
|
|
1628
|
+
function offsetFor(lineStarts, lines, line, column) {
|
|
1629
|
+
const start = lineStarts[line] ?? 0;
|
|
1630
|
+
const maxColumn = lines[line - 1]?.length ?? 0;
|
|
1631
|
+
return start + Math.max(0, Math.min(column, maxColumn));
|
|
1632
|
+
}
|
|
1633
|
+
function columnForOffset(lineStarts, lines, line, offset) {
|
|
1634
|
+
const start = lineStarts[line] ?? 0;
|
|
1635
|
+
const maxColumn = lines[line - 1]?.length ?? 0;
|
|
1636
|
+
return Math.max(0, Math.min(offset - start, maxColumn));
|
|
1637
|
+
}
|
|
1638
|
+
function applyReanchorResults(doc, results, opts = {}) {
|
|
1639
|
+
let changed = 0;
|
|
1640
|
+
const resultMap = new Map(results.map((result) => [result.commentId, result]));
|
|
1641
|
+
for (const comment of doc.comments) {
|
|
1642
|
+
const result = resultMap.get(comment.id);
|
|
1643
|
+
if (!result) continue;
|
|
1644
|
+
let isChanged = false;
|
|
1645
|
+
if (result.newLine != null && result.newLine !== comment.line) {
|
|
1646
|
+
comment.line = result.newLine;
|
|
1647
|
+
isChanged = true;
|
|
1648
|
+
}
|
|
1649
|
+
if (result.newEndLine != null && result.newEndLine !== comment.end_line) {
|
|
1650
|
+
comment.end_line = result.newEndLine;
|
|
1651
|
+
isChanged = true;
|
|
1652
|
+
}
|
|
1653
|
+
if (result.newStartColumn != null && result.newStartColumn !== comment.start_column) {
|
|
1654
|
+
comment.start_column = result.newStartColumn;
|
|
1655
|
+
isChanged = true;
|
|
1656
|
+
}
|
|
1657
|
+
if (result.newEndColumn != null && result.newEndColumn !== comment.end_column) {
|
|
1658
|
+
comment.end_column = result.newEndColumn;
|
|
1659
|
+
isChanged = true;
|
|
1660
|
+
}
|
|
1661
|
+
if (result.anchoredText != null && result.anchoredText !== comment.selected_text) {
|
|
1662
|
+
if (opts.updateText) {
|
|
1663
|
+
comment.selected_text = result.anchoredText;
|
|
1664
|
+
delete comment.anchored_text;
|
|
1665
|
+
} else {
|
|
1666
|
+
comment.anchored_text = result.anchoredText;
|
|
1667
|
+
}
|
|
1668
|
+
isChanged = true;
|
|
1669
|
+
} else if (result.anchoredText != null && result.anchoredText === comment.selected_text) {
|
|
1670
|
+
if (comment.anchored_text) {
|
|
1671
|
+
delete comment.anchored_text;
|
|
1672
|
+
isChanged = true;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
if (isChanged || result.status !== "anchored") {
|
|
1676
|
+
comment.x_reanchor_status = result.status;
|
|
1677
|
+
comment.x_reanchor_score = result.score;
|
|
1678
|
+
}
|
|
1679
|
+
if (opts.force && opts.headCommit && (result.status === "anchored" || result.status === "shifted") && result.score >= HIGH_THRESHOLD) {
|
|
1680
|
+
comment.commit = opts.headCommit;
|
|
1681
|
+
delete comment.x_reanchor_status;
|
|
1682
|
+
delete comment.x_reanchor_score;
|
|
1683
|
+
if (comment.anchored_text && comment.anchored_text === comment.selected_text) {
|
|
1684
|
+
delete comment.anchored_text;
|
|
1685
|
+
}
|
|
1686
|
+
isChanged = true;
|
|
1687
|
+
}
|
|
1688
|
+
if (isChanged) {
|
|
1689
|
+
changed += 1;
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
return changed;
|
|
1693
|
+
}
|
|
1694
|
+
function extractText(lines, line, endLine, startColumn, endColumn) {
|
|
1695
|
+
const startIdx = line;
|
|
1696
|
+
const endIdx = endLine ?? line;
|
|
1697
|
+
if (startIdx < 1 || endIdx >= lines.length) return null;
|
|
1698
|
+
if (startIdx === endIdx) {
|
|
1699
|
+
const text = lines[startIdx];
|
|
1700
|
+
if (startColumn != null && endColumn != null) {
|
|
1701
|
+
return text.slice(startColumn, endColumn);
|
|
1702
|
+
}
|
|
1703
|
+
return text;
|
|
1704
|
+
}
|
|
1705
|
+
const result = [];
|
|
1706
|
+
for (let index = startIdx; index <= endIdx; index += 1) {
|
|
1707
|
+
let currentLine = lines[index];
|
|
1708
|
+
if (index === startIdx && startColumn != null) currentLine = currentLine.slice(startColumn);
|
|
1709
|
+
if (index === endIdx && endColumn != null) currentLine = currentLine.slice(0, endColumn);
|
|
1710
|
+
result.push(currentLine);
|
|
1711
|
+
}
|
|
1712
|
+
return result.join("\n");
|
|
1713
|
+
}
|
|
1714
|
+
function closestToLine(candidates, targetLine) {
|
|
1715
|
+
return candidates.reduce(
|
|
1716
|
+
(best, candidate) => Math.abs(candidate.line - targetLine) < Math.abs(best.line - targetLine) ? candidate : best
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
function isImplausibleExactRelocation(comment, candidate, lines, proximityWindow) {
|
|
1720
|
+
if (comment.line == null) return false;
|
|
1721
|
+
if (comment.line <= 0 || comment.line >= lines.length) return false;
|
|
1722
|
+
if (Math.abs(candidate.line - comment.line) <= proximityWindow) return false;
|
|
1723
|
+
const textAtOrigin = extractText(
|
|
1724
|
+
lines,
|
|
1725
|
+
comment.line,
|
|
1726
|
+
comment.end_line,
|
|
1727
|
+
comment.start_column,
|
|
1728
|
+
comment.end_column
|
|
1729
|
+
);
|
|
1730
|
+
if (textAtOrigin === comment.selected_text) return false;
|
|
1731
|
+
return true;
|
|
1732
|
+
}
|
|
1733
|
+
function getLineShift2(diffHunks, line) {
|
|
1734
|
+
let shift = 0;
|
|
1735
|
+
let modified = false;
|
|
1736
|
+
for (const hunk of diffHunks) {
|
|
1737
|
+
const oldStart = hunk.oldStart;
|
|
1738
|
+
const oldEnd = hunk.oldStart + Math.max(hunk.oldCount, 1) - 1;
|
|
1739
|
+
if (line >= oldStart && line <= oldEnd && hunk.oldCount > 0) {
|
|
1740
|
+
modified = true;
|
|
1741
|
+
}
|
|
1742
|
+
if (line > oldEnd || hunk.oldCount === 0 && line >= oldStart) {
|
|
1743
|
+
shift += hunk.newCount - hunk.oldCount;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
return { shift, modified };
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
// src/lib/reanchor.ts
|
|
1750
|
+
init_parser();
|
|
1751
|
+
init_parser();
|
|
1752
|
+
var import_node_path6 = __toESM(require("node:path"), 1);
|
|
1753
|
+
async function reanchorDocument(doc, documentLines, opts = {}) {
|
|
1754
|
+
const results = [];
|
|
1755
|
+
const threshold = opts.threshold ?? DEFAULT_THRESHOLD;
|
|
1756
|
+
const proximityWindow = opts.proximityWindow;
|
|
1757
|
+
let diffHunks;
|
|
1758
|
+
let commitIsStale = false;
|
|
1759
|
+
if (!opts.noGit && await isGitAvailable()) {
|
|
1760
|
+
const repoRoot = opts.repoRoot ?? await findRepoRoot(opts.cwd);
|
|
1761
|
+
if (repoRoot && opts.documentPath) {
|
|
1762
|
+
const relPath = import_node_path6.default.relative(repoRoot, opts.documentPath);
|
|
1763
|
+
const head = await getCurrentCommit(repoRoot);
|
|
1764
|
+
const globalFrom = opts.fromCommit;
|
|
1765
|
+
for (const comment of doc.comments) {
|
|
1766
|
+
const commentCommit = globalFrom ?? comment.commit;
|
|
1767
|
+
if (commentCommit && head && commentCommit !== head) {
|
|
1768
|
+
commitIsStale = true;
|
|
1769
|
+
const hunks = await getDiff(commentCommit, head, relPath, repoRoot);
|
|
1770
|
+
const result = reanchorComment(comment, documentLines, {
|
|
1771
|
+
diffHunks: hunks,
|
|
1772
|
+
threshold,
|
|
1773
|
+
commitIsStale: true,
|
|
1774
|
+
proximityWindow
|
|
1775
|
+
});
|
|
1776
|
+
results.push(result);
|
|
1777
|
+
continue;
|
|
1778
|
+
}
|
|
1779
|
+
results.push(
|
|
1780
|
+
reanchorComment(comment, documentLines, { threshold, commitIsStale: false, proximityWindow })
|
|
1781
|
+
);
|
|
1782
|
+
}
|
|
1783
|
+
return results;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
return reanchorDocumentLines(doc, documentLines, { threshold, proximityWindow });
|
|
1787
|
+
}
|
|
1788
|
+
async function reanchorFile(sidecarPath, opts = {}) {
|
|
1789
|
+
const doc = await parseSidecar(sidecarPath);
|
|
1790
|
+
const docPath = sidecarToDocument(sidecarPath);
|
|
1791
|
+
const documentLines = await readDocumentLines(docPath);
|
|
1792
|
+
const repoRoot = !opts.noGit ? await findRepoRoot(opts.cwd) : null;
|
|
1793
|
+
const headCommit = repoRoot ? await getCurrentCommit(repoRoot) : void 0;
|
|
1794
|
+
const results = await reanchorDocument(doc, documentLines, {
|
|
1795
|
+
...opts,
|
|
1796
|
+
documentPath: docPath,
|
|
1797
|
+
repoRoot: repoRoot ?? void 0
|
|
1798
|
+
});
|
|
1799
|
+
let changed = 0;
|
|
1800
|
+
let written = false;
|
|
1801
|
+
if (!opts.dryRun) {
|
|
1802
|
+
changed = applyReanchorResults(doc, results, {
|
|
1803
|
+
updateText: opts.updateText,
|
|
1804
|
+
force: opts.force,
|
|
1805
|
+
headCommit: headCommit ?? void 0
|
|
1806
|
+
});
|
|
1807
|
+
if (changed > 0 || opts.autoUpdate) {
|
|
1808
|
+
await writeSidecar(sidecarPath, doc);
|
|
1809
|
+
written = true;
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
return { results, changed, written };
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
// src/lib/identity.ts
|
|
1816
|
+
var import_uuid = require("uuid");
|
|
1817
|
+
function formatAuthor(name, handle) {
|
|
1818
|
+
const trimmedName = name.trim();
|
|
1819
|
+
const trimmedHandle = handle?.trim();
|
|
1820
|
+
return trimmedHandle ? `${trimmedName} (${trimmedHandle})` : trimmedName;
|
|
1821
|
+
}
|
|
1822
|
+
function parseAuthor(author) {
|
|
1823
|
+
const trimmed = author.trim();
|
|
1824
|
+
const match = /^(.*?)\s*\(([^()]*)\)\s*$/.exec(trimmed);
|
|
1825
|
+
if (!match) return { name: trimmed };
|
|
1826
|
+
const name = match[1].trim();
|
|
1827
|
+
const handle = match[2].trim();
|
|
1828
|
+
return handle ? { name, handle } : { name };
|
|
1829
|
+
}
|
|
1830
|
+
function newCommentId() {
|
|
1831
|
+
return (0, import_uuid.v4)();
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
// src/lib/comments.ts
|
|
1835
|
+
function isPlainObject(value) {
|
|
1836
|
+
if (value === null || typeof value !== "object") return false;
|
|
1837
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1838
|
+
return prototype === Object.prototype || prototype === null;
|
|
1839
|
+
}
|
|
1840
|
+
function isCommentExtensionValue(value) {
|
|
1841
|
+
if (value == null) return true;
|
|
1842
|
+
switch (typeof value) {
|
|
1843
|
+
case "boolean":
|
|
1844
|
+
case "string":
|
|
1845
|
+
return true;
|
|
1846
|
+
case "number":
|
|
1847
|
+
return Number.isFinite(value);
|
|
1848
|
+
case "object": {
|
|
1849
|
+
if (Array.isArray(value)) {
|
|
1850
|
+
return value.every((item) => isCommentExtensionValue(item));
|
|
1851
|
+
}
|
|
1852
|
+
if (!isPlainObject(value)) {
|
|
1853
|
+
return false;
|
|
1854
|
+
}
|
|
1855
|
+
return Object.values(value).every((item) => isCommentExtensionValue(item));
|
|
1856
|
+
}
|
|
1857
|
+
default:
|
|
1858
|
+
return false;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
function normalizeCommentExtensions(extensions) {
|
|
1862
|
+
if (!extensions) return {};
|
|
1863
|
+
const normalizedEntries = [];
|
|
1864
|
+
for (const [key, value] of Object.entries(extensions)) {
|
|
1865
|
+
if (!key.startsWith("x_")) {
|
|
1866
|
+
throw new Error(`Comment extension key '${key}' must start with 'x_'.`);
|
|
1867
|
+
}
|
|
1868
|
+
if (!isCommentExtensionValue(value)) {
|
|
1869
|
+
throw new Error(
|
|
1870
|
+
`Comment extension '${key}' must be JSON-serializable (null, boolean, finite number, string, array, or plain object).`
|
|
1871
|
+
);
|
|
1872
|
+
}
|
|
1873
|
+
normalizedEntries.push([key, value]);
|
|
1874
|
+
}
|
|
1875
|
+
return Object.fromEntries(normalizedEntries);
|
|
1876
|
+
}
|
|
1877
|
+
async function addComment(doc, opts, repoRoot) {
|
|
1878
|
+
const id = opts.id ?? newCommentId();
|
|
1879
|
+
const timestamp = opts.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1880
|
+
let commit = opts.commit;
|
|
1881
|
+
if (!commit && repoRoot && await isGitAvailable()) {
|
|
1882
|
+
commit = await getCurrentCommit(repoRoot) ?? void 0;
|
|
1883
|
+
}
|
|
1884
|
+
const comment = {
|
|
1885
|
+
id,
|
|
1886
|
+
author: opts.author,
|
|
1887
|
+
timestamp,
|
|
1888
|
+
text: opts.text,
|
|
1889
|
+
resolved: false
|
|
1890
|
+
};
|
|
1891
|
+
if (opts.line != null) comment.line = opts.line;
|
|
1892
|
+
if (opts.end_line != null) comment.end_line = opts.end_line;
|
|
1893
|
+
if (opts.start_column != null) comment.start_column = opts.start_column;
|
|
1894
|
+
if (opts.end_column != null) comment.end_column = opts.end_column;
|
|
1895
|
+
if (opts.type) comment.type = opts.type;
|
|
1896
|
+
if (opts.severity) comment.severity = opts.severity;
|
|
1897
|
+
if (opts.reply_to) comment.reply_to = opts.reply_to;
|
|
1898
|
+
if (commit) comment.commit = commit;
|
|
1899
|
+
Object.assign(comment, normalizeCommentExtensions(opts.extensions));
|
|
1900
|
+
doc.comments.push(comment);
|
|
1901
|
+
return comment;
|
|
1902
|
+
}
|
|
1903
|
+
function populateSelectedText(comment, documentLines) {
|
|
1904
|
+
if (comment.selected_text) return;
|
|
1905
|
+
if (comment.line == null) return;
|
|
1906
|
+
const startIdx = comment.line - 1;
|
|
1907
|
+
const endIdx = (comment.end_line ?? comment.line) - 1;
|
|
1908
|
+
if (startIdx < 0 || endIdx >= documentLines.length) return;
|
|
1909
|
+
if (startIdx === endIdx) {
|
|
1910
|
+
let line = documentLines[startIdx];
|
|
1911
|
+
if (comment.start_column != null && comment.end_column != null) {
|
|
1912
|
+
line = line.slice(comment.start_column, comment.end_column);
|
|
1913
|
+
}
|
|
1914
|
+
comment.selected_text = line;
|
|
1915
|
+
} else {
|
|
1916
|
+
const lines = [];
|
|
1917
|
+
for (let i = startIdx; i <= endIdx; i++) {
|
|
1918
|
+
let l = documentLines[i];
|
|
1919
|
+
if (i === startIdx && comment.start_column != null) {
|
|
1920
|
+
l = l.slice(comment.start_column);
|
|
1921
|
+
}
|
|
1922
|
+
if (i === endIdx && comment.end_column != null) {
|
|
1923
|
+
l = l.slice(0, comment.end_column);
|
|
1924
|
+
}
|
|
1925
|
+
lines.push(l);
|
|
1926
|
+
}
|
|
1927
|
+
comment.selected_text = lines.join("\n");
|
|
1928
|
+
}
|
|
1929
|
+
if (comment.selected_text) {
|
|
1930
|
+
comment.selected_text_hash = computeHash(comment.selected_text);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
function editComment(doc, commentId, opts) {
|
|
1934
|
+
const comment = doc.comments.find((entry) => entry.id === commentId);
|
|
1935
|
+
if (!comment) {
|
|
1936
|
+
throw new Error(`Unknown comment '${commentId}'.`);
|
|
1937
|
+
}
|
|
1938
|
+
if (opts.actor && comment.author !== opts.actor) {
|
|
1939
|
+
throw new Error("Only the comment author can edit this comment.");
|
|
1940
|
+
}
|
|
1941
|
+
if (opts.text.trim().length === 0) {
|
|
1942
|
+
throw new Error("Comment text cannot be empty.");
|
|
1943
|
+
}
|
|
1944
|
+
comment.text = opts.text;
|
|
1945
|
+
return comment;
|
|
1946
|
+
}
|
|
1947
|
+
function resolveComment(doc, commentId, cascade = false) {
|
|
1948
|
+
const comment = doc.comments.find((c) => c.id === commentId);
|
|
1949
|
+
if (!comment) return false;
|
|
1950
|
+
comment.resolved = true;
|
|
1951
|
+
if (cascade) {
|
|
1952
|
+
for (const c of doc.comments) {
|
|
1953
|
+
if (c.reply_to === commentId) {
|
|
1954
|
+
c.resolved = true;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
return true;
|
|
1959
|
+
}
|
|
1960
|
+
function unresolveComment(doc, commentId) {
|
|
1961
|
+
const comment = doc.comments.find((c) => c.id === commentId);
|
|
1962
|
+
if (!comment) return false;
|
|
1963
|
+
comment.resolved = false;
|
|
1964
|
+
return true;
|
|
1965
|
+
}
|
|
1966
|
+
var ANCHOR_FIELDS = [
|
|
1967
|
+
"line",
|
|
1968
|
+
"end_line",
|
|
1969
|
+
"start_column",
|
|
1970
|
+
"end_column",
|
|
1971
|
+
"selected_text",
|
|
1972
|
+
"selected_text_hash",
|
|
1973
|
+
"anchored_text",
|
|
1974
|
+
"commit"
|
|
1975
|
+
];
|
|
1976
|
+
function removeComment(doc, commentId, opts) {
|
|
1977
|
+
const comment = doc.comments.find((c) => c.id === commentId);
|
|
1978
|
+
if (!comment) return false;
|
|
1979
|
+
if (opts?.cascade) {
|
|
1980
|
+
doc.comments = doc.comments.filter(
|
|
1981
|
+
(c) => c.id === commentId || c.reply_to !== commentId
|
|
1982
|
+
);
|
|
1983
|
+
} else {
|
|
1984
|
+
for (const c of doc.comments) {
|
|
1985
|
+
if (c.reply_to !== commentId) continue;
|
|
1986
|
+
for (const field of ANCHOR_FIELDS) {
|
|
1987
|
+
if (c[field] == null && comment[field] != null) {
|
|
1988
|
+
c[field] = comment[field];
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
if (comment.reply_to) {
|
|
1992
|
+
c.reply_to = comment.reply_to;
|
|
1993
|
+
} else {
|
|
1994
|
+
delete c.reply_to;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
const idx = doc.comments.findIndex((c) => c.id === commentId);
|
|
1999
|
+
if (idx !== -1) doc.comments.splice(idx, 1);
|
|
2000
|
+
return true;
|
|
2001
|
+
}
|
|
2002
|
+
function filterComments(comments, filter) {
|
|
2003
|
+
return comments.filter((c) => {
|
|
2004
|
+
if (filter.open === true && c.resolved) return false;
|
|
2005
|
+
if (filter.resolved === true && !c.resolved) return false;
|
|
2006
|
+
if (filter.author && c.author !== filter.author) return false;
|
|
2007
|
+
if (filter.type && c.type !== filter.type) return false;
|
|
2008
|
+
if (filter.severity && c.severity !== filter.severity) return false;
|
|
2009
|
+
if (filter.orphaned === true && c.x_reanchor_status !== "orphaned") return false;
|
|
2010
|
+
if (filter.orphaned === false && c.x_reanchor_status === "orphaned") return false;
|
|
2011
|
+
return true;
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
2014
|
+
function getThreads(comments) {
|
|
2015
|
+
const threads = /* @__PURE__ */ new Map();
|
|
2016
|
+
const replyMap = /* @__PURE__ */ new Map();
|
|
2017
|
+
for (const c of comments) {
|
|
2018
|
+
if (!c.reply_to) {
|
|
2019
|
+
threads.set(c.id, [c]);
|
|
2020
|
+
} else {
|
|
2021
|
+
replyMap.set(c.id, c.reply_to);
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
function findRoot(id) {
|
|
2025
|
+
const parent = replyMap.get(id);
|
|
2026
|
+
if (!parent) return id;
|
|
2027
|
+
return findRoot(parent);
|
|
2028
|
+
}
|
|
2029
|
+
for (const c of comments) {
|
|
2030
|
+
if (c.reply_to) {
|
|
2031
|
+
const rootId = findRoot(c.id);
|
|
2032
|
+
if (!threads.has(rootId)) {
|
|
2033
|
+
threads.set(rootId, []);
|
|
2034
|
+
}
|
|
2035
|
+
threads.get(rootId).push(c);
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
return threads;
|
|
2039
|
+
}
|
|
2040
|
+
function summarize(comments) {
|
|
2041
|
+
const summary = {
|
|
2042
|
+
total: comments.length,
|
|
2043
|
+
open: 0,
|
|
2044
|
+
resolved: 0,
|
|
2045
|
+
orphaned: 0,
|
|
2046
|
+
threads: 0,
|
|
2047
|
+
byType: {},
|
|
2048
|
+
bySeverity: {}
|
|
2049
|
+
};
|
|
2050
|
+
const roots = /* @__PURE__ */ new Set();
|
|
2051
|
+
for (const c of comments) {
|
|
2052
|
+
if (c.resolved) summary.resolved++;
|
|
2053
|
+
else summary.open++;
|
|
2054
|
+
if (c.x_reanchor_status === "orphaned") summary.orphaned++;
|
|
2055
|
+
if (c.type) {
|
|
2056
|
+
summary.byType[c.type] = (summary.byType[c.type] ?? 0) + 1;
|
|
2057
|
+
}
|
|
2058
|
+
if (c.severity) {
|
|
2059
|
+
summary.bySeverity[c.severity] = (summary.bySeverity[c.severity] ?? 0) + 1;
|
|
2060
|
+
}
|
|
2061
|
+
if (!c.reply_to) roots.add(c.id);
|
|
2062
|
+
}
|
|
2063
|
+
summary.threads = roots.size;
|
|
2064
|
+
return summary;
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
// src/index.ts
|
|
2068
|
+
var findWorkspaceRoot2 = findWorkspaceRoot;
|
|
2069
|
+
var loadConfig2 = loadConfig;
|
|
2070
|
+
var discoverSidecar3 = discoverSidecar;
|
|
2071
|
+
var sidecarToDocument2 = sidecarToDocument;
|
|
2072
|
+
var discoverAllSidecars2 = discoverAllSidecars;
|
|
2073
|
+
var resolveSidecarPaths2 = resolveSidecarPaths;
|
|
2074
|
+
var parseSidecar2 = parseSidecar;
|
|
2075
|
+
var parseSidecarContent2 = parseSidecarContent;
|
|
2076
|
+
var parseSidecarLenient2 = parseSidecarLenient;
|
|
2077
|
+
var parseSidecarContentLenient2 = parseSidecarContentLenient;
|
|
2078
|
+
var readDocumentLines2 = readDocumentLines;
|
|
2079
|
+
var computeHash2 = computeHash;
|
|
2080
|
+
var syncHash2 = syncHash;
|
|
2081
|
+
var toYaml2 = toYaml;
|
|
2082
|
+
var toJson2 = toJson;
|
|
2083
|
+
var writeSidecar2 = writeSidecar;
|
|
2084
|
+
var validate2 = validate;
|
|
2085
|
+
var validateFile2 = validateFile;
|
|
2086
|
+
var validateDocument2 = validateDocument;
|
|
2087
|
+
var exactMatch2 = exactMatch;
|
|
2088
|
+
var normalizedMatch2 = normalizedMatch;
|
|
2089
|
+
var fuzzySearch2 = fuzzySearch;
|
|
2090
|
+
var combinedScore2 = combinedScore;
|
|
2091
|
+
var isGitAvailable2 = isGitAvailable;
|
|
2092
|
+
var findRepoRoot3 = findRepoRoot;
|
|
2093
|
+
var getCurrentCommit2 = getCurrentCommit;
|
|
2094
|
+
var isStale2 = isStale;
|
|
2095
|
+
var getDiff2 = getDiff;
|
|
2096
|
+
var getLineShift4 = getLineShift;
|
|
2097
|
+
var getFileAtCommit3 = getFileAtCommit;
|
|
2098
|
+
var getStagedFiles2 = getStagedFiles;
|
|
2099
|
+
var detectRenames2 = detectRenames;
|
|
2100
|
+
var parseDiffHunks3 = parseDiffHunks;
|
|
2101
|
+
var DEFAULT_THRESHOLD2 = DEFAULT_THRESHOLD;
|
|
2102
|
+
var HIGH_THRESHOLD2 = HIGH_THRESHOLD;
|
|
2103
|
+
var reanchorComment2 = reanchorComment;
|
|
2104
|
+
var reanchorDocumentLines2 = reanchorDocumentLines;
|
|
2105
|
+
var reanchorDocumentText2 = reanchorDocumentText;
|
|
2106
|
+
var toReanchorLines2 = toReanchorLines;
|
|
2107
|
+
var resolveAnchor2 = resolveAnchor;
|
|
2108
|
+
var reanchorDocument2 = reanchorDocument;
|
|
2109
|
+
var applyReanchorResults2 = applyReanchorResults;
|
|
2110
|
+
var reanchorFile2 = reanchorFile;
|
|
2111
|
+
var addComment2 = addComment;
|
|
2112
|
+
var editComment2 = editComment;
|
|
2113
|
+
var normalizeCommentExtensions2 = normalizeCommentExtensions;
|
|
2114
|
+
var populateSelectedText2 = populateSelectedText;
|
|
2115
|
+
var resolveComment2 = resolveComment;
|
|
2116
|
+
var unresolveComment2 = unresolveComment;
|
|
2117
|
+
var removeComment2 = removeComment;
|
|
2118
|
+
var filterComments2 = filterComments;
|
|
2119
|
+
var getThreads2 = getThreads;
|
|
2120
|
+
var summarize2 = summarize;
|
|
2121
|
+
var formatAuthor2 = formatAuthor;
|
|
2122
|
+
var parseAuthor2 = parseAuthor;
|
|
2123
|
+
var newCommentId2 = newCommentId;
|
|
2124
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2125
|
+
0 && (module.exports = {
|
|
2126
|
+
DEFAULT_THRESHOLD,
|
|
2127
|
+
HIGH_THRESHOLD,
|
|
2128
|
+
addComment,
|
|
2129
|
+
applyReanchorResults,
|
|
2130
|
+
combinedScore,
|
|
2131
|
+
computeHash,
|
|
2132
|
+
detectRenames,
|
|
2133
|
+
discoverAllSidecars,
|
|
2134
|
+
discoverSidecar,
|
|
2135
|
+
editComment,
|
|
2136
|
+
exactMatch,
|
|
2137
|
+
filterComments,
|
|
2138
|
+
findRepoRoot,
|
|
2139
|
+
findWorkspaceRoot,
|
|
2140
|
+
formatAuthor,
|
|
2141
|
+
fuzzySearch,
|
|
2142
|
+
getCurrentCommit,
|
|
2143
|
+
getDiff,
|
|
2144
|
+
getFileAtCommit,
|
|
2145
|
+
getLineShift,
|
|
2146
|
+
getStagedFiles,
|
|
2147
|
+
getThreads,
|
|
2148
|
+
isGitAvailable,
|
|
2149
|
+
isStale,
|
|
2150
|
+
loadConfig,
|
|
2151
|
+
mrsfSchema,
|
|
2152
|
+
newCommentId,
|
|
2153
|
+
normalizeCommentExtensions,
|
|
2154
|
+
normalizedMatch,
|
|
2155
|
+
parseAuthor,
|
|
2156
|
+
parseDiffHunks,
|
|
2157
|
+
parseSidecar,
|
|
2158
|
+
parseSidecarContent,
|
|
2159
|
+
parseSidecarContentLenient,
|
|
2160
|
+
parseSidecarLenient,
|
|
2161
|
+
populateSelectedText,
|
|
2162
|
+
readDocumentLines,
|
|
2163
|
+
reanchorComment,
|
|
2164
|
+
reanchorDocument,
|
|
2165
|
+
reanchorDocumentLines,
|
|
2166
|
+
reanchorDocumentText,
|
|
2167
|
+
reanchorFile,
|
|
2168
|
+
removeComment,
|
|
2169
|
+
resolveAnchor,
|
|
2170
|
+
resolveComment,
|
|
2171
|
+
resolveSidecarPaths,
|
|
2172
|
+
sidecarToDocument,
|
|
2173
|
+
summarize,
|
|
2174
|
+
syncHash,
|
|
2175
|
+
toJson,
|
|
2176
|
+
toReanchorLines,
|
|
2177
|
+
toYaml,
|
|
2178
|
+
unresolveComment,
|
|
2179
|
+
validate,
|
|
2180
|
+
validateDocument,
|
|
2181
|
+
validateFile,
|
|
2182
|
+
writeSidecar
|
|
2183
|
+
});
|
|
2184
|
+
//# sourceMappingURL=index.cjs.map
|