@hawkeyexl/manni 2.3.10 → 2.5.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 +1 -1
- package/dist/{chunk-LORLZO3J.js → chunk-3V4EIH7H.js} +1690 -759
- package/dist/chunk-3V4EIH7H.js.map +1 -0
- package/dist/{chunk-TNT3ZROC.js → chunk-SXYSP33V.js} +3 -2
- package/dist/chunk-SXYSP33V.js.map +1 -0
- package/dist/{chunk-WXBLVCOZ.js → chunk-XCZJ25OT.js} +2 -2
- package/dist/cli.js +87 -3
- package/dist/cli.js.map +1 -1
- package/dist/docmeta.js +2 -2
- package/dist/index.d.ts +320 -12
- package/dist/index.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-LORLZO3J.js.map +0 -1
- package/dist/chunk-TNT3ZROC.js.map +0 -1
- /package/dist/{chunk-WXBLVCOZ.js.map → chunk-XCZJ25OT.js.map} +0 -0
|
@@ -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-
|
|
74
|
+
} from "./chunk-SXYSP33V.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,
|
|
@@ -218,16 +220,24 @@ __export(cite_exports, {
|
|
|
218
220
|
historyOf: () => historyOf,
|
|
219
221
|
insertStatementBefore: () => insertStatementBefore,
|
|
220
222
|
insideFence: () => insideFence,
|
|
223
|
+
isBoundLine: () => isBoundLine,
|
|
221
224
|
isCiteRule: () => isCiteRule,
|
|
222
225
|
isKeyedPin: () => isKeyedPin,
|
|
226
|
+
isUnitText: () => isUnitText,
|
|
223
227
|
itemLine: () => itemLine,
|
|
224
228
|
lineAt: () => lineAt,
|
|
225
229
|
lineSpec: () => lineSpec,
|
|
226
230
|
loadCitationSidecars: () => loadCitationSidecars,
|
|
227
231
|
loadCiteConfig: () => loadCiteConfig,
|
|
232
|
+
markerClaimEnd: () => markerClaimEnd,
|
|
233
|
+
markerRunAt: () => markerRunAt,
|
|
228
234
|
markerUnit: () => markerUnit,
|
|
229
235
|
messageFor: () => messageFor,
|
|
230
236
|
mintCitation: () => mintCitation,
|
|
237
|
+
misplacedMarkerAt: () => misplacedMarkerAt,
|
|
238
|
+
misplacedMarkers: () => misplacedMarkers,
|
|
239
|
+
misplacedMessageFor: () => misplacedMessageFor,
|
|
240
|
+
movedUnit: () => movedUnit,
|
|
231
241
|
noGit: () => noGit,
|
|
232
242
|
normalizeText: () => normalizeText,
|
|
233
243
|
normalizeWhitespace: () => normalizeWhitespace,
|
|
@@ -241,15 +251,21 @@ __export(cite_exports, {
|
|
|
241
251
|
parseStatements: () => parseStatements,
|
|
242
252
|
pickExtractor: () => pickExtractor,
|
|
243
253
|
pinOfLines: () => pinOfLines,
|
|
254
|
+
pre43Span: () => pre43Span,
|
|
244
255
|
rangeLines: () => rangeLines,
|
|
245
256
|
readCiteConfigFile: () => readCiteConfigFile,
|
|
246
257
|
readPage: () => readPage,
|
|
247
258
|
readSource: () => readSource,
|
|
248
259
|
reencryptCitationEntries: () => reencryptCitationEntries,
|
|
249
260
|
reencryptCitations: () => reencryptCitations,
|
|
261
|
+
removalLine: () => removalLine,
|
|
262
|
+
removeFrontmatterCitations: () => removeFrontmatterCitations,
|
|
263
|
+
removeLine: () => removeLine,
|
|
250
264
|
renderCheckGithub: () => renderCheckGithub,
|
|
251
265
|
renderCheckJson: () => renderCheckJson,
|
|
252
266
|
renderCheckPretty: () => renderCheckPretty,
|
|
267
|
+
renderRemoveJson: () => renderRemoveJson,
|
|
268
|
+
renderRemovePretty: () => renderRemovePretty,
|
|
253
269
|
renderUpdateJson: () => renderUpdateJson,
|
|
254
270
|
renderUpdatePretty: () => renderUpdatePretty,
|
|
255
271
|
resolveCiteRun: () => resolveCiteRun,
|
|
@@ -258,7 +274,9 @@ __export(cite_exports, {
|
|
|
258
274
|
ruleId: () => ruleId,
|
|
259
275
|
runAdd: () => runAdd,
|
|
260
276
|
runCheck: () => runCheck,
|
|
277
|
+
runRemove: () => runRemove,
|
|
261
278
|
runUpdate: () => runUpdate,
|
|
279
|
+
shiftedEntries: () => shiftedEntries,
|
|
262
280
|
shortCommit: () => shortCommit,
|
|
263
281
|
shortLine: () => shortLine,
|
|
264
282
|
shortPin: () => shortPin,
|
|
@@ -266,11 +284,13 @@ __export(cite_exports, {
|
|
|
266
284
|
sidecarsFor: () => sidecarsFor,
|
|
267
285
|
sliceLines: () => sliceLines2,
|
|
268
286
|
sourceRange: () => sourceRange,
|
|
287
|
+
spellAt: () => spellAt,
|
|
269
288
|
spellLines: () => spellLines,
|
|
270
289
|
spellSource: () => spellSource,
|
|
271
290
|
splice: () => splice,
|
|
272
291
|
spliceEntryField: () => spliceEntryField,
|
|
273
292
|
splitLines: () => splitLines,
|
|
293
|
+
splitUnit: () => splitUnit,
|
|
274
294
|
statementForms: () => statementForms,
|
|
275
295
|
toBodyLines: () => toBodyLines,
|
|
276
296
|
toFileLines: () => toFileLines,
|
|
@@ -278,8 +298,10 @@ __export(cite_exports, {
|
|
|
278
298
|
twoManifestsRefusal: () => twoManifestsRefusal,
|
|
279
299
|
unifiedDiff: () => unifiedDiff,
|
|
280
300
|
unitAt: () => unitAt,
|
|
301
|
+
unitText: () => unitText,
|
|
281
302
|
urlManifestRefusal: () => urlManifestRefusal,
|
|
282
|
-
validateEntry: () => validateEntry
|
|
303
|
+
validateEntry: () => validateEntry,
|
|
304
|
+
withClaimLines: () => withClaimLines
|
|
283
305
|
});
|
|
284
306
|
|
|
285
307
|
// src/cite/types.ts
|
|
@@ -295,6 +317,7 @@ var CITE_RULES = [
|
|
|
295
317
|
"marker-orphan",
|
|
296
318
|
"marker-invalid",
|
|
297
319
|
"marker-repeated",
|
|
320
|
+
"marker-misplaced",
|
|
298
321
|
"anchor-invalid",
|
|
299
322
|
"entry-invalid",
|
|
300
323
|
"quote-drift"
|
|
@@ -329,6 +352,7 @@ var DEFAULT_SEVERITY = {
|
|
|
329
352
|
"marker-orphan": "error",
|
|
330
353
|
"marker-invalid": "error",
|
|
331
354
|
"marker-repeated": "warning",
|
|
355
|
+
"marker-misplaced": "warning",
|
|
332
356
|
"anchor-invalid": "error",
|
|
333
357
|
"entry-invalid": "error",
|
|
334
358
|
"quote-drift": "error"
|
|
@@ -426,6 +450,13 @@ function shortLine(text) {
|
|
|
426
450
|
const one = text.replace(/\s+/g, " ").trim();
|
|
427
451
|
return one.length <= LINE_WIDTH ? one : `${one.slice(0, LINE_WIDTH)}\u2026`;
|
|
428
452
|
}
|
|
453
|
+
function spellAt(lines) {
|
|
454
|
+
return lines.start === lines.end ? `line ${String(lines.start)}` : `lines ${String(lines.start)}-${String(lines.end)}`;
|
|
455
|
+
}
|
|
456
|
+
function listOf(values) {
|
|
457
|
+
if (values.length <= 1) return values.join("");
|
|
458
|
+
return `${values.slice(0, -1).join(", ")} and ${values[values.length - 1] ?? ""}`;
|
|
459
|
+
}
|
|
429
460
|
|
|
430
461
|
// src/cite/core/sources.ts
|
|
431
462
|
import fg from "fast-glob";
|
|
@@ -513,6 +544,127 @@ async function readSource(root, index, range, key) {
|
|
|
513
544
|
}
|
|
514
545
|
}
|
|
515
546
|
|
|
547
|
+
// src/cite/core/page.ts
|
|
548
|
+
import { extname as extname2 } from "node:path";
|
|
549
|
+
import * as Ajv2020Ns from "ajv/dist/2020.js";
|
|
550
|
+
|
|
551
|
+
// src/cite/schema/citations.json
|
|
552
|
+
var citations_default = {
|
|
553
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
554
|
+
$id: "manni:citations:1.0.0-proposal.4",
|
|
555
|
+
title: "manni citations vocabulary (1.0.0-proposal.4)",
|
|
556
|
+
description: "What do this page's claims rest on? For each claim, the page lines that make it, the source lines it was written from, and a hash of each. A manni common vocabulary: any drift check can implement it, and other schemas may compose on top of it. A pin is a record of the past, not a copy of the present. It says what the lines were when the sentence was written. The check compares that record with the files as they are now. Recompute it on every run and there is nothing to compare against. `source-of-truth` (manni:stewardship) names the file a page answers to; a citation names the lines a sentence answers to. The page root stays open so sibling tools' keys pass untouched. The `citation-` prefix is reserved, so an unrecognized `citation-*` key is rejected and a typo fails loudly.",
|
|
557
|
+
type: "object",
|
|
558
|
+
additionalProperties: true,
|
|
559
|
+
properties: {
|
|
560
|
+
citations: {
|
|
561
|
+
type: "array",
|
|
562
|
+
minItems: 1,
|
|
563
|
+
items: {
|
|
564
|
+
$ref: "#/$defs/citationEntry"
|
|
565
|
+
},
|
|
566
|
+
description: "The page's citations, one entry per claim or pin. An empty list is not a declaration; a page with no citations omits the key. Ids are unique within a page, and two entries claiming one id is an error. JSON Schema cannot say that over a list whose members may omit the id. So it is a requirement on implementations, stated here so every tool agrees.",
|
|
567
|
+
"x-manni-location": "external"
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
patternProperties: {
|
|
571
|
+
"^citation-": false
|
|
572
|
+
},
|
|
573
|
+
$defs: {
|
|
574
|
+
citationEntry: {
|
|
575
|
+
type: "object",
|
|
576
|
+
additionalProperties: false,
|
|
577
|
+
required: [
|
|
578
|
+
"source"
|
|
579
|
+
],
|
|
580
|
+
description: "One citation: the page text it supports, the source lines it rests on, and how the two are anchored. Both ends share one shape, a line range and a hash.",
|
|
581
|
+
properties: {
|
|
582
|
+
id: {
|
|
583
|
+
type: "string",
|
|
584
|
+
pattern: "^[a-z0-9][a-z0-9-]*$",
|
|
585
|
+
description: "A kebab-case name for this citation, unique within the page. A body marker names an entry by it, and findings are reported under it. Optional for an entry anchored by its claim lines or pinned bare. Required when a marker names the entry."
|
|
586
|
+
},
|
|
587
|
+
claim: {
|
|
588
|
+
$ref: "#/$defs/claim"
|
|
589
|
+
},
|
|
590
|
+
source: {
|
|
591
|
+
$ref: "#/$defs/source"
|
|
592
|
+
},
|
|
593
|
+
quote: {
|
|
594
|
+
type: "boolean",
|
|
595
|
+
default: false,
|
|
596
|
+
description: "The claim is a fenced block that reproduces the source lines. The pinned claim covers the whole block, fences included. An implementer compares the lines inside the fences with the source under the pin's rule. So a quoted snippet that drifts from its source is a finding even while the pin still holds. Under a marker, the block is the next fence after the marker. Default false."
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
},
|
|
600
|
+
claim: {
|
|
601
|
+
type: "object",
|
|
602
|
+
additionalProperties: false,
|
|
603
|
+
required: [
|
|
604
|
+
"integrity"
|
|
605
|
+
],
|
|
606
|
+
description: "The page text the citation supports, pinned by lines and a hash. Absent, the entry is a bare pin, or a marker with no drift check on its sentence. Without `lines`, the text pinned is what a body marker naming this entry anchors.",
|
|
607
|
+
properties: {
|
|
608
|
+
lines: {
|
|
609
|
+
$ref: "#/$defs/lines",
|
|
610
|
+
description: "Lines of the page body. The body starts on the first line after the closing frontmatter fence, or on line 1 with no frontmatter. So editing the frontmatter never moves a claim. Required unless a marker names the entry, and never inside the frontmatter."
|
|
611
|
+
},
|
|
612
|
+
integrity: {
|
|
613
|
+
type: "string",
|
|
614
|
+
pattern: "^sha256-[0-9a-f]{64}$",
|
|
615
|
+
description: "`sha256-` and sixty-four lowercase hex digits over the claimed lines, under the rule `source.integrity` states. Always plain, never keyed: the page is public, so there is nothing to hide. With a marker, the lines hashed are the ones the marker anchors."
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
source: {
|
|
620
|
+
type: "object",
|
|
621
|
+
additionalProperties: false,
|
|
622
|
+
required: [
|
|
623
|
+
"file",
|
|
624
|
+
"integrity"
|
|
625
|
+
],
|
|
626
|
+
description: "The lines the claim rests on: a file, an optional line range, a pin, and the commit the pin was taken at.",
|
|
627
|
+
properties: {
|
|
628
|
+
file: {
|
|
629
|
+
$ref: "#/$defs/fileRef"
|
|
630
|
+
},
|
|
631
|
+
lines: {
|
|
632
|
+
$ref: "#/$defs/lines",
|
|
633
|
+
description: "File lines. Absent, the whole file is pinned. The lines stay readable even when `file` is encrypted."
|
|
634
|
+
},
|
|
635
|
+
integrity: {
|
|
636
|
+
type: "string",
|
|
637
|
+
pattern: "^(?:sha256|hmac-sha256)-[0-9a-f]{64}$",
|
|
638
|
+
description: "The pin: an algorithm prefix and sixty-four lowercase hex digits over the cited lines under one rule. Decode as UTF-8 and drop one leading byte-order mark. Turn CRLF into LF, split on LF, and discard the empty element a trailing LF leaves. Take lines L1 to L2 inclusive, or every line for a whole file. Join with LF and no trailing LF, and keep trailing whitespace. A plain `file` is pinned `sha256-` over that text. An encrypted `file` is pinned `hmac-sha256-`: the HMAC-SHA256 of that text under a key derived from the encryption key. So a public page carries no verifier a reader could run against a guessed private line. The prefix follows `file`, and any other pairing is an invalid entry."
|
|
639
|
+
},
|
|
640
|
+
"commit-sha": {
|
|
641
|
+
type: "string",
|
|
642
|
+
pattern: "^[0-9a-f]{7,64}$",
|
|
643
|
+
description: "The git commit hash the source pin was taken at, as lowercase hex. Seven digits is what git abbreviates to; sixty-four covers a SHA-256 repository. A tool writes the full hash, because an abbreviation that is unique today need not be next year. Without it, a check can still say the lines changed. With it, the check can say since when, and whether the pin was ever true."
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
},
|
|
647
|
+
lines: {
|
|
648
|
+
oneOf: [
|
|
649
|
+
{
|
|
650
|
+
type: "integer",
|
|
651
|
+
minimum: 1
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
type: "string",
|
|
655
|
+
pattern: "^[1-9][0-9]*-[1-9][0-9]*$"
|
|
656
|
+
}
|
|
657
|
+
],
|
|
658
|
+
description: 'A line range, 1-based and inclusive: an integer for one line, or `"L1-L2"` for several. `L1 <= L2` is a rule on implementations, since a pattern cannot compare two numbers. There is no line 0 to cite.'
|
|
659
|
+
},
|
|
660
|
+
fileRef: {
|
|
661
|
+
type: "string",
|
|
662
|
+
pattern: "^(?:~[A-Za-z0-9_-]{82,}|(?!~)(?:(?!\\.\\.?(?:/|$))[^/\\\\:\\r\\n\\t]+)(?:/(?:(?!\\.\\.?(?:/|$))[^/\\\\:\\r\\n\\t]+))*)$",
|
|
663
|
+
description: "The file: a path relative to the repository root, posix-separated, and free of `.` and `..` segments. Segments may hold spaces and dots but not `/`, `\\`, `:` or a line break. So a URL, a drive letter, a Windows path and an absolute path all fail here. In place of the path, `~` and at least 82 base64url characters is the path encrypted with the family key. Proposal 0045 states the construction. Only a checkout with the key and the files can decrypt the path and read the lines. So a public docs repo can cite a private code repo without publishing its paths. The value carries no encryption mark, because the citation tool encrypts it, and a plain path stays valid."
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
|
|
516
668
|
// src/shared/code-regions.ts
|
|
517
669
|
var SKIP_MARKDOWN_FENCE = /^[ \t]*(`{3,}|~{3,})/;
|
|
518
670
|
var SKIP_ASCIIDOC_FENCE = /^(-{4,})/;
|
|
@@ -679,6 +831,25 @@ function isMarkerLine(line, format) {
|
|
|
679
831
|
return isCite(text.slice(form.open.length, closeAt).trim());
|
|
680
832
|
});
|
|
681
833
|
}
|
|
834
|
+
var ATX_HEADING = /^[ \t]*#{1,6}([ \t]|$)/;
|
|
835
|
+
var ASCIIDOC_TITLE = /^={1,6}[ \t]/;
|
|
836
|
+
var ADORNMENT = /^[ \t]*([-=~^"'*+#_:<>])(?:[ \t]*\1){2,}[ \t]*$/;
|
|
837
|
+
var TAG_LINE = /^[ \t]*<\/?[A-Za-z][^<>]*>[ \t]*$/;
|
|
838
|
+
function isBoundLine(line, format) {
|
|
839
|
+
if (ADORNMENT.test(line)) return true;
|
|
840
|
+
switch (format) {
|
|
841
|
+
case "markdown":
|
|
842
|
+
case "mdx":
|
|
843
|
+
return ATX_HEADING.test(line) || TAG_LINE.test(line);
|
|
844
|
+
case "html":
|
|
845
|
+
case "xml":
|
|
846
|
+
return TAG_LINE.test(line);
|
|
847
|
+
case "asciidoc":
|
|
848
|
+
return ASCIIDOC_TITLE.test(line);
|
|
849
|
+
default:
|
|
850
|
+
return false;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
682
853
|
function parseStatements(body, format, from = { offset: 0, line: 1 }) {
|
|
683
854
|
const out = [];
|
|
684
855
|
const fileLine = (bodyOffset) => from.line + lineAt(body, bodyOffset) - 1;
|
|
@@ -721,6 +892,7 @@ function detectEol(text) {
|
|
|
721
892
|
}
|
|
722
893
|
function paragraphAfter(content, offset, format) {
|
|
723
894
|
const marker = (text) => format !== void 0 && isMarkerLine(text, format);
|
|
895
|
+
const bound = (text) => format !== void 0 && isBoundLine(text, format);
|
|
724
896
|
let pos = offset;
|
|
725
897
|
const firstEnd = lineEnd(content, pos);
|
|
726
898
|
if (lineText(content, pos, firstEnd).trim() === "") pos = firstEnd + 1;
|
|
@@ -734,11 +906,11 @@ function paragraphAfter(content, offset, format) {
|
|
|
734
906
|
if (ANY_FENCE.test(text)) return void 0;
|
|
735
907
|
const start = pos;
|
|
736
908
|
let last = end;
|
|
737
|
-
let cursor = end + 1;
|
|
909
|
+
let cursor = bound(text) ? content.length : end + 1;
|
|
738
910
|
while (cursor < content.length) {
|
|
739
911
|
const e = lineEnd(content, cursor);
|
|
740
912
|
const t = lineText(content, cursor, e);
|
|
741
|
-
if (t.trim() === "" || ANY_FENCE.test(t) || marker(t)) break;
|
|
913
|
+
if (t.trim() === "" || ANY_FENCE.test(t) || marker(t) || bound(t)) break;
|
|
742
914
|
last = e;
|
|
743
915
|
cursor = e + 1;
|
|
744
916
|
}
|
|
@@ -853,9 +1025,11 @@ function unitHolding(text, line, format, body) {
|
|
|
853
1025
|
}
|
|
854
1026
|
if (ANY_FENCE.test(own)) return void 0;
|
|
855
1027
|
let start = line;
|
|
856
|
-
while (start - 1 >= body.line) {
|
|
1028
|
+
while (start - 1 >= body.line && !isBoundLine(own, format)) {
|
|
857
1029
|
const above = textOf(start - 1);
|
|
858
|
-
if (above.trim() === "" || ANY_FENCE.test(above) || isMarkerLine(above, format))
|
|
1030
|
+
if (above.trim() === "" || ANY_FENCE.test(above) || isMarkerLine(above, format) || isBoundLine(above, format)) {
|
|
1031
|
+
break;
|
|
1032
|
+
}
|
|
859
1033
|
start--;
|
|
860
1034
|
}
|
|
861
1035
|
const paragraph = paragraphAfter(text, offsetOfLine(text, start), format);
|
|
@@ -894,7 +1068,9 @@ function continuesList(text, line, format, bodyLine, column) {
|
|
|
894
1068
|
if (columnOf(indentOf(textOfLine(text, n))) > column) return true;
|
|
895
1069
|
for (; n >= bodyLine; n--) {
|
|
896
1070
|
const above = textOfLine(text, n);
|
|
897
|
-
if (above.trim() === "" || ANY_FENCE.test(above) || isMarkerLine(above, format))
|
|
1071
|
+
if (above.trim() === "" || ANY_FENCE.test(above) || isMarkerLine(above, format) || isBoundLine(above, format)) {
|
|
1072
|
+
break;
|
|
1073
|
+
}
|
|
898
1074
|
const item = listItemOf(above);
|
|
899
1075
|
if (item !== void 0 && columnOf(item.lead) === column) return true;
|
|
900
1076
|
}
|
|
@@ -947,718 +1123,797 @@ function offsetOfLine(content, line) {
|
|
|
947
1123
|
return pos;
|
|
948
1124
|
}
|
|
949
1125
|
|
|
950
|
-
// src/cite/core/
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
return out.endsWith("\n") ? out.slice(0, -1) : out;
|
|
958
|
-
}
|
|
959
|
-
function blockMatches(blockText, citedJoined) {
|
|
960
|
-
return normalizeBlock(blockText) === normalizeBlock(citedJoined);
|
|
961
|
-
}
|
|
962
|
-
function textAt(lines, range) {
|
|
963
|
-
return lines.slice(range.start - 1, Math.min(range.end, lines.length));
|
|
1126
|
+
// src/cite/core/page.ts
|
|
1127
|
+
var MAX_MARKERS_PER_PAGE = 500;
|
|
1128
|
+
var ELEMENT_BACKED = /* @__PURE__ */ new Set(["html", "xml"]);
|
|
1129
|
+
var FENCE_ONLY = /* @__PURE__ */ new Set(["markdown", "mdx"]);
|
|
1130
|
+
var MARKER_JSON = "A marker names an entry by id. Write the entry in frontmatter or the sidecar.";
|
|
1131
|
+
function ownedMessage(owner) {
|
|
1132
|
+
return `"citations" is owned by manifest ${owner.file} (collection ${owner.collection}); remove it from the document`;
|
|
964
1133
|
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1134
|
+
var Ajv2020 = Ajv2020Ns.default;
|
|
1135
|
+
var entryValidator;
|
|
1136
|
+
function compileEntryValidator() {
|
|
1137
|
+
if (entryValidator) return entryValidator;
|
|
1138
|
+
const ajv = new Ajv2020({ allErrors: false, strict: false });
|
|
1139
|
+
ajv.addSchema(citations_default);
|
|
1140
|
+
entryValidator = ajv.compile({
|
|
1141
|
+
$ref: `${citations_default.$id}#/$defs/citationEntry`
|
|
1142
|
+
});
|
|
1143
|
+
return entryValidator;
|
|
968
1144
|
}
|
|
969
|
-
function
|
|
970
|
-
const
|
|
971
|
-
|
|
972
|
-
|
|
1145
|
+
function validateEntry(entry) {
|
|
1146
|
+
const validate = compileEntryValidator();
|
|
1147
|
+
if (validate(entry)) return void 0;
|
|
1148
|
+
const first = validate.errors?.[0];
|
|
1149
|
+
if (!first) return "invalid entry";
|
|
1150
|
+
return `${first.instancePath} ${first.message ?? "is invalid"}`.trim();
|
|
973
1151
|
}
|
|
974
|
-
function
|
|
975
|
-
|
|
976
|
-
return spec === void 0 ? void 0 : parseLines(spec);
|
|
1152
|
+
function isRecord(value) {
|
|
1153
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
977
1154
|
}
|
|
978
|
-
function
|
|
979
|
-
const
|
|
980
|
-
const
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
if (claim.lines !== void 0) {
|
|
985
|
-
const recorded = parseLines(claim.lines);
|
|
986
|
-
if (recorded === void 0) return { status: "skipped" };
|
|
987
|
-
const file = toFileLines(recorded, body);
|
|
988
|
-
const end = {
|
|
989
|
-
lines: spellLines(recorded),
|
|
990
|
-
fileLines: spellLines(file),
|
|
991
|
-
status: "changed"
|
|
992
|
-
};
|
|
993
|
-
if (marker !== void 0) return { ...end, status: "skipped" };
|
|
994
|
-
if (pinOfLines(lines, file) === claim.integrity) return { ...end, status: "current" };
|
|
995
|
-
const width = recorded.end - recorded.start + 1;
|
|
996
|
-
const found = findWindows(lines.slice(body - 1), width, claim.integrity, void 0, {
|
|
997
|
-
around: recorded.start
|
|
998
|
-
});
|
|
999
|
-
const spans = found.starts.map((start) => ({ start, end: start + width - 1 }));
|
|
1000
|
-
const [only] = spans;
|
|
1001
|
-
if (spans.length === 1 && only !== void 0) {
|
|
1002
|
-
return {
|
|
1003
|
-
...end,
|
|
1004
|
-
status: "moved",
|
|
1005
|
-
newLines: spellLines(only),
|
|
1006
|
-
newFileLines: spellLines(toFileLines(only, body))
|
|
1007
|
-
};
|
|
1008
|
-
}
|
|
1009
|
-
if (spans.length > 1) {
|
|
1010
|
-
return {
|
|
1011
|
-
...end,
|
|
1012
|
-
status: "moved-ambiguous",
|
|
1013
|
-
candidates: spans.map(spellLines),
|
|
1014
|
-
candidateFileLines: spans.map((span) => spellLines(toFileLines(span, body)))
|
|
1015
|
-
};
|
|
1155
|
+
function toCitation(entry) {
|
|
1156
|
+
const source = isRecord(entry.source) ? entry.source : {};
|
|
1157
|
+
const citation = {
|
|
1158
|
+
source: {
|
|
1159
|
+
file: String(source.file),
|
|
1160
|
+
integrity: String(source.integrity)
|
|
1016
1161
|
}
|
|
1017
|
-
|
|
1162
|
+
};
|
|
1163
|
+
if (typeof source.lines === "number" || typeof source.lines === "string") {
|
|
1164
|
+
citation.source.lines = source.lines;
|
|
1018
1165
|
}
|
|
1019
|
-
if (
|
|
1020
|
-
|
|
1021
|
-
if (unit === void 0) return { status: "changed" };
|
|
1022
|
-
const end = { fileLines: spellLines(unit), status: "changed" };
|
|
1023
|
-
if (pinOfLines(lines, unit) === claim.integrity) return { ...end, status: "current" };
|
|
1024
|
-
return { ...end, text: textAt(lines, unit) };
|
|
1166
|
+
if (typeof source["commit-sha"] === "string") {
|
|
1167
|
+
citation.source["commit-sha"] = source["commit-sha"];
|
|
1025
1168
|
}
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
return { lines: block, kind: "block", text: lines.slice(block.start - 1, block.end) };
|
|
1169
|
+
if (typeof entry.id === "string") citation.id = entry.id;
|
|
1170
|
+
if (isRecord(entry.claim)) {
|
|
1171
|
+
const claim = entry.claim;
|
|
1172
|
+
citation.claim = { integrity: String(claim.integrity) };
|
|
1173
|
+
if (typeof claim.lines === "number" || typeof claim.lines === "string") {
|
|
1174
|
+
citation.claim.lines = claim.lines;
|
|
1175
|
+
}
|
|
1034
1176
|
}
|
|
1035
|
-
if (
|
|
1036
|
-
|
|
1037
|
-
if (paragraph === void 0 || paragraph.line !== line) return void 0;
|
|
1038
|
-
const span = { start: line, end: lineAt(page.content, paragraph.end) };
|
|
1039
|
-
return { lines: span, kind: "paragraph", text: lines.slice(span.start - 1, span.end) };
|
|
1177
|
+
if (typeof entry.quote === "boolean") citation.quote = entry.quote;
|
|
1178
|
+
return citation;
|
|
1040
1179
|
}
|
|
1041
|
-
function
|
|
1042
|
-
const
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1180
|
+
function finding(rule, message, extra) {
|
|
1181
|
+
const level = DEFAULT_SEVERITY[rule];
|
|
1182
|
+
const out = {
|
|
1183
|
+
rule,
|
|
1184
|
+
ruleId: ruleId(rule),
|
|
1185
|
+
// The caller re-applies the configured severity table; no page-side rule
|
|
1186
|
+
// defaults to `off`, so there is nothing to collapse.
|
|
1187
|
+
severity: level === "off" ? "error" : level,
|
|
1188
|
+
message
|
|
1050
1189
|
};
|
|
1190
|
+
if (extra.line !== void 0) out.line = extra.line;
|
|
1191
|
+
if (extra.file !== void 0) out.file = extra.file;
|
|
1192
|
+
if (extra.id !== void 0) out.id = extra.id;
|
|
1193
|
+
if (extra.src !== void 0) out.src = extra.src;
|
|
1194
|
+
if (extra.index !== void 0) out.index = extra.index;
|
|
1195
|
+
return out;
|
|
1051
1196
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
import { execFile } from "node:child_process";
|
|
1055
|
-
var MAX_BUFFER = 32 * 1024 * 1024;
|
|
1056
|
-
var NO_COMMIT = /invalid object name|bad revision|unknown revision|bad object/i;
|
|
1057
|
-
var NO_PATH = /does not exist in|exists on disk, but not in|path .* does not exist/i;
|
|
1058
|
-
function run2(root, args) {
|
|
1059
|
-
return new Promise((resolve8) => {
|
|
1060
|
-
execFile(
|
|
1061
|
-
"git",
|
|
1062
|
-
["-c", "core.quotepath=false", ...args],
|
|
1063
|
-
{ cwd: root, windowsHide: true, maxBuffer: MAX_BUFFER, encoding: "utf8" },
|
|
1064
|
-
(error, stdout, stderr) => {
|
|
1065
|
-
if (error) {
|
|
1066
|
-
const failure = { ok: false, stderr, message: error.message };
|
|
1067
|
-
if (typeof error.code === "number") failure.code = error.code;
|
|
1068
|
-
resolve8(failure);
|
|
1069
|
-
} else resolve8({ ok: true, stdout });
|
|
1070
|
-
}
|
|
1071
|
-
);
|
|
1072
|
-
});
|
|
1073
|
-
}
|
|
1074
|
-
function gitClient(root) {
|
|
1075
|
-
const memo = /* @__PURE__ */ new Map();
|
|
1076
|
-
const once = (key, compute) => {
|
|
1077
|
-
const held = memo.get(key);
|
|
1078
|
-
if (held !== void 0) return held;
|
|
1079
|
-
const made = compute();
|
|
1080
|
-
memo.set(key, made);
|
|
1081
|
-
return made;
|
|
1082
|
-
};
|
|
1083
|
-
const available = () => once("available", async () => (await run2(root, ["rev-parse", "--show-toplevel"])).ok);
|
|
1084
|
-
const failed = (verb, outcome) => new CiteError(`git ${verb} failed: ${outcome.stderr.trim() || outcome.message}`);
|
|
1085
|
-
const hasCommit = (commit) => once(`has\0${commit}`, async () => {
|
|
1086
|
-
const out = await run2(root, ["rev-parse", "--verify", "--quiet", "--end-of-options", `${commit}^{commit}`]);
|
|
1087
|
-
if (out.ok) return true;
|
|
1088
|
-
if (out.code === 1) return false;
|
|
1089
|
-
throw failed("rev-parse", out);
|
|
1090
|
-
});
|
|
1091
|
-
return {
|
|
1092
|
-
available,
|
|
1093
|
-
head: () => once("head", async () => {
|
|
1094
|
-
if (!await available()) return null;
|
|
1095
|
-
const out = await run2(root, ["rev-parse", "HEAD"]);
|
|
1096
|
-
return out.ok ? out.stdout.trim() : null;
|
|
1097
|
-
}),
|
|
1098
|
-
lsFiles: () => once("ls-files", async () => {
|
|
1099
|
-
if (!await available()) return [];
|
|
1100
|
-
const out = await run2(root, ["ls-files", "-z"]);
|
|
1101
|
-
if (!out.ok) throw failed("ls-files", out);
|
|
1102
|
-
return out.stdout.split("\0").filter((entry) => entry !== "");
|
|
1103
|
-
}),
|
|
1104
|
-
showFile: (commit, path) => once(`show\0${commit}\0${path}`, async () => {
|
|
1105
|
-
const out = await run2(root, ["show", "--end-of-options", `${commit}:./${path}`]);
|
|
1106
|
-
if (out.ok) return { text: out.stdout };
|
|
1107
|
-
if (NO_COMMIT.test(out.stderr)) return { missing: "commit" };
|
|
1108
|
-
if (NO_PATH.test(out.stderr)) {
|
|
1109
|
-
return await hasCommit(commit) ? { missing: "path" } : { missing: "commit" };
|
|
1110
|
-
}
|
|
1111
|
-
throw failed("show", out);
|
|
1112
|
-
}),
|
|
1113
|
-
subjectsSince: (commit, path) => once(`log\0${commit}\0${path}`, async () => {
|
|
1114
|
-
const out = await run2(root, ["log", "--format=%s", "--end-of-options", `${commit}..HEAD`, "--", path]);
|
|
1115
|
-
if (!out.ok) throw failed("log", out);
|
|
1116
|
-
return out.stdout.split(/\r?\n/).filter((line) => line !== "");
|
|
1117
|
-
}),
|
|
1118
|
-
diffSince: (commit, path) => once(`diff\0${commit}\0${path}`, async () => {
|
|
1119
|
-
const out = await run2(root, ["diff", "--end-of-options", commit, "--", path]);
|
|
1120
|
-
if (!out.ok) throw failed("diff", out);
|
|
1121
|
-
return out.stdout;
|
|
1122
|
-
})
|
|
1123
|
-
};
|
|
1197
|
+
function named(id, text) {
|
|
1198
|
+
return id === void 0 ? text : `${id}: ${text}`;
|
|
1124
1199
|
}
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
lsFiles: () => Promise.resolve([]),
|
|
1132
|
-
showFile: () => Promise.resolve({ missing: "commit" }),
|
|
1133
|
-
subjectsSince: () => Promise.resolve([]),
|
|
1134
|
-
diffSince: () => Promise.resolve("")
|
|
1135
|
-
};
|
|
1200
|
+
function subjectOf(entry) {
|
|
1201
|
+
const out = {};
|
|
1202
|
+
if (!isRecord(entry)) return out;
|
|
1203
|
+
if (typeof entry.id === "string") out.id = entry.id;
|
|
1204
|
+
if (isRecord(entry.source) && typeof entry.source.file === "string") out.src = entry.source.file;
|
|
1205
|
+
return out;
|
|
1136
1206
|
}
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
try {
|
|
1147
|
-
joined = sliceLines2(then, range);
|
|
1148
|
-
} catch {
|
|
1149
|
-
joined = void 0;
|
|
1150
|
-
}
|
|
1151
|
-
if (joined !== void 0 && hashLines(joined, key) === pin) {
|
|
1152
|
-
return { kind: "original", lines: joined.split("\n") };
|
|
1207
|
+
function pickExtractor(file, format) {
|
|
1208
|
+
if (format !== void 0) {
|
|
1209
|
+
const forced = extractorByName(format);
|
|
1210
|
+
if (forced === void 0) {
|
|
1211
|
+
throw new CiteError(
|
|
1212
|
+
`Unknown format "${format}". Supported extensions: ${supportedExtensions().join(", ")}.`
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
return forced;
|
|
1153
1216
|
}
|
|
1154
|
-
|
|
1155
|
-
const
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
if (first !== void 0) {
|
|
1161
|
-
return { kind: "original", lines: then.slice(first - 1, first - 1 + length) };
|
|
1217
|
+
const extension = extname2(file);
|
|
1218
|
+
const byExt = extractorForExtension(extension);
|
|
1219
|
+
if (!byExt) {
|
|
1220
|
+
throw new CiteError(
|
|
1221
|
+
`Unsupported file type "${extension}" for "${file}". Supported: ${supportedExtensions().join(", ")}. Use --as to override.`
|
|
1222
|
+
);
|
|
1162
1223
|
}
|
|
1163
|
-
return
|
|
1224
|
+
return byExt;
|
|
1164
1225
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1226
|
+
function byLine(a, b) {
|
|
1227
|
+
return (a.line ?? Number.MAX_SAFE_INTEGER) - (b.line ?? Number.MAX_SAFE_INTEGER);
|
|
1228
|
+
}
|
|
1229
|
+
function bodyLineOf(content, bodyOffset) {
|
|
1230
|
+
if (bodyOffset <= 0) return 1;
|
|
1231
|
+
const line = lineAt(content, bodyOffset);
|
|
1232
|
+
return content.charCodeAt(bodyOffset - 1) === 10 ? line : line + 1;
|
|
1233
|
+
}
|
|
1234
|
+
function citationInputs(file, extracted, injected) {
|
|
1235
|
+
if (injected !== void 0) return [...injected];
|
|
1236
|
+
const raw = extracted.data.citations;
|
|
1237
|
+
const list = Array.isArray(raw) ? raw : [];
|
|
1238
|
+
return list.map((entry, index) => {
|
|
1239
|
+
const origin = { kind: "frontmatter", file };
|
|
1240
|
+
const line = extracted.lineFor(`/citations/${String(index)}`);
|
|
1241
|
+
if (line !== void 0) origin.line = line;
|
|
1242
|
+
return { entry, origin };
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
function readPage(file, content, opts) {
|
|
1246
|
+
const extractor = pickExtractor(file, opts?.format);
|
|
1247
|
+
const format = extractor.name;
|
|
1248
|
+
if (FENCE_ONLY.has(format) && hasFrontmatterFence(content) && locateFrontmatter(content) === null) {
|
|
1249
|
+
throw new CiteError(
|
|
1250
|
+
`${file}: Unterminated front matter fence: the opening fence has no matching close, so the page's citations cannot be read. Add a closing fence.`
|
|
1251
|
+
);
|
|
1175
1252
|
}
|
|
1176
|
-
|
|
1177
|
-
const
|
|
1178
|
-
const
|
|
1179
|
-
const
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1253
|
+
const extracted = extractor.extract(content, file);
|
|
1254
|
+
const data = extracted.data;
|
|
1255
|
+
const bodyOffset = ELEMENT_BACKED.has(format) ? 0 : locateFrontmatter(content)?.closeEnd ?? 0;
|
|
1256
|
+
const bodyLine = bodyLineOf(content, bodyOffset);
|
|
1257
|
+
const findings = [];
|
|
1258
|
+
const citations = [];
|
|
1259
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1260
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1261
|
+
const markedAt = /* @__PURE__ */ new Map();
|
|
1262
|
+
for (const key of Object.keys(data)) {
|
|
1263
|
+
if (!key.startsWith("citation-")) continue;
|
|
1264
|
+
findings.push(
|
|
1265
|
+
finding(
|
|
1266
|
+
"entry-invalid",
|
|
1267
|
+
`"${key}" is not a citations key; the page root reserves the citation- prefix`,
|
|
1268
|
+
{ line: extracted.lineFor(`/${key}`) }
|
|
1269
|
+
)
|
|
1270
|
+
);
|
|
1185
1271
|
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1272
|
+
const injected = opts?.citations;
|
|
1273
|
+
const rawCitations = data.citations;
|
|
1274
|
+
if (opts?.owned !== void 0 && rawCitations !== void 0) {
|
|
1275
|
+
findings.push(
|
|
1276
|
+
finding("entry-invalid", ownedMessage(opts.owned), {
|
|
1277
|
+
line: extracted.lineFor("/citations")
|
|
1278
|
+
})
|
|
1279
|
+
);
|
|
1189
1280
|
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1281
|
+
if (injected === void 0 && rawCitations !== void 0 && !Array.isArray(rawCitations)) {
|
|
1282
|
+
findings.push(
|
|
1283
|
+
finding("entry-invalid", "citations must be an array of entries", {
|
|
1284
|
+
line: extracted.lineFor("/citations")
|
|
1285
|
+
})
|
|
1286
|
+
);
|
|
1193
1287
|
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
const
|
|
1197
|
-
const
|
|
1198
|
-
if (
|
|
1199
|
-
if (
|
|
1200
|
-
const
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
result.status = "moved";
|
|
1206
|
-
result.newSrc = spell(only);
|
|
1207
|
-
result.newLines = String(lineSpec({ start: only, end: only + (end - start) }));
|
|
1208
|
-
return result;
|
|
1288
|
+
const inputs = citationInputs(file, extracted, injected);
|
|
1289
|
+
inputs.forEach((input, index) => {
|
|
1290
|
+
const origin = { ...input.origin, index };
|
|
1291
|
+
const where = { index };
|
|
1292
|
+
if (origin.line !== void 0) where.line = origin.line;
|
|
1293
|
+
if (origin.kind === "manifest") where.file = origin.file;
|
|
1294
|
+
const entry = input.entry;
|
|
1295
|
+
const error = validateEntry(entry);
|
|
1296
|
+
if (error !== void 0 || !isRecord(entry)) {
|
|
1297
|
+
findings.push(finding("entry-invalid", error ?? "must be object", { ...where, ...subjectOf(entry) }));
|
|
1298
|
+
return;
|
|
1209
1299
|
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1300
|
+
const citation = toCitation(entry);
|
|
1301
|
+
const subject = { ...where, src: spellSource(citation.source) };
|
|
1302
|
+
if (citation.id !== void 0) subject.id = citation.id;
|
|
1303
|
+
const bad = ["claim", "source"].find((end) => {
|
|
1304
|
+
const lines = end === "claim" ? citation.claim?.lines : citation.source.lines;
|
|
1305
|
+
return lines !== void 0 && parseLines(lines) === void 0;
|
|
1306
|
+
});
|
|
1307
|
+
if (bad !== void 0) {
|
|
1308
|
+
const lines = bad === "claim" ? citation.claim?.lines : citation.source.lines;
|
|
1309
|
+
findings.push(
|
|
1310
|
+
finding(
|
|
1311
|
+
"entry-invalid",
|
|
1312
|
+
named(citation.id, `${bad}.lines "${String(lines)}" ends before it starts`),
|
|
1313
|
+
subject
|
|
1314
|
+
)
|
|
1315
|
+
);
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
const wide = ["claim", "source"].map((end) => {
|
|
1319
|
+
const lines = end === "claim" ? citation.claim?.lines : citation.source.lines;
|
|
1320
|
+
const parsed = lines === void 0 ? void 0 : parseLines(lines);
|
|
1321
|
+
const reason = parsed === void 0 ? void 0 : tooWide(parsed);
|
|
1322
|
+
return reason === void 0 ? void 0 : `${end}.lines "${String(lines)}" ${reason}`;
|
|
1323
|
+
}).find((message) => message !== void 0);
|
|
1324
|
+
if (wide !== void 0) {
|
|
1325
|
+
findings.push(finding("entry-invalid", named(citation.id, wide), subject));
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
const encrypted = isEncryptedValue(citation.source.file);
|
|
1329
|
+
if (encrypted !== isKeyedPin(citation.source.integrity)) {
|
|
1330
|
+
findings.push(
|
|
1331
|
+
finding(
|
|
1332
|
+
"entry-invalid",
|
|
1333
|
+
named(
|
|
1334
|
+
citation.id,
|
|
1335
|
+
encrypted ? "an encrypted source is pinned with hmac-sha256-, not sha256-." : "a plain source is pinned with sha256-, not hmac-sha256-."
|
|
1336
|
+
),
|
|
1337
|
+
subject
|
|
1338
|
+
)
|
|
1339
|
+
);
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
const page = { citation, origin };
|
|
1343
|
+
citations.push(page);
|
|
1344
|
+
if (citation.id !== void 0) {
|
|
1345
|
+
if (ids.has(citation.id)) {
|
|
1346
|
+
findings.push(finding("entry-invalid", `duplicate id "${citation.id}"`, subject));
|
|
1347
|
+
} else {
|
|
1348
|
+
ids.add(citation.id);
|
|
1349
|
+
byId.set(citation.id, page);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
});
|
|
1353
|
+
const body = content.slice(bodyOffset);
|
|
1354
|
+
const statements = parseStatements(body, format, {
|
|
1355
|
+
offset: bodyOffset,
|
|
1356
|
+
line: bodyLine
|
|
1357
|
+
});
|
|
1358
|
+
if (statements.length > MAX_MARKERS_PER_PAGE) {
|
|
1359
|
+
findings.push(
|
|
1360
|
+
finding(
|
|
1361
|
+
"marker-invalid",
|
|
1362
|
+
`more than ${String(MAX_MARKERS_PER_PAGE)} markers on one page (${String(statements.length)}); the rest are not read`,
|
|
1363
|
+
{ line: statements[MAX_MARKERS_PER_PAGE]?.line }
|
|
1364
|
+
)
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
for (const statement of statements.slice(0, MAX_MARKERS_PER_PAGE)) {
|
|
1368
|
+
const { payload } = statement;
|
|
1369
|
+
if (payload.kind === "bad") {
|
|
1370
|
+
findings.push(
|
|
1371
|
+
finding(
|
|
1372
|
+
"marker-invalid",
|
|
1373
|
+
payload.json === true ? MARKER_JSON : `invalid marker: ${payload.reason}`,
|
|
1374
|
+
{ line: statement.line }
|
|
1375
|
+
)
|
|
1376
|
+
);
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
const target = byId.get(payload.id);
|
|
1380
|
+
if (target === void 0) {
|
|
1381
|
+
findings.push(
|
|
1382
|
+
finding("marker-orphan", `no entry has id "${payload.id}"`, {
|
|
1383
|
+
line: statement.line,
|
|
1384
|
+
id: payload.id
|
|
1385
|
+
})
|
|
1386
|
+
);
|
|
1387
|
+
continue;
|
|
1388
|
+
}
|
|
1389
|
+
const first = markedAt.get(target);
|
|
1390
|
+
if (first !== void 0) {
|
|
1391
|
+
findings.push(
|
|
1392
|
+
finding(
|
|
1393
|
+
"marker-repeated",
|
|
1394
|
+
`${payload.id} is named by markers at lines ${String(first)} and ${String(statement.line)}; the first anchors it.`,
|
|
1395
|
+
{
|
|
1396
|
+
line: statement.line,
|
|
1397
|
+
id: payload.id,
|
|
1398
|
+
index: target.origin.index,
|
|
1399
|
+
src: spellSource(target.citation.source)
|
|
1400
|
+
}
|
|
1401
|
+
)
|
|
1402
|
+
);
|
|
1403
|
+
continue;
|
|
1214
1404
|
}
|
|
1405
|
+
markedAt.set(target, statement.line);
|
|
1406
|
+
target.marker = statement;
|
|
1215
1407
|
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
import * as Ajv2020Ns from "ajv/dist/2020.js";
|
|
1234
|
-
|
|
1235
|
-
// src/cite/schema/citations.json
|
|
1236
|
-
var citations_default = {
|
|
1237
|
-
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
1238
|
-
$id: "manni:citations:1.0.0-proposal.4",
|
|
1239
|
-
title: "manni citations vocabulary (1.0.0-proposal.4)",
|
|
1240
|
-
description: "What do this page's claims rest on? For each claim, the page lines that make it, the source lines it was written from, and a hash of each. A manni common vocabulary: any drift check can implement it, and other schemas may compose on top of it. A pin is a record of the past, not a copy of the present. It says what the lines were when the sentence was written. The check compares that record with the files as they are now. Recompute it on every run and there is nothing to compare against. `source-of-truth` (manni:stewardship) names the file a page answers to; a citation names the lines a sentence answers to. The page root stays open so sibling tools' keys pass untouched. The `citation-` prefix is reserved, so an unrecognized `citation-*` key is rejected and a typo fails loudly.",
|
|
1241
|
-
type: "object",
|
|
1242
|
-
additionalProperties: true,
|
|
1243
|
-
properties: {
|
|
1244
|
-
citations: {
|
|
1245
|
-
type: "array",
|
|
1246
|
-
minItems: 1,
|
|
1247
|
-
items: {
|
|
1248
|
-
$ref: "#/$defs/citationEntry"
|
|
1249
|
-
},
|
|
1250
|
-
description: "The page's citations, one entry per claim or pin. An empty list is not a declaration; a page with no citations omits the key. Ids are unique within a page, and two entries claiming one id is an error. JSON Schema cannot say that over a list whose members may omit the id. So it is a requirement on implementations, stated here so every tool agrees.",
|
|
1251
|
-
"x-manni-location": "external"
|
|
1408
|
+
for (const entry of citations) {
|
|
1409
|
+
const { citation, origin, marker } = entry;
|
|
1410
|
+
const subject = { index: origin.index, src: spellSource(citation.source) };
|
|
1411
|
+
if (citation.id !== void 0) subject.id = citation.id;
|
|
1412
|
+
if (origin.line !== void 0) subject.line = origin.line;
|
|
1413
|
+
if (origin.kind === "manifest") subject.file = origin.file;
|
|
1414
|
+
if (citation.claim?.lines !== void 0 && marker !== void 0) {
|
|
1415
|
+
const onPage = { index: origin.index, line: marker.line, src: subject.src };
|
|
1416
|
+
if (citation.id !== void 0) onPage.id = citation.id;
|
|
1417
|
+
findings.push(
|
|
1418
|
+
finding(
|
|
1419
|
+
"anchor-invalid",
|
|
1420
|
+
`${citation.id ?? "the entry"} has claim lines and a marker. Keep one.`,
|
|
1421
|
+
onPage
|
|
1422
|
+
)
|
|
1423
|
+
);
|
|
1424
|
+
continue;
|
|
1252
1425
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
$defs: {
|
|
1258
|
-
citationEntry: {
|
|
1259
|
-
type: "object",
|
|
1260
|
-
additionalProperties: false,
|
|
1261
|
-
required: [
|
|
1262
|
-
"source"
|
|
1263
|
-
],
|
|
1264
|
-
description: "One citation: the page text it supports, the source lines it rests on, and how the two are anchored. Both ends share one shape, a line range and a hash.",
|
|
1265
|
-
properties: {
|
|
1266
|
-
id: {
|
|
1267
|
-
type: "string",
|
|
1268
|
-
pattern: "^[a-z0-9][a-z0-9-]*$",
|
|
1269
|
-
description: "A kebab-case name for this citation, unique within the page. A body marker names an entry by it, and findings are reported under it. Optional for an entry anchored by its claim lines or pinned bare. Required when a marker names the entry."
|
|
1270
|
-
},
|
|
1271
|
-
claim: {
|
|
1272
|
-
$ref: "#/$defs/claim"
|
|
1273
|
-
},
|
|
1274
|
-
source: {
|
|
1275
|
-
$ref: "#/$defs/source"
|
|
1276
|
-
},
|
|
1277
|
-
quote: {
|
|
1278
|
-
type: "boolean",
|
|
1279
|
-
default: false,
|
|
1280
|
-
description: "The claim is a fenced block that reproduces the source lines. The pinned claim covers the whole block, fences included. An implementer compares the lines inside the fences with the source under the pin's rule. So a quoted snippet that drifts from its source is a finding even while the pin still holds. Under a marker, the block is the next fence after the marker. Default false."
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
},
|
|
1284
|
-
claim: {
|
|
1285
|
-
type: "object",
|
|
1286
|
-
additionalProperties: false,
|
|
1287
|
-
required: [
|
|
1288
|
-
"integrity"
|
|
1289
|
-
],
|
|
1290
|
-
description: "The page text the citation supports, pinned by lines and a hash. Absent, the entry is a bare pin, or a marker with no drift check on its sentence. Without `lines`, the text pinned is what a body marker naming this entry anchors.",
|
|
1291
|
-
properties: {
|
|
1292
|
-
lines: {
|
|
1293
|
-
$ref: "#/$defs/lines",
|
|
1294
|
-
description: "Lines of the page body. The body starts on the first line after the closing frontmatter fence, or on line 1 with no frontmatter. So editing the frontmatter never moves a claim. Required unless a marker names the entry, and never inside the frontmatter."
|
|
1295
|
-
},
|
|
1296
|
-
integrity: {
|
|
1297
|
-
type: "string",
|
|
1298
|
-
pattern: "^sha256-[0-9a-f]{64}$",
|
|
1299
|
-
description: "`sha256-` and sixty-four lowercase hex digits over the claimed lines, under the rule `source.integrity` states. Always plain, never keyed: the page is public, so there is nothing to hide. With a marker, the lines hashed are the ones the marker anchors."
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
},
|
|
1303
|
-
source: {
|
|
1304
|
-
type: "object",
|
|
1305
|
-
additionalProperties: false,
|
|
1306
|
-
required: [
|
|
1307
|
-
"file",
|
|
1308
|
-
"integrity"
|
|
1309
|
-
],
|
|
1310
|
-
description: "The lines the claim rests on: a file, an optional line range, a pin, and the commit the pin was taken at.",
|
|
1311
|
-
properties: {
|
|
1312
|
-
file: {
|
|
1313
|
-
$ref: "#/$defs/fileRef"
|
|
1314
|
-
},
|
|
1315
|
-
lines: {
|
|
1316
|
-
$ref: "#/$defs/lines",
|
|
1317
|
-
description: "File lines. Absent, the whole file is pinned. The lines stay readable even when `file` is encrypted."
|
|
1318
|
-
},
|
|
1319
|
-
integrity: {
|
|
1320
|
-
type: "string",
|
|
1321
|
-
pattern: "^(?:sha256|hmac-sha256)-[0-9a-f]{64}$",
|
|
1322
|
-
description: "The pin: an algorithm prefix and sixty-four lowercase hex digits over the cited lines under one rule. Decode as UTF-8 and drop one leading byte-order mark. Turn CRLF into LF, split on LF, and discard the empty element a trailing LF leaves. Take lines L1 to L2 inclusive, or every line for a whole file. Join with LF and no trailing LF, and keep trailing whitespace. A plain `file` is pinned `sha256-` over that text. An encrypted `file` is pinned `hmac-sha256-`: the HMAC-SHA256 of that text under a key derived from the encryption key. So a public page carries no verifier a reader could run against a guessed private line. The prefix follows `file`, and any other pairing is an invalid entry."
|
|
1323
|
-
},
|
|
1324
|
-
"commit-sha": {
|
|
1325
|
-
type: "string",
|
|
1326
|
-
pattern: "^[0-9a-f]{7,64}$",
|
|
1327
|
-
description: "The git commit hash the source pin was taken at, as lowercase hex. Seven digits is what git abbreviates to; sixty-four covers a SHA-256 repository. A tool writes the full hash, because an abbreviation that is unique today need not be next year. Without it, a check can still say the lines changed. With it, the check can say since when, and whether the pin was ever true."
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
},
|
|
1331
|
-
lines: {
|
|
1332
|
-
oneOf: [
|
|
1333
|
-
{
|
|
1334
|
-
type: "integer",
|
|
1335
|
-
minimum: 1
|
|
1336
|
-
},
|
|
1337
|
-
{
|
|
1338
|
-
type: "string",
|
|
1339
|
-
pattern: "^[1-9][0-9]*-[1-9][0-9]*$"
|
|
1340
|
-
}
|
|
1341
|
-
],
|
|
1342
|
-
description: 'A line range, 1-based and inclusive: an integer for one line, or `"L1-L2"` for several. `L1 <= L2` is a rule on implementations, since a pattern cannot compare two numbers. There is no line 0 to cite.'
|
|
1343
|
-
},
|
|
1344
|
-
fileRef: {
|
|
1345
|
-
type: "string",
|
|
1346
|
-
pattern: "^(?:~[A-Za-z0-9_-]{82,}|(?!~)(?:(?!\\.\\.?(?:/|$))[^/\\\\:\\r\\n\\t]+)(?:/(?:(?!\\.\\.?(?:/|$))[^/\\\\:\\r\\n\\t]+))*)$",
|
|
1347
|
-
description: "The file: a path relative to the repository root, posix-separated, and free of `.` and `..` segments. Segments may hold spaces and dots but not `/`, `\\`, `:` or a line break. So a URL, a drive letter, a Windows path and an absolute path all fail here. In place of the path, `~` and at least 82 base64url characters is the path encrypted with the family key. Proposal 0045 states the construction. Only a checkout with the key and the files can decrypt the path and read the lines. So a public docs repo can cite a private code repo without publishing its paths. The value carries no encryption mark, because the citation tool encrypts it, and a plain path stays valid."
|
|
1426
|
+
if (citation.quote === true && citation.claim?.lines === void 0 && marker === void 0) {
|
|
1427
|
+
findings.push(
|
|
1428
|
+
finding("anchor-invalid", named(citation.id, "quote needs a claim or a marker."), subject)
|
|
1429
|
+
);
|
|
1348
1430
|
}
|
|
1349
1431
|
}
|
|
1350
|
-
|
|
1432
|
+
return {
|
|
1433
|
+
file,
|
|
1434
|
+
format,
|
|
1435
|
+
content,
|
|
1436
|
+
bodyOffset,
|
|
1437
|
+
bodyLine,
|
|
1438
|
+
citations,
|
|
1439
|
+
statements,
|
|
1440
|
+
findings: findings.sort(byLine)
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1351
1443
|
|
|
1352
|
-
// src/cite/core/
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
return
|
|
1444
|
+
// src/cite/core/reanchor.ts
|
|
1445
|
+
function isUnitText(line, format) {
|
|
1446
|
+
if (line === void 0) return false;
|
|
1447
|
+
if (line.trim() === "") return false;
|
|
1448
|
+
if (ANY_FENCE.test(line)) return false;
|
|
1449
|
+
if (isMarkerLine(line, format)) return false;
|
|
1450
|
+
return !isBoundLine(line, format);
|
|
1451
|
+
}
|
|
1452
|
+
function markerAt(lines, n, format) {
|
|
1453
|
+
const line = lines[n - 1];
|
|
1454
|
+
return line !== void 0 && isMarkerLine(line, format);
|
|
1455
|
+
}
|
|
1456
|
+
function markerRunAt(lines, line, format, bodyLine) {
|
|
1457
|
+
let start = line;
|
|
1458
|
+
while (start - 1 >= bodyLine && markerAt(lines, start - 1, format)) start--;
|
|
1459
|
+
let end = line;
|
|
1460
|
+
while (end + 1 <= lines.length && markerAt(lines, end + 1, format)) end++;
|
|
1461
|
+
return { start, end };
|
|
1462
|
+
}
|
|
1463
|
+
function splitUnit(lines, run3, format, bodyLine) {
|
|
1464
|
+
if (run3.start - 1 < bodyLine) return void 0;
|
|
1465
|
+
if (!isUnitText(lines[run3.start - 2], format)) return void 0;
|
|
1466
|
+
if (!isUnitText(lines[run3.end], format)) return void 0;
|
|
1467
|
+
let start = run3.start - 1;
|
|
1468
|
+
while (start - 1 >= bodyLine && isUnitText(lines[start - 2], format)) start--;
|
|
1469
|
+
let end = run3.end + 1;
|
|
1470
|
+
while (end + 1 <= lines.length && isUnitText(lines[end], format)) end++;
|
|
1471
|
+
return { start, end };
|
|
1472
|
+
}
|
|
1473
|
+
function pre43Span(lines, markerLine) {
|
|
1474
|
+
const start = markerLine + 1;
|
|
1475
|
+
const first = lines[start - 1];
|
|
1476
|
+
if (first === void 0 || first.trim() === "" || ANY_FENCE.test(first)) return void 0;
|
|
1477
|
+
let end = start;
|
|
1478
|
+
while (end + 1 <= lines.length) {
|
|
1479
|
+
const next = lines[end];
|
|
1480
|
+
if (next === void 0 || next.trim() === "" || ANY_FENCE.test(next)) break;
|
|
1481
|
+
end++;
|
|
1482
|
+
}
|
|
1483
|
+
return { start, end };
|
|
1484
|
+
}
|
|
1485
|
+
function runLength(run3) {
|
|
1486
|
+
return run3.end - run3.start + 1;
|
|
1487
|
+
}
|
|
1488
|
+
function misplacedMarkerAt(page, lines, line, entry) {
|
|
1489
|
+
if (!markerAt(lines, line, page.format)) return void 0;
|
|
1490
|
+
const run3 = markerRunAt(lines, line, page.format, page.bodyLine);
|
|
1491
|
+
const unit = splitUnit(lines, run3, page.format, page.bodyLine);
|
|
1492
|
+
if (unit === void 0) return void 0;
|
|
1493
|
+
const order = line - run3.start;
|
|
1494
|
+
const quote = entry?.citation.quote === true;
|
|
1495
|
+
const marker = entry?.marker;
|
|
1496
|
+
const blockAt = quote && marker !== void 0 ? anchoredLines(page.content, marker.end, page.format, true)?.start : void 0;
|
|
1497
|
+
const out = blockAt === void 0 ? { line, run: run3, order, unit, place: unit.start, block: false, to: unit.start + order } : {
|
|
1498
|
+
line,
|
|
1499
|
+
run: run3,
|
|
1500
|
+
order,
|
|
1501
|
+
unit,
|
|
1502
|
+
place: blockAt,
|
|
1503
|
+
block: true,
|
|
1504
|
+
to: blockAt - runLength(run3) + order
|
|
1505
|
+
};
|
|
1506
|
+
if (entry !== void 0) out.entry = entry;
|
|
1507
|
+
return out;
|
|
1359
1508
|
}
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
});
|
|
1369
|
-
return entryValidator;
|
|
1509
|
+
function misplacedMarkers(page, lines) {
|
|
1510
|
+
const out = [];
|
|
1511
|
+
for (const statement of page.statements.slice(0, MAX_MARKERS_PER_PAGE)) {
|
|
1512
|
+
const entry = page.citations.find((c) => c.marker?.line === statement.line);
|
|
1513
|
+
const found = misplacedMarkerAt(page, lines, statement.line, entry);
|
|
1514
|
+
if (found !== void 0) out.push(found);
|
|
1515
|
+
}
|
|
1516
|
+
return out;
|
|
1370
1517
|
}
|
|
1371
|
-
function
|
|
1372
|
-
|
|
1373
|
-
if (validate(entry)) return void 0;
|
|
1374
|
-
const first = validate.errors?.[0];
|
|
1375
|
-
if (!first) return "invalid entry";
|
|
1376
|
-
return `${first.instancePath} ${first.message ?? "is invalid"}`.trim();
|
|
1518
|
+
function movedUnit(misplaced) {
|
|
1519
|
+
return { start: misplaced.unit.start + runLength(misplaced.run), end: misplaced.unit.end };
|
|
1377
1520
|
}
|
|
1378
|
-
function
|
|
1379
|
-
|
|
1521
|
+
function unitText(lines, misplaced) {
|
|
1522
|
+
const { unit, run: run3 } = misplaced;
|
|
1523
|
+
const out = [];
|
|
1524
|
+
for (let n = unit.start; n <= unit.end; n++) {
|
|
1525
|
+
if (n >= run3.start && n <= run3.end) continue;
|
|
1526
|
+
const line = lines[n - 1];
|
|
1527
|
+
if (line !== void 0) out.push(line);
|
|
1528
|
+
}
|
|
1529
|
+
return out;
|
|
1380
1530
|
}
|
|
1381
|
-
function
|
|
1382
|
-
const
|
|
1383
|
-
const
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1531
|
+
function splitKeepingEol(content) {
|
|
1532
|
+
const parts = content.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
1533
|
+
const texts = [];
|
|
1534
|
+
const eols = [];
|
|
1535
|
+
for (const part of parts) {
|
|
1536
|
+
const eol = /\r?\n$/.exec(part)?.[0] ?? "";
|
|
1537
|
+
texts.push(part.slice(0, part.length - eol.length));
|
|
1538
|
+
eols.push(eol);
|
|
1539
|
+
}
|
|
1540
|
+
return { texts, eols };
|
|
1541
|
+
}
|
|
1542
|
+
function applyMarkerMoves(content, moves, indentAt) {
|
|
1543
|
+
const { texts, eols } = splitKeepingEol(content);
|
|
1544
|
+
const moving = new Set(moves.map((move) => move.from));
|
|
1545
|
+
const inserts = /* @__PURE__ */ new Map();
|
|
1546
|
+
for (const move of moves) {
|
|
1547
|
+
const list = inserts.get(move.place) ?? [];
|
|
1548
|
+
list.push(move.from);
|
|
1549
|
+
inserts.set(move.place, list);
|
|
1550
|
+
}
|
|
1551
|
+
const order = [];
|
|
1552
|
+
for (let n = 1; n <= texts.length; n++) {
|
|
1553
|
+
for (const from of inserts.get(n) ?? []) order.push(from);
|
|
1554
|
+
if (!moving.has(n)) order.push(n);
|
|
1555
|
+
}
|
|
1556
|
+
const moved = /* @__PURE__ */ new Map();
|
|
1557
|
+
for (const move of moves) {
|
|
1558
|
+
moved.set(move.from, indentAt(move.place) + (texts[move.from - 1] ?? "").trim());
|
|
1559
|
+
}
|
|
1560
|
+
const at2 = /* @__PURE__ */ new Map();
|
|
1561
|
+
order.forEach((old, index) => at2.set(old, index + 1));
|
|
1562
|
+
const out = order.map((old, index) => (moved.get(old) ?? texts[old - 1] ?? "") + (eols[index] ?? "")).join("");
|
|
1563
|
+
return { content: out, line: (old) => at2.get(old) ?? old };
|
|
1564
|
+
}
|
|
1565
|
+
function pinOf(lines, span) {
|
|
1566
|
+
if (span.start < 1 || span.end > lines.length || span.end < span.start) return void 0;
|
|
1567
|
+
return hashLines(lines.slice(span.start - 1, span.end).join("\n"));
|
|
1568
|
+
}
|
|
1569
|
+
function markerClaimEnd(page, entry, lines, misplaced) {
|
|
1570
|
+
const { citation, marker } = entry;
|
|
1571
|
+
const pin = citation.claim?.integrity;
|
|
1572
|
+
const quote = citation.quote === true;
|
|
1573
|
+
if (marker === void 0 || pin === void 0) return { status: "changed" };
|
|
1574
|
+
const current = anchoredLines(page.content, marker.end, page.format, quote);
|
|
1575
|
+
if (current === void 0) return { status: "changed" };
|
|
1576
|
+
const claimAt = (span, status, to) => {
|
|
1577
|
+
const end = { fileLines: spellLines(span), status };
|
|
1578
|
+
if (to !== void 0) {
|
|
1579
|
+
end.newLines = spellLines(to);
|
|
1580
|
+
end.newFileLines = end.newLines;
|
|
1387
1581
|
}
|
|
1582
|
+
if (status === "changed") end.text = lines.slice(span.start - 1, Math.min(span.end, lines.length));
|
|
1583
|
+
return end;
|
|
1388
1584
|
};
|
|
1389
|
-
if (
|
|
1390
|
-
|
|
1391
|
-
}
|
|
1392
|
-
if (typeof source["commit-sha"] === "string") {
|
|
1393
|
-
citation.source["commit-sha"] = source["commit-sha"];
|
|
1585
|
+
if (quote) {
|
|
1586
|
+
return claimAt(current, pinOf(lines, current) === pin ? "current" : "changed");
|
|
1394
1587
|
}
|
|
1395
|
-
if (
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1588
|
+
if (misplaced !== void 0) {
|
|
1589
|
+
const to = movedUnit(misplaced);
|
|
1590
|
+
if (hashLines(unitText(lines, misplaced).join("\n")) === pin) return claimAt(to, "current");
|
|
1591
|
+
if (pinOf(lines, current) === pin) return claimAt(current, "moved", to);
|
|
1592
|
+
const pre432 = pre43Span(lines, marker.line);
|
|
1593
|
+
if (pre432 !== void 0 && pinOf(lines, pre432) === pin) return claimAt(pre432, "moved", to);
|
|
1594
|
+
return claimAt(current, "changed");
|
|
1402
1595
|
}
|
|
1403
|
-
if (
|
|
1404
|
-
|
|
1596
|
+
if (pinOf(lines, current) === pin) return claimAt(current, "current");
|
|
1597
|
+
const pre43 = pre43Span(lines, marker.line);
|
|
1598
|
+
if (pre43 !== void 0 && pinOf(lines, pre43) === pin) return claimAt(pre43, "moved", current);
|
|
1599
|
+
return claimAt(current, "changed");
|
|
1405
1600
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
ruleId: ruleId(rule),
|
|
1411
|
-
// The caller re-applies the configured severity table; no page-side rule
|
|
1412
|
-
// defaults to `off`, so there is nothing to collapse.
|
|
1413
|
-
severity: level === "off" ? "error" : level,
|
|
1414
|
-
message
|
|
1415
|
-
};
|
|
1416
|
-
if (extra.line !== void 0) out.line = extra.line;
|
|
1417
|
-
if (extra.file !== void 0) out.file = extra.file;
|
|
1418
|
-
if (extra.id !== void 0) out.id = extra.id;
|
|
1419
|
-
if (extra.src !== void 0) out.src = extra.src;
|
|
1420
|
-
if (extra.index !== void 0) out.index = extra.index;
|
|
1421
|
-
return out;
|
|
1601
|
+
|
|
1602
|
+
// src/cite/core/claims.ts
|
|
1603
|
+
function normalizeWhitespace(text) {
|
|
1604
|
+
return text.replace(/\s+/g, " ").trim();
|
|
1422
1605
|
}
|
|
1423
|
-
function
|
|
1424
|
-
|
|
1606
|
+
function normalizeBlock(text) {
|
|
1607
|
+
let out = text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
1608
|
+
out = out.replace(/\r\n/g, "\n");
|
|
1609
|
+
return out.endsWith("\n") ? out.slice(0, -1) : out;
|
|
1425
1610
|
}
|
|
1426
|
-
function
|
|
1427
|
-
|
|
1428
|
-
if (!isRecord(entry)) return out;
|
|
1429
|
-
if (typeof entry.id === "string") out.id = entry.id;
|
|
1430
|
-
if (isRecord(entry.source) && typeof entry.source.file === "string") out.src = entry.source.file;
|
|
1431
|
-
return out;
|
|
1611
|
+
function blockMatches(blockText, citedJoined) {
|
|
1612
|
+
return normalizeBlock(blockText) === normalizeBlock(citedJoined);
|
|
1432
1613
|
}
|
|
1433
|
-
function
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1614
|
+
function textAt(lines, range) {
|
|
1615
|
+
return lines.slice(range.start - 1, Math.min(range.end, lines.length));
|
|
1616
|
+
}
|
|
1617
|
+
function claimLine(claim) {
|
|
1618
|
+
const at2 = claim.fileLines === void 0 ? void 0 : parseLines(claim.fileLines);
|
|
1619
|
+
return at2?.start;
|
|
1620
|
+
}
|
|
1621
|
+
function claimLineNow(claim) {
|
|
1622
|
+
const spec = claim.newFileLines ?? claim.candidateFileLines?.[0] ?? claim.fileLines;
|
|
1623
|
+
const at2 = spec === void 0 ? void 0 : parseLines(spec);
|
|
1624
|
+
return at2?.start;
|
|
1625
|
+
}
|
|
1626
|
+
function claimLinesNow(claim) {
|
|
1627
|
+
const spec = claim.newFileLines ?? claim.candidateFileLines?.[0] ?? claim.fileLines;
|
|
1628
|
+
return spec === void 0 ? void 0 : parseLines(spec);
|
|
1629
|
+
}
|
|
1630
|
+
function claimEnd(page, entry, lines, misplaced) {
|
|
1631
|
+
const { citation, marker } = entry;
|
|
1632
|
+
const claim = citation.claim;
|
|
1633
|
+
if (claim === void 0) return null;
|
|
1634
|
+
const body = page.bodyLine;
|
|
1635
|
+
if (claim.lines !== void 0) {
|
|
1636
|
+
const recorded = parseLines(claim.lines);
|
|
1637
|
+
if (recorded === void 0) return { status: "skipped" };
|
|
1638
|
+
const file = toFileLines(recorded, body);
|
|
1639
|
+
const end = {
|
|
1640
|
+
lines: spellLines(recorded),
|
|
1641
|
+
fileLines: spellLines(file),
|
|
1642
|
+
status: "changed"
|
|
1643
|
+
};
|
|
1644
|
+
if (marker !== void 0) return { ...end, status: "skipped" };
|
|
1645
|
+
if (pinOfLines(lines, file) === claim.integrity) return { ...end, status: "current" };
|
|
1646
|
+
const width = recorded.end - recorded.start + 1;
|
|
1647
|
+
const found = findWindows(lines.slice(body - 1), width, claim.integrity, void 0, {
|
|
1648
|
+
around: recorded.start
|
|
1649
|
+
});
|
|
1650
|
+
const spans = found.starts.map((start) => ({ start, end: start + width - 1 }));
|
|
1651
|
+
const [only] = spans;
|
|
1652
|
+
if (spans.length === 1 && only !== void 0) {
|
|
1653
|
+
return {
|
|
1654
|
+
...end,
|
|
1655
|
+
status: "moved",
|
|
1656
|
+
newLines: spellLines(only),
|
|
1657
|
+
newFileLines: spellLines(toFileLines(only, body))
|
|
1658
|
+
};
|
|
1440
1659
|
}
|
|
1441
|
-
|
|
1660
|
+
if (spans.length > 1) {
|
|
1661
|
+
return {
|
|
1662
|
+
...end,
|
|
1663
|
+
status: "moved-ambiguous",
|
|
1664
|
+
candidates: spans.map(spellLines),
|
|
1665
|
+
candidateFileLines: spans.map((span) => spellLines(toFileLines(span, body)))
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
return { ...end, text: textAt(lines, file) };
|
|
1442
1669
|
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1670
|
+
if (marker !== void 0) {
|
|
1671
|
+
return markerClaimEnd(
|
|
1672
|
+
page,
|
|
1673
|
+
entry,
|
|
1674
|
+
lines,
|
|
1675
|
+
misplaced ?? misplacedMarkerAt(page, lines, marker.line, entry)
|
|
1448
1676
|
);
|
|
1449
1677
|
}
|
|
1450
|
-
return
|
|
1678
|
+
return { status: "changed" };
|
|
1451
1679
|
}
|
|
1452
|
-
function
|
|
1453
|
-
|
|
1680
|
+
function unitAt(page, line, lines) {
|
|
1681
|
+
if (line < page.bodyLine || line > lines.length) return void 0;
|
|
1682
|
+
if ((lines[line - 1] ?? "").trim() === "") return void 0;
|
|
1683
|
+
const block = fenceSpanAt(page.content, line, page.format);
|
|
1684
|
+
if (block !== void 0) {
|
|
1685
|
+
return { lines: block, kind: "block", text: lines.slice(block.start - 1, block.end) };
|
|
1686
|
+
}
|
|
1687
|
+
if (insideFence(page.content, page.bodyOffset, line, page.format)) return void 0;
|
|
1688
|
+
const paragraph = paragraphAfter(page.content, offsetOfLine(page.content, line));
|
|
1689
|
+
if (paragraph === void 0 || paragraph.line !== line) return void 0;
|
|
1690
|
+
const span = { start: line, end: lineAt(page.content, paragraph.end) };
|
|
1691
|
+
return { lines: span, kind: "paragraph", text: lines.slice(span.start - 1, span.end) };
|
|
1454
1692
|
}
|
|
1455
|
-
function
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1693
|
+
function markerUnit(page, entry, lines) {
|
|
1694
|
+
const { marker, citation } = entry;
|
|
1695
|
+
if (marker === void 0) return void 0;
|
|
1696
|
+
const span = anchoredLines(page.content, marker.end, page.format, citation.quote === true);
|
|
1697
|
+
if (span === void 0 || span.end > lines.length) return void 0;
|
|
1698
|
+
return {
|
|
1699
|
+
lines: span,
|
|
1700
|
+
kind: citation.quote === true ? "block" : "paragraph",
|
|
1701
|
+
text: lines.slice(span.start - 1, span.end)
|
|
1702
|
+
};
|
|
1459
1703
|
}
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1704
|
+
|
|
1705
|
+
// src/cite/core/git.ts
|
|
1706
|
+
import { execFile } from "node:child_process";
|
|
1707
|
+
var MAX_BUFFER = 32 * 1024 * 1024;
|
|
1708
|
+
var NO_COMMIT = /invalid object name|bad revision|unknown revision|bad object/i;
|
|
1709
|
+
var NO_PATH = /does not exist in|exists on disk, but not in|path .* does not exist/i;
|
|
1710
|
+
function run2(root, args) {
|
|
1711
|
+
return new Promise((resolve9) => {
|
|
1712
|
+
execFile(
|
|
1713
|
+
"git",
|
|
1714
|
+
["-c", "core.quotepath=false", ...args],
|
|
1715
|
+
{ cwd: root, windowsHide: true, maxBuffer: MAX_BUFFER, encoding: "utf8" },
|
|
1716
|
+
(error, stdout, stderr) => {
|
|
1717
|
+
if (error) {
|
|
1718
|
+
const failure = { ok: false, stderr, message: error.message };
|
|
1719
|
+
if (typeof error.code === "number") failure.code = error.code;
|
|
1720
|
+
resolve9(failure);
|
|
1721
|
+
} else resolve9({ ok: true, stdout });
|
|
1722
|
+
}
|
|
1466
1723
|
);
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
function gitClient(root) {
|
|
1727
|
+
const memo = /* @__PURE__ */ new Map();
|
|
1728
|
+
const once = (key, compute) => {
|
|
1729
|
+
const held = memo.get(key);
|
|
1730
|
+
if (held !== void 0) return held;
|
|
1731
|
+
const made = compute();
|
|
1732
|
+
memo.set(key, made);
|
|
1733
|
+
return made;
|
|
1734
|
+
};
|
|
1735
|
+
const available = () => once("available", async () => (await run2(root, ["rev-parse", "--show-toplevel"])).ok);
|
|
1736
|
+
const failed = (verb, outcome) => new CiteError(`git ${verb} failed: ${outcome.stderr.trim() || outcome.message}`);
|
|
1737
|
+
const hasCommit = (commit) => once(`has\0${commit}`, async () => {
|
|
1738
|
+
const out = await run2(root, ["rev-parse", "--verify", "--quiet", "--end-of-options", `${commit}^{commit}`]);
|
|
1739
|
+
if (out.ok) return true;
|
|
1740
|
+
if (out.code === 1) return false;
|
|
1741
|
+
throw failed("rev-parse", out);
|
|
1742
|
+
});
|
|
1743
|
+
return {
|
|
1744
|
+
available,
|
|
1745
|
+
head: () => once("head", async () => {
|
|
1746
|
+
if (!await available()) return null;
|
|
1747
|
+
const out = await run2(root, ["rev-parse", "HEAD"]);
|
|
1748
|
+
return out.ok ? out.stdout.trim() : null;
|
|
1749
|
+
}),
|
|
1750
|
+
lsFiles: () => once("ls-files", async () => {
|
|
1751
|
+
if (!await available()) return [];
|
|
1752
|
+
const out = await run2(root, ["ls-files", "-z"]);
|
|
1753
|
+
if (!out.ok) throw failed("ls-files", out);
|
|
1754
|
+
return out.stdout.split("\0").filter((entry) => entry !== "");
|
|
1755
|
+
}),
|
|
1756
|
+
showFile: (commit, path) => once(`show\0${commit}\0${path}`, async () => {
|
|
1757
|
+
const out = await run2(root, ["show", "--end-of-options", `${commit}:./${path}`]);
|
|
1758
|
+
if (out.ok) return { text: out.stdout };
|
|
1759
|
+
if (NO_COMMIT.test(out.stderr)) return { missing: "commit" };
|
|
1760
|
+
if (NO_PATH.test(out.stderr)) {
|
|
1761
|
+
return await hasCommit(commit) ? { missing: "path" } : { missing: "commit" };
|
|
1762
|
+
}
|
|
1763
|
+
throw failed("show", out);
|
|
1764
|
+
}),
|
|
1765
|
+
subjectsSince: (commit, path) => once(`log\0${commit}\0${path}`, async () => {
|
|
1766
|
+
const out = await run2(root, ["log", "--format=%s", "--end-of-options", `${commit}..HEAD`, "--", path]);
|
|
1767
|
+
if (!out.ok) throw failed("log", out);
|
|
1768
|
+
return out.stdout.split(/\r?\n/).filter((line) => line !== "");
|
|
1769
|
+
}),
|
|
1770
|
+
diffSince: (commit, path) => once(`diff\0${commit}\0${path}`, async () => {
|
|
1771
|
+
const out = await run2(root, ["diff", "--end-of-options", commit, "--", path]);
|
|
1772
|
+
if (!out.ok) throw failed("diff", out);
|
|
1773
|
+
return out.stdout;
|
|
1774
|
+
})
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
var GIT_UNAVAILABLE_HISTORY = "git is not available here, so citations are checked without history: no never-true, no diffs, no commit subjects.";
|
|
1778
|
+
var GIT_UNAVAILABLE_COMMIT = "git is not available here, so the citation records no commit.";
|
|
1779
|
+
function noGit() {
|
|
1780
|
+
return {
|
|
1781
|
+
available: () => Promise.resolve(false),
|
|
1782
|
+
head: () => Promise.resolve(null),
|
|
1783
|
+
lsFiles: () => Promise.resolve([]),
|
|
1784
|
+
showFile: () => Promise.resolve({ missing: "commit" }),
|
|
1785
|
+
subjectsSince: () => Promise.resolve([]),
|
|
1786
|
+
diffSince: () => Promise.resolve("")
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
// src/cite/core/classify.ts
|
|
1791
|
+
async function historyOf(git, commit, path, range, pin, key, budget) {
|
|
1792
|
+
const shown = await git.showFile(commit, path);
|
|
1793
|
+
if ("missing" in shown) {
|
|
1794
|
+
return shown.missing === "commit" ? { kind: "unavailable" } : { kind: "never-true" };
|
|
1467
1795
|
}
|
|
1468
|
-
const
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
);
|
|
1796
|
+
const then = splitLines(shown.text);
|
|
1797
|
+
let joined;
|
|
1798
|
+
try {
|
|
1799
|
+
joined = sliceLines2(then, range);
|
|
1800
|
+
} catch {
|
|
1801
|
+
joined = void 0;
|
|
1802
|
+
}
|
|
1803
|
+
if (joined !== void 0 && hashLines(joined, key) === pin) {
|
|
1804
|
+
return { kind: "original", lines: joined.split("\n") };
|
|
1805
|
+
}
|
|
1806
|
+
if (range.start === void 0) return { kind: "never-true" };
|
|
1807
|
+
const length = (range.end ?? range.start) - range.start + 1;
|
|
1808
|
+
const search = { around: range.start };
|
|
1809
|
+
if (budget !== void 0) search.budget = budget;
|
|
1810
|
+
const found = findWindows(then, length, pin, key, search);
|
|
1811
|
+
const [first] = found.starts;
|
|
1812
|
+
if (first !== void 0) {
|
|
1813
|
+
return { kind: "original", lines: then.slice(first - 1, first - 1 + length) };
|
|
1814
|
+
}
|
|
1815
|
+
return found.truncated ? { kind: "unknown" } : { kind: "never-true" };
|
|
1816
|
+
}
|
|
1817
|
+
async function classifyCitation(entry, opts) {
|
|
1818
|
+
const { citation } = entry;
|
|
1819
|
+
const range = sourceRange(citation.source);
|
|
1820
|
+
const commit = citation.source["commit-sha"];
|
|
1821
|
+
const result = { src: spellSource(citation.source), status: "missing" };
|
|
1822
|
+
if (commit !== void 0) result.commitSha = commit;
|
|
1823
|
+
const source = await readSource(opts.root, opts.index, range, opts.key);
|
|
1824
|
+
if (source.kind === "missing") {
|
|
1825
|
+
result.missingReason = source.reason;
|
|
1826
|
+
return result;
|
|
1486
1827
|
}
|
|
1487
|
-
|
|
1488
|
-
const
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1828
|
+
result.resolvedPath = source.resolvedPath;
|
|
1829
|
+
const key = range.encrypted ? opts.key : void 0;
|
|
1830
|
+
const pin = citation.source.integrity;
|
|
1831
|
+
const lines = splitLines(source.text);
|
|
1832
|
+
let here;
|
|
1833
|
+
try {
|
|
1834
|
+
here = hashLines(sliceLines2(lines, range), key);
|
|
1835
|
+
} catch {
|
|
1836
|
+
here = void 0;
|
|
1495
1837
|
}
|
|
1496
|
-
if (
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
line: extracted.lineFor("/citations")
|
|
1500
|
-
})
|
|
1501
|
-
);
|
|
1838
|
+
if (here === pin) {
|
|
1839
|
+
result.status = "current";
|
|
1840
|
+
return result;
|
|
1502
1841
|
}
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
if (line !== void 0) origin.line = line;
|
|
1507
|
-
return { entry, origin };
|
|
1508
|
-
});
|
|
1509
|
-
inputs.forEach((input, index) => {
|
|
1510
|
-
const origin = { ...input.origin, index };
|
|
1511
|
-
const where = { index };
|
|
1512
|
-
if (origin.line !== void 0) where.line = origin.line;
|
|
1513
|
-
if (origin.kind === "manifest") where.file = origin.file;
|
|
1514
|
-
const entry = input.entry;
|
|
1515
|
-
const error = validateEntry(entry);
|
|
1516
|
-
if (error !== void 0 || !isRecord(entry)) {
|
|
1517
|
-
findings.push(finding("entry-invalid", error ?? "must be object", { ...where, ...subjectOf(entry) }));
|
|
1518
|
-
return;
|
|
1519
|
-
}
|
|
1520
|
-
const citation = toCitation(entry);
|
|
1521
|
-
const subject = { ...where, src: spellSource(citation.source) };
|
|
1522
|
-
if (citation.id !== void 0) subject.id = citation.id;
|
|
1523
|
-
const bad = ["claim", "source"].find((end) => {
|
|
1524
|
-
const lines = end === "claim" ? citation.claim?.lines : citation.source.lines;
|
|
1525
|
-
return lines !== void 0 && parseLines(lines) === void 0;
|
|
1526
|
-
});
|
|
1527
|
-
if (bad !== void 0) {
|
|
1528
|
-
const lines = bad === "claim" ? citation.claim?.lines : citation.source.lines;
|
|
1529
|
-
findings.push(
|
|
1530
|
-
finding(
|
|
1531
|
-
"entry-invalid",
|
|
1532
|
-
named(citation.id, `${bad}.lines "${String(lines)}" ends before it starts`),
|
|
1533
|
-
subject
|
|
1534
|
-
)
|
|
1535
|
-
);
|
|
1536
|
-
return;
|
|
1537
|
-
}
|
|
1538
|
-
const wide = ["claim", "source"].map((end) => {
|
|
1539
|
-
const lines = end === "claim" ? citation.claim?.lines : citation.source.lines;
|
|
1540
|
-
const parsed = lines === void 0 ? void 0 : parseLines(lines);
|
|
1541
|
-
const reason = parsed === void 0 ? void 0 : tooWide(parsed);
|
|
1542
|
-
return reason === void 0 ? void 0 : `${end}.lines "${String(lines)}" ${reason}`;
|
|
1543
|
-
}).find((message) => message !== void 0);
|
|
1544
|
-
if (wide !== void 0) {
|
|
1545
|
-
findings.push(finding("entry-invalid", named(citation.id, wide), subject));
|
|
1546
|
-
return;
|
|
1547
|
-
}
|
|
1548
|
-
const encrypted = isEncryptedValue(citation.source.file);
|
|
1549
|
-
if (encrypted !== isKeyedPin(citation.source.integrity)) {
|
|
1550
|
-
findings.push(
|
|
1551
|
-
finding(
|
|
1552
|
-
"entry-invalid",
|
|
1553
|
-
named(
|
|
1554
|
-
citation.id,
|
|
1555
|
-
encrypted ? "an encrypted source is pinned with hmac-sha256-, not sha256-." : "a plain source is pinned with sha256-, not hmac-sha256-."
|
|
1556
|
-
),
|
|
1557
|
-
subject
|
|
1558
|
-
)
|
|
1559
|
-
);
|
|
1560
|
-
return;
|
|
1561
|
-
}
|
|
1562
|
-
const page = { citation, origin };
|
|
1563
|
-
citations.push(page);
|
|
1564
|
-
if (citation.id !== void 0) {
|
|
1565
|
-
if (ids.has(citation.id)) {
|
|
1566
|
-
findings.push(finding("entry-invalid", `duplicate id "${citation.id}"`, subject));
|
|
1567
|
-
} else {
|
|
1568
|
-
ids.add(citation.id);
|
|
1569
|
-
byId.set(citation.id, page);
|
|
1570
|
-
}
|
|
1571
|
-
}
|
|
1572
|
-
});
|
|
1573
|
-
const body = content.slice(bodyOffset);
|
|
1574
|
-
const statements = parseStatements(body, format, {
|
|
1575
|
-
offset: bodyOffset,
|
|
1576
|
-
line: bodyLine
|
|
1577
|
-
});
|
|
1578
|
-
if (statements.length > MAX_MARKERS_PER_PAGE) {
|
|
1579
|
-
findings.push(
|
|
1580
|
-
finding(
|
|
1581
|
-
"marker-invalid",
|
|
1582
|
-
`more than ${String(MAX_MARKERS_PER_PAGE)} markers on one page (${String(statements.length)}); the rest are not read`,
|
|
1583
|
-
{ line: statements[MAX_MARKERS_PER_PAGE]?.line }
|
|
1584
|
-
)
|
|
1585
|
-
);
|
|
1842
|
+
let history = { kind: "none" };
|
|
1843
|
+
if (commit !== void 0 && await opts.git.available()) {
|
|
1844
|
+
history = await historyOf(opts.git, commit, source.resolvedPath, range, pin, key, opts.budget);
|
|
1586
1845
|
}
|
|
1587
|
-
|
|
1588
|
-
const
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
finding("marker-orphan", `no entry has id "${payload.id}"`, {
|
|
1603
|
-
line: statement.line,
|
|
1604
|
-
id: payload.id
|
|
1605
|
-
})
|
|
1606
|
-
);
|
|
1607
|
-
continue;
|
|
1846
|
+
if (range.start !== void 0) {
|
|
1847
|
+
const start = range.start;
|
|
1848
|
+
const end = range.end ?? start;
|
|
1849
|
+
const search = { around: start };
|
|
1850
|
+
if (history.kind === "original") search.original = history.lines;
|
|
1851
|
+
if (opts.budget !== void 0) search.budget = opts.budget;
|
|
1852
|
+
const found = findWindows(lines, end - start + 1, pin, key, search);
|
|
1853
|
+
if (found.truncated) result.truncatedSearch = true;
|
|
1854
|
+
const spell = (at2) => formatSrc({ ...range, start: at2, end: at2 + (end - start) });
|
|
1855
|
+
const [only] = found.starts;
|
|
1856
|
+
if (found.starts.length === 1 && only !== void 0) {
|
|
1857
|
+
result.status = "moved";
|
|
1858
|
+
result.newSrc = spell(only);
|
|
1859
|
+
result.newLines = String(lineSpec({ start: only, end: only + (end - start) }));
|
|
1860
|
+
return result;
|
|
1608
1861
|
}
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
"marker-repeated",
|
|
1614
|
-
`${payload.id} is named by markers at lines ${String(first)} and ${String(statement.line)}; the first anchors it.`,
|
|
1615
|
-
{
|
|
1616
|
-
line: statement.line,
|
|
1617
|
-
id: payload.id,
|
|
1618
|
-
index: target.origin.index,
|
|
1619
|
-
src: spellSource(target.citation.source)
|
|
1620
|
-
}
|
|
1621
|
-
)
|
|
1622
|
-
);
|
|
1623
|
-
continue;
|
|
1862
|
+
if (found.starts.length > 1) {
|
|
1863
|
+
result.status = "moved-ambiguous";
|
|
1864
|
+
result.candidates = found.starts.map(spell);
|
|
1865
|
+
return result;
|
|
1624
1866
|
}
|
|
1625
|
-
markedAt.set(target, statement.line);
|
|
1626
|
-
target.marker = statement;
|
|
1627
1867
|
}
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1868
|
+
if (history.kind === "never-true") {
|
|
1869
|
+
result.status = "never-true";
|
|
1870
|
+
return result;
|
|
1871
|
+
}
|
|
1872
|
+
result.status = "changed";
|
|
1873
|
+
if (history.kind === "unavailable") result.historyAvailable = false;
|
|
1874
|
+
if (history.kind === "unknown") result.truncatedSearch = true;
|
|
1875
|
+
if (history.kind === "original" && commit !== void 0) {
|
|
1876
|
+
result.historyAvailable = true;
|
|
1877
|
+
result.commitsSince = await opts.git.subjectsSince(commit, source.resolvedPath);
|
|
1878
|
+
result.diff = await opts.git.diffSince(commit, source.resolvedPath);
|
|
1879
|
+
}
|
|
1880
|
+
return result;
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
// src/cite/core/shift.ts
|
|
1884
|
+
function shiftedEntries(opts) {
|
|
1885
|
+
const { citations, at: at2, delta, bodyLine, label } = opts;
|
|
1886
|
+
const out = { frontmatter: [], manifest: /* @__PURE__ */ new Map() };
|
|
1887
|
+
if (at2.length === 0) return out;
|
|
1888
|
+
for (const { citation, origin } of citations) {
|
|
1889
|
+
const spec = citation.claim?.lines;
|
|
1890
|
+
const recorded = spec === void 0 ? void 0 : parseLines(spec);
|
|
1891
|
+
if (recorded === void 0) continue;
|
|
1892
|
+
const file = toFileLines(recorded, bodyLine);
|
|
1893
|
+
const inside = at2.find(
|
|
1894
|
+
(line) => delta === 1 ? line > file.start && line <= file.end : line >= file.start && line <= file.end
|
|
1895
|
+
);
|
|
1896
|
+
if (inside !== void 0) {
|
|
1897
|
+
const whose = citation.id === void 0 ? `the claim at ${spellAt(file)}` : `the claim of ${citation.id} (${spellAt(file)})`;
|
|
1898
|
+
const why = delta === 1 ? "A marker there would change its pin." : "Removing the marker there would change its pin.";
|
|
1899
|
+
throw new CiteError(`${label}:${String(inside)} is inside ${whose}. ${why}`);
|
|
1650
1900
|
}
|
|
1901
|
+
const lines = at2.filter((line) => delta === 1 ? line <= file.start : line < file.start).length;
|
|
1902
|
+
if (lines === 0) continue;
|
|
1903
|
+
const moved = lineSpec({
|
|
1904
|
+
start: recorded.start + lines * delta,
|
|
1905
|
+
end: recorded.end + lines * delta
|
|
1906
|
+
});
|
|
1907
|
+
if (origin.kind === "manifest") out.manifest.set(origin.index, moved);
|
|
1908
|
+
else out.frontmatter.push({ index: origin.index, lines: moved });
|
|
1651
1909
|
}
|
|
1652
|
-
return
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
statements,
|
|
1660
|
-
findings: findings.sort(byLine)
|
|
1661
|
-
};
|
|
1910
|
+
return out;
|
|
1911
|
+
}
|
|
1912
|
+
function withClaimLines(entry, lines) {
|
|
1913
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return entry;
|
|
1914
|
+
const claim = entry.claim;
|
|
1915
|
+
if (typeof claim !== "object" || claim === null || Array.isArray(claim)) return entry;
|
|
1916
|
+
return { ...entry, claim: { ...claim, lines } };
|
|
1662
1917
|
}
|
|
1663
1918
|
|
|
1664
1919
|
// src/cite/core/sidecar.ts
|
|
@@ -1816,12 +2071,48 @@ function appendFrontmatterCitation(content, format, citation, filePath) {
|
|
|
1816
2071
|
const existing = raw;
|
|
1817
2072
|
const entry = entryObject(citation);
|
|
1818
2073
|
try {
|
|
1819
|
-
return extractor.apply(content, { citations: [...existing, entry] });
|
|
2074
|
+
return extractor.apply(content, { citations: [...existing, entry] });
|
|
2075
|
+
} catch (e) {
|
|
2076
|
+
if (e instanceof DocmetaError) throw new CiteError(`${label}: ${e.message}`);
|
|
2077
|
+
throw e;
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
function removeFrontmatterCitations(content, format, drop, filePath) {
|
|
2081
|
+
const label = filePath ?? "the page";
|
|
2082
|
+
const extractor = extractorFor(format);
|
|
2083
|
+
if (!extractor.apply || !readsFencedFrontmatter(extractor, label)) {
|
|
2084
|
+
throw new CiteError(
|
|
2085
|
+
`${label} has no frontmatter to write to; keep its citations in a manifest instead.`
|
|
2086
|
+
);
|
|
2087
|
+
}
|
|
2088
|
+
if (locateFrontmatter(content)?.flavor === "toml") {
|
|
2089
|
+
throw new CiteError(
|
|
2090
|
+
`${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.`
|
|
2091
|
+
);
|
|
2092
|
+
}
|
|
2093
|
+
const raw = extractor.extract(content, label).data.citations ?? [];
|
|
2094
|
+
if (!Array.isArray(raw)) {
|
|
2095
|
+
throw new CiteError(`${label}: \`citations\` is not a list; edit it by hand.`);
|
|
2096
|
+
}
|
|
2097
|
+
const existing = raw;
|
|
2098
|
+
const kept = existing.filter((_entry, index) => !drop.has(index));
|
|
2099
|
+
try {
|
|
2100
|
+
return kept.length === 0 ? extractor.apply(content, {}, { deletions: ["citations"] }) : extractor.apply(content, { citations: kept });
|
|
1820
2101
|
} catch (e) {
|
|
1821
2102
|
if (e instanceof DocmetaError) throw new CiteError(`${label}: ${e.message}`);
|
|
1822
2103
|
throw e;
|
|
1823
2104
|
}
|
|
1824
2105
|
}
|
|
2106
|
+
function removeLine(content, line) {
|
|
2107
|
+
const start = offsetOfLine(content, line);
|
|
2108
|
+
const nl = content.indexOf("\n", start);
|
|
2109
|
+
if (nl === -1) {
|
|
2110
|
+
let above = start > 0 ? content.lastIndexOf("\n", start - 1) : -1;
|
|
2111
|
+
if (above > 0 && content.charAt(above - 1) === "\r") above--;
|
|
2112
|
+
return content.slice(0, above === -1 ? 0 : above);
|
|
2113
|
+
}
|
|
2114
|
+
return content.slice(0, start) + content.slice(nl + 1);
|
|
2115
|
+
}
|
|
1825
2116
|
function entryObject(citation) {
|
|
1826
2117
|
const source = { file: citation.source.file };
|
|
1827
2118
|
if (citation.source.lines !== void 0) source.lines = citation.source.lines;
|
|
@@ -2013,6 +2304,25 @@ var ManifestSet = class {
|
|
|
2013
2304
|
held.text = spliced.text;
|
|
2014
2305
|
return { file: manifest.file, line: itemLine(spliced.text, entry, index, manifest.join) ?? spliced.line };
|
|
2015
2306
|
}
|
|
2307
|
+
/**
|
|
2308
|
+
* Take a page's `citations` key out of the manifest, for the removal that
|
|
2309
|
+
* leaves it with no entries. The entry itself goes when that key was all it
|
|
2310
|
+
* had, so a manifest never keeps a document nothing is recorded about.
|
|
2311
|
+
*/
|
|
2312
|
+
async remove(manifest, entry) {
|
|
2313
|
+
const held = await this.hold(manifest);
|
|
2314
|
+
try {
|
|
2315
|
+
held.text = removeManifestKey(held.text, {
|
|
2316
|
+
entry,
|
|
2317
|
+
key: CITATIONS_KEY2,
|
|
2318
|
+
join: manifest.join,
|
|
2319
|
+
file: manifest.file
|
|
2320
|
+
}).text;
|
|
2321
|
+
} catch (error) {
|
|
2322
|
+
if (error instanceof DocmetaError) throw new CiteError(error.message);
|
|
2323
|
+
throw error;
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2016
2326
|
/** The manifests this run rewrote, with the diff of each. */
|
|
2017
2327
|
changed() {
|
|
2018
2328
|
return [...this.held.values()].filter((m) => m.text !== m.before).map((m) => ({ ...m, diff: unifiedDiff(m.file, m.before, m.text) }));
|
|
@@ -2057,10 +2367,6 @@ function short(commit) {
|
|
|
2057
2367
|
function plural(n, noun) {
|
|
2058
2368
|
return `${String(n)} ${noun}${n === 1 ? "" : "s"}`;
|
|
2059
2369
|
}
|
|
2060
|
-
function listOf(values) {
|
|
2061
|
-
if (values.length <= 1) return values.join("");
|
|
2062
|
-
return `${values.slice(0, -1).join(", ")} and ${values[values.length - 1] ?? ""}`;
|
|
2063
|
-
}
|
|
2064
2370
|
function citesEncrypted(src) {
|
|
2065
2371
|
try {
|
|
2066
2372
|
return parseSrc(src).encrypted;
|
|
@@ -2138,6 +2444,13 @@ function claimMessageFor(result) {
|
|
|
2138
2444
|
return "";
|
|
2139
2445
|
}
|
|
2140
2446
|
}
|
|
2447
|
+
function misplacedMessageFor(id, marker) {
|
|
2448
|
+
const place = marker.block ? `above the block at line ${String(marker.place)}` : `above line ${String(marker.place)}`;
|
|
2449
|
+
return named2(
|
|
2450
|
+
id,
|
|
2451
|
+
`the marker at line ${String(marker.line)} splits the paragraph at ${at(marker.unit)}. Its place is ${place}.`
|
|
2452
|
+
);
|
|
2453
|
+
}
|
|
2141
2454
|
function sourceRuleOf(status) {
|
|
2142
2455
|
if (status === "skipped" || status === "current") return void 0;
|
|
2143
2456
|
return `source-${status}`;
|
|
@@ -2261,7 +2574,7 @@ async function quoteFindings(input, entry, result) {
|
|
|
2261
2574
|
const { citation } = entry;
|
|
2262
2575
|
if (citation.quote !== true || result.anchor === null) return [];
|
|
2263
2576
|
const id = citation.id;
|
|
2264
|
-
const
|
|
2577
|
+
const named4 = (text) => id === void 0 ? text : `${id}: ${text}`;
|
|
2265
2578
|
const { page } = input;
|
|
2266
2579
|
let block;
|
|
2267
2580
|
if (result.anchor === "marker" && entry.marker !== void 0) {
|
|
@@ -2283,7 +2596,7 @@ async function quoteFindings(input, entry, result) {
|
|
|
2283
2596
|
return [
|
|
2284
2597
|
{
|
|
2285
2598
|
rule: "anchor-invalid",
|
|
2286
|
-
message:
|
|
2599
|
+
message: named4(
|
|
2287
2600
|
`the quote's claim lines ${at2.start === at2.end ? String(at2.start) : `${String(at2.start)}-${String(at2.end)}`} are no longer a fenced block.`
|
|
2288
2601
|
),
|
|
2289
2602
|
line: at2.start
|
|
@@ -2305,6 +2618,17 @@ async function quoteFindings(input, entry, result) {
|
|
|
2305
2618
|
}
|
|
2306
2619
|
];
|
|
2307
2620
|
}
|
|
2621
|
+
function markerEnd(entry, misplaced) {
|
|
2622
|
+
const { marker } = entry;
|
|
2623
|
+
if (marker === void 0) return {};
|
|
2624
|
+
if (misplaced === void 0) return { marker: { line: marker.line } };
|
|
2625
|
+
return {
|
|
2626
|
+
marker: {
|
|
2627
|
+
line: marker.line,
|
|
2628
|
+
misplaced: { unit: spellLines(misplaced.unit), to: misplaced.to }
|
|
2629
|
+
}
|
|
2630
|
+
};
|
|
2631
|
+
}
|
|
2308
2632
|
function anchorOf(entry, claim, page) {
|
|
2309
2633
|
const { citation, marker } = entry;
|
|
2310
2634
|
const hasLines = citation.claim?.lines !== void 0;
|
|
@@ -2351,13 +2675,17 @@ async function checkCitations(page, opts) {
|
|
|
2351
2675
|
checkSources,
|
|
2352
2676
|
...opts.key === void 0 ? {} : { key: opts.key }
|
|
2353
2677
|
};
|
|
2678
|
+
const misplaced = misplacedMarkers(read, lines);
|
|
2679
|
+
const misplacedAt = new Map(misplaced.map((found) => [found.line, found]));
|
|
2354
2680
|
for (const entry of read.citations) {
|
|
2355
|
-
const
|
|
2681
|
+
const split = entry.marker === void 0 ? void 0 : misplacedAt.get(entry.marker.line);
|
|
2682
|
+
const claim = claimEnd(read, entry, lines, split);
|
|
2356
2683
|
const source = classifyOpts === void 0 ? skippedSource(entry) : await classifyCitation(entry, classifyOpts);
|
|
2357
2684
|
const result = {
|
|
2358
2685
|
citation: entry.citation,
|
|
2359
2686
|
origin: entry.origin,
|
|
2360
2687
|
...anchorOf(entry, claim, read),
|
|
2688
|
+
...markerEnd(entry, split),
|
|
2361
2689
|
claim,
|
|
2362
2690
|
source
|
|
2363
2691
|
};
|
|
@@ -2378,6 +2706,30 @@ async function checkCitations(page, opts) {
|
|
|
2378
2706
|
findings.push(finding2);
|
|
2379
2707
|
}
|
|
2380
2708
|
}
|
|
2709
|
+
const misplacedLevel = severity["marker-misplaced"];
|
|
2710
|
+
if (misplacedLevel !== "off") {
|
|
2711
|
+
for (const found of misplaced) {
|
|
2712
|
+
const entry = found.entry;
|
|
2713
|
+
const finding2 = {
|
|
2714
|
+
rule: "marker-misplaced",
|
|
2715
|
+
ruleId: ruleId("marker-misplaced"),
|
|
2716
|
+
severity: misplacedLevel,
|
|
2717
|
+
message: misplacedMessageFor(entry?.citation.id, {
|
|
2718
|
+
line: found.line,
|
|
2719
|
+
unit: spellLines(found.unit),
|
|
2720
|
+
place: found.place,
|
|
2721
|
+
block: found.block
|
|
2722
|
+
}),
|
|
2723
|
+
line: found.line
|
|
2724
|
+
};
|
|
2725
|
+
if (entry !== void 0) {
|
|
2726
|
+
finding2.index = entry.origin.index;
|
|
2727
|
+
finding2.src = spellSource(entry.citation.source);
|
|
2728
|
+
if (entry.citation.id !== void 0) finding2.id = entry.citation.id;
|
|
2729
|
+
}
|
|
2730
|
+
findings.push(finding2);
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2381
2733
|
findings.push(...findingsFor(results, severity));
|
|
2382
2734
|
const wantsHistory = checkSources && read.citations.some(({ citation }) => citation.source["commit-sha"] !== void 0);
|
|
2383
2735
|
if (wantsHistory && !await client.available()) notices.push(GIT_UNAVAILABLE_HISTORY);
|
|
@@ -2629,7 +2981,9 @@ async function resolveCiteRun(opts) {
|
|
|
2629
2981
|
root = gitRoot;
|
|
2630
2982
|
} else {
|
|
2631
2983
|
root = cwd;
|
|
2632
|
-
opts.
|
|
2984
|
+
if (opts.resolvesSources !== false) {
|
|
2985
|
+
opts.onNotice?.(`No git root found; resolving src: paths from ${cwd}`);
|
|
2986
|
+
}
|
|
2633
2987
|
}
|
|
2634
2988
|
}
|
|
2635
2989
|
const { key, source: keySource } = resolveEncryptionKey({
|
|
@@ -2678,7 +3032,8 @@ function joinHits() {
|
|
|
2678
3032
|
}
|
|
2679
3033
|
};
|
|
2680
3034
|
}
|
|
2681
|
-
async function prepareRun(opts, action, verb,
|
|
3035
|
+
async function prepareRun(opts, action, verb, sources = {}) {
|
|
3036
|
+
const requireSources = sources.require === true;
|
|
2682
3037
|
const cwd = resolve3(opts.cwd ?? process.cwd());
|
|
2683
3038
|
const run3 = await resolveCiteRun({
|
|
2684
3039
|
cwd,
|
|
@@ -2689,7 +3044,8 @@ async function prepareRun(opts, action, verb, requireSources = false) {
|
|
|
2689
3044
|
root: opts.root,
|
|
2690
3045
|
onConfigLoaded: opts.onConfigLoaded,
|
|
2691
3046
|
onNotice: opts.onNotice,
|
|
2692
|
-
env: opts.env
|
|
3047
|
+
env: opts.env,
|
|
3048
|
+
...sources.resolve === false ? { resolvesSources: false } : {}
|
|
2693
3049
|
});
|
|
2694
3050
|
const { config, inputs, base } = run3;
|
|
2695
3051
|
const checkSources = opts.checkSources ?? config?.checkSources ?? true;
|
|
@@ -2869,9 +3225,6 @@ async function citedText(root, index, src, key) {
|
|
|
2869
3225
|
}
|
|
2870
3226
|
return sliceLines2(splitLines(source.text), range, range.path);
|
|
2871
3227
|
}
|
|
2872
|
-
function spellAt(lines) {
|
|
2873
|
-
return lines.start === lines.end ? `line ${String(lines.start)}` : `lines ${String(lines.start)}-${String(lines.end)}`;
|
|
2874
|
-
}
|
|
2875
3228
|
function entryName(entry) {
|
|
2876
3229
|
return entry.citation.id ?? `/citations/${String(entry.origin.index)}`;
|
|
2877
3230
|
}
|
|
@@ -2910,37 +3263,11 @@ var NAMED_SPANS = 3;
|
|
|
2910
3263
|
function spellElsewhere(spans) {
|
|
2911
3264
|
const first = spans[0];
|
|
2912
3265
|
const noun = spans.length === 1 && first !== void 0 && first.start === first.end ? "line" : "lines";
|
|
2913
|
-
const
|
|
2914
|
-
const rest = spans.length -
|
|
2915
|
-
if (rest > 0) return `${noun} ${
|
|
2916
|
-
const last =
|
|
2917
|
-
return
|
|
2918
|
-
}
|
|
2919
|
-
function shiftedEntries(citations, insertLine, bodyLine, label) {
|
|
2920
|
-
const out = { frontmatter: [], manifest: /* @__PURE__ */ new Map() };
|
|
2921
|
-
for (const { citation, origin } of citations) {
|
|
2922
|
-
const spec = citation.claim?.lines;
|
|
2923
|
-
const recorded = spec === void 0 ? void 0 : parseLines(spec);
|
|
2924
|
-
if (recorded === void 0) continue;
|
|
2925
|
-
const file = toFileLines(recorded, bodyLine);
|
|
2926
|
-
if (file.start < insertLine) {
|
|
2927
|
-
if (file.end < insertLine) continue;
|
|
2928
|
-
const whose = citation.id === void 0 ? `the claim at ${spellAt(file)}` : `the claim of ${citation.id} (${spellAt(file)})`;
|
|
2929
|
-
throw new CiteError(
|
|
2930
|
-
`${label}:${String(insertLine)} is inside ${whose}. A marker there would change its pin.`
|
|
2931
|
-
);
|
|
2932
|
-
}
|
|
2933
|
-
const moved = lineSpec({ start: recorded.start + 1, end: recorded.end + 1 });
|
|
2934
|
-
if (origin.kind === "manifest") out.manifest.set(origin.index, moved);
|
|
2935
|
-
else out.frontmatter.push({ index: origin.index, lines: moved });
|
|
2936
|
-
}
|
|
2937
|
-
return out;
|
|
2938
|
-
}
|
|
2939
|
-
function withClaimLines(entry, lines) {
|
|
2940
|
-
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return entry;
|
|
2941
|
-
const claim = entry.claim;
|
|
2942
|
-
if (typeof claim !== "object" || claim === null || Array.isArray(claim)) return entry;
|
|
2943
|
-
return { ...entry, claim: { ...claim, lines } };
|
|
3266
|
+
const named4 = spans.slice(0, NAMED_SPANS).map((span) => spellLines(span));
|
|
3267
|
+
const rest = spans.length - named4.length;
|
|
3268
|
+
if (rest > 0) return `${noun} ${named4.join(", ")} and ${String(rest)} more`;
|
|
3269
|
+
const last = named4.pop() ?? "";
|
|
3270
|
+
return named4.length === 0 ? `${noun} ${last}` : `${noun} ${named4.join(", ")} and ${last}`;
|
|
2944
3271
|
}
|
|
2945
3272
|
async function runAdd(opts) {
|
|
2946
3273
|
const cwd = resolve4(opts.cwd ?? process.cwd());
|
|
@@ -3037,11 +3364,28 @@ async function runAdd(opts) {
|
|
|
3037
3364
|
const empty = emptyClaimRefusal(lines, pageLines, at2);
|
|
3038
3365
|
if (empty !== void 0) throw new CiteError(empty);
|
|
3039
3366
|
}
|
|
3367
|
+
const split = misplacedMarkers(page, lines);
|
|
3368
|
+
const held = split.find(
|
|
3369
|
+
(found) => found.line >= pageLines.start && found.line <= pageLines.end
|
|
3370
|
+
);
|
|
3371
|
+
if (held !== void 0) {
|
|
3372
|
+
throw new CiteError(
|
|
3373
|
+
`${at2} holds the marker at line ${String(held.line)}, which splits its paragraph. Run manni cite update first.`
|
|
3374
|
+
);
|
|
3375
|
+
}
|
|
3376
|
+
const inside = marker ? split.find(
|
|
3377
|
+
(found) => pageLines.start >= found.unit.start && pageLines.start <= found.unit.end
|
|
3378
|
+
) : void 0;
|
|
3379
|
+
if (inside !== void 0) {
|
|
3380
|
+
throw new CiteError(
|
|
3381
|
+
`${at2} is in the paragraph at ${spellAt(inside.unit)}, which the marker at line ${String(inside.line)} splits. Run manni cite update first.`
|
|
3382
|
+
);
|
|
3383
|
+
}
|
|
3040
3384
|
}
|
|
3041
3385
|
const client = opts.gitClient ?? gitClient(root);
|
|
3042
3386
|
const sourceIndex = await sourceIndexFor(root, client);
|
|
3043
3387
|
let body = content;
|
|
3044
|
-
let
|
|
3388
|
+
let markerAt2;
|
|
3045
3389
|
let claimSpan;
|
|
3046
3390
|
let claim;
|
|
3047
3391
|
let elsewhere = [];
|
|
@@ -3065,11 +3409,17 @@ async function runAdd(opts) {
|
|
|
3065
3409
|
`${at2} is in a ${holding.kind} at ${spellAt(holding)} that ${unitWide}. A marker anchors the whole ${holding.kind}.`
|
|
3066
3410
|
);
|
|
3067
3411
|
}
|
|
3068
|
-
shifted = shiftedEntries(
|
|
3412
|
+
shifted = shiftedEntries({
|
|
3413
|
+
citations: page.citations,
|
|
3414
|
+
at: [holding.start],
|
|
3415
|
+
delta: 1,
|
|
3416
|
+
bodyLine: page.bodyLine,
|
|
3417
|
+
label
|
|
3418
|
+
});
|
|
3069
3419
|
const statement = markerIndent(content, holding.start, format, page.bodyLine) + formatStatement(format, { kind: "ref", id: opts.id ?? "" });
|
|
3070
3420
|
const insertAt = offsetOfLine(content, holding.start);
|
|
3071
3421
|
body = insertStatementBefore(content, insertAt, statement);
|
|
3072
|
-
|
|
3422
|
+
markerAt2 = holding.start;
|
|
3073
3423
|
const unit = anchoredLines(body, insertAt + statement.length, format, quote);
|
|
3074
3424
|
const pin = unit === void 0 ? void 0 : pinOfLines(splitLines(body), unit);
|
|
3075
3425
|
if (unit === void 0 || pin === void 0) {
|
|
@@ -3165,7 +3515,7 @@ async function runAdd(opts) {
|
|
|
3165
3515
|
...sourceLine === void 0 ? {} : { sourceLine },
|
|
3166
3516
|
...placed === void 0 ? {} : { manifest: placed }
|
|
3167
3517
|
};
|
|
3168
|
-
if (
|
|
3518
|
+
if (markerAt2 !== void 0) result.markerLine = markerAt2 + shift;
|
|
3169
3519
|
if (claimSpan !== void 0) {
|
|
3170
3520
|
result.claimLines = { start: claimSpan.start + shift, end: claimSpan.end + shift };
|
|
3171
3521
|
}
|
|
@@ -3176,10 +3526,10 @@ async function runAdd(opts) {
|
|
|
3176
3526
|
result.manifest.written = true;
|
|
3177
3527
|
}
|
|
3178
3528
|
if (elsewhere.length > 0) {
|
|
3179
|
-
const
|
|
3529
|
+
const named4 = elsewhere.map((span) => ({ start: span.start + shift, end: span.end + shift }));
|
|
3180
3530
|
const who = citation.id === void 0 ? "" : `${citation.id}: `;
|
|
3181
3531
|
opts.onNotice?.(
|
|
3182
|
-
`${who}the claim's text also appears at ${spellElsewhere(
|
|
3532
|
+
`${who}the claim's text also appears at ${spellElsewhere(named4)}, so a move would be ambiguous. Use --marker, or pin more lines.`
|
|
3183
3533
|
);
|
|
3184
3534
|
}
|
|
3185
3535
|
if (opts.commitSha !== false && !await client.available()) {
|
|
@@ -3188,8 +3538,175 @@ async function runAdd(opts) {
|
|
|
3188
3538
|
return result;
|
|
3189
3539
|
}
|
|
3190
3540
|
|
|
3191
|
-
// src/cite/commands/
|
|
3541
|
+
// src/cite/commands/remove.ts
|
|
3192
3542
|
import { resolve as resolve5 } from "node:path";
|
|
3543
|
+
var POINTER = /^\/citations\/(0|[1-9][0-9]*)$/;
|
|
3544
|
+
function entryCount(n) {
|
|
3545
|
+
return `${String(n)} ${n === 1 ? "entry" : "entries"}`;
|
|
3546
|
+
}
|
|
3547
|
+
function idOf(entry) {
|
|
3548
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return void 0;
|
|
3549
|
+
const id = entry.id;
|
|
3550
|
+
return typeof id === "string" ? id : void 0;
|
|
3551
|
+
}
|
|
3552
|
+
async function runRemove(opts) {
|
|
3553
|
+
const only = [...new Set(opts.only)];
|
|
3554
|
+
if (only.length === 0) {
|
|
3555
|
+
throw new CiteError("remove needs --only <id>; it never removes every citation on a page.");
|
|
3556
|
+
}
|
|
3557
|
+
const prepared = await prepareRun({ ...opts, checkSources: false }, "removed", "remove", {
|
|
3558
|
+
resolve: false
|
|
3559
|
+
});
|
|
3560
|
+
const { run: run3, files, usingStdin, forced } = prepared;
|
|
3561
|
+
assertNoOrphans(prepared);
|
|
3562
|
+
const hits = joinHits();
|
|
3563
|
+
const manifests = new ManifestSet();
|
|
3564
|
+
const matched = /* @__PURE__ */ new Set();
|
|
3565
|
+
const pageCount = files.length + (usingStdin ? 1 : 0);
|
|
3566
|
+
const planned = [];
|
|
3567
|
+
const removeOne = async (label, content, path) => {
|
|
3568
|
+
const setup = prepared.setupFor(label, content);
|
|
3569
|
+
if (setup.sidecar !== void 0) hits.record(setup.sidecar, label);
|
|
3570
|
+
const owner = setup.sidecar?.owner;
|
|
3571
|
+
const page = readPage(label, content, {
|
|
3572
|
+
...forced === void 0 ? {} : { format: forced.name },
|
|
3573
|
+
...setup.options.citations === void 0 ? {} : { citations: setup.options.citations },
|
|
3574
|
+
...owner === void 0 ? {} : { owned: { file: owner.file, collection: owner.collection } }
|
|
3575
|
+
});
|
|
3576
|
+
const { format } = page;
|
|
3577
|
+
const extractor = pickExtractor(label, forced?.name);
|
|
3578
|
+
const inputs = citationInputs(
|
|
3579
|
+
label,
|
|
3580
|
+
extractor.extract(content, label),
|
|
3581
|
+
setup.sidecar?.citations
|
|
3582
|
+
);
|
|
3583
|
+
const lines = splitLines(content);
|
|
3584
|
+
const entriesFor = (value) => {
|
|
3585
|
+
const [, digits] = POINTER.exec(value) ?? [];
|
|
3586
|
+
if (digits === void 0) {
|
|
3587
|
+
return inputs.flatMap((input, index2) => idOf(input.entry) === value ? [index2] : []);
|
|
3588
|
+
}
|
|
3589
|
+
const index = Number(digits);
|
|
3590
|
+
if (index < inputs.length) return [index];
|
|
3591
|
+
if (pageCount === 1) {
|
|
3592
|
+
throw new CiteError(
|
|
3593
|
+
`"${value}" is past the last entry of ${label} (${entryCount(inputs.length)}).`
|
|
3594
|
+
);
|
|
3595
|
+
}
|
|
3596
|
+
return [];
|
|
3597
|
+
};
|
|
3598
|
+
const markersFor = (id) => id === void 0 ? [] : page.statements.filter((s) => s.payload.kind === "ref" && s.payload.id === id).map((s) => s.line);
|
|
3599
|
+
const removed = [];
|
|
3600
|
+
const indices = /* @__PURE__ */ new Set();
|
|
3601
|
+
const markerLines = [];
|
|
3602
|
+
for (const value of only) {
|
|
3603
|
+
const found = entriesFor(value);
|
|
3604
|
+
for (const index of found) {
|
|
3605
|
+
const input = inputs[index];
|
|
3606
|
+
const id = idOf(input?.entry);
|
|
3607
|
+
const markers2 = markersFor(id);
|
|
3608
|
+
indices.add(index);
|
|
3609
|
+
markerLines.push(...markers2);
|
|
3610
|
+
removed.push({
|
|
3611
|
+
...id === void 0 ? {} : { id },
|
|
3612
|
+
index,
|
|
3613
|
+
...input === void 0 ? {} : { origin: input.origin },
|
|
3614
|
+
markerLines: markers2
|
|
3615
|
+
});
|
|
3616
|
+
matched.add(value);
|
|
3617
|
+
}
|
|
3618
|
+
if (found.length > 0) continue;
|
|
3619
|
+
const markers = markersFor(value);
|
|
3620
|
+
if (markers.length === 0) continue;
|
|
3621
|
+
markerLines.push(...markers);
|
|
3622
|
+
removed.push({ id: value, markerLines: markers });
|
|
3623
|
+
matched.add(value);
|
|
3624
|
+
}
|
|
3625
|
+
const nothing = { file: label, removed: [], diff: "", written: false };
|
|
3626
|
+
if (removed.length === 0) {
|
|
3627
|
+
if (path === void 0) nothing.content = content;
|
|
3628
|
+
planned.push({ page: nothing, ...path === void 0 ? {} : { path } });
|
|
3629
|
+
return;
|
|
3630
|
+
}
|
|
3631
|
+
const ordered = [...new Set(markerLines)].sort((a, b) => a - b);
|
|
3632
|
+
for (const line of ordered) {
|
|
3633
|
+
if (!isMarkerLine(lines[line - 1] ?? "", format)) {
|
|
3634
|
+
const id = page.statements.find((s) => s.line === line)?.payload;
|
|
3635
|
+
const named4 = id?.kind === "ref" ? ` ${id.id}` : "";
|
|
3636
|
+
throw new CiteError(
|
|
3637
|
+
`the marker${named4} at ${label}:${String(line)} shares its line with text; remove it by hand.`
|
|
3638
|
+
);
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
const survivors = page.citations.filter((c) => !indices.has(c.origin.index));
|
|
3642
|
+
const shifted = shiftedEntries({
|
|
3643
|
+
citations: survivors,
|
|
3644
|
+
at: ordered,
|
|
3645
|
+
delta: -1,
|
|
3646
|
+
bodyLine: page.bodyLine,
|
|
3647
|
+
label
|
|
3648
|
+
});
|
|
3649
|
+
let after = content;
|
|
3650
|
+
for (const { index, lines: moved } of shifted.frontmatter) {
|
|
3651
|
+
after = spliceEntryField(after, format, index, ["claim", "lines"], moved);
|
|
3652
|
+
}
|
|
3653
|
+
for (const line of [...ordered].sort((a, b) => b - a)) after = removeLine(after, line);
|
|
3654
|
+
if (owner === void 0) {
|
|
3655
|
+
after = removeFrontmatterCitations(after, format, indices, label);
|
|
3656
|
+
} else {
|
|
3657
|
+
if (setup.sidecar?.entry === void 0) {
|
|
3658
|
+
throw new CiteError(
|
|
3659
|
+
`${label} carries no ${owner.join}: value, so its citations cannot be keyed in ${owner.file}.`
|
|
3660
|
+
);
|
|
3661
|
+
}
|
|
3662
|
+
const kept = inputs.map((input, index) => {
|
|
3663
|
+
const moved = shifted.manifest.get(index);
|
|
3664
|
+
return moved === void 0 ? input.entry : withClaimLines(input.entry, moved);
|
|
3665
|
+
}).filter((_entry, index) => !indices.has(index));
|
|
3666
|
+
if (kept.length === 0) await manifests.remove(owner, setup.sidecar.entry);
|
|
3667
|
+
else void await manifests.write(owner, setup.sidecar.entry, kept, 0);
|
|
3668
|
+
}
|
|
3669
|
+
const out = {
|
|
3670
|
+
file: label,
|
|
3671
|
+
removed,
|
|
3672
|
+
diff: after === content ? "" : unifiedDiff(label, content, after),
|
|
3673
|
+
written: false
|
|
3674
|
+
};
|
|
3675
|
+
if (path === void 0) out.content = after;
|
|
3676
|
+
planned.push({ page: out, ...after === content ? {} : { content: after }, ...path === void 0 ? {} : { path } });
|
|
3677
|
+
};
|
|
3678
|
+
if (usingStdin) await removeOne(STDIN_LABEL, opts.stdinContent ?? "");
|
|
3679
|
+
for (const file of files) {
|
|
3680
|
+
await removeOne(file, await readTarget(run3, file), resolve5(run3.base, file));
|
|
3681
|
+
}
|
|
3682
|
+
assertNoOrphanJoins(prepared, hits);
|
|
3683
|
+
const missing = only.find((value) => !matched.has(value));
|
|
3684
|
+
if (missing !== void 0) {
|
|
3685
|
+
const where = pageCount === 1 ? `${planned[0]?.page.file ?? "the page"} has no` : `none of ${String(pageCount)} pages has an`;
|
|
3686
|
+
const what = POINTER.test(missing) ? `entry ${missing}` : `entry or marker ${missing}`;
|
|
3687
|
+
throw new CiteError(`${where} ${what}.`);
|
|
3688
|
+
}
|
|
3689
|
+
const write = opts.dryRun !== true;
|
|
3690
|
+
for (const item of planned) {
|
|
3691
|
+
if (item.content === void 0 || item.path === void 0) continue;
|
|
3692
|
+
if (write) await writeFileAtomic(item.path, item.content);
|
|
3693
|
+
item.page.written = write;
|
|
3694
|
+
}
|
|
3695
|
+
const rewritten = [];
|
|
3696
|
+
for (const changed of manifests.changed()) {
|
|
3697
|
+
if (write) await writeFileAtomic(changed.path, changed.text);
|
|
3698
|
+
rewritten.push({ file: changed.file, diff: changed.diff, written: write });
|
|
3699
|
+
}
|
|
3700
|
+
const pages = planned.map((item) => item.page);
|
|
3701
|
+
return {
|
|
3702
|
+
pages,
|
|
3703
|
+
removed: pages.reduce((n, page) => n + page.removed.length, 0),
|
|
3704
|
+
...rewritten.length > 0 ? { manifests: rewritten } : {}
|
|
3705
|
+
};
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3708
|
+
// src/cite/commands/update.ts
|
|
3709
|
+
import { resolve as resolve6 } from "node:path";
|
|
3193
3710
|
function settles(plan) {
|
|
3194
3711
|
switch (plan.kind) {
|
|
3195
3712
|
case "claim-moved":
|
|
@@ -3200,13 +3717,26 @@ function settles(plan) {
|
|
|
3200
3717
|
return "claim-changed";
|
|
3201
3718
|
case "source-accepted":
|
|
3202
3719
|
return plan.result.source.status === "never-true" ? "source-never-true" : "source-changed";
|
|
3720
|
+
case "marker-moved":
|
|
3721
|
+
return "marker-misplaced";
|
|
3722
|
+
case "claim-reanchored":
|
|
3723
|
+
return "claim-moved";
|
|
3724
|
+
// A shifted entry was `current`, so it had no finding to settle.
|
|
3725
|
+
case "claim-shifted":
|
|
3726
|
+
return "";
|
|
3203
3727
|
}
|
|
3204
3728
|
}
|
|
3205
3729
|
function apply(content, format, plan) {
|
|
3206
3730
|
const index = plan.result.origin.index;
|
|
3207
3731
|
switch (plan.kind) {
|
|
3208
3732
|
case "claim-moved":
|
|
3733
|
+
case "claim-shifted":
|
|
3209
3734
|
return spliceEntryField(content, format, index, ["claim", "lines"], plan.lines);
|
|
3735
|
+
case "claim-reanchored":
|
|
3736
|
+
return spliceEntryField(content, format, index, ["claim", "integrity"], plan.pin);
|
|
3737
|
+
// The move is a page rewrite, applied to the whole page before any splice.
|
|
3738
|
+
case "marker-moved":
|
|
3739
|
+
return content;
|
|
3210
3740
|
case "source-moved":
|
|
3211
3741
|
return spliceEntryField(content, format, index, ["source", "lines"], plan.lines);
|
|
3212
3742
|
case "claim-accepted": {
|
|
@@ -3239,12 +3769,22 @@ function applyToEntry(entry, plan) {
|
|
|
3239
3769
|
if (!isRecord3(entry)) return false;
|
|
3240
3770
|
const end = (name) => isRecord3(entry[name]) ? entry[name] : void 0;
|
|
3241
3771
|
switch (plan.kind) {
|
|
3242
|
-
case "claim-moved":
|
|
3772
|
+
case "claim-moved":
|
|
3773
|
+
case "claim-shifted": {
|
|
3243
3774
|
const claim = end("claim");
|
|
3244
3775
|
if (claim === void 0) return false;
|
|
3245
3776
|
claim.lines = plan.lines;
|
|
3246
3777
|
return true;
|
|
3247
3778
|
}
|
|
3779
|
+
case "claim-reanchored": {
|
|
3780
|
+
const claim = end("claim");
|
|
3781
|
+
if (claim === void 0) return false;
|
|
3782
|
+
claim.integrity = plan.pin;
|
|
3783
|
+
return true;
|
|
3784
|
+
}
|
|
3785
|
+
// The page holds the marker, so the manifest has nothing to change.
|
|
3786
|
+
case "marker-moved":
|
|
3787
|
+
return true;
|
|
3248
3788
|
case "source-moved": {
|
|
3249
3789
|
const source = end("source");
|
|
3250
3790
|
if (source === void 0) return false;
|
|
@@ -3275,7 +3815,56 @@ function rewriteOf(plan) {
|
|
|
3275
3815
|
if (origin.line !== void 0) base.line = origin.line;
|
|
3276
3816
|
switch (plan.kind) {
|
|
3277
3817
|
case "claim-moved":
|
|
3278
|
-
return {
|
|
3818
|
+
return {
|
|
3819
|
+
...base,
|
|
3820
|
+
end: "claim",
|
|
3821
|
+
reason: "moved",
|
|
3822
|
+
status: "moved",
|
|
3823
|
+
from: plan.from,
|
|
3824
|
+
to: plan.to,
|
|
3825
|
+
fromLines: plan.from,
|
|
3826
|
+
toLines: plan.to
|
|
3827
|
+
};
|
|
3828
|
+
case "marker-moved": {
|
|
3829
|
+
const from = String(plan.from);
|
|
3830
|
+
const to = String(plan.to);
|
|
3831
|
+
return {
|
|
3832
|
+
...base,
|
|
3833
|
+
end: "marker",
|
|
3834
|
+
reason: "re-anchored",
|
|
3835
|
+
status: "misplaced",
|
|
3836
|
+
from,
|
|
3837
|
+
to,
|
|
3838
|
+
fromLines: from,
|
|
3839
|
+
toLines: to
|
|
3840
|
+
};
|
|
3841
|
+
}
|
|
3842
|
+
case "claim-reanchored": {
|
|
3843
|
+
const was = citation.claim?.integrity ?? "";
|
|
3844
|
+
return {
|
|
3845
|
+
...base,
|
|
3846
|
+
end: "claim",
|
|
3847
|
+
reason: "re-anchored",
|
|
3848
|
+
status: "moved",
|
|
3849
|
+
from: was,
|
|
3850
|
+
to: plan.pin,
|
|
3851
|
+
fromPin: was,
|
|
3852
|
+
toPin: plan.pin,
|
|
3853
|
+
lines: plan.held,
|
|
3854
|
+
newLines: plan.now
|
|
3855
|
+
};
|
|
3856
|
+
}
|
|
3857
|
+
case "claim-shifted":
|
|
3858
|
+
return {
|
|
3859
|
+
...base,
|
|
3860
|
+
end: "claim",
|
|
3861
|
+
reason: "shifted",
|
|
3862
|
+
status: "current",
|
|
3863
|
+
from: plan.from,
|
|
3864
|
+
to: plan.to,
|
|
3865
|
+
fromLines: plan.from,
|
|
3866
|
+
toLines: plan.to
|
|
3867
|
+
};
|
|
3279
3868
|
case "source-moved":
|
|
3280
3869
|
return {
|
|
3281
3870
|
...base,
|
|
@@ -3286,19 +3875,21 @@ function rewriteOf(plan) {
|
|
|
3286
3875
|
to: plan.to
|
|
3287
3876
|
};
|
|
3288
3877
|
case "claim-accepted": {
|
|
3878
|
+
const was = citation.claim?.integrity ?? "";
|
|
3289
3879
|
const out = {
|
|
3290
3880
|
...base,
|
|
3291
3881
|
end: "claim",
|
|
3292
3882
|
reason: "accepted",
|
|
3293
3883
|
status: "changed",
|
|
3294
|
-
from:
|
|
3884
|
+
from: was,
|
|
3295
3885
|
to: plan.pin,
|
|
3886
|
+
fromPin: was,
|
|
3887
|
+
toPin: plan.pin,
|
|
3296
3888
|
at: plan.unit.lines.start,
|
|
3297
3889
|
text: normalizeWhitespace(plan.unit.text.join("\n"))
|
|
3298
3890
|
};
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
}
|
|
3891
|
+
const markerLine = plan.markerLine ?? (plan.result.anchor === "marker" ? plan.result.markerLine : void 0);
|
|
3892
|
+
if (markerLine !== void 0) out.markerLine = markerLine;
|
|
3302
3893
|
return out;
|
|
3303
3894
|
}
|
|
3304
3895
|
case "source-accepted": {
|
|
@@ -3309,6 +3900,8 @@ function rewriteOf(plan) {
|
|
|
3309
3900
|
status: source.status === "never-true" ? "never-true" : "changed",
|
|
3310
3901
|
from: citation.source.integrity,
|
|
3311
3902
|
to: plan.minted.source.integrity,
|
|
3903
|
+
fromPin: citation.source.integrity,
|
|
3904
|
+
toPin: plan.minted.source.integrity,
|
|
3312
3905
|
src: source.src
|
|
3313
3906
|
};
|
|
3314
3907
|
const commit = plan.minted.source["commit-sha"];
|
|
@@ -3317,26 +3910,157 @@ function rewriteOf(plan) {
|
|
|
3317
3910
|
}
|
|
3318
3911
|
}
|
|
3319
3912
|
}
|
|
3913
|
+
function named3(id, text) {
|
|
3914
|
+
return id === void 0 ? text : `${id}: ${text}`;
|
|
3915
|
+
}
|
|
3916
|
+
function crossing(page, misplaced) {
|
|
3917
|
+
for (const other of page.citations) {
|
|
3918
|
+
const spec = other.citation.claim?.lines;
|
|
3919
|
+
const recorded = spec === void 0 ? void 0 : parseLines(spec);
|
|
3920
|
+
if (recorded === void 0) continue;
|
|
3921
|
+
const file = toFileLines(recorded, page.bodyLine);
|
|
3922
|
+
const holdsMarker = file.start <= misplaced.line && misplaced.line <= file.end;
|
|
3923
|
+
const holdsPlace = file.start < misplaced.place && misplaced.place <= file.end;
|
|
3924
|
+
if (!holdsMarker && !holdsPlace) continue;
|
|
3925
|
+
return other.citation.id === void 0 ? `the claim at ${spellAt(file)}` : `the claim of ${other.citation.id} (${spellAt(file)})`;
|
|
3926
|
+
}
|
|
3927
|
+
return void 0;
|
|
3928
|
+
}
|
|
3929
|
+
function decideMarkers(input) {
|
|
3930
|
+
const { page, report, lines, accept, only } = input;
|
|
3931
|
+
const out = { moves: [], stays: [] };
|
|
3932
|
+
for (const misplaced of misplacedMarkers(page, lines)) {
|
|
3933
|
+
const entry = misplaced.entry;
|
|
3934
|
+
const line = misplaced.line;
|
|
3935
|
+
const stay = (rule, message) => {
|
|
3936
|
+
out.stays.push({ line, rule, message });
|
|
3937
|
+
};
|
|
3938
|
+
if (entry === void 0) {
|
|
3939
|
+
if (only !== void 0) continue;
|
|
3940
|
+
const own = report.findings.find(
|
|
3941
|
+
(finding2) => finding2.line === line && finding2.rule.startsWith("marker-") && finding2.rule !== "marker-misplaced"
|
|
3942
|
+
);
|
|
3943
|
+
stay(
|
|
3944
|
+
own?.rule ?? "marker-orphan",
|
|
3945
|
+
`the marker at line ${String(line)} stays, because it names no entry update can check.`
|
|
3946
|
+
);
|
|
3947
|
+
continue;
|
|
3948
|
+
}
|
|
3949
|
+
const id = entry.citation.id;
|
|
3950
|
+
if (only !== void 0 && (id === void 0 || !only.has(id))) continue;
|
|
3951
|
+
const result = report.citations.find((r) => r.origin.index === entry.origin.index);
|
|
3952
|
+
if (result === void 0) continue;
|
|
3953
|
+
if (entry.citation.claim?.lines !== void 0) {
|
|
3954
|
+
stay(
|
|
3955
|
+
"anchor-invalid",
|
|
3956
|
+
named3(id, `the marker at line ${String(line)} stays, because the entry also has claim lines. Keep one.`)
|
|
3957
|
+
);
|
|
3958
|
+
continue;
|
|
3959
|
+
}
|
|
3960
|
+
const across = crossing(page, misplaced);
|
|
3961
|
+
if (across !== void 0) {
|
|
3962
|
+
stay(
|
|
3963
|
+
"marker-misplaced",
|
|
3964
|
+
named3(id, `the marker at line ${String(line)} stays, because moving it would change ${across}.`)
|
|
3965
|
+
);
|
|
3966
|
+
continue;
|
|
3967
|
+
}
|
|
3968
|
+
if (result.claim !== null && result.claim.status === "changed" && !accept) {
|
|
3969
|
+
stay(
|
|
3970
|
+
"marker-misplaced",
|
|
3971
|
+
named3(
|
|
3972
|
+
id,
|
|
3973
|
+
`the marker at line ${String(line)} stays, because its claim changed since it was pinned. update --accept moves it and re-pins.`
|
|
3974
|
+
)
|
|
3975
|
+
);
|
|
3976
|
+
continue;
|
|
3977
|
+
}
|
|
3978
|
+
out.moves.push({ result, entry, misplaced });
|
|
3979
|
+
}
|
|
3980
|
+
return out;
|
|
3981
|
+
}
|
|
3982
|
+
function anchoredNow(content, format, lines, markerLine, quote) {
|
|
3983
|
+
const at2 = offsetOfLine(content, markerLine) + (lines[markerLine - 1]?.length ?? 0);
|
|
3984
|
+
const span = anchoredLines(content, at2, format, quote);
|
|
3985
|
+
if (span === void 0) return void 0;
|
|
3986
|
+
const pin = pinOfLines(lines, span);
|
|
3987
|
+
return pin === void 0 ? void 0 : { span, pin };
|
|
3988
|
+
}
|
|
3989
|
+
function codeOf(error) {
|
|
3990
|
+
if (typeof error !== "object" || error === null || !("code" in error)) return void 0;
|
|
3991
|
+
const code = error.code;
|
|
3992
|
+
return typeof code === "string" ? code : void 0;
|
|
3993
|
+
}
|
|
3994
|
+
var WrittenFiles = class {
|
|
3995
|
+
before = /* @__PURE__ */ new Map();
|
|
3996
|
+
labels = /* @__PURE__ */ new Map();
|
|
3997
|
+
/** Remember what `path` held before this run wrote it. */
|
|
3998
|
+
record(path, label, content) {
|
|
3999
|
+
if (!this.before.has(path)) {
|
|
4000
|
+
this.before.set(path, content);
|
|
4001
|
+
this.labels.set(path, label);
|
|
4002
|
+
}
|
|
4003
|
+
}
|
|
4004
|
+
/**
|
|
4005
|
+
* Put every file back, and say what happened: `<file> could not be written
|
|
4006
|
+
* (<code>). <pages> was restored.`
|
|
4007
|
+
*/
|
|
4008
|
+
async restore(file, error) {
|
|
4009
|
+
const restored = [];
|
|
4010
|
+
const failed = [];
|
|
4011
|
+
for (const [path, content] of this.before) {
|
|
4012
|
+
const label = this.labels.get(path) ?? path;
|
|
4013
|
+
try {
|
|
4014
|
+
await writeFileAtomic(path, content);
|
|
4015
|
+
restored.push(label);
|
|
4016
|
+
} catch {
|
|
4017
|
+
failed.push(label);
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
const code = codeOf(error);
|
|
4021
|
+
const why = code === void 0 ? "" : ` (${code})`;
|
|
4022
|
+
const parts = [`${file} could not be written${why}.`];
|
|
4023
|
+
if (restored.length > 0) {
|
|
4024
|
+
parts.push(`${listOf(restored)} ${restored.length === 1 ? "was" : "were"} restored.`);
|
|
4025
|
+
}
|
|
4026
|
+
if (failed.length > 0) {
|
|
4027
|
+
parts.push(
|
|
4028
|
+
`${listOf(failed)} could not be restored, and ${failed.length === 1 ? "holds" : "hold"} an intermediate state.`
|
|
4029
|
+
);
|
|
4030
|
+
}
|
|
4031
|
+
return new CiteError(parts.join(" "));
|
|
4032
|
+
}
|
|
4033
|
+
};
|
|
4034
|
+
function citationsIn(rows) {
|
|
4035
|
+
const withMarker = new Set(rows.filter((row) => row.end === "marker").map((row) => row.index));
|
|
4036
|
+
return withMarker.size + rows.filter((row) => !withMarker.has(row.index)).length;
|
|
4037
|
+
}
|
|
3320
4038
|
async function runUpdate(opts) {
|
|
3321
|
-
const prepared = await prepareRun(opts, "updated", "update", true);
|
|
4039
|
+
const prepared = await prepareRun(opts, "updated", "update", { require: true });
|
|
3322
4040
|
const { run: run3, files, usingStdin, forced, pageOptions, git } = prepared;
|
|
3323
4041
|
assertNoOrphans(prepared);
|
|
3324
4042
|
const hits = joinHits();
|
|
3325
4043
|
const manifests = new ManifestSet();
|
|
4044
|
+
const writes = new WrittenFiles();
|
|
3326
4045
|
const only = opts.only !== void 0 && opts.only.length > 0 ? new Set(opts.only) : void 0;
|
|
3327
4046
|
const accept = opts.accept === true;
|
|
3328
|
-
const plansFor = async (result, entry, page, lines, declined) => {
|
|
4047
|
+
const plansFor = async (result, entry, page, lines, lineNow, declined) => {
|
|
3329
4048
|
const out = [];
|
|
3330
4049
|
const claim = result.claim;
|
|
3331
|
-
if (claim !== null && claim.status === "moved" && claim.newLines !== void 0) {
|
|
4050
|
+
if (result.anchor !== "marker" && claim !== null && claim.status === "moved" && claim.newLines !== void 0) {
|
|
3332
4051
|
const at2 = parseLines(claim.newLines);
|
|
3333
|
-
|
|
4052
|
+
const shifted = at2 === void 0 ? void 0 : (() => {
|
|
4053
|
+
const file = toFileLines(at2, page.bodyLine);
|
|
4054
|
+
const to = { start: lineNow(file.start), end: lineNow(file.end) };
|
|
4055
|
+
return to.end - to.start === file.end - file.start ? toBodyLines(to, page.bodyLine) : void 0;
|
|
4056
|
+
})();
|
|
4057
|
+
if (at2 !== void 0 && shifted !== void 0) {
|
|
3334
4058
|
out.push({
|
|
3335
4059
|
kind: "claim-moved",
|
|
3336
4060
|
result,
|
|
3337
|
-
lines: lineSpec(
|
|
4061
|
+
lines: lineSpec(shifted),
|
|
3338
4062
|
from: claim.fileLines ?? "",
|
|
3339
|
-
to:
|
|
4063
|
+
to: spellLines(toFileLines(shifted, page.bodyLine))
|
|
3340
4064
|
});
|
|
3341
4065
|
}
|
|
3342
4066
|
}
|
|
@@ -3347,8 +4071,8 @@ async function runUpdate(opts) {
|
|
|
3347
4071
|
}
|
|
3348
4072
|
}
|
|
3349
4073
|
if (!accept) return out;
|
|
3350
|
-
if (claim !== null && claim.status === "changed" && entry !== void 0) {
|
|
3351
|
-
const unit =
|
|
4074
|
+
if (claim !== null && claim.status === "changed" && entry !== void 0 && result.anchor !== "marker") {
|
|
4075
|
+
const unit = (() => {
|
|
3352
4076
|
const at2 = claimLine(claim);
|
|
3353
4077
|
return at2 === void 0 ? void 0 : unitAt(page, at2, lines);
|
|
3354
4078
|
})();
|
|
@@ -3356,7 +4080,10 @@ async function runUpdate(opts) {
|
|
|
3356
4080
|
const pin = unit === void 0 ? void 0 : pinOfLines(lines, unit.lines);
|
|
3357
4081
|
const wide = unit === void 0 ? void 0 : tooWide(unit.lines);
|
|
3358
4082
|
if (unit !== void 0 && wide !== void 0) {
|
|
3359
|
-
declined.set(result.origin.index,
|
|
4083
|
+
declined.set(result.origin.index, {
|
|
4084
|
+
rule: "claim-changed",
|
|
4085
|
+
why: `Not re-pinned: the ${unit.kind} ${wide}.`
|
|
4086
|
+
});
|
|
3360
4087
|
} else if (unit !== void 0 && pin !== void 0 && (!wantsBlock || unit.kind === "block")) {
|
|
3361
4088
|
const plan = { kind: "claim-accepted", result, unit, pin };
|
|
3362
4089
|
if (entry.citation.claim?.lines !== void 0) {
|
|
@@ -3392,6 +4119,7 @@ async function runUpdate(opts) {
|
|
|
3392
4119
|
if (setup.sidecar !== void 0) hits.record(setup.sidecar, label);
|
|
3393
4120
|
const report = await checkCitations({ file: label, content, format: forced?.name }, setup.options);
|
|
3394
4121
|
const { format } = report;
|
|
4122
|
+
const severity = resolveSeverity(setup.options.severity);
|
|
3395
4123
|
const page = readPage(label, content, {
|
|
3396
4124
|
...forced === void 0 ? {} : { format: forced.name },
|
|
3397
4125
|
...setup.options.citations === void 0 ? {} : { citations: setup.options.citations },
|
|
@@ -3401,18 +4129,105 @@ async function runUpdate(opts) {
|
|
|
3401
4129
|
const rewritten2 = [];
|
|
3402
4130
|
const settled = /* @__PURE__ */ new Set();
|
|
3403
4131
|
const declined = /* @__PURE__ */ new Map();
|
|
3404
|
-
|
|
4132
|
+
const markers = decideMarkers({ page, report, lines, accept, only });
|
|
4133
|
+
const movedPage = markers.moves.length === 0 ? void 0 : applyMarkerMoves(
|
|
4134
|
+
content,
|
|
4135
|
+
markers.moves.map(({ misplaced }) => ({
|
|
4136
|
+
from: misplaced.line,
|
|
4137
|
+
place: misplaced.place
|
|
4138
|
+
})),
|
|
4139
|
+
(line) => markerIndent(content, line, format, page.bodyLine)
|
|
4140
|
+
);
|
|
4141
|
+
let after = movedPage?.content ?? content;
|
|
4142
|
+
const afterLines = movedPage === void 0 ? lines : splitLines(after);
|
|
4143
|
+
const movedIndexes = new Set(markers.moves.map(({ entry }) => entry.origin.index));
|
|
4144
|
+
const lineNow = (line) => movedPage?.line(line) ?? line;
|
|
3405
4145
|
const owner = setup.sidecar?.owner;
|
|
3406
4146
|
const entries = setup.sidecar?.citations === void 0 ? void 0 : JSON.parse(JSON.stringify(setup.sidecar.citations.map((c) => c.entry)));
|
|
3407
4147
|
let manifestDirty = false;
|
|
3408
4148
|
for (const result of report.citations) {
|
|
3409
|
-
|
|
3410
|
-
continue;
|
|
3411
|
-
}
|
|
4149
|
+
const selected = only === void 0 || result.citation.id !== void 0 && only.has(result.citation.id);
|
|
3412
4150
|
const entry = page.citations.find((c) => c.origin.index === result.origin.index);
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
4151
|
+
const plans = [];
|
|
4152
|
+
const move = selected ? markers.moves.find((m) => m.entry.origin.index === result.origin.index) : void 0;
|
|
4153
|
+
if (move !== void 0) {
|
|
4154
|
+
plans.push({
|
|
4155
|
+
kind: "marker-moved",
|
|
4156
|
+
result,
|
|
4157
|
+
from: move.misplaced.line,
|
|
4158
|
+
to: lineNow(move.misplaced.line)
|
|
4159
|
+
});
|
|
4160
|
+
}
|
|
4161
|
+
const marker = entry?.marker;
|
|
4162
|
+
const recorded = entry?.citation.claim?.integrity;
|
|
4163
|
+
if (selected && result.anchor === "marker" && marker !== void 0 && recorded !== void 0 && result.claim !== null) {
|
|
4164
|
+
const quote = entry?.citation.quote === true;
|
|
4165
|
+
const now = anchoredNow(after, format, afterLines, lineNow(marker.line), quote);
|
|
4166
|
+
const status = result.claim.status;
|
|
4167
|
+
if (now !== void 0 && now.pin !== recorded) {
|
|
4168
|
+
if (status === "moved" || status === "current" && move !== void 0) {
|
|
4169
|
+
plans.push({
|
|
4170
|
+
kind: "claim-reanchored",
|
|
4171
|
+
result,
|
|
4172
|
+
pin: now.pin,
|
|
4173
|
+
held: result.claim.fileLines ?? "",
|
|
4174
|
+
now: spellLines(now.span)
|
|
4175
|
+
});
|
|
4176
|
+
} else if (status === "changed" && accept) {
|
|
4177
|
+
const kind = quote ? "block" : "paragraph";
|
|
4178
|
+
const wide = tooWide(now.span);
|
|
4179
|
+
if (wide !== void 0) {
|
|
4180
|
+
declined.set(result.origin.index, {
|
|
4181
|
+
rule: "claim-changed",
|
|
4182
|
+
why: `Not re-pinned: the ${kind} ${wide}.`
|
|
4183
|
+
});
|
|
4184
|
+
} else {
|
|
4185
|
+
plans.push({
|
|
4186
|
+
kind: "claim-accepted",
|
|
4187
|
+
result,
|
|
4188
|
+
pin: now.pin,
|
|
4189
|
+
markerLine: lineNow(marker.line),
|
|
4190
|
+
unit: {
|
|
4191
|
+
lines: now.span,
|
|
4192
|
+
kind,
|
|
4193
|
+
text: afterLines.slice(now.span.start - 1, now.span.end)
|
|
4194
|
+
}
|
|
4195
|
+
});
|
|
4196
|
+
}
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4199
|
+
}
|
|
4200
|
+
if (selected) {
|
|
4201
|
+
plans.push(...await plansFor(result, entry, page, lines, lineNow, declined));
|
|
4202
|
+
}
|
|
4203
|
+
const spec = entry?.citation.claim?.lines;
|
|
4204
|
+
const wentElsewhere = result.claim?.status === "moved";
|
|
4205
|
+
if (movedPage !== void 0 && spec !== void 0 && !wentElsewhere && !plans.some((p) => p.kind === "claim-moved")) {
|
|
4206
|
+
const at2 = parseLines(spec);
|
|
4207
|
+
const file = at2 === void 0 ? void 0 : toFileLines(at2, page.bodyLine);
|
|
4208
|
+
const to = file === void 0 ? void 0 : { start: lineNow(file.start), end: lineNow(file.end) };
|
|
4209
|
+
if (file !== void 0 && to !== void 0 && to.end - to.start === file.end - file.start && (to.start !== file.start || to.end !== file.end)) {
|
|
4210
|
+
plans.push({
|
|
4211
|
+
kind: "claim-shifted",
|
|
4212
|
+
result,
|
|
4213
|
+
lines: lineSpec(toBodyLines(to, page.bodyLine)),
|
|
4214
|
+
from: spellLines(file),
|
|
4215
|
+
to: spellLines(to)
|
|
4216
|
+
});
|
|
4217
|
+
}
|
|
4218
|
+
}
|
|
4219
|
+
for (const plan of plans) {
|
|
4220
|
+
if (plan.kind === "marker-moved") {
|
|
4221
|
+
} else if (result.origin.kind === "manifest") {
|
|
4222
|
+
const held = entries?.[result.origin.index];
|
|
4223
|
+
if (held === void 0) {
|
|
4224
|
+
declined.set(result.origin.index, {
|
|
4225
|
+
rule: settles(plan),
|
|
4226
|
+
why: `Not rewritten: ${owner?.file ?? "the manifest"} has no entry at index ${String(result.origin.index)}.`
|
|
4227
|
+
});
|
|
4228
|
+
continue;
|
|
4229
|
+
}
|
|
4230
|
+
if (!applyToEntry(held, plan)) continue;
|
|
3416
4231
|
manifestDirty = true;
|
|
3417
4232
|
} else {
|
|
3418
4233
|
after = apply(after, format, plan);
|
|
@@ -3421,6 +4236,7 @@ async function runUpdate(opts) {
|
|
|
3421
4236
|
settled.add(`${String(result.origin.index)}\0${settles(plan)}`);
|
|
3422
4237
|
}
|
|
3423
4238
|
}
|
|
4239
|
+
for (const index of movedIndexes) settled.add(`${String(index)}\0marker-misplaced`);
|
|
3424
4240
|
if (manifestDirty && owner !== void 0 && entries !== void 0) {
|
|
3425
4241
|
if (setup.sidecar?.entry === void 0) {
|
|
3426
4242
|
throw new CiteError(
|
|
@@ -3429,15 +4245,34 @@ async function runUpdate(opts) {
|
|
|
3429
4245
|
}
|
|
3430
4246
|
await manifests.write(owner, setup.sidecar.entry, entries, 0);
|
|
3431
4247
|
}
|
|
4248
|
+
const stayAt = new Map(markers.stays.map((stay) => [stay.line, stay]));
|
|
3432
4249
|
const skipped2 = report.findings.filter(
|
|
3433
4250
|
(finding2) => (only === void 0 || finding2.id !== void 0 && only.has(finding2.id)) && !settled.has(`${String(finding2.index ?? -1)}\0${finding2.rule}`)
|
|
3434
4251
|
).map((finding2) => {
|
|
3435
|
-
|
|
4252
|
+
if (finding2.rule === "marker-misplaced") {
|
|
4253
|
+
const stay = stayAt.get(finding2.line ?? -1);
|
|
4254
|
+
if (stay === void 0) return finding2;
|
|
4255
|
+
const level = severity[stay.rule];
|
|
4256
|
+
return {
|
|
4257
|
+
...finding2,
|
|
4258
|
+
message: stay.message,
|
|
4259
|
+
...level === "off" ? {} : { severity: level }
|
|
4260
|
+
};
|
|
4261
|
+
}
|
|
4262
|
+
const turned = finding2.index === void 0 ? void 0 : declined.get(finding2.index);
|
|
4263
|
+
const why = turned?.rule === finding2.rule ? turned.why : void 0;
|
|
3436
4264
|
return why === void 0 ? finding2 : { ...finding2, message: `${finding2.message} ${why}` };
|
|
3437
4265
|
});
|
|
3438
4266
|
const diff = after === content ? "" : unifiedDiff(label, content, after);
|
|
3439
4267
|
const written = after !== content && path !== void 0 && opts.dryRun !== true;
|
|
3440
|
-
if (written)
|
|
4268
|
+
if (written) {
|
|
4269
|
+
try {
|
|
4270
|
+
await writeFileAtomic(path, after);
|
|
4271
|
+
} catch (error) {
|
|
4272
|
+
throw await writes.restore(label, error);
|
|
4273
|
+
}
|
|
4274
|
+
writes.record(path, label, content);
|
|
4275
|
+
}
|
|
3441
4276
|
const out = { file: label, rewritten: rewritten2, skipped: skipped2, diff, written };
|
|
3442
4277
|
if (path === void 0) out.content = after;
|
|
3443
4278
|
pages.push(out);
|
|
@@ -3446,21 +4281,28 @@ async function runUpdate(opts) {
|
|
|
3446
4281
|
const reports = [];
|
|
3447
4282
|
if (usingStdin) reports.push(await updateOne(STDIN_LABEL, opts.stdinContent ?? ""));
|
|
3448
4283
|
for (const file of files) {
|
|
3449
|
-
reports.push(await updateOne(file, await readTarget(run3, file),
|
|
4284
|
+
reports.push(await updateOne(file, await readTarget(run3, file), resolve6(run3.base, file)));
|
|
3450
4285
|
}
|
|
3451
4286
|
assertNoOrphanJoins(prepared, hits);
|
|
3452
4287
|
const changedManifests = manifests.changed();
|
|
3453
4288
|
const rewrittenManifests = [];
|
|
3454
4289
|
for (const changed of changedManifests) {
|
|
3455
4290
|
const write = opts.dryRun !== true;
|
|
3456
|
-
if (write)
|
|
4291
|
+
if (write) {
|
|
4292
|
+
try {
|
|
4293
|
+
await writeFileAtomic(changed.path, changed.text);
|
|
4294
|
+
} catch (error) {
|
|
4295
|
+
throw await writes.restore(changed.file, error);
|
|
4296
|
+
}
|
|
4297
|
+
writes.record(changed.path, changed.file, changed.before);
|
|
4298
|
+
}
|
|
3457
4299
|
rewrittenManifests.push({ file: changed.file, diff: changed.diff, written: write });
|
|
3458
4300
|
}
|
|
3459
4301
|
const reminted = pages.some(
|
|
3460
4302
|
(page) => page.rewritten.some((r) => r.reason === "accepted" && r.end === "source")
|
|
3461
4303
|
);
|
|
3462
4304
|
sayNotices(reports, opts.onNotice, reminted && !await git.available() ? [GIT_UNAVAILABLE_COMMIT] : []);
|
|
3463
|
-
const rewritten = pages.reduce((n, page) => n + page.rewritten
|
|
4305
|
+
const rewritten = pages.reduce((n, page) => n + citationsIn(page.rewritten), 0);
|
|
3464
4306
|
const skipped = pages.reduce((n, page) => n + page.skipped.length, 0);
|
|
3465
4307
|
const undone = pages.some((page) => page.skipped.some((finding2) => finding2.severity === "error"));
|
|
3466
4308
|
return {
|
|
@@ -3648,13 +4490,15 @@ function citesEncrypted2(src) {
|
|
|
3648
4490
|
}
|
|
3649
4491
|
}
|
|
3650
4492
|
function claimColumn(result) {
|
|
3651
|
-
const { claim } = result;
|
|
3652
|
-
const
|
|
4493
|
+
const { claim, marker } = result;
|
|
4494
|
+
const anchoredByMarker = result.anchor === "marker" && result.markerLine !== void 0;
|
|
4495
|
+
const moves = marker?.misplaced === void 0 ? "" : ` -> :${String(marker.misplaced.to)}`;
|
|
4496
|
+
const where = anchoredByMarker ? `marker :${String(result.markerLine ?? 0)}${moves}` : claim?.fileLines === void 0 ? "" : `:${claim.fileLines}`;
|
|
3653
4497
|
if (claim === null) return where;
|
|
3654
4498
|
let status;
|
|
3655
4499
|
switch (claim.status) {
|
|
3656
4500
|
case "moved":
|
|
3657
|
-
status = `moved -> :${claim.newFileLines ?? "?"}`;
|
|
4501
|
+
status = anchoredByMarker ? "moved" : `moved -> :${claim.newFileLines ?? "?"}`;
|
|
3658
4502
|
break;
|
|
3659
4503
|
case "moved-ambiguous": {
|
|
3660
4504
|
const at2 = (claim.candidateFileLines ?? []).map((lines) => `:${lines}`);
|
|
@@ -3744,7 +4588,7 @@ function renderCheckPretty(run3, opts) {
|
|
|
3744
4588
|
if (own.length === 0 && quiet) continue;
|
|
3745
4589
|
const worst = live.find((f) => f.severity === "error") ?? live.find((f) => f.severity === "warning") ?? live[0];
|
|
3746
4590
|
const mark = worst !== void 0 ? severityMark(worst.severity, c) : own.length > 0 ? c.dim("\xB7") : result.source.status === "skipped" && result.claim === null ? c.dim("\xB7") : c.green("\u2713");
|
|
3747
|
-
const endRules = /* @__PURE__ */ new Set(["claim-", "source-"]);
|
|
4591
|
+
const endRules = /* @__PURE__ */ new Set(["claim-", "source-", "marker-misplaced"]);
|
|
3748
4592
|
const under = [];
|
|
3749
4593
|
for (const finding2 of own) {
|
|
3750
4594
|
const isEnd = [...endRules].some((prefix) => finding2.rule.startsWith(prefix));
|
|
@@ -3803,9 +4647,45 @@ function renderCheckPretty(run3, opts) {
|
|
|
3803
4647
|
}
|
|
3804
4648
|
return lines.join("\n");
|
|
3805
4649
|
}
|
|
4650
|
+
function spellAtSpec(spec) {
|
|
4651
|
+
const span = spanOf(spec);
|
|
4652
|
+
return span === void 0 ? `lines ${spec}` : spellAt(span);
|
|
4653
|
+
}
|
|
4654
|
+
function spanOf(spec) {
|
|
4655
|
+
const [first, second] = spec.split("-");
|
|
4656
|
+
const start = Number(first);
|
|
4657
|
+
const end = second === void 0 ? start : Number(second);
|
|
4658
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) return void 0;
|
|
4659
|
+
return { start, end };
|
|
4660
|
+
}
|
|
4661
|
+
function widening(held, now) {
|
|
4662
|
+
const was = spanOf(held);
|
|
4663
|
+
const is = spanOf(now);
|
|
4664
|
+
if (was === void 0 || is === void 0) return "";
|
|
4665
|
+
const parts = [];
|
|
4666
|
+
if (is.start < was.start) {
|
|
4667
|
+
parts.push(spellAt({ start: is.start, end: Math.min(was.start - 1, is.end) }));
|
|
4668
|
+
}
|
|
4669
|
+
if (is.end > was.end) {
|
|
4670
|
+
parts.push(spellAt({ start: Math.max(was.end + 1, is.start), end: is.end }));
|
|
4671
|
+
}
|
|
4672
|
+
if (parts.length === 0) return ", which held a marker line";
|
|
4673
|
+
return `, ${parts.join(" and ")} newly pinned`;
|
|
4674
|
+
}
|
|
3806
4675
|
function rewriteLine(rewrite) {
|
|
3807
4676
|
const word = rewrite.from.includes("-") ? "lines" : "line";
|
|
3808
4677
|
const status = rewrite.status === "never-true" ? "never true" : rewrite.status;
|
|
4678
|
+
if (rewrite.end === "marker") {
|
|
4679
|
+
return `marker line ${rewrite.from} -> ${rewrite.to} (misplaced)`;
|
|
4680
|
+
}
|
|
4681
|
+
if (rewrite.reason === "shifted") {
|
|
4682
|
+
return `claim ${word} ${rewrite.from} -> ${rewrite.to} (shifted by a marker)`;
|
|
4683
|
+
}
|
|
4684
|
+
if (rewrite.reason === "re-anchored") {
|
|
4685
|
+
const held = rewrite.lines ?? "";
|
|
4686
|
+
const now = rewrite.newLines ?? "";
|
|
4687
|
+
return `claim re-pinned over ${spellAtSpec(now)} (moved; was ${spellAtSpec(held)}${widening(held, now)})`;
|
|
4688
|
+
}
|
|
3809
4689
|
if (rewrite.reason === "moved") {
|
|
3810
4690
|
return rewrite.end === "claim" ? `claim ${word} ${rewrite.from} -> ${rewrite.to} (moved)` : `source ${shortSrc(rewrite.from)} -> ${shortSrc(rewrite.to)} (moved)`;
|
|
3811
4691
|
}
|
|
@@ -3849,6 +4729,50 @@ function renderUpdatePretty(run3, opts) {
|
|
|
3849
4729
|
lines.push(run3.exitCode === 0 ? c.green(summary) : c.red(summary));
|
|
3850
4730
|
return lines.join("\n");
|
|
3851
4731
|
}
|
|
4732
|
+
function spellLineList(at2, noun) {
|
|
4733
|
+
if (at2.length === 0) return noun;
|
|
4734
|
+
const spelled = at2.map((line) => String(line));
|
|
4735
|
+
if (spelled.length === 1) return `${noun} ${spelled[0] ?? ""}`;
|
|
4736
|
+
const last = spelled[spelled.length - 1] ?? "";
|
|
4737
|
+
return `${noun}s ${spelled.slice(0, -1).join(", ")} and ${last}`;
|
|
4738
|
+
}
|
|
4739
|
+
function removalLine(removal, c) {
|
|
4740
|
+
const markers = spellLineList(removal.markerLines, "line");
|
|
4741
|
+
if (removal.origin === void 0) {
|
|
4742
|
+
const named4 = removal.markerLines.length === 1 ? "the marker" : "the markers";
|
|
4743
|
+
return `removed ${named4} ${c.cyan(removal.id ?? "")} at ${markers}, which named no entry`;
|
|
4744
|
+
}
|
|
4745
|
+
const name = removal.id ?? `/citations/${String(removal.index ?? 0)}`;
|
|
4746
|
+
const where = removal.origin.kind === "manifest" ? `${removal.origin.file}${removal.origin.line === void 0 ? "" : `:${String(removal.origin.line)}`}` : "frontmatter";
|
|
4747
|
+
const also = removal.markerLines.length === 0 ? "" : `, and ${removal.markerLines.length === 1 ? "its marker" : "its markers"} at ${markers}`;
|
|
4748
|
+
return `removed ${c.cyan(name)} from ${where}${also}`;
|
|
4749
|
+
}
|
|
4750
|
+
function renderRemovePretty(run3, opts) {
|
|
4751
|
+
const c = palette(opts.color);
|
|
4752
|
+
const lines = [];
|
|
4753
|
+
let files = 0;
|
|
4754
|
+
for (const page of run3.pages) {
|
|
4755
|
+
if (page.removed.length > 0) files += 1;
|
|
4756
|
+
if (opts.showDiff && page.diff !== "") {
|
|
4757
|
+
for (const line of page.diff.split(/\r?\n/)) {
|
|
4758
|
+
if (line !== "") lines.push(c.dim(line));
|
|
4759
|
+
}
|
|
4760
|
+
}
|
|
4761
|
+
for (const removal of page.removed) {
|
|
4762
|
+
lines.push(`${page.file}: ${removalLine(removal, c)}`);
|
|
4763
|
+
}
|
|
4764
|
+
}
|
|
4765
|
+
if (opts.showDiff) {
|
|
4766
|
+
for (const manifest of run3.manifests ?? []) {
|
|
4767
|
+
for (const line of manifest.diff.split(/\r?\n/)) {
|
|
4768
|
+
if (line !== "") lines.push(c.dim(line));
|
|
4769
|
+
}
|
|
4770
|
+
}
|
|
4771
|
+
}
|
|
4772
|
+
const verb = opts.dryRun === true ? "would be removed" : "removed";
|
|
4773
|
+
lines.push(c.green(`${plural2(run3.removed, "citation")} ${verb} from ${plural2(files, "file")}`));
|
|
4774
|
+
return lines.join("\n");
|
|
4775
|
+
}
|
|
3852
4776
|
|
|
3853
4777
|
// src/cite/reporters/json.ts
|
|
3854
4778
|
function publicCitation(result) {
|
|
@@ -3880,6 +4804,7 @@ function publicCitation(result) {
|
|
|
3880
4804
|
...result.citation.id === void 0 ? {} : { id: result.citation.id },
|
|
3881
4805
|
origin,
|
|
3882
4806
|
anchor: result.anchor,
|
|
4807
|
+
...result.marker === void 0 ? {} : { marker: result.marker },
|
|
3883
4808
|
claim,
|
|
3884
4809
|
source
|
|
3885
4810
|
};
|
|
@@ -3890,6 +4815,9 @@ function publicPage(page) {
|
|
|
3890
4815
|
function renderCheckJson(run3) {
|
|
3891
4816
|
return JSON.stringify({ summary: run3.summary, pages: run3.pages.map(publicPage) }, null, 2);
|
|
3892
4817
|
}
|
|
4818
|
+
function renderRemoveJson(run3) {
|
|
4819
|
+
return JSON.stringify(run3, null, 2);
|
|
4820
|
+
}
|
|
3893
4821
|
function renderUpdateJson(run3) {
|
|
3894
4822
|
const pages = run3.pages.map((page) => ({
|
|
3895
4823
|
...page,
|
|
@@ -3944,7 +4872,7 @@ var KeyError = class extends ToolError {
|
|
|
3944
4872
|
// src/key/commands/rotate.ts
|
|
3945
4873
|
import { existsSync as existsSync2 } from "node:fs";
|
|
3946
4874
|
import { readFile as readFile5 } from "node:fs/promises";
|
|
3947
|
-
import { isAbsolute as isAbsolute3, join as join2, relative as relative3, resolve as
|
|
4875
|
+
import { isAbsolute as isAbsolute3, join as join2, relative as relative3, resolve as resolve7, sep as sep3 } from "node:path";
|
|
3948
4876
|
import { parse as parseYaml } from "yaml";
|
|
3949
4877
|
|
|
3950
4878
|
// src/key/core/config.ts
|
|
@@ -3980,9 +4908,9 @@ function citeSection(file) {
|
|
|
3980
4908
|
return parseCiteConfig(doc.cite, file.source);
|
|
3981
4909
|
}
|
|
3982
4910
|
function rootFor(flag, cite, file, cwd) {
|
|
3983
|
-
if (flag !== void 0) return { root:
|
|
4911
|
+
if (flag !== void 0) return { root: resolve7(cwd, flag), fellBack: false };
|
|
3984
4912
|
if (file !== null && cite.root !== void 0) {
|
|
3985
|
-
return { root:
|
|
4913
|
+
return { root: resolve7(file.dir, cite.root), fellBack: false };
|
|
3986
4914
|
}
|
|
3987
4915
|
const gitRoot = findGitRoot(cwd);
|
|
3988
4916
|
return gitRoot === null ? { root: cwd, fellBack: true } : { root: gitRoot, fellBack: false };
|
|
@@ -4044,7 +4972,7 @@ function reportedPath2(abs, base) {
|
|
|
4044
4972
|
function manifestCollections(collections, opts) {
|
|
4045
4973
|
if (opts.fromCollections) return [...collections];
|
|
4046
4974
|
const rels = opts.files.map(
|
|
4047
|
-
(rel) => reportedPath2(
|
|
4975
|
+
(rel) => reportedPath2(resolve7(opts.base, rel), opts.configDir)
|
|
4048
4976
|
);
|
|
4049
4977
|
return collections.filter((c) => rels.some((rel) => isMember(c, rel)));
|
|
4050
4978
|
}
|
|
@@ -4073,7 +5001,7 @@ async function planManifests(opts) {
|
|
|
4073
5001
|
for (const c of local) {
|
|
4074
5002
|
for (const m of c.externalMetadata) {
|
|
4075
5003
|
const join4 = externalMetadataJoin(m);
|
|
4076
|
-
const abs = isAbsolute3(m.file) ? m.file :
|
|
5004
|
+
const abs = isAbsolute3(m.file) ? m.file : resolve7(opts.configDir, m.file);
|
|
4077
5005
|
const file = reportedPath2(abs, opts.base);
|
|
4078
5006
|
located.set(`${c.name}\0${file}\0${join4}`, abs);
|
|
4079
5007
|
if (plans.has(abs)) continue;
|
|
@@ -4118,7 +5046,7 @@ async function planManifests(opts) {
|
|
|
4118
5046
|
);
|
|
4119
5047
|
}
|
|
4120
5048
|
async function runKeyRotate(opts) {
|
|
4121
|
-
const cwd =
|
|
5049
|
+
const cwd = resolve7(opts.cwd ?? process.cwd());
|
|
4122
5050
|
const env = opts.env ?? process.env;
|
|
4123
5051
|
if (opts.to !== void 0 && !isValidEncryptionKey(opts.to)) throw new KeyError(BAD_TO);
|
|
4124
5052
|
if (opts.inputs.includes(STDIN_TOKEN)) throw new KeyError(NO_STDIN);
|
|
@@ -4202,7 +5130,7 @@ async function runKeyRotate(opts) {
|
|
|
4202
5130
|
const planned = [];
|
|
4203
5131
|
let wantsHistory = false;
|
|
4204
5132
|
for (const rel of files) {
|
|
4205
|
-
const path =
|
|
5133
|
+
const path = resolve7(base, rel);
|
|
4206
5134
|
const before = await readFile5(path, "utf8");
|
|
4207
5135
|
const page = { file: rel, content: before, ...format === void 0 ? {} : { format } };
|
|
4208
5136
|
const sidecarWork = await rotateManifestCitations(sidecars2, manifests, page, {
|
|
@@ -4293,7 +5221,7 @@ async function runKeyRotate(opts) {
|
|
|
4293
5221
|
else outcome = previous === void 0 ? "written" : "finished";
|
|
4294
5222
|
}
|
|
4295
5223
|
const wrote = outcome !== "skipped" && outcome !== "dry-run";
|
|
4296
|
-
const baseline = file !== null && cite.baseline !== void 0 ?
|
|
5224
|
+
const baseline = file !== null && cite.baseline !== void 0 ? resolve7(file.dir, cite.baseline) : join2(file?.dir ?? cwd, DEFAULT_CITE_BASELINE_PATH);
|
|
4297
5225
|
return {
|
|
4298
5226
|
pages: planned.map((p) => p.page),
|
|
4299
5227
|
manifests: valueManifests.map((m) => m.manifest),
|
|
@@ -4309,7 +5237,7 @@ async function runKeyRotate(opts) {
|
|
|
4309
5237
|
|
|
4310
5238
|
// src/key/commands/set.ts
|
|
4311
5239
|
import { existsSync as existsSync3 } from "node:fs";
|
|
4312
|
-
import { basename, dirname, join as join3, relative as relative4, resolve as
|
|
5240
|
+
import { basename, dirname, join as join3, relative as relative4, resolve as resolve8 } from "node:path";
|
|
4313
5241
|
var BAD_VALUE = "The key must be at least 32 hex or base64url characters. Run `manni key set` with no value to generate one.";
|
|
4314
5242
|
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.`;
|
|
4315
5243
|
var alreadyConfigured = (source) => `An encryption key is already configured in ${source}. Run \`manni key rotate\` to replace it and re-encrypt every value.`;
|
|
@@ -4326,7 +5254,7 @@ function refuseBesideLegacy(path, cwd) {
|
|
|
4326
5254
|
}
|
|
4327
5255
|
}
|
|
4328
5256
|
async function runKeySet(opts) {
|
|
4329
|
-
const cwd =
|
|
5257
|
+
const cwd = resolve8(opts.cwd ?? process.cwd());
|
|
4330
5258
|
const env = opts.env ?? process.env;
|
|
4331
5259
|
if (opts.value !== void 0 && !isValidEncryptionKey(opts.value)) {
|
|
4332
5260
|
throw new KeyError(BAD_VALUE);
|
|
@@ -4358,13 +5286,13 @@ export {
|
|
|
4358
5286
|
isSeverity,
|
|
4359
5287
|
meetsSeverity,
|
|
4360
5288
|
reencryptMetadata,
|
|
4361
|
-
codeRegions,
|
|
4362
|
-
codeEndAt,
|
|
4363
5289
|
CiteError,
|
|
4364
5290
|
asCiteError,
|
|
5291
|
+
spellSource,
|
|
4365
5292
|
RULE_ID_PREFIX,
|
|
4366
5293
|
isCiteRule,
|
|
4367
|
-
|
|
5294
|
+
codeRegions,
|
|
5295
|
+
codeEndAt,
|
|
4368
5296
|
shortPin,
|
|
4369
5297
|
shortCommit,
|
|
4370
5298
|
shortSrc,
|
|
@@ -4372,16 +5300,19 @@ export {
|
|
|
4372
5300
|
runAdd,
|
|
4373
5301
|
errorSite,
|
|
4374
5302
|
runCheck,
|
|
5303
|
+
runRemove,
|
|
4375
5304
|
runUpdate,
|
|
4376
5305
|
renderCheckPretty,
|
|
4377
5306
|
renderUpdatePretty,
|
|
5307
|
+
renderRemovePretty,
|
|
4378
5308
|
findingMessage,
|
|
4379
5309
|
renderCheckGithub,
|
|
4380
5310
|
renderCheckJson,
|
|
5311
|
+
renderRemoveJson,
|
|
4381
5312
|
renderUpdateJson,
|
|
4382
5313
|
cite_exports,
|
|
4383
5314
|
KeyError,
|
|
4384
5315
|
runKeyRotate,
|
|
4385
5316
|
runKeySet
|
|
4386
5317
|
};
|
|
4387
|
-
//# sourceMappingURL=chunk-
|
|
5318
|
+
//# sourceMappingURL=chunk-3V4EIH7H.js.map
|