@hawkeyexl/manni 2.3.9 → 2.4.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.
@@ -55,6 +55,7 @@ import {
55
55
  pinOfLines,
56
56
  readConfigFile,
57
57
  readFamilyConfigFile,
58
+ removeManifestKey,
58
59
  resolveBaselineRequest,
59
60
  resolveEncryptionKey,
60
61
  resolveTargetSet,
@@ -70,7 +71,7 @@ import {
70
71
  withValueAt,
71
72
  writeEncryptionKey,
72
73
  writeFileAtomic
73
- } from "./chunk-LC4H35XX.js";
74
+ } from "./chunk-LYLNOJ32.js";
74
75
 
75
76
  // src/meta/core/reencrypt.ts
76
77
  import { extname } from "node:path";
@@ -196,6 +197,7 @@ __export(cite_exports, {
196
197
  bodyLineOf: () => bodyLineOf,
197
198
  buildSourceIndex: () => buildSourceIndex,
198
199
  checkCitations: () => checkCitations,
200
+ citationInputs: () => citationInputs,
199
201
  claimEnd: () => claimEnd,
200
202
  claimLine: () => claimLine,
201
203
  claimMessageFor: () => claimMessageFor,
@@ -247,9 +249,14 @@ __export(cite_exports, {
247
249
  readSource: () => readSource,
248
250
  reencryptCitationEntries: () => reencryptCitationEntries,
249
251
  reencryptCitations: () => reencryptCitations,
252
+ removalLine: () => removalLine,
253
+ removeFrontmatterCitations: () => removeFrontmatterCitations,
254
+ removeLine: () => removeLine,
250
255
  renderCheckGithub: () => renderCheckGithub,
251
256
  renderCheckJson: () => renderCheckJson,
252
257
  renderCheckPretty: () => renderCheckPretty,
258
+ renderRemoveJson: () => renderRemoveJson,
259
+ renderRemovePretty: () => renderRemovePretty,
253
260
  renderUpdateJson: () => renderUpdateJson,
254
261
  renderUpdatePretty: () => renderUpdatePretty,
255
262
  resolveCiteRun: () => resolveCiteRun,
@@ -258,13 +265,17 @@ __export(cite_exports, {
258
265
  ruleId: () => ruleId,
259
266
  runAdd: () => runAdd,
260
267
  runCheck: () => runCheck,
268
+ runRemove: () => runRemove,
261
269
  runUpdate: () => runUpdate,
270
+ shiftedEntries: () => shiftedEntries,
262
271
  shortCommit: () => shortCommit,
272
+ shortLine: () => shortLine,
263
273
  shortPin: () => shortPin,
264
274
  shortSrc: () => shortSrc,
265
275
  sidecarsFor: () => sidecarsFor,
266
276
  sliceLines: () => sliceLines2,
267
277
  sourceRange: () => sourceRange,
278
+ spellAt: () => spellAt,
268
279
  spellLines: () => spellLines,
269
280
  spellSource: () => spellSource,
270
281
  splice: () => splice,
@@ -278,7 +289,8 @@ __export(cite_exports, {
278
289
  unifiedDiff: () => unifiedDiff,
279
290
  unitAt: () => unitAt,
280
291
  urlManifestRefusal: () => urlManifestRefusal,
281
- validateEntry: () => validateEntry
292
+ validateEntry: () => validateEntry,
293
+ withClaimLines: () => withClaimLines
282
294
  });
283
295
 
284
296
  // src/cite/types.ts
@@ -420,6 +432,11 @@ function shortSrc(src) {
420
432
  const lines = colon === -1 ? "" : src.slice(colon);
421
433
  return token.length <= 5 ? src : `${token.slice(0, 5)}\u2026${lines}`;
422
434
  }
435
+ var LINE_WIDTH = 60;
436
+ function shortLine(text) {
437
+ const one = text.replace(/\s+/g, " ").trim();
438
+ return one.length <= LINE_WIDTH ? one : `${one.slice(0, LINE_WIDTH)}\u2026`;
439
+ }
423
440
 
424
441
  // src/cite/core/sources.ts
425
442
  import fg from "fast-glob";
@@ -1050,7 +1067,7 @@ var MAX_BUFFER = 32 * 1024 * 1024;
1050
1067
  var NO_COMMIT = /invalid object name|bad revision|unknown revision|bad object/i;
1051
1068
  var NO_PATH = /does not exist in|exists on disk, but not in|path .* does not exist/i;
1052
1069
  function run2(root, args) {
1053
- return new Promise((resolve8) => {
1070
+ return new Promise((resolve9) => {
1054
1071
  execFile(
1055
1072
  "git",
1056
1073
  ["-c", "core.quotepath=false", ...args],
@@ -1059,8 +1076,8 @@ function run2(root, args) {
1059
1076
  if (error) {
1060
1077
  const failure = { ok: false, stderr, message: error.message };
1061
1078
  if (typeof error.code === "number") failure.code = error.code;
1062
- resolve8(failure);
1063
- } else resolve8({ ok: true, stdout });
1079
+ resolve9(failure);
1080
+ } else resolve9({ ok: true, stdout });
1064
1081
  }
1065
1082
  );
1066
1083
  });
@@ -1451,6 +1468,17 @@ function bodyLineOf(content, bodyOffset) {
1451
1468
  const line = lineAt(content, bodyOffset);
1452
1469
  return content.charCodeAt(bodyOffset - 1) === 10 ? line : line + 1;
1453
1470
  }
1471
+ function citationInputs(file, extracted, injected) {
1472
+ if (injected !== void 0) return [...injected];
1473
+ const raw = extracted.data.citations;
1474
+ const list = Array.isArray(raw) ? raw : [];
1475
+ return list.map((entry, index) => {
1476
+ const origin = { kind: "frontmatter", file };
1477
+ const line = extracted.lineFor(`/citations/${String(index)}`);
1478
+ if (line !== void 0) origin.line = line;
1479
+ return { entry, origin };
1480
+ });
1481
+ }
1454
1482
  function readPage(file, content, opts) {
1455
1483
  const extractor = pickExtractor(file, opts?.format);
1456
1484
  const format = extractor.name;
@@ -1494,12 +1522,7 @@ function readPage(file, content, opts) {
1494
1522
  })
1495
1523
  );
1496
1524
  }
1497
- const inputs = injected !== void 0 ? [...injected] : (Array.isArray(rawCitations) ? rawCitations : []).map((entry, index) => {
1498
- const origin = { kind: "frontmatter", file };
1499
- const line = extracted.lineFor(`/citations/${String(index)}`);
1500
- if (line !== void 0) origin.line = line;
1501
- return { entry, origin };
1502
- });
1525
+ const inputs = citationInputs(file, extracted, injected);
1503
1526
  inputs.forEach((input, index) => {
1504
1527
  const origin = { ...input.origin, index };
1505
1528
  const where = { index };
@@ -1655,6 +1678,45 @@ function readPage(file, content, opts) {
1655
1678
  };
1656
1679
  }
1657
1680
 
1681
+ // src/cite/core/shift.ts
1682
+ function spellAt(lines) {
1683
+ return lines.start === lines.end ? `line ${String(lines.start)}` : `lines ${String(lines.start)}-${String(lines.end)}`;
1684
+ }
1685
+ function shiftedEntries(opts) {
1686
+ const { citations, at: at2, delta, bodyLine, label } = opts;
1687
+ const out = { frontmatter: [], manifest: /* @__PURE__ */ new Map() };
1688
+ if (at2.length === 0) return out;
1689
+ for (const { citation, origin } of citations) {
1690
+ const spec = citation.claim?.lines;
1691
+ const recorded = spec === void 0 ? void 0 : parseLines(spec);
1692
+ if (recorded === void 0) continue;
1693
+ const file = toFileLines(recorded, bodyLine);
1694
+ const inside = at2.find(
1695
+ (line) => delta === 1 ? line > file.start && line <= file.end : line >= file.start && line <= file.end
1696
+ );
1697
+ if (inside !== void 0) {
1698
+ const whose = citation.id === void 0 ? `the claim at ${spellAt(file)}` : `the claim of ${citation.id} (${spellAt(file)})`;
1699
+ const why = delta === 1 ? "A marker there would change its pin." : "Removing the marker there would change its pin.";
1700
+ throw new CiteError(`${label}:${String(inside)} is inside ${whose}. ${why}`);
1701
+ }
1702
+ const lines = at2.filter((line) => delta === 1 ? line <= file.start : line < file.start).length;
1703
+ if (lines === 0) continue;
1704
+ const moved = lineSpec({
1705
+ start: recorded.start + lines * delta,
1706
+ end: recorded.end + lines * delta
1707
+ });
1708
+ if (origin.kind === "manifest") out.manifest.set(origin.index, moved);
1709
+ else out.frontmatter.push({ index: origin.index, lines: moved });
1710
+ }
1711
+ return out;
1712
+ }
1713
+ function withClaimLines(entry, lines) {
1714
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return entry;
1715
+ const claim = entry.claim;
1716
+ if (typeof claim !== "object" || claim === null || Array.isArray(claim)) return entry;
1717
+ return { ...entry, claim: { ...claim, lines } };
1718
+ }
1719
+
1658
1720
  // src/cite/core/sidecar.ts
1659
1721
  import { isAbsolute, relative, resolve, sep as sep2 } from "node:path";
1660
1722
  var CITATIONS_KEY2 = "citations";
@@ -1816,6 +1878,42 @@ function appendFrontmatterCitation(content, format, citation, filePath) {
1816
1878
  throw e;
1817
1879
  }
1818
1880
  }
1881
+ function removeFrontmatterCitations(content, format, drop, filePath) {
1882
+ const label = filePath ?? "the page";
1883
+ const extractor = extractorFor(format);
1884
+ if (!extractor.apply || !readsFencedFrontmatter(extractor, label)) {
1885
+ throw new CiteError(
1886
+ `${label} has no frontmatter to write to; keep its citations in a manifest instead.`
1887
+ );
1888
+ }
1889
+ if (locateFrontmatter(content)?.flavor === "toml") {
1890
+ throw new CiteError(
1891
+ `${label} has TOML frontmatter; manni cannot rewrite a citations table without re-emitting the whole block, which would drop its comments. Remove the entry by hand.`
1892
+ );
1893
+ }
1894
+ const raw = extractor.extract(content, label).data.citations ?? [];
1895
+ if (!Array.isArray(raw)) {
1896
+ throw new CiteError(`${label}: \`citations\` is not a list; edit it by hand.`);
1897
+ }
1898
+ const existing = raw;
1899
+ const kept = existing.filter((_entry, index) => !drop.has(index));
1900
+ try {
1901
+ return kept.length === 0 ? extractor.apply(content, {}, { deletions: ["citations"] }) : extractor.apply(content, { citations: kept });
1902
+ } catch (e) {
1903
+ if (e instanceof DocmetaError) throw new CiteError(`${label}: ${e.message}`);
1904
+ throw e;
1905
+ }
1906
+ }
1907
+ function removeLine(content, line) {
1908
+ const start = offsetOfLine(content, line);
1909
+ const nl = content.indexOf("\n", start);
1910
+ if (nl === -1) {
1911
+ let above = start > 0 ? content.lastIndexOf("\n", start - 1) : -1;
1912
+ if (above > 0 && content.charAt(above - 1) === "\r") above--;
1913
+ return content.slice(0, above === -1 ? 0 : above);
1914
+ }
1915
+ return content.slice(0, start) + content.slice(nl + 1);
1916
+ }
1819
1917
  function entryObject(citation) {
1820
1918
  const source = { file: citation.source.file };
1821
1919
  if (citation.source.lines !== void 0) source.lines = citation.source.lines;
@@ -2007,6 +2105,25 @@ var ManifestSet = class {
2007
2105
  held.text = spliced.text;
2008
2106
  return { file: manifest.file, line: itemLine(spliced.text, entry, index, manifest.join) ?? spliced.line };
2009
2107
  }
2108
+ /**
2109
+ * Take a page's `citations` key out of the manifest, for the removal that
2110
+ * leaves it with no entries. The entry itself goes when that key was all it
2111
+ * had, so a manifest never keeps a document nothing is recorded about.
2112
+ */
2113
+ async remove(manifest, entry) {
2114
+ const held = await this.hold(manifest);
2115
+ try {
2116
+ held.text = removeManifestKey(held.text, {
2117
+ entry,
2118
+ key: CITATIONS_KEY2,
2119
+ join: manifest.join,
2120
+ file: manifest.file
2121
+ }).text;
2122
+ } catch (error) {
2123
+ if (error instanceof DocmetaError) throw new CiteError(error.message);
2124
+ throw error;
2125
+ }
2126
+ }
2010
2127
  /** The manifests this run rewrote, with the diff of each. */
2011
2128
  changed() {
2012
2129
  return [...this.held.values()].filter((m) => m.text !== m.before).map((m) => ({ ...m, diff: unifiedDiff(m.file, m.before, m.text) }));
@@ -2623,7 +2740,9 @@ async function resolveCiteRun(opts) {
2623
2740
  root = gitRoot;
2624
2741
  } else {
2625
2742
  root = cwd;
2626
- opts.onNotice?.(`No git root found; resolving src: paths from ${cwd}`);
2743
+ if (opts.resolvesSources !== false) {
2744
+ opts.onNotice?.(`No git root found; resolving src: paths from ${cwd}`);
2745
+ }
2627
2746
  }
2628
2747
  }
2629
2748
  const { key, source: keySource } = resolveEncryptionKey({
@@ -2672,7 +2791,8 @@ function joinHits() {
2672
2791
  }
2673
2792
  };
2674
2793
  }
2675
- async function prepareRun(opts, action, verb, requireSources = false) {
2794
+ async function prepareRun(opts, action, verb, sources = {}) {
2795
+ const requireSources = sources.require === true;
2676
2796
  const cwd = resolve3(opts.cwd ?? process.cwd());
2677
2797
  const run3 = await resolveCiteRun({
2678
2798
  cwd,
@@ -2683,7 +2803,8 @@ async function prepareRun(opts, action, verb, requireSources = false) {
2683
2803
  root: opts.root,
2684
2804
  onConfigLoaded: opts.onConfigLoaded,
2685
2805
  onNotice: opts.onNotice,
2686
- env: opts.env
2806
+ env: opts.env,
2807
+ ...sources.resolve === false ? { resolvesSources: false } : {}
2687
2808
  });
2688
2809
  const { config, inputs, base } = run3;
2689
2810
  const checkSources = opts.checkSources ?? config?.checkSources ?? true;
@@ -2863,34 +2984,49 @@ async function citedText(root, index, src, key) {
2863
2984
  }
2864
2985
  return sliceLines2(splitLines(source.text), range, range.path);
2865
2986
  }
2866
- function spellAt(lines) {
2867
- return lines.start === lines.end ? `line ${String(lines.start)}` : `lines ${String(lines.start)}-${String(lines.end)}`;
2987
+ function entryName(entry) {
2988
+ return entry.citation.id ?? `/citations/${String(entry.origin.index)}`;
2868
2989
  }
2869
- function shiftedEntries(citations, insertLine, bodyLine, label) {
2870
- const out = { frontmatter: [], manifest: /* @__PURE__ */ new Map() };
2871
- for (const { citation, origin } of citations) {
2990
+ function duplicateOf(page, src, pageLines) {
2991
+ const wanted = formatSrc(parseSrc(src));
2992
+ return page.citations.find((entry) => {
2993
+ const { citation } = entry;
2994
+ if (spellSource(citation.source) !== wanted) return false;
2995
+ if (pageLines === void 0) return citation.claim === void 0;
2872
2996
  const spec = citation.claim?.lines;
2873
- const recorded = spec === void 0 ? void 0 : parseLines(spec);
2874
- if (recorded === void 0) continue;
2875
- const file = toFileLines(recorded, bodyLine);
2876
- if (file.start < insertLine) {
2877
- if (file.end < insertLine) continue;
2878
- const whose = citation.id === void 0 ? `the claim at ${spellAt(file)}` : `the claim of ${citation.id} (${spellAt(file)})`;
2879
- throw new CiteError(
2880
- `${label}:${String(insertLine)} is inside ${whose}. A marker there would change its pin.`
2881
- );
2882
- }
2883
- const moved = lineSpec({ start: recorded.start + 1, end: recorded.end + 1 });
2884
- if (origin.kind === "manifest") out.manifest.set(origin.index, moved);
2885
- else out.frontmatter.push({ index: origin.index, lines: moved });
2886
- }
2887
- return out;
2997
+ if (spec === void 0) return false;
2998
+ const recorded = parseLines(spec);
2999
+ if (recorded === void 0) return false;
3000
+ const file = toFileLines(recorded, page.bodyLine);
3001
+ return file.start === pageLines.start && file.end === pageLines.end;
3002
+ });
2888
3003
  }
2889
- function withClaimLines(entry, lines) {
2890
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return entry;
2891
- const claim = entry.claim;
2892
- if (typeof claim !== "object" || claim === null || Array.isArray(claim)) return entry;
2893
- return { ...entry, claim: { ...claim, lines } };
3004
+ function holdsNoText(text) {
3005
+ return text.trim() === "" || ANY_FENCE.test(text);
3006
+ }
3007
+ function emptyClaimRefusal(lines, pageLines, at2) {
3008
+ const span = lines.slice(pageLines.start - 1, pageLines.end);
3009
+ if (span.some((text) => !holdsNoText(text))) return void 0;
3010
+ if (pageLines.start !== pageLines.end) return `${at2} holds no claim text.`;
3011
+ const only = span[0] ?? "";
3012
+ return only.trim() === "" ? `${at2} is blank.` : `${at2} is a fence line, not claim text.`;
3013
+ }
3014
+ function otherClaimSpans(lines, pageLines, pin, bodyLine) {
3015
+ const width = pageLines.end - pageLines.start + 1;
3016
+ const found = findWindows(lines.slice(bodyLine - 1), width, pin, void 0, {
3017
+ around: toBodyLines(pageLines, bodyLine).start
3018
+ });
3019
+ return found.starts.map((start) => toFileLines({ start, end: start + width - 1 }, bodyLine)).filter((span) => span.start !== pageLines.start);
3020
+ }
3021
+ var NAMED_SPANS = 3;
3022
+ function spellElsewhere(spans) {
3023
+ const first = spans[0];
3024
+ const noun = spans.length === 1 && first !== void 0 && first.start === first.end ? "line" : "lines";
3025
+ const named3 = spans.slice(0, NAMED_SPANS).map((span) => spellLines(span));
3026
+ const rest = spans.length - named3.length;
3027
+ if (rest > 0) return `${noun} ${named3.join(", ")} and ${String(rest)} more`;
3028
+ const last = named3.pop() ?? "";
3029
+ return named3.length === 0 ? `${noun} ${last}` : `${noun} ${named3.join(", ")} and ${last}`;
2894
3030
  }
2895
3031
  async function runAdd(opts) {
2896
3032
  const cwd = resolve4(opts.cwd ?? process.cwd());
@@ -2964,6 +3100,12 @@ async function runAdd(opts) {
2964
3100
  );
2965
3101
  }
2966
3102
  }
3103
+ const duplicate = marker ? void 0 : duplicateOf(page, opts.src, pageLines);
3104
+ if (duplicate !== void 0) {
3105
+ const src = shortSrc(spellSource(duplicate.citation.source));
3106
+ const what = pageLines === void 0 ? `a bare pin for ${src}` : `an entry for ${spellAt(pageLines)} and ${src}`;
3107
+ throw new CiteError(`${label} already has ${what} (${entryName(duplicate)}).`);
3108
+ }
2967
3109
  const lines = splitLines(content);
2968
3110
  if (pageLines !== void 0) {
2969
3111
  if (pageLines.end > lines.length) {
@@ -2977,6 +3119,9 @@ async function runAdd(opts) {
2977
3119
  if (span === void 0 || span.end !== pageLines.end) {
2978
3120
  throw new CiteError(`${at2} is not a fenced block, so it cannot be a quote.`);
2979
3121
  }
3122
+ } else if (!marker) {
3123
+ const empty = emptyClaimRefusal(lines, pageLines, at2);
3124
+ if (empty !== void 0) throw new CiteError(empty);
2980
3125
  }
2981
3126
  }
2982
3127
  const client = opts.gitClient ?? gitClient(root);
@@ -2985,6 +3130,7 @@ async function runAdd(opts) {
2985
3130
  let markerAt;
2986
3131
  let claimSpan;
2987
3132
  let claim;
3133
+ let elsewhere = [];
2988
3134
  let shifted = { frontmatter: [], manifest: /* @__PURE__ */ new Map() };
2989
3135
  if (marker && pageLines !== void 0) {
2990
3136
  const holding = unitHolding(content, pageLines.start, format, {
@@ -3005,7 +3151,13 @@ async function runAdd(opts) {
3005
3151
  `${at2} is in a ${holding.kind} at ${spellAt(holding)} that ${unitWide}. A marker anchors the whole ${holding.kind}.`
3006
3152
  );
3007
3153
  }
3008
- shifted = shiftedEntries(page.citations, holding.start, page.bodyLine, label);
3154
+ shifted = shiftedEntries({
3155
+ citations: page.citations,
3156
+ at: [holding.start],
3157
+ delta: 1,
3158
+ bodyLine: page.bodyLine,
3159
+ label
3160
+ });
3009
3161
  const statement = markerIndent(content, holding.start, format, page.bodyLine) + formatStatement(format, { kind: "ref", id: opts.id ?? "" });
3010
3162
  const insertAt = offsetOfLine(content, holding.start);
3011
3163
  body = insertStatementBefore(content, insertAt, statement);
@@ -3027,6 +3179,7 @@ async function runAdd(opts) {
3027
3179
  }
3028
3180
  claimSpan = pageLines;
3029
3181
  claim = { lines: lineSpec(toBodyLines(pageLines, page.bodyLine)), integrity: pin };
3182
+ elsewhere = otherClaimSpans(lines, pageLines, pin, page.bodyLine);
3030
3183
  }
3031
3184
  const mint = (key, encrypt) => mintCitation({
3032
3185
  root,
@@ -3063,6 +3216,12 @@ async function runAdd(opts) {
3063
3216
  });
3064
3217
  citation = await mint(key, true);
3065
3218
  }
3219
+ const srcRange = parseSrc(opts.src);
3220
+ let sourceLine;
3221
+ if (srcRange.start !== void 0 && !isEncryptedValue(citation.source.file)) {
3222
+ const read = await readSource(root, sourceIndex, srcRange, void 0);
3223
+ if (read.kind === "ok") sourceLine = splitLines(read.text)[srcRange.start - 1];
3224
+ }
3066
3225
  const manifests = new ManifestSet();
3067
3226
  let placed;
3068
3227
  if (owner !== void 0) {
@@ -3095,6 +3254,7 @@ async function runAdd(opts) {
3095
3254
  content: after,
3096
3255
  diff: unifiedDiff(label, content, after),
3097
3256
  written: false,
3257
+ ...sourceLine === void 0 ? {} : { sourceLine },
3098
3258
  ...placed === void 0 ? {} : { manifest: placed }
3099
3259
  };
3100
3260
  if (markerAt !== void 0) result.markerLine = markerAt + shift;
@@ -3107,14 +3267,188 @@ async function runAdd(opts) {
3107
3267
  for (const changed of manifests.changed()) await writeFileAtomic(changed.path, changed.text);
3108
3268
  result.manifest.written = true;
3109
3269
  }
3270
+ if (elsewhere.length > 0) {
3271
+ const named3 = elsewhere.map((span) => ({ start: span.start + shift, end: span.end + shift }));
3272
+ const who = citation.id === void 0 ? "" : `${citation.id}: `;
3273
+ opts.onNotice?.(
3274
+ `${who}the claim's text also appears at ${spellElsewhere(named3)}, so a move would be ambiguous. Use --marker, or pin more lines.`
3275
+ );
3276
+ }
3110
3277
  if (opts.commitSha !== false && !await client.available()) {
3111
3278
  opts.onNotice?.(GIT_UNAVAILABLE_COMMIT);
3112
3279
  }
3113
3280
  return result;
3114
3281
  }
3115
3282
 
3116
- // src/cite/commands/update.ts
3283
+ // src/cite/commands/remove.ts
3117
3284
  import { resolve as resolve5 } from "node:path";
3285
+ var POINTER = /^\/citations\/(0|[1-9][0-9]*)$/;
3286
+ function entryCount(n) {
3287
+ return `${String(n)} ${n === 1 ? "entry" : "entries"}`;
3288
+ }
3289
+ function idOf(entry) {
3290
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return void 0;
3291
+ const id = entry.id;
3292
+ return typeof id === "string" ? id : void 0;
3293
+ }
3294
+ async function runRemove(opts) {
3295
+ const only = [...new Set(opts.only)];
3296
+ if (only.length === 0) {
3297
+ throw new CiteError("remove needs --only <id>; it never removes every citation on a page.");
3298
+ }
3299
+ const prepared = await prepareRun({ ...opts, checkSources: false }, "removed", "remove", {
3300
+ resolve: false
3301
+ });
3302
+ const { run: run3, files, usingStdin, forced } = prepared;
3303
+ assertNoOrphans(prepared);
3304
+ const hits = joinHits();
3305
+ const manifests = new ManifestSet();
3306
+ const matched = /* @__PURE__ */ new Set();
3307
+ const pageCount = files.length + (usingStdin ? 1 : 0);
3308
+ const planned = [];
3309
+ const removeOne = async (label, content, path) => {
3310
+ const setup = prepared.setupFor(label, content);
3311
+ if (setup.sidecar !== void 0) hits.record(setup.sidecar, label);
3312
+ const owner = setup.sidecar?.owner;
3313
+ const page = readPage(label, content, {
3314
+ ...forced === void 0 ? {} : { format: forced.name },
3315
+ ...setup.options.citations === void 0 ? {} : { citations: setup.options.citations },
3316
+ ...owner === void 0 ? {} : { owned: { file: owner.file, collection: owner.collection } }
3317
+ });
3318
+ const { format } = page;
3319
+ const extractor = pickExtractor(label, forced?.name);
3320
+ const inputs = citationInputs(
3321
+ label,
3322
+ extractor.extract(content, label),
3323
+ setup.sidecar?.citations
3324
+ );
3325
+ const lines = splitLines(content);
3326
+ const entriesFor = (value) => {
3327
+ const [, digits] = POINTER.exec(value) ?? [];
3328
+ if (digits === void 0) {
3329
+ return inputs.flatMap((input, index2) => idOf(input.entry) === value ? [index2] : []);
3330
+ }
3331
+ const index = Number(digits);
3332
+ if (index < inputs.length) return [index];
3333
+ if (pageCount === 1) {
3334
+ throw new CiteError(
3335
+ `"${value}" is past the last entry of ${label} (${entryCount(inputs.length)}).`
3336
+ );
3337
+ }
3338
+ return [];
3339
+ };
3340
+ const markersFor = (id) => id === void 0 ? [] : page.statements.filter((s) => s.payload.kind === "ref" && s.payload.id === id).map((s) => s.line);
3341
+ const removed = [];
3342
+ const indices = /* @__PURE__ */ new Set();
3343
+ const markerLines = [];
3344
+ for (const value of only) {
3345
+ const found = entriesFor(value);
3346
+ for (const index of found) {
3347
+ const input = inputs[index];
3348
+ const id = idOf(input?.entry);
3349
+ const markers2 = markersFor(id);
3350
+ indices.add(index);
3351
+ markerLines.push(...markers2);
3352
+ removed.push({
3353
+ ...id === void 0 ? {} : { id },
3354
+ index,
3355
+ ...input === void 0 ? {} : { origin: input.origin },
3356
+ markerLines: markers2
3357
+ });
3358
+ matched.add(value);
3359
+ }
3360
+ if (found.length > 0) continue;
3361
+ const markers = markersFor(value);
3362
+ if (markers.length === 0) continue;
3363
+ markerLines.push(...markers);
3364
+ removed.push({ id: value, markerLines: markers });
3365
+ matched.add(value);
3366
+ }
3367
+ const nothing = { file: label, removed: [], diff: "", written: false };
3368
+ if (removed.length === 0) {
3369
+ if (path === void 0) nothing.content = content;
3370
+ planned.push({ page: nothing, ...path === void 0 ? {} : { path } });
3371
+ return;
3372
+ }
3373
+ const ordered = [...new Set(markerLines)].sort((a, b) => a - b);
3374
+ for (const line of ordered) {
3375
+ if (!isMarkerLine(lines[line - 1] ?? "", format)) {
3376
+ const id = page.statements.find((s) => s.line === line)?.payload;
3377
+ const named3 = id?.kind === "ref" ? ` ${id.id}` : "";
3378
+ throw new CiteError(
3379
+ `the marker${named3} at ${label}:${String(line)} shares its line with text; remove it by hand.`
3380
+ );
3381
+ }
3382
+ }
3383
+ const survivors = page.citations.filter((c) => !indices.has(c.origin.index));
3384
+ const shifted = shiftedEntries({
3385
+ citations: survivors,
3386
+ at: ordered,
3387
+ delta: -1,
3388
+ bodyLine: page.bodyLine,
3389
+ label
3390
+ });
3391
+ let after = content;
3392
+ for (const { index, lines: moved } of shifted.frontmatter) {
3393
+ after = spliceEntryField(after, format, index, ["claim", "lines"], moved);
3394
+ }
3395
+ for (const line of [...ordered].sort((a, b) => b - a)) after = removeLine(after, line);
3396
+ if (owner === void 0) {
3397
+ after = removeFrontmatterCitations(after, format, indices, label);
3398
+ } else {
3399
+ if (setup.sidecar?.entry === void 0) {
3400
+ throw new CiteError(
3401
+ `${label} carries no ${owner.join}: value, so its citations cannot be keyed in ${owner.file}.`
3402
+ );
3403
+ }
3404
+ const kept = inputs.map((input, index) => {
3405
+ const moved = shifted.manifest.get(index);
3406
+ return moved === void 0 ? input.entry : withClaimLines(input.entry, moved);
3407
+ }).filter((_entry, index) => !indices.has(index));
3408
+ if (kept.length === 0) await manifests.remove(owner, setup.sidecar.entry);
3409
+ else void await manifests.write(owner, setup.sidecar.entry, kept, 0);
3410
+ }
3411
+ const out = {
3412
+ file: label,
3413
+ removed,
3414
+ diff: after === content ? "" : unifiedDiff(label, content, after),
3415
+ written: false
3416
+ };
3417
+ if (path === void 0) out.content = after;
3418
+ planned.push({ page: out, ...after === content ? {} : { content: after }, ...path === void 0 ? {} : { path } });
3419
+ };
3420
+ if (usingStdin) await removeOne(STDIN_LABEL, opts.stdinContent ?? "");
3421
+ for (const file of files) {
3422
+ await removeOne(file, await readTarget(run3, file), resolve5(run3.base, file));
3423
+ }
3424
+ assertNoOrphanJoins(prepared, hits);
3425
+ const missing = only.find((value) => !matched.has(value));
3426
+ if (missing !== void 0) {
3427
+ const where = pageCount === 1 ? `${planned[0]?.page.file ?? "the page"} has no` : `none of ${String(pageCount)} pages has an`;
3428
+ const what = POINTER.test(missing) ? `entry ${missing}` : `entry or marker ${missing}`;
3429
+ throw new CiteError(`${where} ${what}.`);
3430
+ }
3431
+ const write = opts.dryRun !== true;
3432
+ for (const item of planned) {
3433
+ if (item.content === void 0 || item.path === void 0) continue;
3434
+ if (write) await writeFileAtomic(item.path, item.content);
3435
+ item.page.written = write;
3436
+ }
3437
+ const rewritten = [];
3438
+ for (const changed of manifests.changed()) {
3439
+ if (write) await writeFileAtomic(changed.path, changed.text);
3440
+ rewritten.push({ file: changed.file, diff: changed.diff, written: write });
3441
+ }
3442
+ const pages = planned.map((item) => item.page);
3443
+ return {
3444
+ pages,
3445
+ removed: pages.reduce((n, page) => n + page.removed.length, 0),
3446
+ ...rewritten.length > 0 ? { manifests: rewritten } : {}
3447
+ };
3448
+ }
3449
+
3450
+ // src/cite/commands/update.ts
3451
+ import { resolve as resolve6 } from "node:path";
3118
3452
  function settles(plan) {
3119
3453
  switch (plan.kind) {
3120
3454
  case "claim-moved":
@@ -3243,7 +3577,7 @@ function rewriteOf(plan) {
3243
3577
  }
3244
3578
  }
3245
3579
  async function runUpdate(opts) {
3246
- const prepared = await prepareRun(opts, "updated", "update", true);
3580
+ const prepared = await prepareRun(opts, "updated", "update", { require: true });
3247
3581
  const { run: run3, files, usingStdin, forced, pageOptions, git } = prepared;
3248
3582
  assertNoOrphans(prepared);
3249
3583
  const hits = joinHits();
@@ -3371,7 +3705,7 @@ async function runUpdate(opts) {
3371
3705
  const reports = [];
3372
3706
  if (usingStdin) reports.push(await updateOne(STDIN_LABEL, opts.stdinContent ?? ""));
3373
3707
  for (const file of files) {
3374
- reports.push(await updateOne(file, await readTarget(run3, file), resolve5(run3.base, file)));
3708
+ reports.push(await updateOne(file, await readTarget(run3, file), resolve6(run3.base, file)));
3375
3709
  }
3376
3710
  assertNoOrphanJoins(prepared, hits);
3377
3711
  const changedManifests = manifests.changed();
@@ -3774,6 +4108,50 @@ function renderUpdatePretty(run3, opts) {
3774
4108
  lines.push(run3.exitCode === 0 ? c.green(summary) : c.red(summary));
3775
4109
  return lines.join("\n");
3776
4110
  }
4111
+ function spellLineList(at2, noun) {
4112
+ if (at2.length === 0) return noun;
4113
+ const spelled = at2.map((line) => String(line));
4114
+ if (spelled.length === 1) return `${noun} ${spelled[0] ?? ""}`;
4115
+ const last = spelled[spelled.length - 1] ?? "";
4116
+ return `${noun}s ${spelled.slice(0, -1).join(", ")} and ${last}`;
4117
+ }
4118
+ function removalLine(removal, c) {
4119
+ const markers = spellLineList(removal.markerLines, "line");
4120
+ if (removal.origin === void 0) {
4121
+ const named3 = removal.markerLines.length === 1 ? "the marker" : "the markers";
4122
+ return `removed ${named3} ${c.cyan(removal.id ?? "")} at ${markers}, which named no entry`;
4123
+ }
4124
+ const name = removal.id ?? `/citations/${String(removal.index ?? 0)}`;
4125
+ const where = removal.origin.kind === "manifest" ? `${removal.origin.file}${removal.origin.line === void 0 ? "" : `:${String(removal.origin.line)}`}` : "frontmatter";
4126
+ const also = removal.markerLines.length === 0 ? "" : `, and ${removal.markerLines.length === 1 ? "its marker" : "its markers"} at ${markers}`;
4127
+ return `removed ${c.cyan(name)} from ${where}${also}`;
4128
+ }
4129
+ function renderRemovePretty(run3, opts) {
4130
+ const c = palette(opts.color);
4131
+ const lines = [];
4132
+ let files = 0;
4133
+ for (const page of run3.pages) {
4134
+ if (page.removed.length > 0) files += 1;
4135
+ if (opts.showDiff && page.diff !== "") {
4136
+ for (const line of page.diff.split(/\r?\n/)) {
4137
+ if (line !== "") lines.push(c.dim(line));
4138
+ }
4139
+ }
4140
+ for (const removal of page.removed) {
4141
+ lines.push(`${page.file}: ${removalLine(removal, c)}`);
4142
+ }
4143
+ }
4144
+ if (opts.showDiff) {
4145
+ for (const manifest of run3.manifests ?? []) {
4146
+ for (const line of manifest.diff.split(/\r?\n/)) {
4147
+ if (line !== "") lines.push(c.dim(line));
4148
+ }
4149
+ }
4150
+ }
4151
+ const verb = opts.dryRun === true ? "would be removed" : "removed";
4152
+ lines.push(c.green(`${plural2(run3.removed, "citation")} ${verb} from ${plural2(files, "file")}`));
4153
+ return lines.join("\n");
4154
+ }
3777
4155
 
3778
4156
  // src/cite/reporters/json.ts
3779
4157
  function publicCitation(result) {
@@ -3815,6 +4193,9 @@ function publicPage(page) {
3815
4193
  function renderCheckJson(run3) {
3816
4194
  return JSON.stringify({ summary: run3.summary, pages: run3.pages.map(publicPage) }, null, 2);
3817
4195
  }
4196
+ function renderRemoveJson(run3) {
4197
+ return JSON.stringify(run3, null, 2);
4198
+ }
3818
4199
  function renderUpdateJson(run3) {
3819
4200
  const pages = run3.pages.map((page) => ({
3820
4201
  ...page,
@@ -3869,7 +4250,7 @@ var KeyError = class extends ToolError {
3869
4250
  // src/key/commands/rotate.ts
3870
4251
  import { existsSync as existsSync2 } from "node:fs";
3871
4252
  import { readFile as readFile5 } from "node:fs/promises";
3872
- import { isAbsolute as isAbsolute3, join as join2, relative as relative3, resolve as resolve6, sep as sep3 } from "node:path";
4253
+ import { isAbsolute as isAbsolute3, join as join2, relative as relative3, resolve as resolve7, sep as sep3 } from "node:path";
3873
4254
  import { parse as parseYaml } from "yaml";
3874
4255
 
3875
4256
  // src/key/core/config.ts
@@ -3905,9 +4286,9 @@ function citeSection(file) {
3905
4286
  return parseCiteConfig(doc.cite, file.source);
3906
4287
  }
3907
4288
  function rootFor(flag, cite, file, cwd) {
3908
- if (flag !== void 0) return { root: resolve6(cwd, flag), fellBack: false };
4289
+ if (flag !== void 0) return { root: resolve7(cwd, flag), fellBack: false };
3909
4290
  if (file !== null && cite.root !== void 0) {
3910
- return { root: resolve6(file.dir, cite.root), fellBack: false };
4291
+ return { root: resolve7(file.dir, cite.root), fellBack: false };
3911
4292
  }
3912
4293
  const gitRoot = findGitRoot(cwd);
3913
4294
  return gitRoot === null ? { root: cwd, fellBack: true } : { root: gitRoot, fellBack: false };
@@ -3969,7 +4350,7 @@ function reportedPath2(abs, base) {
3969
4350
  function manifestCollections(collections, opts) {
3970
4351
  if (opts.fromCollections) return [...collections];
3971
4352
  const rels = opts.files.map(
3972
- (rel) => reportedPath2(resolve6(opts.base, rel), opts.configDir)
4353
+ (rel) => reportedPath2(resolve7(opts.base, rel), opts.configDir)
3973
4354
  );
3974
4355
  return collections.filter((c) => rels.some((rel) => isMember(c, rel)));
3975
4356
  }
@@ -3998,7 +4379,7 @@ async function planManifests(opts) {
3998
4379
  for (const c of local) {
3999
4380
  for (const m of c.externalMetadata) {
4000
4381
  const join4 = externalMetadataJoin(m);
4001
- const abs = isAbsolute3(m.file) ? m.file : resolve6(opts.configDir, m.file);
4382
+ const abs = isAbsolute3(m.file) ? m.file : resolve7(opts.configDir, m.file);
4002
4383
  const file = reportedPath2(abs, opts.base);
4003
4384
  located.set(`${c.name}\0${file}\0${join4}`, abs);
4004
4385
  if (plans.has(abs)) continue;
@@ -4043,7 +4424,7 @@ async function planManifests(opts) {
4043
4424
  );
4044
4425
  }
4045
4426
  async function runKeyRotate(opts) {
4046
- const cwd = resolve6(opts.cwd ?? process.cwd());
4427
+ const cwd = resolve7(opts.cwd ?? process.cwd());
4047
4428
  const env = opts.env ?? process.env;
4048
4429
  if (opts.to !== void 0 && !isValidEncryptionKey(opts.to)) throw new KeyError(BAD_TO);
4049
4430
  if (opts.inputs.includes(STDIN_TOKEN)) throw new KeyError(NO_STDIN);
@@ -4127,7 +4508,7 @@ async function runKeyRotate(opts) {
4127
4508
  const planned = [];
4128
4509
  let wantsHistory = false;
4129
4510
  for (const rel of files) {
4130
- const path = resolve6(base, rel);
4511
+ const path = resolve7(base, rel);
4131
4512
  const before = await readFile5(path, "utf8");
4132
4513
  const page = { file: rel, content: before, ...format === void 0 ? {} : { format } };
4133
4514
  const sidecarWork = await rotateManifestCitations(sidecars2, manifests, page, {
@@ -4218,7 +4599,7 @@ async function runKeyRotate(opts) {
4218
4599
  else outcome = previous === void 0 ? "written" : "finished";
4219
4600
  }
4220
4601
  const wrote = outcome !== "skipped" && outcome !== "dry-run";
4221
- const baseline = file !== null && cite.baseline !== void 0 ? resolve6(file.dir, cite.baseline) : join2(file?.dir ?? cwd, DEFAULT_CITE_BASELINE_PATH);
4602
+ const baseline = file !== null && cite.baseline !== void 0 ? resolve7(file.dir, cite.baseline) : join2(file?.dir ?? cwd, DEFAULT_CITE_BASELINE_PATH);
4222
4603
  return {
4223
4604
  pages: planned.map((p) => p.page),
4224
4605
  manifests: valueManifests.map((m) => m.manifest),
@@ -4234,7 +4615,7 @@ async function runKeyRotate(opts) {
4234
4615
 
4235
4616
  // src/key/commands/set.ts
4236
4617
  import { existsSync as existsSync3 } from "node:fs";
4237
- import { basename, dirname, join as join3, relative as relative4, resolve as resolve7 } from "node:path";
4618
+ import { basename, dirname, join as join3, relative as relative4, resolve as resolve8 } from "node:path";
4238
4619
  var BAD_VALUE = "The key must be at least 32 hex or base64url characters. Run `manni key set` with no value to generate one.";
4239
4620
  var ENV_SET = `${ENCRYPTION_KEY_ENV} is set, so a key written to config would never be read. Unset it, or keep the key in the secret.`;
4240
4621
  var alreadyConfigured = (source) => `An encryption key is already configured in ${source}. Run \`manni key rotate\` to replace it and re-encrypt every value.`;
@@ -4251,7 +4632,7 @@ function refuseBesideLegacy(path, cwd) {
4251
4632
  }
4252
4633
  }
4253
4634
  async function runKeySet(opts) {
4254
- const cwd = resolve7(opts.cwd ?? process.cwd());
4635
+ const cwd = resolve8(opts.cwd ?? process.cwd());
4255
4636
  const env = opts.env ?? process.env;
4256
4637
  if (opts.value !== void 0 && !isValidEncryptionKey(opts.value)) {
4257
4638
  throw new KeyError(BAD_VALUE);
@@ -4290,22 +4671,26 @@ export {
4290
4671
  RULE_ID_PREFIX,
4291
4672
  isCiteRule,
4292
4673
  spellSource,
4674
+ shortPin,
4675
+ shortCommit,
4676
+ shortSrc,
4677
+ shortLine,
4293
4678
  runAdd,
4294
4679
  errorSite,
4295
4680
  runCheck,
4681
+ runRemove,
4296
4682
  runUpdate,
4297
- shortPin,
4298
- shortCommit,
4299
- shortSrc,
4300
4683
  renderCheckPretty,
4301
4684
  renderUpdatePretty,
4685
+ renderRemovePretty,
4302
4686
  findingMessage,
4303
4687
  renderCheckGithub,
4304
4688
  renderCheckJson,
4689
+ renderRemoveJson,
4305
4690
  renderUpdateJson,
4306
4691
  cite_exports,
4307
4692
  KeyError,
4308
4693
  runKeyRotate,
4309
4694
  runKeySet
4310
4695
  };
4311
- //# sourceMappingURL=chunk-YSI2VNKE.js.map
4696
+ //# sourceMappingURL=chunk-P4KR3XFD.js.map