overleaf-review 0.6.0 → 0.6.2
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
CHANGED
|
@@ -212,6 +212,13 @@ Grouping applies to new pushes; it does not consolidate or accept suggestions al
|
|
|
212
212
|
### Holistic paragraph rewrites over reviewed suggestions and comments
|
|
213
213
|
|
|
214
214
|
Use this when a paragraph rewrite intentionally incorporates or supersedes existing suggestions.
|
|
215
|
+
|
|
216
|
+
To regroup existing fragmented suggestions **without changing their proposed wording**, set
|
|
217
|
+
`before` and `after` to the same paragraph text and explicitly list the suggestion IDs in
|
|
218
|
+
`supersedes`. Keep the local file unchanged for that block. This is supported from v0.6.1;
|
|
219
|
+
the proposal must still differ from the underlying text obtained by rejecting those suggestions.
|
|
220
|
+
Affected comments still require explicit anchor mappings, and all stale-plan and verification
|
|
221
|
+
checks still apply. Ordinary replacements without selected suggestions cannot be no-ops.
|
|
215
222
|
It differs from consolidation: the proposed text may change, and selected older suggestion cards
|
|
216
223
|
are replaced by a new pending revision under the authenticated account.
|
|
217
224
|
|
|
@@ -332,6 +339,17 @@ project, verifies consolidation and protected review state, and checks that exis
|
|
|
332
339
|
were unchanged. It does not require or spoof another account. Foreign-author preservation is also
|
|
333
340
|
covered by the upstream model check. Never run this probe against a manuscript project.
|
|
334
341
|
|
|
342
|
+
From v0.6.2, rejection and underlying-text reconstruction account for insertions and deletions
|
|
343
|
+
at the same position, including multiple co-located deletions. The undo first restores selected
|
|
344
|
+
deletions, then removes insertions at their adjusted positions; exact-text and range verification
|
|
345
|
+
remain mandatory. Recreate unsubmitted plans from a fresh snapshot after upgrading, since the
|
|
346
|
+
operation ordering has changed. Do not re-plan to bypass an uncertain earlier submission.
|
|
347
|
+
|
|
348
|
+
`npm run probe:rejection-model` checks this ordering against pinned upstream range logic in both
|
|
349
|
+
tracking modes, including consolidation and reviewed paragraph rewrites. Add `-- --backup <file>`
|
|
350
|
+
to check a saved full snapshot (or a plan containing `backup`) offline. The backup stays local;
|
|
351
|
+
the probe fetches only public upstream code and never connects to an Overleaf project.
|
|
352
|
+
|
|
335
353
|
### Push overrides and submission
|
|
336
354
|
|
|
337
355
|
Use `--allow-overlap` only after inspecting those tracked changes. `--unsafe-no-base` is a deliberate
|
|
@@ -924,11 +924,11 @@ function buildReviewEdits(source, target, options = {}) {
|
|
|
924
924
|
}
|
|
925
925
|
return groups;
|
|
926
926
|
}
|
|
927
|
-
function validateExplicitEdits(source, value) {
|
|
927
|
+
function validateExplicitEdits(source, value, validation = {}) {
|
|
928
928
|
if (!Array.isArray(value) || !value.length) throw new Error("Explicit blocks must be a nonempty array.");
|
|
929
929
|
let previousEnd = -1;
|
|
930
930
|
return value.map((edit) => {
|
|
931
|
-
if (!edit || !Number.isSafeInteger(edit.start) || !Number.isSafeInteger(edit.end) || edit.start < 0 || edit.end < edit.start || edit.end > source.length || typeof edit.text !== "string" || edit.start <= previousEnd || source.slice(edit.start, edit.end) === edit.text) {
|
|
931
|
+
if (!edit || !Number.isSafeInteger(edit.start) || !Number.isSafeInteger(edit.end) || edit.start < 0 || edit.end < edit.start || edit.end > source.length || typeof edit.text !== "string" || edit.start <= previousEnd || !validation.allowUnchanged && source.slice(edit.start, edit.end) === edit.text) {
|
|
932
932
|
throw new Error("Explicit blocks must be valid, ordered, separated replacements with a text change.");
|
|
933
933
|
}
|
|
934
934
|
previousEnd = edit.end;
|
|
@@ -941,7 +941,7 @@ function applyTextEdits(source, edits) {
|
|
|
941
941
|
}
|
|
942
942
|
return source;
|
|
943
943
|
}
|
|
944
|
-
function parseReplacementManifest(base, value) {
|
|
944
|
+
function parseReplacementManifest(base, value, validation = {}) {
|
|
945
945
|
const manifest = value;
|
|
946
946
|
if (!Array.isArray(manifest?.replacements) || !manifest.replacements.length) {
|
|
947
947
|
throw new Error("Replacement manifest requires a nonempty replacements array.");
|
|
@@ -962,10 +962,10 @@ function parseReplacementManifest(base, value) {
|
|
|
962
962
|
if (start === void 0) throw new Error("Replacement occurrence is absent from the saved base.");
|
|
963
963
|
return { start, end: start + block.before.length, text: block.after };
|
|
964
964
|
}).sort((a, b) => a.start - b.start);
|
|
965
|
-
return validateExplicitEdits(base, edits);
|
|
965
|
+
return validateExplicitEdits(base, edits, validation);
|
|
966
966
|
}
|
|
967
|
-
function bindExplicitEdits(base, local, live, edits, options) {
|
|
968
|
-
const checked = validateExplicitEdits(base, edits);
|
|
967
|
+
function bindExplicitEdits(base, local, live, edits, options, validation = {}) {
|
|
968
|
+
const checked = validateExplicitEdits(base, edits, validation);
|
|
969
969
|
if (applyTextEdits(base, checked) !== local) {
|
|
970
970
|
throw new Error("Replacement manifest must describe every local change exactly.");
|
|
971
971
|
}
|
|
@@ -1285,17 +1285,26 @@ function applyUndo(text, op, changeId) {
|
|
|
1285
1285
|
}
|
|
1286
1286
|
function buildRejectionPlan(currentText, ranges, requestedIds) {
|
|
1287
1287
|
const requested = new Set(uniqueChangeIds(requestedIds));
|
|
1288
|
-
const fragments = ranges.filter((range) => requested.has(range.id));
|
|
1289
|
-
fragments.sort((a, b) => b.op.p - a.op.p);
|
|
1288
|
+
const fragments = ranges.map((range, index) => ({ range, index })).filter(({ range }) => requested.has(range.id)).map((fragment) => ({ ...fragment, inverse: inverseOf(fragment.range) }));
|
|
1289
|
+
fragments.sort((a, b) => Number(b.range.op.d !== void 0) - Number(a.range.op.d !== void 0) || b.range.op.p - a.range.op.p || a.index - b.index);
|
|
1290
|
+
const restorations = fragments.filter((fragment) => "i" in fragment.inverse);
|
|
1290
1291
|
const operations = [];
|
|
1291
1292
|
let expectedText = currentText;
|
|
1292
|
-
|
|
1293
|
-
|
|
1293
|
+
let restorePosition = -1;
|
|
1294
|
+
let restoredAtPosition = 0;
|
|
1295
|
+
for (const { range, inverse: originalInverse } of fragments) {
|
|
1296
|
+
if ("i" in originalInverse && originalInverse.p !== restorePosition) {
|
|
1297
|
+
restorePosition = originalInverse.p;
|
|
1298
|
+
restoredAtPosition = 0;
|
|
1299
|
+
}
|
|
1300
|
+
const shift = "d" in originalInverse ? restorations.reduce((sum, restored) => sum + (restored.range.op.p <= range.op.p ? restored.range.op.d.length : 0), 0) : restoredAtPosition;
|
|
1301
|
+
const inverse = { ...originalInverse, p: originalInverse.p + shift };
|
|
1294
1302
|
expectedText = applyUndo(expectedText, inverse, range.id);
|
|
1295
1303
|
operations.push(inverse);
|
|
1304
|
+
if ("i" in inverse) restoredAtPosition += inverse.i.length;
|
|
1296
1305
|
}
|
|
1297
1306
|
return {
|
|
1298
|
-
changeIds: changeIdsInRanges(fragments),
|
|
1307
|
+
changeIds: changeIdsInRanges(fragments.map((fragment) => fragment.range)),
|
|
1299
1308
|
fragmentCount: fragments.length,
|
|
1300
1309
|
operations,
|
|
1301
1310
|
expectedText
|
|
@@ -2299,7 +2308,7 @@ ${doc.localPath} \u2192 ${doc.docPath} (v${doc.liveVersion}): ${doc.ops.length}
|
|
|
2299
2308
|
}
|
|
2300
2309
|
async function push(opts) {
|
|
2301
2310
|
validatePushOptions(opts);
|
|
2302
|
-
const { isReviewedManifest, createReviewedFilePlan, applyReviewedFilePlan, REVIEWED_FILE_KIND } = await import("./reviewed-replace-
|
|
2311
|
+
const { isReviewedManifest, createReviewedFilePlan, applyReviewedFilePlan, REVIEWED_FILE_KIND } = await import("./reviewed-replace-CW447HNG.js");
|
|
2303
2312
|
if (opts.edits && isReviewedManifest(JSON.parse(readFileSync6(workspaceReadPath(opts.edits), "utf8")))) {
|
|
2304
2313
|
await createReviewedFilePlan(opts);
|
|
2305
2314
|
return;
|
package/dist/cli.js
CHANGED
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
workspaceRelativePath,
|
|
35
35
|
workspaceWritePath,
|
|
36
36
|
writeJsonAtomic
|
|
37
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-FBISFRYR.js";
|
|
38
38
|
|
|
39
39
|
// src/commands/reviewed-replace.ts
|
|
40
40
|
import { readFileSync } from "fs";
|
|
@@ -315,10 +315,13 @@ function resolveReviewedBlocks(base, local, live, value) {
|
|
|
315
315
|
const manifest = value;
|
|
316
316
|
if (!Array.isArray(manifest?.replacements) || !manifest.replacements.length) throw new Error("Reviewed manifest requires replacements.");
|
|
317
317
|
const resolved = manifest.replacements.map((block) => {
|
|
318
|
-
const
|
|
318
|
+
const allowUnchanged = Array.isArray(block.supersedes) && block.supersedes.length > 0;
|
|
319
|
+
const [edit] = parseReplacementManifest(base, {
|
|
320
|
+
replacements: [{ before: block.before, after: block.after, occurrence: block.occurrence }]
|
|
321
|
+
}, { allowUnchanged });
|
|
319
322
|
return { ...edit, supersedes: block.supersedes ?? [], reason: block.reason, comments: block.comments ?? [] };
|
|
320
323
|
}).sort((a, b) => a.start - b.start);
|
|
321
|
-
const mapped = bindExplicitEdits(base, local, live, resolved, {}).explicitEdits;
|
|
324
|
+
const mapped = bindExplicitEdits(base, local, live, resolved, {}, { allowUnchanged: true }).explicitEdits;
|
|
322
325
|
return resolved.map((block, index) => ({ ...block, ...mapped[index] }));
|
|
323
326
|
}
|
|
324
327
|
async function createReviewedFilePlan(opts) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "overleaf-review",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "The missing review layer for Overleaf's Git bridge — sync comments and tracked changes between Overleaf and your local repo.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"probe:review-live": "tsx src/probes/05-review-live.ts",
|
|
45
45
|
"probe:reviewed-model": "tsx src/probes/06-reviewed-model.ts",
|
|
46
46
|
"probe:reviewed-live": "tsx src/probes/07-reviewed-live.ts",
|
|
47
|
+
"probe:rejection-model": "tsx src/probes/08-rejection-model.ts",
|
|
47
48
|
"typecheck": "tsc --noEmit",
|
|
48
49
|
"test": "tsx --test test/*.test.ts",
|
|
49
50
|
"check": "npm run typecheck && npm test && npm run build"
|