@hawkeyexl/manni 2.8.0 → 2.8.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.
@@ -55,6 +55,7 @@ import {
55
55
  pinOfLines,
56
56
  readConfigFile,
57
57
  readFamilyConfigFile,
58
+ readManifestValue,
58
59
  removeManifestKey,
59
60
  resolveBaselineRequest,
60
61
  resolveEncryptionKey,
@@ -71,7 +72,7 @@ import {
71
72
  withValueAt,
72
73
  writeEncryptionKey,
73
74
  writeFileAtomic
74
- } from "./chunk-MB3DIO7S.js";
75
+ } from "./chunk-KEW557TN.js";
75
76
 
76
77
  // src/meta/core/reencrypt.ts
77
78
  import { extname } from "node:path";
@@ -151,15 +152,15 @@ function reencryptWith(extractor, page, opts) {
151
152
  }
152
153
  };
153
154
  const data = read(page.content);
154
- const apply2 = extractor.apply;
155
- const plan = reencryptData(data, { ...opts, writable: apply2 !== void 0 });
155
+ const apply3 = extractor.apply;
156
+ const plan = reencryptData(data, { ...opts, writable: apply3 !== void 0 });
156
157
  const { rewritten, skipped } = plan;
157
- if (rewritten.length === 0 || apply2 === void 0) {
158
+ if (rewritten.length === 0 || apply3 === void 0) {
158
159
  return { content: page.content, rewritten: [], skipped };
159
160
  }
160
161
  const patch = {};
161
162
  for (const top of plan.keys) patch[top] = plan.data[top];
162
- const content = apply2(page.content, patch, { filePath: page.file });
163
+ const content = apply3(page.content, patch, { filePath: page.file });
163
164
  const reread = tokensIn(read(content));
164
165
  for (const r of rewritten) {
165
166
  if (!reread.some((f) => f.pointer === r.pointer && f.token === r.to)) {
@@ -855,6 +856,10 @@ var TABLE_ROW = /^[ \t]*\|/;
855
856
  function isTableRow(line) {
856
857
  return TABLE_ROW.test(line);
857
858
  }
859
+ var TABLE_RULE = /^(?=[^|]*\|)[ \t]*\|?(?:[ \t]*:?-+:?[ \t]*\|)*[ \t]*:?-+:?[ \t]*\|?[ \t]*$/;
860
+ function isTableSeparator(line) {
861
+ return TABLE_RULE.test(line);
862
+ }
858
863
  function isBoundLine(line, format) {
859
864
  if (ADORNMENT.test(line)) return true;
860
865
  switch (format) {
@@ -1697,36 +1702,61 @@ function claimEnd(page, entry, lines, misplaced) {
1697
1702
  }
1698
1703
  return { status: "changed" };
1699
1704
  }
1700
- function rowsAt(line, lines, recorded) {
1705
+ function otherClaimSpans(lines, pageLines, pin, bodyLine) {
1706
+ const width = pageLines.end - pageLines.start + 1;
1707
+ const found = findWindows(lines.slice(bodyLine - 1), width, pin, void 0, {
1708
+ around: toBodyLines(pageLines, bodyLine).start
1709
+ });
1710
+ return found.starts.map((start) => toFileLines({ start, end: start + width - 1 }, bodyLine)).filter((span) => span.start !== pageLines.start);
1711
+ }
1712
+ var NAMED_SPANS = 3;
1713
+ function spellElsewhere(spans) {
1714
+ const first = spans[0];
1715
+ const noun = spans.length === 1 && first !== void 0 && first.start === first.end ? "line" : "lines";
1716
+ const named4 = spans.slice(0, NAMED_SPANS).map((span) => spellLines(span));
1717
+ const rest = spans.length - named4.length;
1718
+ if (rest > 0) return `${noun} ${named4.join(", ")} and ${String(rest)} more`;
1719
+ const last = named4.pop() ?? "";
1720
+ return named4.length === 0 ? `${noun} ${last}` : `${noun} ${named4.join(", ")} and ${last}`;
1721
+ }
1722
+ function rowsAt(line, lines, recorded, named4) {
1701
1723
  const width = recorded === void 0 ? 1 : Math.max(1, recorded.end - recorded.start + 1);
1702
1724
  const end = line + width - 1;
1703
1725
  if (end > lines.length) return "table-short";
1704
1726
  for (let n = line; n <= end; n++) {
1705
1727
  if (!isTableRow(lines[n - 1] ?? "")) return "table-short";
1706
1728
  }
1707
- return { lines: { start: line, end }, kind: "paragraph", text: lines.slice(line - 1, end) };
1729
+ const text = lines.slice(line - 1, end);
1730
+ if (!named4 && text.every((row) => isTableSeparator(row))) return "table-rule";
1731
+ return { lines: { start: line, end }, kind: "paragraph", text };
1708
1732
  }
1709
- function unitOrWhy(page, line, lines, recorded) {
1733
+ function unitOrWhy(page, line, lines, recorded, named4 = false) {
1710
1734
  if (line < page.bodyLine || line > lines.length) return "outside";
1711
1735
  const own = lines[line - 1] ?? "";
1712
1736
  if (own.trim() === "") return "blank";
1737
+ const marker = isMarkerLine(own, page.format);
1738
+ if (marker && !named4) return "marker";
1713
1739
  const block = fenceSpanAt(page.content, line, page.format);
1714
1740
  if (block !== void 0) {
1715
1741
  return { lines: block, kind: "block", text: lines.slice(block.start - 1, block.end) };
1716
1742
  }
1717
1743
  if (insideFence(page.content, page.bodyOffset, line, page.format)) return "fenced";
1718
- if (isTableRow(own)) return rowsAt(line, lines, recorded);
1719
- const paragraph = paragraphAfter(page.content, offsetOfLine(page.content, line));
1744
+ if (isTableRow(own)) return rowsAt(line, lines, recorded, named4);
1745
+ const paragraph = paragraphAfter(
1746
+ page.content,
1747
+ offsetOfLine(page.content, line),
1748
+ marker ? void 0 : page.format
1749
+ );
1720
1750
  if (paragraph === void 0 || paragraph.line !== line) return "not-a-paragraph";
1721
1751
  const span = { start: line, end: lineAt(page.content, paragraph.end) };
1722
1752
  return { lines: span, kind: "paragraph", text: lines.slice(span.start - 1, span.end) };
1723
1753
  }
1724
- function unitAt(page, line, lines, recorded) {
1725
- const found = unitOrWhy(page, line, lines, recorded);
1754
+ function unitAt(page, line, lines, recorded, named4 = false) {
1755
+ const found = unitOrWhy(page, line, lines, recorded, named4);
1726
1756
  return typeof found === "string" ? void 0 : found;
1727
1757
  }
1728
- function noUnitAt(page, line, lines, recorded) {
1729
- const found = unitOrWhy(page, line, lines, recorded);
1758
+ function noUnitAt(page, line, lines, recorded, named4 = false) {
1759
+ const found = unitOrWhy(page, line, lines, recorded, named4);
1730
1760
  return typeof found === "string" ? found : void 0;
1731
1761
  }
1732
1762
  function markerUnit(page, entry, lines) {
@@ -1870,6 +1900,19 @@ function sharesSentence(a, b) {
1870
1900
  const left = new Set(sentencesOf(a));
1871
1901
  return sentencesOf(b).some((sentence) => left.has(sentence));
1872
1902
  }
1903
+ function wordSet(text) {
1904
+ return new Set(
1905
+ text.toLowerCase().split(/\s+/).filter((word) => word !== "")
1906
+ );
1907
+ }
1908
+ function wordShare(was, now) {
1909
+ const left = wordSet(was);
1910
+ if (left.size === 0) return 1;
1911
+ const right = wordSet(now);
1912
+ let held = 0;
1913
+ for (const word of left) if (right.has(word)) held++;
1914
+ return held / left.size;
1915
+ }
1873
1916
  function holdsWords(outer, inner) {
1874
1917
  if (inner === "") return true;
1875
1918
  if (outer === inner) return true;
@@ -2620,6 +2663,13 @@ function unifiedDiff(label, before, after) {
2620
2663
  }
2621
2664
 
2622
2665
  // src/cite/core/manifest.ts
2666
+ var COMMIT_ATTEMPTS = 5;
2667
+ function conflictRefusal(file) {
2668
+ return `${file} changed under the command while it was being written. Nothing was written to it. Re-run the command.`;
2669
+ }
2670
+ function encodeValue(value) {
2671
+ return value === void 0 ? "absent" : JSON.stringify(value);
2672
+ }
2623
2673
  var ManifestSet = class {
2624
2674
  held = /* @__PURE__ */ new Map();
2625
2675
  /** Read a manifest, or hand back the copy this run has already rewritten. */
@@ -2633,7 +2683,14 @@ var ManifestSet = class {
2633
2683
  const reason = error instanceof Error ? error.message : String(error);
2634
2684
  throw new CiteError(`Manifest ${manifest.file} could not be read: ${reason}`);
2635
2685
  }
2636
- const fresh = { path: manifest.path, file: manifest.file, before, text: before };
2686
+ const fresh = {
2687
+ path: manifest.path,
2688
+ file: manifest.file,
2689
+ before,
2690
+ text: before,
2691
+ ops: [],
2692
+ bases: /* @__PURE__ */ new Map()
2693
+ };
2637
2694
  this.held.set(manifest.path, fresh);
2638
2695
  return fresh;
2639
2696
  }
@@ -2644,16 +2701,36 @@ var ManifestSet = class {
2644
2701
  */
2645
2702
  async write(manifest, entry, citations, index) {
2646
2703
  const held = await this.hold(manifest);
2704
+ const op = {
2705
+ kind: "splice",
2706
+ entry,
2707
+ join: manifest.join,
2708
+ file: manifest.file,
2709
+ value: [...citations]
2710
+ };
2711
+ rebaseable(held, op);
2647
2712
  const spliced = splice(held.text, {
2648
2713
  entry,
2649
2714
  key: CITATIONS_KEY2,
2650
- value: [...citations],
2715
+ value: op.value,
2651
2716
  join: manifest.join,
2652
2717
  file: manifest.file
2653
2718
  });
2654
2719
  held.text = spliced.text;
2720
+ held.ops.push(op);
2655
2721
  return { file: manifest.file, line: itemLine(spliced.text, entry, index, manifest.join) ?? spliced.line };
2656
2722
  }
2723
+ /**
2724
+ * Where one item of an entry's `citations` sits in the manifest as this run
2725
+ * will write it. `add` names a line in its report, and `commit` may have
2726
+ * replayed the entry onto bytes that moved it, so the line is read back from
2727
+ * the settled text rather than kept from the splice.
2728
+ */
2729
+ lineOf(manifest, entry, index) {
2730
+ const held = this.held.get(manifest.path);
2731
+ if (held === void 0) return void 0;
2732
+ return itemLine(held.text, entry, index, manifest.join);
2733
+ }
2657
2734
  /**
2658
2735
  * Take a page's `citations` key out of the manifest, for the removal that
2659
2736
  * leaves it with no entries. The entry itself goes when that key was all it
@@ -2661,23 +2738,114 @@ var ManifestSet = class {
2661
2738
  */
2662
2739
  async remove(manifest, entry) {
2663
2740
  const held = await this.hold(manifest);
2664
- try {
2665
- held.text = removeManifestKey(held.text, {
2666
- entry,
2667
- key: CITATIONS_KEY2,
2668
- join: manifest.join,
2669
- file: manifest.file
2670
- }).text;
2671
- } catch (error) {
2672
- if (error instanceof DocmetaError) throw new CiteError(error.message);
2673
- throw error;
2674
- }
2741
+ const op = { kind: "remove", entry, join: manifest.join, file: manifest.file };
2742
+ rebaseable(held, op);
2743
+ held.text = apply(held.text, op);
2744
+ held.ops.push(op);
2675
2745
  }
2676
2746
  /** The manifests this run rewrote, with the diff of each. */
2677
2747
  changed() {
2678
2748
  return [...this.held.values()].filter((m) => m.text !== m.before).map((m) => ({ ...m, diff: unifiedDiff(m.file, m.before, m.text) }));
2679
2749
  }
2750
+ /**
2751
+ * Write every manifest this run rewrote, and report what landed.
2752
+ *
2753
+ * The write is the last thing a run does, so this is the one place that
2754
+ * knows a manifest is about to be replaced by bytes derived from a read
2755
+ * that may be seconds old.
2756
+ */
2757
+ async commit(io = {}) {
2758
+ const write = io.write ?? writeFileAtomic;
2759
+ const read = io.read ?? ((at2) => readFile2(at2, "utf8"));
2760
+ const landed = [];
2761
+ for (const held of this.held.values()) {
2762
+ if (held.text === held.before) continue;
2763
+ try {
2764
+ await settle(held, write, read);
2765
+ } catch (error) {
2766
+ if (io.onError !== void 0) await io.onError(changeOf(held), error);
2767
+ throw error;
2768
+ }
2769
+ const change = changeOf(held);
2770
+ landed.push(change);
2771
+ io.after?.(change);
2772
+ }
2773
+ return landed;
2774
+ }
2680
2775
  };
2776
+ function changeOf(held) {
2777
+ return {
2778
+ path: held.path,
2779
+ file: held.file,
2780
+ before: held.before,
2781
+ text: held.text,
2782
+ diff: unifiedDiff(held.file, held.before, held.text)
2783
+ };
2784
+ }
2785
+ function rebaseable(held, op) {
2786
+ const at2 = op.join === "path" ? posix.normalize(op.entry) : op.entry;
2787
+ if (held.bases.has(at2)) return;
2788
+ held.bases.set(at2, {
2789
+ entry: op.entry,
2790
+ join: op.join,
2791
+ value: encodeValue(
2792
+ readManifestValue(held.before, { entry: op.entry, key: CITATIONS_KEY2, join: op.join })
2793
+ )
2794
+ });
2795
+ }
2796
+ function apply(text, op) {
2797
+ if (op.kind === "splice") {
2798
+ return splice(text, {
2799
+ entry: op.entry,
2800
+ key: CITATIONS_KEY2,
2801
+ value: op.value,
2802
+ join: op.join,
2803
+ file: op.file
2804
+ }).text;
2805
+ }
2806
+ try {
2807
+ return removeManifestKey(text, {
2808
+ entry: op.entry,
2809
+ key: CITATIONS_KEY2,
2810
+ join: op.join,
2811
+ file: op.file
2812
+ }).text;
2813
+ } catch (error) {
2814
+ if (error instanceof DocmetaError) throw new CiteError(error.message);
2815
+ throw error;
2816
+ }
2817
+ }
2818
+ function rebase(held, current) {
2819
+ for (const base of held.bases.values()) {
2820
+ const now = encodeValue(
2821
+ readManifestValue(current, { entry: base.entry, key: CITATIONS_KEY2, join: base.join })
2822
+ );
2823
+ if (now !== base.value) return void 0;
2824
+ }
2825
+ let text = current;
2826
+ for (const op of held.ops) text = apply(text, op);
2827
+ return text;
2828
+ }
2829
+ async function settle(held, write, read) {
2830
+ for (let attempt = 1; attempt <= COMMIT_ATTEMPTS; attempt++) {
2831
+ let current;
2832
+ try {
2833
+ current = await read(held.path);
2834
+ } catch (error) {
2835
+ const reason = error instanceof Error ? error.message : String(error);
2836
+ throw new CiteError(`Manifest ${held.file} could not be read: ${reason}`);
2837
+ }
2838
+ if (current === held.before) {
2839
+ await write(held.path, held.text);
2840
+ return;
2841
+ }
2842
+ const replayed = rebase(held, current);
2843
+ if (replayed === void 0) throw new CiteError(conflictRefusal(held.file));
2844
+ held.before = current;
2845
+ held.text = replayed;
2846
+ }
2847
+ throw new CiteError(conflictRefusal(held.file));
2848
+ }
2681
2849
  function splice(text, options) {
2682
2850
  try {
2683
2851
  return spliceManifestValue(text, options);
@@ -3648,23 +3816,6 @@ function emptyClaimRefusal(lines, pageLines, at2) {
3648
3816
  const only = span[0] ?? "";
3649
3817
  return only.trim() === "" ? `${at2} is blank.` : `${at2} is a fence line, not claim text.`;
3650
3818
  }
3651
- function otherClaimSpans(lines, pageLines, pin, bodyLine) {
3652
- const width = pageLines.end - pageLines.start + 1;
3653
- const found = findWindows(lines.slice(bodyLine - 1), width, pin, void 0, {
3654
- around: toBodyLines(pageLines, bodyLine).start
3655
- });
3656
- return found.starts.map((start) => toFileLines({ start, end: start + width - 1 }, bodyLine)).filter((span) => span.start !== pageLines.start);
3657
- }
3658
- var NAMED_SPANS = 3;
3659
- function spellElsewhere(spans) {
3660
- const first = spans[0];
3661
- const noun = spans.length === 1 && first !== void 0 && first.start === first.end ? "line" : "lines";
3662
- const named4 = spans.slice(0, NAMED_SPANS).map((span) => spellLines(span));
3663
- const rest = spans.length - named4.length;
3664
- if (rest > 0) return `${noun} ${named4.join(", ")} and ${String(rest)} more`;
3665
- const last = named4.pop() ?? "";
3666
- return named4.length === 0 ? `${noun} ${last}` : `${noun} ${named4.join(", ")} and ${last}`;
3667
- }
3668
3819
  async function runAdd(opts) {
3669
3820
  const cwd = resolve4(opts.cwd ?? process.cwd());
3670
3821
  const run3 = await resolveCiteRun({
@@ -3821,8 +3972,8 @@ async function runAdd(opts) {
3821
3972
  if (unit === void 0 || pin === void 0) {
3822
3973
  throw new CiteError(`${at2} has no paragraph or block for a marker to anchor.`);
3823
3974
  }
3824
- for (const { index, lines: moved } of shifted.frontmatter) {
3825
- body = spliceEntryField(body, format, index, ["claim", "lines"], moved);
3975
+ for (const { index: index2, lines: moved } of shifted.frontmatter) {
3976
+ body = spliceEntryField(body, format, index2, ["claim", "lines"], moved);
3826
3977
  }
3827
3978
  claimSpan = unit;
3828
3979
  claim = { integrity: pin };
@@ -3878,18 +4029,20 @@ async function runAdd(opts) {
3878
4029
  }
3879
4030
  const manifests = new ManifestSet();
3880
4031
  let placed;
4032
+ let index = 0;
3881
4033
  if (owner !== void 0) {
3882
4034
  if (sidecar?.entry === void 0) {
3883
4035
  throw new CiteError(
3884
4036
  `${label} carries no ${owner.join}: value, so its citations cannot be keyed in ${owner.file}.`
3885
4037
  );
3886
4038
  }
3887
- const existing = (sidecar.citations ?? []).map((input, index) => {
3888
- const moved = shifted.manifest.get(index);
4039
+ const existing = (sidecar.citations ?? []).map((input, index2) => {
4040
+ const moved = shifted.manifest.get(index2);
3889
4041
  return moved === void 0 ? input.entry : withClaimLines(input.entry, moved);
3890
4042
  });
3891
4043
  const list = [...existing, entryObject(citation)];
3892
- const at3 = await manifests.write(owner, sidecar.entry, list, list.length - 1);
4044
+ index = list.length - 1;
4045
+ const at3 = await manifests.write(owner, sidecar.entry, list, index);
3893
4046
  const [changed] = manifests.changed();
3894
4047
  placed = {
3895
4048
  file: at3.file,
@@ -3918,7 +4071,14 @@ async function runAdd(opts) {
3918
4071
  result.written = path !== void 0 && opts.dryRun !== true && after !== content;
3919
4072
  if (result.written && path !== void 0) await writeFileAtomic(path, after);
3920
4073
  if (result.manifest !== void 0 && opts.dryRun !== true) {
3921
- for (const changed of manifests.changed()) await writeFileAtomic(changed.path, changed.text);
4074
+ const [settled] = await manifests.commit();
4075
+ if (owner !== void 0 && sidecar?.entry !== void 0) {
4076
+ result.manifest.line = manifests.lineOf(owner, sidecar.entry, index) ?? result.manifest.line;
4077
+ }
4078
+ if (settled !== void 0) {
4079
+ result.manifest.content = settled.text;
4080
+ result.manifest.diff = settled.diff;
4081
+ }
3922
4082
  result.manifest.written = true;
3923
4083
  }
3924
4084
  if (elsewhere.length > 0) {
@@ -4089,10 +4249,11 @@ async function runRemove(opts) {
4089
4249
  item.page.written = write;
4090
4250
  }
4091
4251
  const rewritten = [];
4092
- for (const changed of manifests.changed()) {
4093
- if (write) await writeFileAtomic(changed.path, changed.text);
4252
+ const record = (changed) => {
4094
4253
  rewritten.push({ file: changed.file, diff: changed.diff, written: write });
4095
- }
4254
+ };
4255
+ if (write) await manifests.commit({ after: record });
4256
+ else for (const changed of manifests.changed()) record(changed);
4096
4257
  const pages = planned.map((item) => item.page);
4097
4258
  return {
4098
4259
  pages,
@@ -4111,10 +4272,14 @@ function sayNoUnit(reason) {
4111
4272
  return "the line is blank";
4112
4273
  case "fenced":
4113
4274
  return "the line sits inside a fenced block";
4275
+ case "marker":
4276
+ return "that line now holds a cite marker, not claim text";
4114
4277
  case "not-a-paragraph":
4115
4278
  return "the line does not start a paragraph";
4116
4279
  case "table-short":
4117
4280
  return "the table no longer holds every row the claim covers";
4281
+ case "table-rule":
4282
+ return "that line is a table rule, not claim text";
4118
4283
  }
4119
4284
  }
4120
4285
  function settles(plan) {
@@ -4142,7 +4307,7 @@ function settles(plan) {
4142
4307
  return "";
4143
4308
  }
4144
4309
  }
4145
- function apply(content, format, plan) {
4310
+ function apply2(content, format, plan) {
4146
4311
  const index = plan.result.origin.index;
4147
4312
  switch (plan.kind) {
4148
4313
  case "claim-moved":
@@ -4317,7 +4482,8 @@ function rewriteOf(plan) {
4317
4482
  fromPin: plan.pin,
4318
4483
  toPin: plan.would,
4319
4484
  at: plan.at,
4320
- commitSha: plan.commitSha
4485
+ commitSha: plan.commitSha,
4486
+ ...plan.wordShare === void 0 ? {} : { wordShare: plan.wordShare }
4321
4487
  };
4322
4488
  case "claim-shifted":
4323
4489
  return {
@@ -4383,12 +4549,17 @@ function rewriteOf(plan) {
4383
4549
  }
4384
4550
  }
4385
4551
  }
4552
+ var CLAIM_WORD_SHARE = 0.5;
4386
4553
  function replacementAt(claim, text, format) {
4387
4554
  const was = claim.baselineText;
4388
4555
  const commit = claim.commitSha;
4389
4556
  if (was === void 0 || commit === void 0) return void 0;
4390
- if (sharesSentence(claimWords(was, format), claimWords(text, format))) return void 0;
4391
- return commit;
4557
+ const before = claimWords(was, format);
4558
+ const now = claimWords(text, format);
4559
+ if (!sharesSentence(before, now)) return { commit };
4560
+ const share = wordShare(before, now);
4561
+ if (share < CLAIM_WORD_SHARE) return { commit, share };
4562
+ return void 0;
4392
4563
  }
4393
4564
  var QUOTE_CAP = 200;
4394
4565
  function quoted(text) {
@@ -4578,7 +4749,8 @@ async function runUpdate(opts) {
4578
4749
  if (claim !== null && claim.status === "changed" && entry !== void 0 && result.anchor !== "marker") {
4579
4750
  const held = claim.fileLines === void 0 ? void 0 : parseLines(claim.fileLines);
4580
4751
  const at2 = held?.start;
4581
- const unit = at2 === void 0 ? void 0 : unitAt(page, at2, lines, held);
4752
+ const named4 = only !== void 0;
4753
+ const unit = at2 === void 0 ? void 0 : unitAt(page, at2, lines, held, named4);
4582
4754
  const wantsBlock = entry.citation.quote === true;
4583
4755
  const pin = unit === void 0 ? void 0 : pinOfLines(lines, unit.lines);
4584
4756
  const wide = unit === void 0 ? void 0 : tooWide(unit.lines);
@@ -4588,7 +4760,7 @@ async function runUpdate(opts) {
4588
4760
  why: `Not re-pinned: the ${unit.kind} ${wide}.`
4589
4761
  });
4590
4762
  } else if (unit === void 0) {
4591
- const why = at2 === void 0 ? void 0 : noUnitAt(page, at2, lines, held);
4763
+ const why = at2 === void 0 ? void 0 : noUnitAt(page, at2, lines, held, named4);
4592
4764
  if (why !== void 0) {
4593
4765
  declined.set(result.origin.index, {
4594
4766
  rule: "claim-changed",
@@ -4596,16 +4768,25 @@ async function runUpdate(opts) {
4596
4768
  });
4597
4769
  }
4598
4770
  } else if (pin !== void 0 && (!wantsBlock || unit.kind === "block")) {
4771
+ const elsewhere = named4 ? [] : otherClaimSpans(lines, unit.lines, pin, page.bodyLine);
4599
4772
  const replaced = replacementAt(claim, unit.text, page.format);
4600
4773
  if (replaced !== void 0 && only === void 0) {
4601
4774
  out.push({
4602
4775
  kind: "claim-replaced",
4603
4776
  result,
4604
4777
  at: unit.lines.start,
4605
- commitSha: replaced,
4778
+ commitSha: replaced.commit,
4779
+ ...replaced.share === void 0 ? {} : { wordShare: replaced.share },
4606
4780
  pin: entry.citation.claim?.integrity ?? "",
4607
4781
  would: pin
4608
4782
  });
4783
+ } else if (elsewhere.length > 0) {
4784
+ const id = result.citation.id;
4785
+ const anyway = id === void 0 ? "" : ` Re-run with --only ${id} to accept it anyway.`;
4786
+ declined.set(result.origin.index, {
4787
+ rule: "claim-changed",
4788
+ why: `Not re-pinned: the text there also appears at ${spellElsewhere(elsewhere)}, so a pin cannot identify it.${anyway}`
4789
+ });
4609
4790
  } else {
4610
4791
  const plan = { kind: "claim-accepted", result, unit, pin };
4611
4792
  if (replaced !== void 0) plan.replaced = true;
@@ -4732,7 +4913,8 @@ async function runUpdate(opts) {
4732
4913
  kind: "claim-replaced",
4733
4914
  result,
4734
4915
  at: now.span.start,
4735
- commitSha: replaced,
4916
+ commitSha: replaced.commit,
4917
+ ...replaced.share === void 0 ? {} : { wordShare: replaced.share },
4736
4918
  pin: recorded,
4737
4919
  would: now.pin
4738
4920
  }
@@ -4779,7 +4961,7 @@ async function runUpdate(opts) {
4779
4961
  if (!applyToEntry(held, plan)) continue;
4780
4962
  manifestDirty = true;
4781
4963
  } else {
4782
- after = apply(after, format, plan);
4964
+ after = apply2(after, format, plan);
4783
4965
  }
4784
4966
  rewritten2.push(rewriteOf(plan));
4785
4967
  settled.add(`${String(result.origin.index)}\0${settles(plan)}`);
@@ -4833,20 +5015,25 @@ async function runUpdate(opts) {
4833
5015
  reports.push(await updateOne(file, await readTarget(run3, file), resolve6(run3.base, file)));
4834
5016
  }
4835
5017
  assertNoOrphanJoins(prepared, hits);
4836
- const changedManifests = manifests.changed();
5018
+ const write = opts.dryRun !== true;
4837
5019
  const rewrittenManifests = [];
4838
- for (const changed of changedManifests) {
4839
- const write = opts.dryRun !== true;
4840
- if (write) {
4841
- try {
4842
- await writeFileAtomic(changed.path, changed.text);
4843
- } catch (error) {
5020
+ const record = (changed) => {
5021
+ rewrittenManifests.push({ file: changed.file, diff: changed.diff, written: write });
5022
+ };
5023
+ if (write) {
5024
+ await manifests.commit({
5025
+ // A manifest joins the run's written files the moment it lands, so a
5026
+ // later manifest's failure puts this one back with the pages. Without
5027
+ // it the tree would keep pins no page content matches.
5028
+ after: (changed) => {
5029
+ writes.record(changed.path, changed.file, changed.before);
5030
+ record(changed);
5031
+ },
5032
+ onError: async (changed, error) => {
4844
5033
  throw await writes.restore(changed.file, error);
4845
5034
  }
4846
- writes.record(changed.path, changed.file, changed.before);
4847
- }
4848
- rewrittenManifests.push({ file: changed.file, diff: changed.diff, written: write });
4849
- }
5035
+ });
5036
+ } else for (const changed of manifests.changed()) record(changed);
4850
5037
  const reminted = pages.some(
4851
5038
  (page) => page.rewritten.some((r) => r.reason === "accepted" && r.end === "source")
4852
5039
  );
@@ -5260,7 +5447,12 @@ function movedSpan(rewrite) {
5260
5447
  }
5261
5448
  function refusalLine(rewrite) {
5262
5449
  const at2 = shortCommit(rewrite.commitSha ?? "");
5263
- return `claim at ${spellAtSpec(String(rewrite.at ?? 0))} skipped: that line now holds different text than the claim at ${at2}. Re-add it with cite add.`;
5450
+ const where = `claim at ${spellAtSpec(String(rewrite.at ?? 0))} skipped:`;
5451
+ if (rewrite.wordShare !== void 0) {
5452
+ const share = Math.round(rewrite.wordShare * 100);
5453
+ return `${where} that line now holds text sharing ${String(share)}% of the claim's words at ${at2}. Re-add it with cite add.`;
5454
+ }
5455
+ return `${where} that line now holds different text than the claim at ${at2}. Re-add it with cite add.`;
5264
5456
  }
5265
5457
  function rewriteLine(rewrite) {
5266
5458
  const word = rewrite.from.includes("-") ? "lines" : "line";
@@ -5922,4 +6114,4 @@ export {
5922
6114
  runKeyRotate,
5923
6115
  runKeySet
5924
6116
  };
5925
- //# sourceMappingURL=chunk-O6RXG33V.js.map
6117
+ //# sourceMappingURL=chunk-WCNU53MJ.js.map