@hawkeyexl/manni 1.1.1 → 2.0.1
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 +4 -4
- package/dist/{chunk-DOQUHXK3.js → chunk-MV2T7XAW.js} +69 -43
- package/dist/chunk-MV2T7XAW.js.map +1 -0
- package/dist/{chunk-HYPDUKRU.js → chunk-PDNSN5UJ.js} +13462 -12212
- package/dist/chunk-PDNSN5UJ.js.map +1 -0
- package/dist/chunk-QLVNSAHR.js +4492 -0
- package/dist/chunk-QLVNSAHR.js.map +1 -0
- package/dist/cli.js +558 -27
- package/dist/cli.js.map +1 -1
- package/dist/docmeta.js +2 -2
- package/dist/index.d.ts +2098 -12
- package/dist/index.js +30 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
- package/dist/chunk-DOQUHXK3.js.map +0 -1
- package/dist/chunk-HYPDUKRU.js.map +0 -1
|
@@ -0,0 +1,4492 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DocmetaError,
|
|
4
|
+
ENCRYPTION_KEY_ENV,
|
|
5
|
+
FILE_SCHEMA_KEY,
|
|
6
|
+
KEYED_PIN_PREFIX,
|
|
7
|
+
LEGACY_CONFIG_NAMES,
|
|
8
|
+
META_CONTEXT,
|
|
9
|
+
PATH_JOIN,
|
|
10
|
+
STDIN_LABEL,
|
|
11
|
+
STDIN_TOKEN,
|
|
12
|
+
ToolError,
|
|
13
|
+
__export,
|
|
14
|
+
assertNonEmpty,
|
|
15
|
+
classifyRef,
|
|
16
|
+
committedKeyWarning,
|
|
17
|
+
decryptValue,
|
|
18
|
+
deepEqual,
|
|
19
|
+
encryptValue,
|
|
20
|
+
ensureEncryptionKey,
|
|
21
|
+
errorMessage,
|
|
22
|
+
escapePointerSegment,
|
|
23
|
+
escapeWorkflowCommandMessage,
|
|
24
|
+
escapeWorkflowCommandProperty,
|
|
25
|
+
externalMetadataJoin,
|
|
26
|
+
extractorByName,
|
|
27
|
+
extractorForExtension,
|
|
28
|
+
findConfigFile,
|
|
29
|
+
findFamilyConfigFile,
|
|
30
|
+
findGitRoot,
|
|
31
|
+
generateEncryptionKey,
|
|
32
|
+
gitignoreOptions,
|
|
33
|
+
integrityOf,
|
|
34
|
+
isEncryptedValue,
|
|
35
|
+
isErrorSeverity,
|
|
36
|
+
isIgnoredByGit,
|
|
37
|
+
isMember,
|
|
38
|
+
isValidEncryptionKey,
|
|
39
|
+
keyedPin,
|
|
40
|
+
loadExternalMetadata,
|
|
41
|
+
locateFrontmatter,
|
|
42
|
+
memberOf,
|
|
43
|
+
mergeExternalMetadata,
|
|
44
|
+
orphanEntries,
|
|
45
|
+
orphanError,
|
|
46
|
+
orphanJoins,
|
|
47
|
+
palette,
|
|
48
|
+
readConfigFile,
|
|
49
|
+
readFamilyConfigFile,
|
|
50
|
+
resolveBaselineRequest,
|
|
51
|
+
resolveEncryptionKey,
|
|
52
|
+
resolveTargetSet,
|
|
53
|
+
selectCollections,
|
|
54
|
+
settleBaseline,
|
|
55
|
+
supportedExtensions,
|
|
56
|
+
withValueAt,
|
|
57
|
+
writeEncryptionKey,
|
|
58
|
+
writeFileAtomic
|
|
59
|
+
} from "./chunk-PDNSN5UJ.js";
|
|
60
|
+
|
|
61
|
+
// src/meta/core/reencrypt.ts
|
|
62
|
+
import { extname } from "node:path";
|
|
63
|
+
var CITATIONS_KEY = "citations";
|
|
64
|
+
var NOT_OURS = "does not decrypt under the current key";
|
|
65
|
+
var READ_ONLY = "this format cannot be written";
|
|
66
|
+
function reencryptMetadata(page, opts) {
|
|
67
|
+
const extractor = page.format !== void 0 ? extractorByName(page.format) : extractorForExtension(extname(page.file));
|
|
68
|
+
if (!extractor) {
|
|
69
|
+
throw new DocmetaError(
|
|
70
|
+
page.format !== void 0 ? `Unknown format "${page.format}" for "${page.file}".` : `Unsupported file type "${extname(page.file)}" for "${page.file}". Supported: ${supportedExtensions().join(", ")}.`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return reencryptWith(extractor, page, opts);
|
|
74
|
+
}
|
|
75
|
+
function tokensIn(data) {
|
|
76
|
+
const found = [];
|
|
77
|
+
const walk = (node, pointer, top) => {
|
|
78
|
+
if (isEncryptedValue(node)) {
|
|
79
|
+
found.push({ pointer, top, token: node });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (Array.isArray(node)) {
|
|
83
|
+
node.forEach((item, i) => {
|
|
84
|
+
walk(item, `${pointer}/${String(i)}`, top);
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (typeof node === "object" && node !== null && !(node instanceof Date)) {
|
|
89
|
+
for (const [k, v] of Object.entries(node)) {
|
|
90
|
+
walk(v, `${pointer}/${escapePointerSegment(k)}`, top);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
for (const [key, value] of Object.entries(data)) {
|
|
95
|
+
if (key === CITATIONS_KEY) continue;
|
|
96
|
+
walk(value, `/${escapePointerSegment(key)}`, key);
|
|
97
|
+
}
|
|
98
|
+
return found;
|
|
99
|
+
}
|
|
100
|
+
function reencryptData(data, opts) {
|
|
101
|
+
const writable = opts.writable ?? true;
|
|
102
|
+
const rewritten = [];
|
|
103
|
+
const skipped = [];
|
|
104
|
+
const found = tokensIn(data);
|
|
105
|
+
for (const { pointer, token } of found) {
|
|
106
|
+
const opened = decryptValue(token, opts.fromKey, META_CONTEXT);
|
|
107
|
+
if (opened.ok) {
|
|
108
|
+
if (!writable) {
|
|
109
|
+
skipped.push({ pointer, message: READ_ONLY });
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
rewritten.push({
|
|
113
|
+
pointer,
|
|
114
|
+
from: token,
|
|
115
|
+
to: encryptValue(opened.value, opts.toKey, META_CONTEXT)
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (decryptValue(token, opts.toKey, META_CONTEXT).ok) continue;
|
|
120
|
+
skipped.push({ pointer, message: NOT_OURS });
|
|
121
|
+
}
|
|
122
|
+
if (rewritten.length === 0) return { data, rewritten, skipped, keys: [] };
|
|
123
|
+
let next = data;
|
|
124
|
+
for (const r of rewritten) next = withValueAt(next, r.pointer, r.to);
|
|
125
|
+
const done = new Set(rewritten.map((r) => r.pointer));
|
|
126
|
+
const keys = [...new Set(found.filter((f) => done.has(f.pointer)).map((f) => f.top))];
|
|
127
|
+
return { data: next, rewritten, skipped, keys };
|
|
128
|
+
}
|
|
129
|
+
function reencryptWith(extractor, page, opts) {
|
|
130
|
+
const read = (content2) => {
|
|
131
|
+
try {
|
|
132
|
+
return extractor.extract(content2, page.file).data;
|
|
133
|
+
} catch (err) {
|
|
134
|
+
if (err instanceof DocmetaError) throw err;
|
|
135
|
+
throw new DocmetaError(`${page.file}: ${errorMessage(err)}`);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
const data = read(page.content);
|
|
139
|
+
const apply2 = extractor.apply;
|
|
140
|
+
const plan = reencryptData(data, { ...opts, writable: apply2 !== void 0 });
|
|
141
|
+
const { rewritten, skipped } = plan;
|
|
142
|
+
if (rewritten.length === 0 || apply2 === void 0) {
|
|
143
|
+
return { content: page.content, rewritten: [], skipped };
|
|
144
|
+
}
|
|
145
|
+
const patch = {};
|
|
146
|
+
for (const top of plan.keys) patch[top] = plan.data[top];
|
|
147
|
+
const content = apply2(page.content, patch, { filePath: page.file });
|
|
148
|
+
const reread = tokensIn(read(content));
|
|
149
|
+
for (const r of rewritten) {
|
|
150
|
+
if (!reread.some((f) => f.pointer === r.pointer && f.token === r.to)) {
|
|
151
|
+
throw new DocmetaError(
|
|
152
|
+
`${page.file}: the ${extractor.name} writer did not keep ${r.pointer}; nothing was re-encrypted.`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { content, rewritten, skipped };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/cite/index.ts
|
|
160
|
+
var cite_exports = {};
|
|
161
|
+
__export(cite_exports, {
|
|
162
|
+
CITATIONS_KEY: () => CITATIONS_KEY2,
|
|
163
|
+
CITE_RULES: () => CITE_RULES,
|
|
164
|
+
CITE_SECTION: () => CITE_SECTION,
|
|
165
|
+
CiteError: () => CiteError,
|
|
166
|
+
DEFAULT_CITE_BASELINE_PATH: () => DEFAULT_CITE_BASELINE_PATH,
|
|
167
|
+
DEFAULT_SEVERITY: () => DEFAULT_SEVERITY,
|
|
168
|
+
FILE_PATTERN: () => FILE_PATTERN,
|
|
169
|
+
GIT_UNAVAILABLE_COMMIT: () => GIT_UNAVAILABLE_COMMIT,
|
|
170
|
+
GIT_UNAVAILABLE_HISTORY: () => GIT_UNAVAILABLE_HISTORY,
|
|
171
|
+
MARKER_JSON: () => MARKER_JSON,
|
|
172
|
+
MAX_MARKERS_PER_PAGE: () => MAX_MARKERS_PER_PAGE,
|
|
173
|
+
MAX_RANGE_LINES: () => MAX_RANGE_LINES,
|
|
174
|
+
MOVE_BUDGET_BYTES: () => MOVE_BUDGET_BYTES,
|
|
175
|
+
MOVE_WINDOW_LINES: () => MOVE_WINDOW_LINES,
|
|
176
|
+
ManifestSet: () => ManifestSet,
|
|
177
|
+
RULE_ID_PREFIX: () => RULE_ID_PREFIX,
|
|
178
|
+
SRC_PATTERN: () => SRC_PATTERN,
|
|
179
|
+
anchoredLines: () => anchoredLines,
|
|
180
|
+
appendFrontmatterCitation: () => appendFrontmatterCitation,
|
|
181
|
+
blockMatches: () => blockMatches,
|
|
182
|
+
bodyLineOf: () => bodyLineOf,
|
|
183
|
+
buildSourceIndex: () => buildSourceIndex,
|
|
184
|
+
checkCitations: () => checkCitations,
|
|
185
|
+
claimEnd: () => claimEnd,
|
|
186
|
+
claimLine: () => claimLine,
|
|
187
|
+
claimMessageFor: () => claimMessageFor,
|
|
188
|
+
classifyCitation: () => classifyCitation,
|
|
189
|
+
detectEol: () => detectEol,
|
|
190
|
+
duplicateJoinRefusal: () => duplicateJoinRefusal,
|
|
191
|
+
entryObject: () => entryObject,
|
|
192
|
+
errorSite: () => errorSite,
|
|
193
|
+
fenceSpanAt: () => fenceSpanAt,
|
|
194
|
+
fencedBlockAfter: () => fencedBlockAfter,
|
|
195
|
+
fencedBlockAt: () => fencedBlockAt,
|
|
196
|
+
fencedBlocks: () => fencedBlocks,
|
|
197
|
+
findWindows: () => findWindows,
|
|
198
|
+
findingsFor: () => findingsFor,
|
|
199
|
+
formatSrc: () => formatSrc,
|
|
200
|
+
formatStatement: () => formatStatement,
|
|
201
|
+
gitClient: () => gitClient,
|
|
202
|
+
hashLines: () => hashLines,
|
|
203
|
+
hashRange: () => hashRange,
|
|
204
|
+
historyOf: () => historyOf,
|
|
205
|
+
insertStatementBefore: () => insertStatementBefore,
|
|
206
|
+
insideFence: () => insideFence,
|
|
207
|
+
isCiteRule: () => isCiteRule,
|
|
208
|
+
isKeyedPin: () => isKeyedPin,
|
|
209
|
+
itemLine: () => itemLine,
|
|
210
|
+
lineAt: () => lineAt,
|
|
211
|
+
lineSpec: () => lineSpec,
|
|
212
|
+
loadCitationSidecars: () => loadCitationSidecars,
|
|
213
|
+
loadCiteConfig: () => loadCiteConfig,
|
|
214
|
+
markerUnit: () => markerUnit,
|
|
215
|
+
messageFor: () => messageFor,
|
|
216
|
+
mintCitation: () => mintCitation,
|
|
217
|
+
noGit: () => noGit,
|
|
218
|
+
normalizeText: () => normalizeText,
|
|
219
|
+
normalizeWhitespace: () => normalizeWhitespace,
|
|
220
|
+
offsetOfLine: () => offsetOfLine,
|
|
221
|
+
orphanRefusal: () => orphanRefusal,
|
|
222
|
+
ownedMessage: () => ownedMessage,
|
|
223
|
+
paragraphAfter: () => paragraphAfter,
|
|
224
|
+
parseCiteConfig: () => parseCiteConfig,
|
|
225
|
+
parseLines: () => parseLines,
|
|
226
|
+
parseSrc: () => parseSrc,
|
|
227
|
+
parseStatements: () => parseStatements,
|
|
228
|
+
pickExtractor: () => pickExtractor,
|
|
229
|
+
pinOfLines: () => pinOfLines,
|
|
230
|
+
rangeLines: () => rangeLines,
|
|
231
|
+
readCiteConfigFile: () => readCiteConfigFile,
|
|
232
|
+
readPage: () => readPage,
|
|
233
|
+
readSource: () => readSource,
|
|
234
|
+
reencryptCitationEntries: () => reencryptCitationEntries,
|
|
235
|
+
reencryptCitations: () => reencryptCitations,
|
|
236
|
+
renderCheckGithub: () => renderCheckGithub,
|
|
237
|
+
renderCheckJson: () => renderCheckJson,
|
|
238
|
+
renderCheckPretty: () => renderCheckPretty,
|
|
239
|
+
renderUpdateJson: () => renderUpdateJson,
|
|
240
|
+
renderUpdatePretty: () => renderUpdatePretty,
|
|
241
|
+
resolveCiteRun: () => resolveCiteRun,
|
|
242
|
+
resolveSeverity: () => resolveSeverity,
|
|
243
|
+
rewriteLine: () => rewriteLine,
|
|
244
|
+
ruleId: () => ruleId,
|
|
245
|
+
runAdd: () => runAdd,
|
|
246
|
+
runCheck: () => runCheck,
|
|
247
|
+
runUpdate: () => runUpdate,
|
|
248
|
+
shortCommit: () => shortCommit,
|
|
249
|
+
shortPin: () => shortPin,
|
|
250
|
+
shortSrc: () => shortSrc,
|
|
251
|
+
sidecarsFor: () => sidecarsFor,
|
|
252
|
+
sliceLines: () => sliceLines,
|
|
253
|
+
sourceRange: () => sourceRange,
|
|
254
|
+
spellLines: () => spellLines,
|
|
255
|
+
spellSource: () => spellSource,
|
|
256
|
+
splice: () => splice,
|
|
257
|
+
spliceEntryField: () => spliceEntryField,
|
|
258
|
+
splitLines: () => splitLines,
|
|
259
|
+
statementForms: () => statementForms,
|
|
260
|
+
toBodyLines: () => toBodyLines,
|
|
261
|
+
toFileLines: () => toFileLines,
|
|
262
|
+
toValidationResult: () => toValidationResult,
|
|
263
|
+
twoManifestsRefusal: () => twoManifestsRefusal,
|
|
264
|
+
unifiedDiff: () => unifiedDiff,
|
|
265
|
+
unitAt: () => unitAt,
|
|
266
|
+
urlManifestRefusal: () => urlManifestRefusal,
|
|
267
|
+
validateEntry: () => validateEntry
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// src/cite/types.ts
|
|
271
|
+
var CITE_RULES = [
|
|
272
|
+
"source-moved",
|
|
273
|
+
"source-moved-ambiguous",
|
|
274
|
+
"source-changed",
|
|
275
|
+
"source-never-true",
|
|
276
|
+
"source-missing",
|
|
277
|
+
"claim-moved",
|
|
278
|
+
"claim-moved-ambiguous",
|
|
279
|
+
"claim-changed",
|
|
280
|
+
"marker-orphan",
|
|
281
|
+
"marker-invalid",
|
|
282
|
+
"marker-repeated",
|
|
283
|
+
"anchor-invalid",
|
|
284
|
+
"entry-invalid",
|
|
285
|
+
"quote-drift"
|
|
286
|
+
];
|
|
287
|
+
|
|
288
|
+
// src/cite/errors.ts
|
|
289
|
+
var CiteError = class extends ToolError {
|
|
290
|
+
constructor(message) {
|
|
291
|
+
super(message);
|
|
292
|
+
this.name = "CiteError";
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// src/cite/core/severity.ts
|
|
297
|
+
var DEFAULT_SEVERITY = {
|
|
298
|
+
"source-moved": "warning",
|
|
299
|
+
"source-moved-ambiguous": "error",
|
|
300
|
+
"source-changed": "error",
|
|
301
|
+
"source-never-true": "error",
|
|
302
|
+
"source-missing": "error",
|
|
303
|
+
"claim-moved": "notice",
|
|
304
|
+
"claim-moved-ambiguous": "warning",
|
|
305
|
+
"claim-changed": "warning",
|
|
306
|
+
"marker-orphan": "error",
|
|
307
|
+
"marker-invalid": "error",
|
|
308
|
+
"marker-repeated": "warning",
|
|
309
|
+
"anchor-invalid": "error",
|
|
310
|
+
"entry-invalid": "error",
|
|
311
|
+
"quote-drift": "error"
|
|
312
|
+
};
|
|
313
|
+
var RULE_ID_PREFIX = "manni:cite";
|
|
314
|
+
function ruleId(rule) {
|
|
315
|
+
return `${RULE_ID_PREFIX}/${rule}`;
|
|
316
|
+
}
|
|
317
|
+
function isCiteRule(value) {
|
|
318
|
+
return CITE_RULES.includes(value);
|
|
319
|
+
}
|
|
320
|
+
function resolveSeverity(overrides) {
|
|
321
|
+
return { ...DEFAULT_SEVERITY, ...overrides ?? {} };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/cite/core/range.ts
|
|
325
|
+
var FILE_PATTERN = /^(?:~[A-Za-z0-9_-]{82,}|(?!~)(?:(?!\.\.?(?:\/|$))[^/\\:\r\n\t]+)(?:\/(?:(?!\.\.?(?:\/|$))[^/\\:\r\n\t]+))*)$/;
|
|
326
|
+
var SRC_PATTERN = /^(?:~[A-Za-z0-9_-]{82,}|(?!~)(?:(?!\.\.?(?:\/|:|$))[^/\\:\r\n\t]+)(?:\/(?:(?!\.\.?(?:\/|:|$))[^/\\:\r\n\t]+))*)(?::[1-9][0-9]*(?:-[1-9][0-9]*)?)?$/;
|
|
327
|
+
var RANGE_SPEC = /^([1-9][0-9]*)(?:-([1-9][0-9]*))?$/;
|
|
328
|
+
function parseSrc(src) {
|
|
329
|
+
if (!SRC_PATTERN.test(src)) {
|
|
330
|
+
throw new CiteError(
|
|
331
|
+
`Invalid src "${src}": expected path, path:L, path:L1-L2, or an encrypted source (~ and at least 82 base64url characters) with the same line forms; the path is repo-root-relative, posix, and free of . and .. segments.`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
const colon = src.indexOf(":");
|
|
335
|
+
const path = colon === -1 ? src : src.slice(0, colon);
|
|
336
|
+
const range = { path, encrypted: isEncryptedValue(path) };
|
|
337
|
+
if (colon === -1) return range;
|
|
338
|
+
const [startText, endText] = src.slice(colon + 1).split("-");
|
|
339
|
+
const start = Number(startText);
|
|
340
|
+
const end = endText === void 0 ? start : Number(endText);
|
|
341
|
+
if (end < start) {
|
|
342
|
+
throw new CiteError(`Invalid range "${src}": end line ${end} is before start line ${start}.`);
|
|
343
|
+
}
|
|
344
|
+
range.start = start;
|
|
345
|
+
range.end = end;
|
|
346
|
+
return range;
|
|
347
|
+
}
|
|
348
|
+
function formatSrc(range) {
|
|
349
|
+
const { path, start, end } = range;
|
|
350
|
+
if (start === void 0) return path;
|
|
351
|
+
if (end === void 0 || end === start) return `${path}:${start}`;
|
|
352
|
+
return `${path}:${start}-${end}`;
|
|
353
|
+
}
|
|
354
|
+
function parseLines(spec) {
|
|
355
|
+
if (typeof spec === "number") {
|
|
356
|
+
return Number.isInteger(spec) && spec >= 1 ? { start: spec, end: spec } : void 0;
|
|
357
|
+
}
|
|
358
|
+
const m = RANGE_SPEC.exec(spec);
|
|
359
|
+
const startText = m?.[1];
|
|
360
|
+
if (startText === void 0) return void 0;
|
|
361
|
+
const start = Number(startText);
|
|
362
|
+
const endText = m?.[2];
|
|
363
|
+
const end = endText === void 0 ? start : Number(endText);
|
|
364
|
+
return end < start ? void 0 : { start, end };
|
|
365
|
+
}
|
|
366
|
+
function lineSpec(lines) {
|
|
367
|
+
return lines.start === lines.end ? lines.start : `${String(lines.start)}-${String(lines.end)}`;
|
|
368
|
+
}
|
|
369
|
+
function spellLines(lines) {
|
|
370
|
+
return String(lineSpec(lines));
|
|
371
|
+
}
|
|
372
|
+
function sourceRange(source) {
|
|
373
|
+
const range = { path: source.file, encrypted: isEncryptedValue(source.file) };
|
|
374
|
+
if (source.lines === void 0) return range;
|
|
375
|
+
const lines = parseLines(source.lines);
|
|
376
|
+
if (lines !== void 0) {
|
|
377
|
+
range.start = lines.start;
|
|
378
|
+
range.end = lines.end;
|
|
379
|
+
}
|
|
380
|
+
return range;
|
|
381
|
+
}
|
|
382
|
+
function spellSource(source) {
|
|
383
|
+
return formatSrc(sourceRange(source));
|
|
384
|
+
}
|
|
385
|
+
function rangeLines(range) {
|
|
386
|
+
if (range.start === void 0) return void 0;
|
|
387
|
+
return lineSpec({ start: range.start, end: range.end ?? range.start });
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// src/cite/core/hash.ts
|
|
391
|
+
function normalizeText(text) {
|
|
392
|
+
const body = text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
393
|
+
return body.replace(/\r\n/g, "\n");
|
|
394
|
+
}
|
|
395
|
+
function splitLines(text) {
|
|
396
|
+
const parts = normalizeText(text).split("\n");
|
|
397
|
+
if (parts.length > 0 && parts[parts.length - 1] === "") parts.pop();
|
|
398
|
+
return parts;
|
|
399
|
+
}
|
|
400
|
+
function sliceLines(lines, range, label) {
|
|
401
|
+
const start = range?.start;
|
|
402
|
+
if (start === void 0) return lines.join("\n");
|
|
403
|
+
const end = range?.end ?? start;
|
|
404
|
+
if (start > lines.length || end > lines.length) {
|
|
405
|
+
const first = start > lines.length ? start : end;
|
|
406
|
+
throw new CiteError(`${label ?? "the source"} has ${lines.length} lines; line ${first} is out of range.`);
|
|
407
|
+
}
|
|
408
|
+
return lines.slice(start - 1, end).join("\n");
|
|
409
|
+
}
|
|
410
|
+
function hashRange(text, range, key) {
|
|
411
|
+
return hashLines(sliceLines(splitLines(text), range), key);
|
|
412
|
+
}
|
|
413
|
+
function hashLines(joined, key) {
|
|
414
|
+
return key === void 0 ? integrityOf(Buffer.from(joined, "utf8")) : keyedPin(joined, key);
|
|
415
|
+
}
|
|
416
|
+
function isKeyedPin(pin) {
|
|
417
|
+
return pin.startsWith(KEYED_PIN_PREFIX);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/cite/core/spell.ts
|
|
421
|
+
function shortPin(integrity) {
|
|
422
|
+
const m = /^((?:hmac-)?sha256-)([0-9a-f]{8})/i.exec(integrity);
|
|
423
|
+
const prefix = m?.[1];
|
|
424
|
+
const digits = m?.[2];
|
|
425
|
+
if (prefix === void 0 || digits === void 0) return integrity;
|
|
426
|
+
return `${prefix}${digits}\u2026`;
|
|
427
|
+
}
|
|
428
|
+
function shortCommit(commit) {
|
|
429
|
+
return commit.slice(0, 7);
|
|
430
|
+
}
|
|
431
|
+
function shortSrc(src) {
|
|
432
|
+
if (!src.startsWith("~")) return src;
|
|
433
|
+
const colon = src.indexOf(":");
|
|
434
|
+
const token = colon === -1 ? src : src.slice(0, colon);
|
|
435
|
+
const lines = colon === -1 ? "" : src.slice(colon);
|
|
436
|
+
return token.length <= 5 ? src : `${token.slice(0, 5)}\u2026${lines}`;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// src/cite/core/sources.ts
|
|
440
|
+
import fg from "fast-glob";
|
|
441
|
+
import { lstat, readFile, realpath } from "node:fs/promises";
|
|
442
|
+
import { join, sep } from "node:path";
|
|
443
|
+
var CONTEXT = "cite-src";
|
|
444
|
+
function encryptSourcePath(path, key) {
|
|
445
|
+
return encryptValue(path, key, CONTEXT);
|
|
446
|
+
}
|
|
447
|
+
function decryptSourcePath(token, key) {
|
|
448
|
+
const opened = decryptValue(token, key, CONTEXT);
|
|
449
|
+
if (!opened.ok || typeof opened.value !== "string") return void 0;
|
|
450
|
+
const path = opened.value;
|
|
451
|
+
if (path.includes(":") || path.startsWith("~") || !SRC_PATTERN.test(path)) return void 0;
|
|
452
|
+
return path;
|
|
453
|
+
}
|
|
454
|
+
function isUnder(real, realRoot) {
|
|
455
|
+
return real === realRoot || real.startsWith(realRoot.endsWith(sep) ? realRoot : realRoot + sep);
|
|
456
|
+
}
|
|
457
|
+
async function admit(root, realRoot, rel) {
|
|
458
|
+
const abs = join(root, rel);
|
|
459
|
+
try {
|
|
460
|
+
const info = await lstat(abs);
|
|
461
|
+
if (!info.isFile()) return void 0;
|
|
462
|
+
const real = await realpath(abs);
|
|
463
|
+
return isUnder(real, realRoot) ? rel : void 0;
|
|
464
|
+
} catch {
|
|
465
|
+
return void 0;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
async function candidates(root, opts) {
|
|
469
|
+
const client = opts?.gitClient;
|
|
470
|
+
if (client !== void 0 && await client.available()) {
|
|
471
|
+
return client.lsFiles();
|
|
472
|
+
}
|
|
473
|
+
return fg("**/*", {
|
|
474
|
+
cwd: root,
|
|
475
|
+
onlyFiles: true,
|
|
476
|
+
dot: true,
|
|
477
|
+
followSymbolicLinks: false,
|
|
478
|
+
ignore: ["**/.git/**", "**/node_modules/**"]
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
async function buildSourceIndex(root, opts) {
|
|
482
|
+
const realRoot = await realpath(root);
|
|
483
|
+
const admitted = await Promise.all(
|
|
484
|
+
(await candidates(root, opts)).map((rel) => admit(root, realRoot, rel))
|
|
485
|
+
);
|
|
486
|
+
const files = Object.freeze(
|
|
487
|
+
[...new Set(admitted.filter((rel) => rel !== void 0))].sort()
|
|
488
|
+
);
|
|
489
|
+
const byPath = new Set(files);
|
|
490
|
+
return {
|
|
491
|
+
files: () => files,
|
|
492
|
+
has: (path) => byPath.has(path)
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
async function readSource(root, index, range, key) {
|
|
496
|
+
let resolvedPath;
|
|
497
|
+
if (range.encrypted) {
|
|
498
|
+
if (key === void 0) return { kind: "missing", reason: "no-key" };
|
|
499
|
+
const path = decryptSourcePath(range.path, key);
|
|
500
|
+
if (path === void 0) return { kind: "missing", reason: "undecryptable" };
|
|
501
|
+
resolvedPath = path;
|
|
502
|
+
} else {
|
|
503
|
+
resolvedPath = range.path;
|
|
504
|
+
}
|
|
505
|
+
if (!index.has(resolvedPath)) return { kind: "missing", reason: "untracked" };
|
|
506
|
+
try {
|
|
507
|
+
const text = await readFile(join(root, resolvedPath), "utf8");
|
|
508
|
+
return { kind: "ok", resolvedPath, text };
|
|
509
|
+
} catch {
|
|
510
|
+
return { kind: "missing", reason: "unreadable" };
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/cite/core/statements.ts
|
|
515
|
+
var HTML_COMMENT = { open: "<!--", close: "-->" };
|
|
516
|
+
var JSX_COMMENT = { open: "{/*", close: "*/}" };
|
|
517
|
+
var LINK_REFERENCE = { open: "[comment]: # (", close: ")" };
|
|
518
|
+
var MARKDOWN_FORMS = [HTML_COMMENT, JSX_COMMENT, LINK_REFERENCE];
|
|
519
|
+
var MDX_FORMS = [JSX_COMMENT, HTML_COMMENT, LINK_REFERENCE];
|
|
520
|
+
var COMMENT_FORMS = [{ open: "<!--", close: "-->" }];
|
|
521
|
+
var ASCIIDOC_FORMS = [{ open: "// (", close: ")" }];
|
|
522
|
+
var RST_FORMS = [{ open: ".. (", close: ")" }];
|
|
523
|
+
var ID = /^[a-z0-9][a-z0-9-]*$/;
|
|
524
|
+
var ANY_FENCE = /^(?:[ \t]*(?:`{3,}|~{3,})|-{4,})/;
|
|
525
|
+
var MARKDOWN_FENCE = /^(`{3,}|~{3,})/;
|
|
526
|
+
var ASCIIDOC_FENCE = /^(-{4,})/;
|
|
527
|
+
var SKIP_MARKDOWN_FENCE = /^[ \t]*(`{3,}|~{3,})/;
|
|
528
|
+
var SKIP_ASCIIDOC_FENCE = ASCIIDOC_FENCE;
|
|
529
|
+
var SPAN_FORMATS = /* @__PURE__ */ new Set(["markdown", "mdx", "asciidoc", "rst"]);
|
|
530
|
+
function statementForms(format) {
|
|
531
|
+
switch (format) {
|
|
532
|
+
case "markdown":
|
|
533
|
+
return MARKDOWN_FORMS;
|
|
534
|
+
case "mdx":
|
|
535
|
+
return MDX_FORMS;
|
|
536
|
+
case "html":
|
|
537
|
+
case "xml":
|
|
538
|
+
return COMMENT_FORMS;
|
|
539
|
+
case "asciidoc":
|
|
540
|
+
return ASCIIDOC_FORMS;
|
|
541
|
+
case "rst":
|
|
542
|
+
return RST_FORMS;
|
|
543
|
+
default:
|
|
544
|
+
return [];
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
function fenceFor(format) {
|
|
548
|
+
switch (format) {
|
|
549
|
+
case "markdown":
|
|
550
|
+
case "mdx":
|
|
551
|
+
return MARKDOWN_FENCE;
|
|
552
|
+
case "asciidoc":
|
|
553
|
+
return ASCIIDOC_FENCE;
|
|
554
|
+
default:
|
|
555
|
+
return void 0;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function skipFenceFor(format) {
|
|
559
|
+
switch (format) {
|
|
560
|
+
case "markdown":
|
|
561
|
+
case "mdx":
|
|
562
|
+
return SKIP_MARKDOWN_FENCE;
|
|
563
|
+
case "asciidoc":
|
|
564
|
+
return SKIP_ASCIIDOC_FENCE;
|
|
565
|
+
default:
|
|
566
|
+
return void 0;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
var BACKTICK = 96;
|
|
570
|
+
function backtickRun(text, i) {
|
|
571
|
+
let n = 0;
|
|
572
|
+
while (text.charCodeAt(i + n) === BACKTICK) n++;
|
|
573
|
+
return n;
|
|
574
|
+
}
|
|
575
|
+
function spanRegions(text, lineStart, out) {
|
|
576
|
+
let i = 0;
|
|
577
|
+
while (i < text.length) {
|
|
578
|
+
const n = backtickRun(text, i);
|
|
579
|
+
if (n === 0) {
|
|
580
|
+
i++;
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
let j = i + n;
|
|
584
|
+
let closer = -1;
|
|
585
|
+
while (j < text.length) {
|
|
586
|
+
const m = backtickRun(text, j);
|
|
587
|
+
if (m === 0) {
|
|
588
|
+
j++;
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
if (m === n) {
|
|
592
|
+
closer = j;
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
j += m;
|
|
596
|
+
}
|
|
597
|
+
if (closer === -1) {
|
|
598
|
+
i += n;
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
out.push({ start: lineStart + i, end: lineStart + closer + n });
|
|
602
|
+
i = closer + n;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
function closesFence(text, open) {
|
|
606
|
+
const trimmed = text.trim();
|
|
607
|
+
let n = 0;
|
|
608
|
+
while (trimmed.charAt(n) === open.char) n++;
|
|
609
|
+
return n >= open.length && n === trimmed.length;
|
|
610
|
+
}
|
|
611
|
+
function codeRegions(body, format) {
|
|
612
|
+
const fence = skipFenceFor(format);
|
|
613
|
+
const spans = SPAN_FORMATS.has(format);
|
|
614
|
+
const out = [];
|
|
615
|
+
if (fence === void 0 && !spans) return out;
|
|
616
|
+
let open;
|
|
617
|
+
let pos = 0;
|
|
618
|
+
while (pos < body.length) {
|
|
619
|
+
const end = lineEnd(body, pos);
|
|
620
|
+
const text = lineText(body, pos, end);
|
|
621
|
+
if (open !== void 0) {
|
|
622
|
+
if (closesFence(text, open)) {
|
|
623
|
+
out.push({ start: open.start, end });
|
|
624
|
+
open = void 0;
|
|
625
|
+
}
|
|
626
|
+
} else {
|
|
627
|
+
const opener = fence?.exec(text)?.[1];
|
|
628
|
+
if (opener !== void 0) {
|
|
629
|
+
open = { char: opener.charAt(0), length: opener.length, start: pos };
|
|
630
|
+
} else if (spans) {
|
|
631
|
+
spanRegions(text, pos, out);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
pos = end + 1;
|
|
635
|
+
}
|
|
636
|
+
if (open !== void 0) out.push({ start: open.start, end: body.length });
|
|
637
|
+
return out;
|
|
638
|
+
}
|
|
639
|
+
function lineEnd(content, pos) {
|
|
640
|
+
const nl = content.indexOf("\n", pos);
|
|
641
|
+
return nl === -1 ? content.length : nl;
|
|
642
|
+
}
|
|
643
|
+
function lineText(content, pos, end) {
|
|
644
|
+
const text = content.slice(pos, end);
|
|
645
|
+
return text.endsWith("\r") ? text.slice(0, -1) : text;
|
|
646
|
+
}
|
|
647
|
+
function beforeCr(content, end) {
|
|
648
|
+
return end > 0 && content.charCodeAt(end - 1) === 13 ? end - 1 : end;
|
|
649
|
+
}
|
|
650
|
+
function nextLineStart(content, offset) {
|
|
651
|
+
if (offset === 0 || content.charCodeAt(offset - 1) === 10) return offset;
|
|
652
|
+
return lineEnd(content, offset) + 1;
|
|
653
|
+
}
|
|
654
|
+
function payloadOf(payload) {
|
|
655
|
+
if (payload === "") return { kind: "bad", reason: "empty payload" };
|
|
656
|
+
if (payload.startsWith("{")) return { kind: "bad", reason: "a JSON payload", json: true };
|
|
657
|
+
if (ID.test(payload)) return { kind: "ref", id: payload };
|
|
658
|
+
return { kind: "bad", reason: "payload is not an id" };
|
|
659
|
+
}
|
|
660
|
+
function parseStatements(body, format, from = { offset: 0, line: 1 }) {
|
|
661
|
+
const out = [];
|
|
662
|
+
const fileLine = (bodyOffset) => from.line + lineAt(body, bodyOffset) - 1;
|
|
663
|
+
const code = codeRegions(body, format);
|
|
664
|
+
const codeEndAt = (at2) => code.find((r) => at2 >= r.start && at2 < r.end)?.end;
|
|
665
|
+
for (const form of statementForms(format)) {
|
|
666
|
+
let cursor = 0;
|
|
667
|
+
for (; ; ) {
|
|
668
|
+
const at2 = body.indexOf(form.open, cursor);
|
|
669
|
+
if (at2 === -1) break;
|
|
670
|
+
const codeEnd = codeEndAt(at2);
|
|
671
|
+
if (codeEnd !== void 0) {
|
|
672
|
+
cursor = codeEnd;
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
const closeAt = body.indexOf(form.close, at2 + form.open.length);
|
|
676
|
+
if (closeAt === -1) break;
|
|
677
|
+
const end = closeAt + form.close.length;
|
|
678
|
+
cursor = end;
|
|
679
|
+
const inner = body.slice(at2 + form.open.length, closeAt).trim();
|
|
680
|
+
if (!(inner === "cite" || inner.startsWith("cite") && /\s/.test(inner.charAt(4)))) {
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
const payload = inner.slice(4).trim();
|
|
684
|
+
const unit = anchoredLines(body, end, format);
|
|
685
|
+
const anchorLine = unit === void 0 ? void 0 : from.line + unit.start - 1;
|
|
686
|
+
const statement = {
|
|
687
|
+
line: fileLine(at2),
|
|
688
|
+
payload: payloadOf(payload),
|
|
689
|
+
raw: inner,
|
|
690
|
+
start: from.offset + at2,
|
|
691
|
+
end: from.offset + end
|
|
692
|
+
};
|
|
693
|
+
if (anchorLine !== void 0) statement.anchorLine = anchorLine;
|
|
694
|
+
out.push(statement);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return out.sort((a, b) => a.start - b.start);
|
|
698
|
+
}
|
|
699
|
+
function detectEol(text) {
|
|
700
|
+
const nl = text.indexOf("\n");
|
|
701
|
+
return nl > 0 && text.charCodeAt(nl - 1) === 13 ? "\r\n" : "\n";
|
|
702
|
+
}
|
|
703
|
+
function paragraphAfter(content, offset) {
|
|
704
|
+
let pos = offset;
|
|
705
|
+
const firstEnd = lineEnd(content, pos);
|
|
706
|
+
if (lineText(content, pos, firstEnd).trim() === "") pos = firstEnd + 1;
|
|
707
|
+
while (pos < content.length) {
|
|
708
|
+
const end = lineEnd(content, pos);
|
|
709
|
+
const text = lineText(content, pos, end);
|
|
710
|
+
if (text.trim() === "") {
|
|
711
|
+
pos = end + 1;
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
if (ANY_FENCE.test(text)) return void 0;
|
|
715
|
+
const start = pos;
|
|
716
|
+
let last = end;
|
|
717
|
+
let cursor = end + 1;
|
|
718
|
+
while (cursor < content.length) {
|
|
719
|
+
const e = lineEnd(content, cursor);
|
|
720
|
+
const t = lineText(content, cursor, e);
|
|
721
|
+
if (t.trim() === "" || ANY_FENCE.test(t)) break;
|
|
722
|
+
last = e;
|
|
723
|
+
cursor = e + 1;
|
|
724
|
+
}
|
|
725
|
+
return { start, end: beforeCr(content, last), line: lineAt(content, start) };
|
|
726
|
+
}
|
|
727
|
+
return void 0;
|
|
728
|
+
}
|
|
729
|
+
function scanFence(content, offset, fence) {
|
|
730
|
+
let pos = nextLineStart(content, offset);
|
|
731
|
+
while (pos < content.length) {
|
|
732
|
+
const end = lineEnd(content, pos);
|
|
733
|
+
const text = lineText(content, pos, end);
|
|
734
|
+
const opener = fence.exec(text)?.[1];
|
|
735
|
+
if (opener === void 0) {
|
|
736
|
+
pos = end + 1;
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
const line = lineAt(content, pos);
|
|
740
|
+
const start = Math.min(end + 1, content.length);
|
|
741
|
+
let cursor = start;
|
|
742
|
+
while (cursor < content.length) {
|
|
743
|
+
const e = lineEnd(content, cursor);
|
|
744
|
+
if (lineText(content, cursor, e).startsWith(opener)) {
|
|
745
|
+
return {
|
|
746
|
+
start,
|
|
747
|
+
end: cursor,
|
|
748
|
+
line,
|
|
749
|
+
text: content.slice(start, cursor),
|
|
750
|
+
next: e + 1
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
cursor = e + 1;
|
|
754
|
+
}
|
|
755
|
+
return void 0;
|
|
756
|
+
}
|
|
757
|
+
return void 0;
|
|
758
|
+
}
|
|
759
|
+
function fencedBlockAfter(content, offset, format) {
|
|
760
|
+
const fence = fenceFor(format);
|
|
761
|
+
if (!fence) return void 0;
|
|
762
|
+
const block = scanFence(content, offset, fence);
|
|
763
|
+
if (!block) return void 0;
|
|
764
|
+
const { start, end, line, text } = block;
|
|
765
|
+
return { start, end, line, text };
|
|
766
|
+
}
|
|
767
|
+
function fencedBlockAt(content, offset) {
|
|
768
|
+
const pos = nextLineStart(content, offset);
|
|
769
|
+
if (!ANY_FENCE.test(lineText(content, pos, lineEnd(content, pos)))) {
|
|
770
|
+
return void 0;
|
|
771
|
+
}
|
|
772
|
+
const block = scanFence(content, pos, MARKDOWN_FENCE) ?? scanFence(content, pos, ASCIIDOC_FENCE);
|
|
773
|
+
if (!block || block.line !== lineAt(content, pos)) return void 0;
|
|
774
|
+
const { start, end, line, text } = block;
|
|
775
|
+
return { start, end, line, text };
|
|
776
|
+
}
|
|
777
|
+
function fencedBlocks(content, bodyOffset, format) {
|
|
778
|
+
const fence = fenceFor(format);
|
|
779
|
+
if (!fence) return [];
|
|
780
|
+
const out = [];
|
|
781
|
+
let pos = bodyOffset;
|
|
782
|
+
for (; ; ) {
|
|
783
|
+
const block = scanFence(content, pos, fence);
|
|
784
|
+
if (!block) return out;
|
|
785
|
+
const { start, end, line, text } = block;
|
|
786
|
+
out.push({ start, end, line, text });
|
|
787
|
+
pos = block.next;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
function writtenForm(format) {
|
|
791
|
+
const form = statementForms(format)[0];
|
|
792
|
+
if (!form) {
|
|
793
|
+
throw new CiteError(`No marker syntax for format "${format}".`);
|
|
794
|
+
}
|
|
795
|
+
return form;
|
|
796
|
+
}
|
|
797
|
+
function formatStatement(format, payload) {
|
|
798
|
+
const form = writtenForm(format);
|
|
799
|
+
const pad = form.open.endsWith("(") ? "" : " ";
|
|
800
|
+
return `${form.open}${pad}cite ${payload.id}${pad}${form.close}`;
|
|
801
|
+
}
|
|
802
|
+
function nextNonBlankLine(text, offset) {
|
|
803
|
+
let pos = nextLineStart(text, offset);
|
|
804
|
+
while (pos < text.length) {
|
|
805
|
+
const end = lineEnd(text, pos);
|
|
806
|
+
if (lineText(text, pos, end).trim() !== "") return lineAt(text, pos);
|
|
807
|
+
pos = end + 1;
|
|
808
|
+
}
|
|
809
|
+
return void 0;
|
|
810
|
+
}
|
|
811
|
+
function fenceSpanAt(text, line, format) {
|
|
812
|
+
const fence = fenceFor(format);
|
|
813
|
+
if (!fence) return void 0;
|
|
814
|
+
const block = scanFence(text, offsetOfLine(text, line), fence);
|
|
815
|
+
if (!block || block.line !== line) return void 0;
|
|
816
|
+
return { start: line, end: lineAt(text, block.end) };
|
|
817
|
+
}
|
|
818
|
+
function insideFence(text, bodyOffset, line, format) {
|
|
819
|
+
return fencedBlocks(text, bodyOffset, format).some(
|
|
820
|
+
(block) => line > block.line && line <= lineAt(text, block.end)
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
function anchoredLines(text, after, format, quote = false) {
|
|
824
|
+
if (quote) {
|
|
825
|
+
const block = fencedBlockAfter(text, after, format);
|
|
826
|
+
return block === void 0 ? void 0 : { start: block.line, end: lineAt(text, block.end) };
|
|
827
|
+
}
|
|
828
|
+
const restEnd = lineEnd(text, after);
|
|
829
|
+
if (text.slice(after, restEnd).trim() !== "") {
|
|
830
|
+
const line2 = lineAt(text, after);
|
|
831
|
+
return { start: line2, end: line2 };
|
|
832
|
+
}
|
|
833
|
+
const paragraph = paragraphAfter(text, after);
|
|
834
|
+
if (paragraph !== void 0) {
|
|
835
|
+
return { start: paragraph.line, end: lineAt(text, paragraph.end) };
|
|
836
|
+
}
|
|
837
|
+
const line = nextNonBlankLine(text, after);
|
|
838
|
+
return line === void 0 ? void 0 : fenceSpanAt(text, line, format);
|
|
839
|
+
}
|
|
840
|
+
function lineAt(content, offset) {
|
|
841
|
+
const stop = Math.min(offset, content.length);
|
|
842
|
+
let line = 1;
|
|
843
|
+
for (let i = 0; i < stop; i++) {
|
|
844
|
+
if (content.charCodeAt(i) === 10) line++;
|
|
845
|
+
}
|
|
846
|
+
return line;
|
|
847
|
+
}
|
|
848
|
+
function offsetOfLine(content, line) {
|
|
849
|
+
let pos = 0;
|
|
850
|
+
for (let n = 1; n < line; n++) {
|
|
851
|
+
const nl = content.indexOf("\n", pos);
|
|
852
|
+
if (nl === -1) return content.length;
|
|
853
|
+
pos = nl + 1;
|
|
854
|
+
}
|
|
855
|
+
return pos;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// src/cite/core/classify.ts
|
|
859
|
+
var MOVE_WINDOW_LINES = 2e3;
|
|
860
|
+
var MOVE_BUDGET_BYTES = 64 * 1024 * 1024;
|
|
861
|
+
var MAX_RANGE_LINES = 5e3;
|
|
862
|
+
function findWindows(lines, length, pin, key, opts) {
|
|
863
|
+
const starts = [];
|
|
864
|
+
const lastStart = lines.length - length + 1;
|
|
865
|
+
if (length < 1 || lastStart < 1) return { starts, truncated: false };
|
|
866
|
+
const joined = (start) => lines.slice(start - 1, start - 1 + length).join("\n");
|
|
867
|
+
const original = opts?.original;
|
|
868
|
+
if (original !== void 0) {
|
|
869
|
+
const first = original[0];
|
|
870
|
+
for (let start = 1; start <= lastStart; start++) {
|
|
871
|
+
if (lines[start - 1] !== first) continue;
|
|
872
|
+
let same = true;
|
|
873
|
+
for (let i = 1; i < length; i++) {
|
|
874
|
+
if (lines[start - 1 + i] !== original[i]) {
|
|
875
|
+
same = false;
|
|
876
|
+
break;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
if (same && hashLines(joined(start), key) === pin) starts.push(start);
|
|
880
|
+
}
|
|
881
|
+
return { starts, truncated: false };
|
|
882
|
+
}
|
|
883
|
+
const budget = opts?.budget ?? MOVE_BUDGET_BYTES;
|
|
884
|
+
const around = opts?.around ?? 1;
|
|
885
|
+
const bandStart = Math.max(1, around - MOVE_WINDOW_LINES);
|
|
886
|
+
const bandEnd = Math.min(lastStart, around + MOVE_WINDOW_LINES);
|
|
887
|
+
const order = [];
|
|
888
|
+
for (let start = bandStart; start <= bandEnd; start++) order.push(start);
|
|
889
|
+
for (let start = 1; start < bandStart; start++) order.push(start);
|
|
890
|
+
for (let start = bandEnd + 1; start <= lastStart; start++) order.push(start);
|
|
891
|
+
let spent = 0;
|
|
892
|
+
for (const start of order) {
|
|
893
|
+
const text = joined(start);
|
|
894
|
+
const bytes = Buffer.byteLength(text, "utf8");
|
|
895
|
+
if (spent + bytes > budget) return { starts, truncated: true };
|
|
896
|
+
spent += bytes;
|
|
897
|
+
if (hashLines(text, key) === pin) starts.push(start);
|
|
898
|
+
}
|
|
899
|
+
return { starts, truncated: false };
|
|
900
|
+
}
|
|
901
|
+
async function historyOf(git, commit, path, range, pin, key, budget) {
|
|
902
|
+
const shown = await git.showFile(commit, path);
|
|
903
|
+
if ("missing" in shown) {
|
|
904
|
+
return shown.missing === "commit" ? { kind: "unavailable" } : { kind: "never-true" };
|
|
905
|
+
}
|
|
906
|
+
const then = splitLines(shown.text);
|
|
907
|
+
let joined;
|
|
908
|
+
try {
|
|
909
|
+
joined = sliceLines(then, range);
|
|
910
|
+
} catch {
|
|
911
|
+
joined = void 0;
|
|
912
|
+
}
|
|
913
|
+
if (joined !== void 0 && hashLines(joined, key) === pin) {
|
|
914
|
+
return { kind: "original", lines: joined.split("\n") };
|
|
915
|
+
}
|
|
916
|
+
if (range.start === void 0) return { kind: "never-true" };
|
|
917
|
+
const length = (range.end ?? range.start) - range.start + 1;
|
|
918
|
+
const search = { around: range.start };
|
|
919
|
+
if (budget !== void 0) search.budget = budget;
|
|
920
|
+
const found = findWindows(then, length, pin, key, search);
|
|
921
|
+
const [first] = found.starts;
|
|
922
|
+
if (first !== void 0) {
|
|
923
|
+
return { kind: "original", lines: then.slice(first - 1, first - 1 + length) };
|
|
924
|
+
}
|
|
925
|
+
return found.truncated ? { kind: "unknown" } : { kind: "never-true" };
|
|
926
|
+
}
|
|
927
|
+
async function classifyCitation(entry, opts) {
|
|
928
|
+
const { citation } = entry;
|
|
929
|
+
const range = sourceRange(citation.source);
|
|
930
|
+
const commit = citation.source["commit-sha"];
|
|
931
|
+
const result = { src: spellSource(citation.source), status: "missing" };
|
|
932
|
+
if (commit !== void 0) result.commitSha = commit;
|
|
933
|
+
const source = await readSource(opts.root, opts.index, range, opts.key);
|
|
934
|
+
if (source.kind === "missing") {
|
|
935
|
+
result.missingReason = source.reason;
|
|
936
|
+
return result;
|
|
937
|
+
}
|
|
938
|
+
result.resolvedPath = source.resolvedPath;
|
|
939
|
+
const key = range.encrypted ? opts.key : void 0;
|
|
940
|
+
const pin = citation.source.integrity;
|
|
941
|
+
const lines = splitLines(source.text);
|
|
942
|
+
let here;
|
|
943
|
+
try {
|
|
944
|
+
here = hashLines(sliceLines(lines, range), key);
|
|
945
|
+
} catch {
|
|
946
|
+
here = void 0;
|
|
947
|
+
}
|
|
948
|
+
if (here === pin) {
|
|
949
|
+
result.status = "current";
|
|
950
|
+
return result;
|
|
951
|
+
}
|
|
952
|
+
let history = { kind: "none" };
|
|
953
|
+
if (commit !== void 0 && await opts.git.available()) {
|
|
954
|
+
history = await historyOf(opts.git, commit, source.resolvedPath, range, pin, key, opts.budget);
|
|
955
|
+
}
|
|
956
|
+
if (range.start !== void 0) {
|
|
957
|
+
const start = range.start;
|
|
958
|
+
const end = range.end ?? start;
|
|
959
|
+
const search = { around: start };
|
|
960
|
+
if (history.kind === "original") search.original = history.lines;
|
|
961
|
+
if (opts.budget !== void 0) search.budget = opts.budget;
|
|
962
|
+
const found = findWindows(lines, end - start + 1, pin, key, search);
|
|
963
|
+
if (found.truncated) result.truncatedSearch = true;
|
|
964
|
+
const spell = (at2) => formatSrc({ ...range, start: at2, end: at2 + (end - start) });
|
|
965
|
+
const [only] = found.starts;
|
|
966
|
+
if (found.starts.length === 1 && only !== void 0) {
|
|
967
|
+
result.status = "moved";
|
|
968
|
+
result.newSrc = spell(only);
|
|
969
|
+
result.newLines = String(lineSpec({ start: only, end: only + (end - start) }));
|
|
970
|
+
return result;
|
|
971
|
+
}
|
|
972
|
+
if (found.starts.length > 1) {
|
|
973
|
+
result.status = "moved-ambiguous";
|
|
974
|
+
result.candidates = found.starts.map(spell);
|
|
975
|
+
return result;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
if (history.kind === "never-true") {
|
|
979
|
+
result.status = "never-true";
|
|
980
|
+
return result;
|
|
981
|
+
}
|
|
982
|
+
result.status = "changed";
|
|
983
|
+
if (history.kind === "unavailable") result.historyAvailable = false;
|
|
984
|
+
if (history.kind === "unknown") result.truncatedSearch = true;
|
|
985
|
+
if (history.kind === "original" && commit !== void 0) {
|
|
986
|
+
result.historyAvailable = true;
|
|
987
|
+
result.commitsSince = await opts.git.subjectsSince(commit, source.resolvedPath);
|
|
988
|
+
result.diff = await opts.git.diffSince(commit, source.resolvedPath);
|
|
989
|
+
}
|
|
990
|
+
return result;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// src/cite/core/claims.ts
|
|
994
|
+
function normalizeWhitespace(text) {
|
|
995
|
+
return text.replace(/\s+/g, " ").trim();
|
|
996
|
+
}
|
|
997
|
+
function normalizeBlock(text) {
|
|
998
|
+
let out = text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
999
|
+
out = out.replace(/\r\n/g, "\n");
|
|
1000
|
+
return out.endsWith("\n") ? out.slice(0, -1) : out;
|
|
1001
|
+
}
|
|
1002
|
+
function blockMatches(blockText, citedJoined) {
|
|
1003
|
+
return normalizeBlock(blockText) === normalizeBlock(citedJoined);
|
|
1004
|
+
}
|
|
1005
|
+
function toFileLines(lines, bodyLine) {
|
|
1006
|
+
return { start: bodyLine + lines.start - 1, end: bodyLine + lines.end - 1 };
|
|
1007
|
+
}
|
|
1008
|
+
function toBodyLines(lines, bodyLine) {
|
|
1009
|
+
return { start: lines.start - bodyLine + 1, end: lines.end - bodyLine + 1 };
|
|
1010
|
+
}
|
|
1011
|
+
function textAt(lines, range) {
|
|
1012
|
+
return lines.slice(range.start - 1, Math.min(range.end, lines.length));
|
|
1013
|
+
}
|
|
1014
|
+
function pinOfLines(lines, range) {
|
|
1015
|
+
if (range.start < 1 || range.end > lines.length) return void 0;
|
|
1016
|
+
return hashLines(lines.slice(range.start - 1, range.end).join("\n"));
|
|
1017
|
+
}
|
|
1018
|
+
function claimLine(claim) {
|
|
1019
|
+
const at2 = claim.fileLines === void 0 ? void 0 : parseLines(claim.fileLines);
|
|
1020
|
+
return at2?.start;
|
|
1021
|
+
}
|
|
1022
|
+
function claimLineNow(claim) {
|
|
1023
|
+
const spec = claim.newFileLines ?? claim.candidateFileLines?.[0] ?? claim.fileLines;
|
|
1024
|
+
const at2 = spec === void 0 ? void 0 : parseLines(spec);
|
|
1025
|
+
return at2?.start;
|
|
1026
|
+
}
|
|
1027
|
+
function claimLinesNow(claim) {
|
|
1028
|
+
const spec = claim.newFileLines ?? claim.candidateFileLines?.[0] ?? claim.fileLines;
|
|
1029
|
+
return spec === void 0 ? void 0 : parseLines(spec);
|
|
1030
|
+
}
|
|
1031
|
+
function claimEnd(page, entry, lines) {
|
|
1032
|
+
const { citation, marker } = entry;
|
|
1033
|
+
const claim = citation.claim;
|
|
1034
|
+
if (claim === void 0) return null;
|
|
1035
|
+
const quote = citation.quote === true;
|
|
1036
|
+
const body = page.bodyLine;
|
|
1037
|
+
if (claim.lines !== void 0) {
|
|
1038
|
+
const recorded = parseLines(claim.lines);
|
|
1039
|
+
if (recorded === void 0) return { status: "skipped" };
|
|
1040
|
+
const file = toFileLines(recorded, body);
|
|
1041
|
+
const end = {
|
|
1042
|
+
lines: spellLines(recorded),
|
|
1043
|
+
fileLines: spellLines(file),
|
|
1044
|
+
status: "changed"
|
|
1045
|
+
};
|
|
1046
|
+
if (marker !== void 0) return { ...end, status: "skipped" };
|
|
1047
|
+
if (pinOfLines(lines, file) === claim.integrity) return { ...end, status: "current" };
|
|
1048
|
+
const width = recorded.end - recorded.start + 1;
|
|
1049
|
+
const found = findWindows(lines.slice(body - 1), width, claim.integrity, void 0, {
|
|
1050
|
+
around: recorded.start
|
|
1051
|
+
});
|
|
1052
|
+
const spans = found.starts.map((start) => ({ start, end: start + width - 1 }));
|
|
1053
|
+
const [only] = spans;
|
|
1054
|
+
if (spans.length === 1 && only !== void 0) {
|
|
1055
|
+
return {
|
|
1056
|
+
...end,
|
|
1057
|
+
status: "moved",
|
|
1058
|
+
newLines: spellLines(only),
|
|
1059
|
+
newFileLines: spellLines(toFileLines(only, body))
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
if (spans.length > 1) {
|
|
1063
|
+
return {
|
|
1064
|
+
...end,
|
|
1065
|
+
status: "moved-ambiguous",
|
|
1066
|
+
candidates: spans.map(spellLines),
|
|
1067
|
+
candidateFileLines: spans.map((span) => spellLines(toFileLines(span, body)))
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
return { ...end, text: textAt(lines, file) };
|
|
1071
|
+
}
|
|
1072
|
+
if (marker !== void 0) {
|
|
1073
|
+
const unit = anchoredLines(page.content, marker.end, page.format, quote);
|
|
1074
|
+
if (unit === void 0) return { status: "changed" };
|
|
1075
|
+
const end = { fileLines: spellLines(unit), status: "changed" };
|
|
1076
|
+
if (pinOfLines(lines, unit) === claim.integrity) return { ...end, status: "current" };
|
|
1077
|
+
return { ...end, text: textAt(lines, unit) };
|
|
1078
|
+
}
|
|
1079
|
+
return { status: "changed" };
|
|
1080
|
+
}
|
|
1081
|
+
function unitAt(page, line, lines) {
|
|
1082
|
+
if (line < page.bodyLine || line > lines.length) return void 0;
|
|
1083
|
+
if ((lines[line - 1] ?? "").trim() === "") return void 0;
|
|
1084
|
+
const block = fenceSpanAt(page.content, line, page.format);
|
|
1085
|
+
if (block !== void 0) {
|
|
1086
|
+
return { lines: block, kind: "block", text: lines.slice(block.start - 1, block.end) };
|
|
1087
|
+
}
|
|
1088
|
+
if (insideFence(page.content, page.bodyOffset, line, page.format)) return void 0;
|
|
1089
|
+
const paragraph = paragraphAfter(page.content, offsetOfLine(page.content, line));
|
|
1090
|
+
if (paragraph === void 0 || paragraph.line !== line) return void 0;
|
|
1091
|
+
const span = { start: line, end: lineAt(page.content, paragraph.end) };
|
|
1092
|
+
return { lines: span, kind: "paragraph", text: lines.slice(span.start - 1, span.end) };
|
|
1093
|
+
}
|
|
1094
|
+
function markerUnit(page, entry, lines) {
|
|
1095
|
+
const { marker, citation } = entry;
|
|
1096
|
+
if (marker === void 0) return void 0;
|
|
1097
|
+
const span = anchoredLines(page.content, marker.end, page.format, citation.quote === true);
|
|
1098
|
+
if (span === void 0 || span.end > lines.length) return void 0;
|
|
1099
|
+
return {
|
|
1100
|
+
lines: span,
|
|
1101
|
+
kind: citation.quote === true ? "block" : "paragraph",
|
|
1102
|
+
text: lines.slice(span.start - 1, span.end)
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// src/cite/core/git.ts
|
|
1107
|
+
import { execFile } from "node:child_process";
|
|
1108
|
+
var MAX_BUFFER = 32 * 1024 * 1024;
|
|
1109
|
+
var NO_COMMIT = /invalid object name|bad revision|unknown revision|bad object/i;
|
|
1110
|
+
var NO_PATH = /does not exist in|exists on disk, but not in|path .* does not exist/i;
|
|
1111
|
+
function run2(root, args) {
|
|
1112
|
+
return new Promise((resolve8) => {
|
|
1113
|
+
execFile(
|
|
1114
|
+
"git",
|
|
1115
|
+
["-c", "core.quotepath=false", ...args],
|
|
1116
|
+
{ cwd: root, windowsHide: true, maxBuffer: MAX_BUFFER, encoding: "utf8" },
|
|
1117
|
+
(error, stdout, stderr) => {
|
|
1118
|
+
if (error) {
|
|
1119
|
+
const failure = { ok: false, stderr, message: error.message };
|
|
1120
|
+
if (typeof error.code === "number") failure.code = error.code;
|
|
1121
|
+
resolve8(failure);
|
|
1122
|
+
} else resolve8({ ok: true, stdout });
|
|
1123
|
+
}
|
|
1124
|
+
);
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
function gitClient(root) {
|
|
1128
|
+
const memo = /* @__PURE__ */ new Map();
|
|
1129
|
+
const once = (key, compute) => {
|
|
1130
|
+
const held = memo.get(key);
|
|
1131
|
+
if (held !== void 0) return held;
|
|
1132
|
+
const made = compute();
|
|
1133
|
+
memo.set(key, made);
|
|
1134
|
+
return made;
|
|
1135
|
+
};
|
|
1136
|
+
const available = () => once("available", async () => (await run2(root, ["rev-parse", "--show-toplevel"])).ok);
|
|
1137
|
+
const failed = (verb, outcome) => new CiteError(`git ${verb} failed: ${outcome.stderr.trim() || outcome.message}`);
|
|
1138
|
+
const hasCommit = (commit) => once(`has\0${commit}`, async () => {
|
|
1139
|
+
const out = await run2(root, ["rev-parse", "--verify", "--quiet", "--end-of-options", `${commit}^{commit}`]);
|
|
1140
|
+
if (out.ok) return true;
|
|
1141
|
+
if (out.code === 1) return false;
|
|
1142
|
+
throw failed("rev-parse", out);
|
|
1143
|
+
});
|
|
1144
|
+
return {
|
|
1145
|
+
available,
|
|
1146
|
+
head: () => once("head", async () => {
|
|
1147
|
+
if (!await available()) return null;
|
|
1148
|
+
const out = await run2(root, ["rev-parse", "HEAD"]);
|
|
1149
|
+
return out.ok ? out.stdout.trim() : null;
|
|
1150
|
+
}),
|
|
1151
|
+
lsFiles: () => once("ls-files", async () => {
|
|
1152
|
+
if (!await available()) return [];
|
|
1153
|
+
const out = await run2(root, ["ls-files", "-z"]);
|
|
1154
|
+
if (!out.ok) throw failed("ls-files", out);
|
|
1155
|
+
return out.stdout.split("\0").filter((entry) => entry !== "");
|
|
1156
|
+
}),
|
|
1157
|
+
showFile: (commit, path) => once(`show\0${commit}\0${path}`, async () => {
|
|
1158
|
+
const out = await run2(root, ["show", "--end-of-options", `${commit}:./${path}`]);
|
|
1159
|
+
if (out.ok) return { text: out.stdout };
|
|
1160
|
+
if (NO_COMMIT.test(out.stderr)) return { missing: "commit" };
|
|
1161
|
+
if (NO_PATH.test(out.stderr)) {
|
|
1162
|
+
return await hasCommit(commit) ? { missing: "path" } : { missing: "commit" };
|
|
1163
|
+
}
|
|
1164
|
+
throw failed("show", out);
|
|
1165
|
+
}),
|
|
1166
|
+
subjectsSince: (commit, path) => once(`log\0${commit}\0${path}`, async () => {
|
|
1167
|
+
const out = await run2(root, ["log", "--format=%s", "--end-of-options", `${commit}..HEAD`, "--", path]);
|
|
1168
|
+
if (!out.ok) throw failed("log", out);
|
|
1169
|
+
return out.stdout.split(/\r?\n/).filter((line) => line !== "");
|
|
1170
|
+
}),
|
|
1171
|
+
diffSince: (commit, path) => once(`diff\0${commit}\0${path}`, async () => {
|
|
1172
|
+
const out = await run2(root, ["diff", "--end-of-options", commit, "--", path]);
|
|
1173
|
+
if (!out.ok) throw failed("diff", out);
|
|
1174
|
+
return out.stdout;
|
|
1175
|
+
})
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
var GIT_UNAVAILABLE_HISTORY = "git is not available here, so citations are checked without history: no never-true, no diffs, no commit subjects.";
|
|
1179
|
+
var GIT_UNAVAILABLE_COMMIT = "git is not available here, so the citation records no commit.";
|
|
1180
|
+
function noGit() {
|
|
1181
|
+
return {
|
|
1182
|
+
available: () => Promise.resolve(false),
|
|
1183
|
+
head: () => Promise.resolve(null),
|
|
1184
|
+
lsFiles: () => Promise.resolve([]),
|
|
1185
|
+
showFile: () => Promise.resolve({ missing: "commit" }),
|
|
1186
|
+
subjectsSince: () => Promise.resolve([]),
|
|
1187
|
+
diffSince: () => Promise.resolve("")
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// src/cite/core/page.ts
|
|
1192
|
+
import { extname as extname2 } from "node:path";
|
|
1193
|
+
import * as Ajv2020Ns from "ajv/dist/2020.js";
|
|
1194
|
+
|
|
1195
|
+
// src/cite/schema/citations.json
|
|
1196
|
+
var citations_default = {
|
|
1197
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
1198
|
+
$id: "manni:citations:1.0.0-proposal.3",
|
|
1199
|
+
title: "manni citations vocabulary (1.0.0-proposal.3)",
|
|
1200
|
+
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.",
|
|
1201
|
+
type: "object",
|
|
1202
|
+
additionalProperties: true,
|
|
1203
|
+
properties: {
|
|
1204
|
+
citations: {
|
|
1205
|
+
type: "array",
|
|
1206
|
+
minItems: 1,
|
|
1207
|
+
items: {
|
|
1208
|
+
$ref: "#/$defs/citationEntry"
|
|
1209
|
+
},
|
|
1210
|
+
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."
|
|
1211
|
+
}
|
|
1212
|
+
},
|
|
1213
|
+
patternProperties: {
|
|
1214
|
+
"^citation-": false
|
|
1215
|
+
},
|
|
1216
|
+
$defs: {
|
|
1217
|
+
citationEntry: {
|
|
1218
|
+
type: "object",
|
|
1219
|
+
additionalProperties: false,
|
|
1220
|
+
required: [
|
|
1221
|
+
"source"
|
|
1222
|
+
],
|
|
1223
|
+
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.",
|
|
1224
|
+
properties: {
|
|
1225
|
+
id: {
|
|
1226
|
+
type: "string",
|
|
1227
|
+
pattern: "^[a-z0-9][a-z0-9-]*$",
|
|
1228
|
+
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."
|
|
1229
|
+
},
|
|
1230
|
+
claim: {
|
|
1231
|
+
$ref: "#/$defs/claim"
|
|
1232
|
+
},
|
|
1233
|
+
source: {
|
|
1234
|
+
$ref: "#/$defs/source"
|
|
1235
|
+
},
|
|
1236
|
+
quote: {
|
|
1237
|
+
type: "boolean",
|
|
1238
|
+
default: false,
|
|
1239
|
+
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."
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
},
|
|
1243
|
+
claim: {
|
|
1244
|
+
type: "object",
|
|
1245
|
+
additionalProperties: false,
|
|
1246
|
+
required: [
|
|
1247
|
+
"integrity"
|
|
1248
|
+
],
|
|
1249
|
+
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.",
|
|
1250
|
+
properties: {
|
|
1251
|
+
lines: {
|
|
1252
|
+
$ref: "#/$defs/lines",
|
|
1253
|
+
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."
|
|
1254
|
+
},
|
|
1255
|
+
integrity: {
|
|
1256
|
+
type: "string",
|
|
1257
|
+
pattern: "^sha256-[0-9a-f]{64}$",
|
|
1258
|
+
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."
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
},
|
|
1262
|
+
source: {
|
|
1263
|
+
type: "object",
|
|
1264
|
+
additionalProperties: false,
|
|
1265
|
+
required: [
|
|
1266
|
+
"file",
|
|
1267
|
+
"integrity"
|
|
1268
|
+
],
|
|
1269
|
+
description: "The lines the claim rests on: a file, an optional line range, a pin, and the commit the pin was taken at.",
|
|
1270
|
+
properties: {
|
|
1271
|
+
file: {
|
|
1272
|
+
$ref: "#/$defs/fileRef"
|
|
1273
|
+
},
|
|
1274
|
+
lines: {
|
|
1275
|
+
$ref: "#/$defs/lines",
|
|
1276
|
+
description: "File lines. Absent, the whole file is pinned. The lines stay readable even when `file` is encrypted."
|
|
1277
|
+
},
|
|
1278
|
+
integrity: {
|
|
1279
|
+
type: "string",
|
|
1280
|
+
pattern: "^(?:sha256|hmac-sha256)-[0-9a-f]{64}$",
|
|
1281
|
+
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."
|
|
1282
|
+
},
|
|
1283
|
+
"commit-sha": {
|
|
1284
|
+
type: "string",
|
|
1285
|
+
pattern: "^[0-9a-f]{7,64}$",
|
|
1286
|
+
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."
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
},
|
|
1290
|
+
lines: {
|
|
1291
|
+
oneOf: [
|
|
1292
|
+
{
|
|
1293
|
+
type: "integer",
|
|
1294
|
+
minimum: 1
|
|
1295
|
+
},
|
|
1296
|
+
{
|
|
1297
|
+
type: "string",
|
|
1298
|
+
pattern: "^[1-9][0-9]*-[1-9][0-9]*$"
|
|
1299
|
+
}
|
|
1300
|
+
],
|
|
1301
|
+
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.'
|
|
1302
|
+
},
|
|
1303
|
+
fileRef: {
|
|
1304
|
+
type: "string",
|
|
1305
|
+
pattern: "^(?:~[A-Za-z0-9_-]{82,}|(?!~)(?:(?!\\.\\.?(?:/|$))[^/\\\\:\\r\\n\\t]+)(?:/(?:(?!\\.\\.?(?:/|$))[^/\\\\:\\r\\n\\t]+))*)$",
|
|
1306
|
+
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."
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
};
|
|
1310
|
+
|
|
1311
|
+
// src/meta/core/external-metadata-write.ts
|
|
1312
|
+
import { posix } from "node:path";
|
|
1313
|
+
import {
|
|
1314
|
+
LineCounter,
|
|
1315
|
+
isMap,
|
|
1316
|
+
isNode,
|
|
1317
|
+
isPair,
|
|
1318
|
+
isScalar,
|
|
1319
|
+
isSeq,
|
|
1320
|
+
parseDocument,
|
|
1321
|
+
stringify
|
|
1322
|
+
} from "yaml";
|
|
1323
|
+
function spliceManifestValue(text, options) {
|
|
1324
|
+
const { entry, key, value, file } = options;
|
|
1325
|
+
const join4 = options.join ?? PATH_JOIN;
|
|
1326
|
+
const at2 = (line2) => {
|
|
1327
|
+
const name = file === void 0 ? "Manifest" : `Manifest ${file}`;
|
|
1328
|
+
if (line2 === void 0) return name;
|
|
1329
|
+
return file === void 0 ? `${name} line ${String(line2)}` : `${name}:${String(line2)}`;
|
|
1330
|
+
};
|
|
1331
|
+
if (key === FILE_SCHEMA_KEY) {
|
|
1332
|
+
throw new DocmetaError(
|
|
1333
|
+
`${at2()}: refusing to write "${FILE_SCHEMA_KEY}" for "${entry}"; a manifest never chooses the schema a document is judged by. Use "overrides" in the config.`
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
if (value === void 0) {
|
|
1337
|
+
throw new DocmetaError(`${at2()}: refusing to write "${key}" for "${entry}" with no value.`);
|
|
1338
|
+
}
|
|
1339
|
+
const eol = detectEol2(text);
|
|
1340
|
+
const lc = new LineCounter();
|
|
1341
|
+
const doc = parseDocument(text, { lineCounter: lc, uniqueKeys: false });
|
|
1342
|
+
const problem = doc.errors[0];
|
|
1343
|
+
if (problem) {
|
|
1344
|
+
throw new DocmetaError(`${at2()} is not valid YAML: ${problem.message}`);
|
|
1345
|
+
}
|
|
1346
|
+
const keyLine = (node) => {
|
|
1347
|
+
const r = rangeOf(node);
|
|
1348
|
+
return r ? lc.linePos(r[0]).line : void 0;
|
|
1349
|
+
};
|
|
1350
|
+
const same = join4 === PATH_JOIN ? (a, b) => posix.normalize(a) === posix.normalize(b) : (a, b) => a === b;
|
|
1351
|
+
const root = doc.contents;
|
|
1352
|
+
let spelled = entry;
|
|
1353
|
+
let edit;
|
|
1354
|
+
if (root === null || isScalar(root) && root.value === null && emptySource(text, root)) {
|
|
1355
|
+
edit = startManifest(text, entry, key, value, eol);
|
|
1356
|
+
} else if (!isMap(root)) {
|
|
1357
|
+
throw new DocmetaError(
|
|
1358
|
+
`${at2()}: the manifest must be a mapping from document ${join4 === PATH_JOIN ? "path" : `"${join4}"`} to owned keys.`
|
|
1359
|
+
);
|
|
1360
|
+
} else {
|
|
1361
|
+
const style = detectStyle(text, root, eol);
|
|
1362
|
+
const matches = root.items.filter((p) => same(keyString(p.key), entry));
|
|
1363
|
+
const [first, second] = matches;
|
|
1364
|
+
if (first && second) {
|
|
1365
|
+
throw new DocmetaError(
|
|
1366
|
+
`${at2(keyLine(second.key))}: "${keyString(second.key)}" is named twice (first at line ${String(keyLine(first.key) ?? "?")}). Merge the two entries into one.`
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
if (first === void 0) {
|
|
1370
|
+
if (root.flow === true && root.items.length > 0) {
|
|
1371
|
+
throw new DocmetaError(
|
|
1372
|
+
`${at2()}: the manifest is a flow mapping ({ \u2026 }), which this writer does not add entries to. Rewrite it in block style.`
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
edit = appendEntry(text, root, entry, key, value, style);
|
|
1376
|
+
} else {
|
|
1377
|
+
spelled = keyString(first.key);
|
|
1378
|
+
const where = at2(keyLine(first.key));
|
|
1379
|
+
const v = first.value;
|
|
1380
|
+
if (!isMap(v)) {
|
|
1381
|
+
throw new DocmetaError(`${where}: "${spelled}" must be a mapping of owned keys to values.`);
|
|
1382
|
+
}
|
|
1383
|
+
const hits = v.items.filter((kv) => keyString(kv.key) === key);
|
|
1384
|
+
const [hit, again] = hits;
|
|
1385
|
+
if (hit && again) {
|
|
1386
|
+
throw new DocmetaError(
|
|
1387
|
+
`${at2(keyLine(again.key))}: "${spelled}" sets "${key}" twice (first at line ${String(keyLine(hit.key) ?? "?")}). Remove one.`
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
const flowRefusal = () => new DocmetaError(
|
|
1391
|
+
`${where}: "${spelled}" is a flow mapping ({ \u2026 }), which this writer does not add keys to. Rewrite the entry in block style.`
|
|
1392
|
+
);
|
|
1393
|
+
const formRefusal = () => new DocmetaError(
|
|
1394
|
+
`${where}: "${spelled}" writes a key in a form this writer does not edit. Write it as "${key}: \u2026".`
|
|
1395
|
+
);
|
|
1396
|
+
let made;
|
|
1397
|
+
if (v.flow === true) {
|
|
1398
|
+
if (hit) made = replaceFlowValue(text, hit, value);
|
|
1399
|
+
else if (v.items.length === 0) made = fillEmptyEntry(text, first, key, value, style);
|
|
1400
|
+
else throw flowRefusal();
|
|
1401
|
+
if (!made) throw hit ? flowRefusal() : formRefusal();
|
|
1402
|
+
} else {
|
|
1403
|
+
made = hit ? replaceBlockValue(text, hit, value, style) : insertKey(text, v, key, value, style);
|
|
1404
|
+
if (!made) throw formRefusal();
|
|
1405
|
+
}
|
|
1406
|
+
edit = made;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
const out = text.slice(0, edit.start) + edit.insert + text.slice(edit.end);
|
|
1410
|
+
const line = readBack(
|
|
1411
|
+
doc,
|
|
1412
|
+
out,
|
|
1413
|
+
spelled,
|
|
1414
|
+
key,
|
|
1415
|
+
value,
|
|
1416
|
+
(why) => new DocmetaError(
|
|
1417
|
+
`${at2()}: writing "${key}" for "${entry}" did not read back as written, so nothing was written. ${why}`
|
|
1418
|
+
)
|
|
1419
|
+
);
|
|
1420
|
+
return { text: out, line };
|
|
1421
|
+
}
|
|
1422
|
+
function startManifest(text, entry, key, value, eol) {
|
|
1423
|
+
const style = { step: 2, indentSeq: true, eol };
|
|
1424
|
+
const block = blockLines({ [entry]: { [key]: value } }, 0, style);
|
|
1425
|
+
let insert;
|
|
1426
|
+
if (text === "" || text.endsWith("\n")) insert = block + eol;
|
|
1427
|
+
else insert = eol + block;
|
|
1428
|
+
return { start: text.length, end: text.length, insert };
|
|
1429
|
+
}
|
|
1430
|
+
function appendEntry(text, root, entry, key, value, style) {
|
|
1431
|
+
const body = { [entry]: { [key]: value } };
|
|
1432
|
+
if (root.flow === true) {
|
|
1433
|
+
const r = rangeOf(root);
|
|
1434
|
+
const start = r ? r[0] : text.length;
|
|
1435
|
+
const end = contentEnd(text, root) ?? start;
|
|
1436
|
+
return { start, end, insert: blockLines(body, colOf(text, start), style) };
|
|
1437
|
+
}
|
|
1438
|
+
const firstKey = rangeOf(root.items[0]?.key);
|
|
1439
|
+
const col = firstKey ? colOf(text, firstKey[0]) : 0;
|
|
1440
|
+
const pos = lineEnd2(text, contentEnd(text, root) ?? text.length);
|
|
1441
|
+
const last = root.items.at(-1);
|
|
1442
|
+
const spaced = root.items.length > 1 && isNode(last?.key) && last.key.spaceBefore === true;
|
|
1443
|
+
return {
|
|
1444
|
+
start: pos,
|
|
1445
|
+
end: pos,
|
|
1446
|
+
insert: style.eol + (spaced ? style.eol : "") + blockLines(body, col, style)
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
function insertKey(text, entryMap, key, value, style) {
|
|
1450
|
+
const firstKey = rangeOf(entryMap.items[0]?.key);
|
|
1451
|
+
const end = contentEnd(text, entryMap);
|
|
1452
|
+
if (!firstKey || end === void 0) return void 0;
|
|
1453
|
+
const pos = lineEnd2(text, end);
|
|
1454
|
+
return {
|
|
1455
|
+
start: pos,
|
|
1456
|
+
end: pos,
|
|
1457
|
+
insert: style.eol + blockLines({ [key]: value }, colOf(text, firstKey[0]), style)
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
function fillEmptyEntry(text, entryPair, key, value, style) {
|
|
1461
|
+
const start = colonEnd(text, entryPair);
|
|
1462
|
+
const keyRange = rangeOf(entryPair.key);
|
|
1463
|
+
const end = contentEnd(text, entryPair.value);
|
|
1464
|
+
if (start === void 0 || !keyRange || end === void 0) return void 0;
|
|
1465
|
+
const col = colOf(text, keyRange[0]) + style.step;
|
|
1466
|
+
return { start, end, insert: style.eol + blockLines({ [key]: value }, col, style) };
|
|
1467
|
+
}
|
|
1468
|
+
function replaceBlockValue(text, pair, value, style) {
|
|
1469
|
+
const colon = colonEnd(text, pair);
|
|
1470
|
+
const keyRange = rangeOf(pair.key);
|
|
1471
|
+
if (colon === void 0 || !keyRange) return void 0;
|
|
1472
|
+
const keyCol = colOf(text, keyRange[0]);
|
|
1473
|
+
let start = colon;
|
|
1474
|
+
let ownLine = false;
|
|
1475
|
+
let i = colon;
|
|
1476
|
+
while (text.charAt(i) === " " || text.charAt(i) === " ") i++;
|
|
1477
|
+
if (text.charAt(i) === "#") {
|
|
1478
|
+
start = lineEnd2(text, i);
|
|
1479
|
+
ownLine = true;
|
|
1480
|
+
}
|
|
1481
|
+
const old = pair.value;
|
|
1482
|
+
const oldEnd = isScalar(old) && old.value === null && emptySource(text, old) ? start : contentEnd(text, old) ?? start;
|
|
1483
|
+
const end = Math.max(start, oldEnd);
|
|
1484
|
+
let insert = afterColon(value, keyCol, style);
|
|
1485
|
+
if (ownLine && !insert.startsWith(style.eol)) {
|
|
1486
|
+
insert = style.eol + " ".repeat(keyCol + style.step) + insert.trimStart();
|
|
1487
|
+
}
|
|
1488
|
+
return { start, end, insert };
|
|
1489
|
+
}
|
|
1490
|
+
function replaceFlowValue(text, pair, value) {
|
|
1491
|
+
const r = rangeOf(pair.value);
|
|
1492
|
+
if (!r || emptySource(text, pair.value)) return void 0;
|
|
1493
|
+
const end = contentEnd(text, pair.value) ?? r[1];
|
|
1494
|
+
return { start: r[0], end, insert: flowText(value) };
|
|
1495
|
+
}
|
|
1496
|
+
function readBack(before, out, spelled, key, value, fail) {
|
|
1497
|
+
const lc = new LineCounter();
|
|
1498
|
+
const after = parseDocument(out, { lineCounter: lc, uniqueKeys: false });
|
|
1499
|
+
const problem = after.errors[0];
|
|
1500
|
+
if (problem) throw fail(`The result is not valid YAML: ${problem.message}`);
|
|
1501
|
+
const was = before.toJS({ maxAliasCount: 100 });
|
|
1502
|
+
const now = after.toJS({ maxAliasCount: 100 });
|
|
1503
|
+
const wasRoot = isRecord(was) ? was : {};
|
|
1504
|
+
const wasEntry = wasRoot[spelled];
|
|
1505
|
+
const expected = {
|
|
1506
|
+
...wasRoot,
|
|
1507
|
+
[spelled]: { ...isRecord(wasEntry) ? wasEntry : {}, [key]: value }
|
|
1508
|
+
};
|
|
1509
|
+
const nowEntry = isRecord(now) ? now[spelled] : void 0;
|
|
1510
|
+
if (!isRecord(nowEntry) || !deepEqual(nowEntry[key], value)) {
|
|
1511
|
+
throw fail(
|
|
1512
|
+
"The value reads back differently. A Date, an undefined member, or another value YAML cannot hold does not survive the trip."
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
if (!deepEqual(now, expected)) {
|
|
1516
|
+
throw fail("Another value in the manifest would have changed.");
|
|
1517
|
+
}
|
|
1518
|
+
const root = after.contents;
|
|
1519
|
+
const pair = isMap(root) ? root.items.find((p) => keyString(p.key) === spelled) : void 0;
|
|
1520
|
+
const entryMap = pair?.value;
|
|
1521
|
+
const kv = isMap(entryMap) ? entryMap.items.find((p) => keyString(p.key) === key) : void 0;
|
|
1522
|
+
const r = rangeOf(kv?.value) ?? rangeOf(kv?.key);
|
|
1523
|
+
if (!r) throw fail("The written value could not be found again.");
|
|
1524
|
+
return lc.linePos(r[0]).line;
|
|
1525
|
+
}
|
|
1526
|
+
function blockLines(pair, col, style) {
|
|
1527
|
+
const s = stringify(pair, { lineWidth: 0, indent: style.step, indentSeq: style.indentSeq });
|
|
1528
|
+
return indentLines(s.replace(/\n$/, "").split("\n"), col, 0).join(style.eol);
|
|
1529
|
+
}
|
|
1530
|
+
function afterColon(value, col, style) {
|
|
1531
|
+
const s = stringify({ k: value }, { lineWidth: 0, indent: style.step, indentSeq: style.indentSeq });
|
|
1532
|
+
const lines = s.replace(/\n$/, "").slice("k:".length).split("\n");
|
|
1533
|
+
return indentLines(lines, col, 1).join(style.eol);
|
|
1534
|
+
}
|
|
1535
|
+
function indentLines(lines, col, from) {
|
|
1536
|
+
const pad = " ".repeat(col);
|
|
1537
|
+
return lines.map((l, i) => i < from || l === "" ? l : pad + l);
|
|
1538
|
+
}
|
|
1539
|
+
function flowText(value) {
|
|
1540
|
+
const s = stringify(value, { lineWidth: 0, collectionStyle: "flow" }).replace(/\n$/, "");
|
|
1541
|
+
return s.includes("\n") ? JSON.stringify(value) : s;
|
|
1542
|
+
}
|
|
1543
|
+
function detectEol2(text) {
|
|
1544
|
+
const i = text.indexOf("\n");
|
|
1545
|
+
return i > 0 && text.charAt(i - 1) === "\r" ? "\r\n" : "\n";
|
|
1546
|
+
}
|
|
1547
|
+
function detectStyle(text, root, eol) {
|
|
1548
|
+
let step = 2;
|
|
1549
|
+
for (const pair of root.items) {
|
|
1550
|
+
const v = pair.value;
|
|
1551
|
+
const entryKey = rangeOf(pair.key);
|
|
1552
|
+
const ownedKey = isMap(v) && v.flow !== true ? rangeOf(v.items[0]?.key) : void 0;
|
|
1553
|
+
if (entryKey && ownedKey) {
|
|
1554
|
+
const d = colOf(text, ownedKey[0]) - colOf(text, entryKey[0]);
|
|
1555
|
+
if (d > 0) {
|
|
1556
|
+
step = d;
|
|
1557
|
+
break;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
return { step, indentSeq: seqIndented(text, root) ?? true, eol };
|
|
1562
|
+
}
|
|
1563
|
+
function seqIndented(text, node) {
|
|
1564
|
+
if (isMap(node)) {
|
|
1565
|
+
for (const pair of node.items) {
|
|
1566
|
+
const v = pair.value;
|
|
1567
|
+
if (isSeq(v) && v.flow !== true && v.items.length > 0) {
|
|
1568
|
+
const k = rangeOf(pair.key);
|
|
1569
|
+
const s = rangeOf(v);
|
|
1570
|
+
if (k && s) return colOf(text, s[0]) > colOf(text, k[0]);
|
|
1571
|
+
}
|
|
1572
|
+
const inner = seqIndented(text, v);
|
|
1573
|
+
if (inner !== void 0) return inner;
|
|
1574
|
+
}
|
|
1575
|
+
} else if (isSeq(node)) {
|
|
1576
|
+
for (const item of node.items) {
|
|
1577
|
+
const inner = seqIndented(text, item);
|
|
1578
|
+
if (inner !== void 0) return inner;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
return void 0;
|
|
1582
|
+
}
|
|
1583
|
+
function contentEnd(text, node) {
|
|
1584
|
+
if (isMap(node) && node.flow !== true) {
|
|
1585
|
+
const last = node.items.at(-1);
|
|
1586
|
+
if (last) return contentEnd(text, last.value) ?? contentEnd(text, last.key);
|
|
1587
|
+
} else if (isSeq(node) && node.flow !== true) {
|
|
1588
|
+
const last = node.items.at(-1);
|
|
1589
|
+
if (last !== void 0) {
|
|
1590
|
+
return isPair(last) ? contentEnd(text, last.value) ?? contentEnd(text, last.key) : contentEnd(text, last);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
const r = rangeOf(node);
|
|
1594
|
+
if (!r) return void 0;
|
|
1595
|
+
let end = r[1];
|
|
1596
|
+
while (end > r[0] && /\s/.test(text.charAt(end - 1))) end--;
|
|
1597
|
+
return end;
|
|
1598
|
+
}
|
|
1599
|
+
function colonEnd(text, pair) {
|
|
1600
|
+
const r = rangeOf(pair.key);
|
|
1601
|
+
if (!r) return void 0;
|
|
1602
|
+
let i = r[1];
|
|
1603
|
+
while (text.charAt(i) === " " || text.charAt(i) === " ") i++;
|
|
1604
|
+
return text.charAt(i) === ":" ? i + 1 : void 0;
|
|
1605
|
+
}
|
|
1606
|
+
function lineEnd2(text, i) {
|
|
1607
|
+
const j = text.indexOf("\n", i);
|
|
1608
|
+
if (j < 0) return text.length;
|
|
1609
|
+
return j > 0 && text.charAt(j - 1) === "\r" ? j - 1 : j;
|
|
1610
|
+
}
|
|
1611
|
+
function colOf(text, offset) {
|
|
1612
|
+
if (offset === 0) return 0;
|
|
1613
|
+
let start = text.lastIndexOf("\n", offset - 1) + 1;
|
|
1614
|
+
if (start === 0 && text.startsWith("\uFEFF")) start = 1;
|
|
1615
|
+
return offset - start;
|
|
1616
|
+
}
|
|
1617
|
+
function rangeOf(node) {
|
|
1618
|
+
return isNode(node) ? node.range ?? void 0 : void 0;
|
|
1619
|
+
}
|
|
1620
|
+
function emptySource(text, node) {
|
|
1621
|
+
const r = rangeOf(node);
|
|
1622
|
+
return !r || text.slice(r[0], r[1]).trim() === "";
|
|
1623
|
+
}
|
|
1624
|
+
function keyString(key) {
|
|
1625
|
+
return isScalar(key) ? String(key.value) : String(key);
|
|
1626
|
+
}
|
|
1627
|
+
function isRecord(x) {
|
|
1628
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// src/cite/core/page.ts
|
|
1632
|
+
var MAX_MARKERS_PER_PAGE = 500;
|
|
1633
|
+
var ELEMENT_BACKED = /* @__PURE__ */ new Set(["html", "xml"]);
|
|
1634
|
+
var MARKER_JSON = "A marker names an entry by id. Write the entry in frontmatter or the sidecar.";
|
|
1635
|
+
function ownedMessage(owner) {
|
|
1636
|
+
return `"citations" is owned by manifest ${owner.file} (collection ${owner.collection}); remove it from the document`;
|
|
1637
|
+
}
|
|
1638
|
+
var Ajv2020 = Ajv2020Ns.default;
|
|
1639
|
+
var entryValidator;
|
|
1640
|
+
function compileEntryValidator() {
|
|
1641
|
+
if (entryValidator) return entryValidator;
|
|
1642
|
+
const ajv = new Ajv2020({ allErrors: false, strict: false });
|
|
1643
|
+
ajv.addSchema(citations_default);
|
|
1644
|
+
entryValidator = ajv.compile({
|
|
1645
|
+
$ref: `${citations_default.$id}#/$defs/citationEntry`
|
|
1646
|
+
});
|
|
1647
|
+
return entryValidator;
|
|
1648
|
+
}
|
|
1649
|
+
function validateEntry(entry) {
|
|
1650
|
+
const validate = compileEntryValidator();
|
|
1651
|
+
if (validate(entry)) return void 0;
|
|
1652
|
+
const first = validate.errors?.[0];
|
|
1653
|
+
if (!first) return "invalid entry";
|
|
1654
|
+
return `${first.instancePath} ${first.message ?? "is invalid"}`.trim();
|
|
1655
|
+
}
|
|
1656
|
+
function isRecord2(value) {
|
|
1657
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1658
|
+
}
|
|
1659
|
+
function toCitation(entry) {
|
|
1660
|
+
const source = isRecord2(entry.source) ? entry.source : {};
|
|
1661
|
+
const citation = {
|
|
1662
|
+
source: {
|
|
1663
|
+
file: String(source.file),
|
|
1664
|
+
integrity: String(source.integrity)
|
|
1665
|
+
}
|
|
1666
|
+
};
|
|
1667
|
+
if (typeof source.lines === "number" || typeof source.lines === "string") {
|
|
1668
|
+
citation.source.lines = source.lines;
|
|
1669
|
+
}
|
|
1670
|
+
if (typeof source["commit-sha"] === "string") {
|
|
1671
|
+
citation.source["commit-sha"] = source["commit-sha"];
|
|
1672
|
+
}
|
|
1673
|
+
if (typeof entry.id === "string") citation.id = entry.id;
|
|
1674
|
+
if (isRecord2(entry.claim)) {
|
|
1675
|
+
const claim = entry.claim;
|
|
1676
|
+
citation.claim = { integrity: String(claim.integrity) };
|
|
1677
|
+
if (typeof claim.lines === "number" || typeof claim.lines === "string") {
|
|
1678
|
+
citation.claim.lines = claim.lines;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
if (typeof entry.quote === "boolean") citation.quote = entry.quote;
|
|
1682
|
+
return citation;
|
|
1683
|
+
}
|
|
1684
|
+
function finding(rule, message, extra) {
|
|
1685
|
+
const level = DEFAULT_SEVERITY[rule];
|
|
1686
|
+
const out = {
|
|
1687
|
+
rule,
|
|
1688
|
+
ruleId: ruleId(rule),
|
|
1689
|
+
// The caller re-applies the configured severity table; no page-side rule
|
|
1690
|
+
// defaults to `off`, so there is nothing to collapse.
|
|
1691
|
+
severity: level === "off" ? "error" : level,
|
|
1692
|
+
message
|
|
1693
|
+
};
|
|
1694
|
+
if (extra.line !== void 0) out.line = extra.line;
|
|
1695
|
+
if (extra.file !== void 0) out.file = extra.file;
|
|
1696
|
+
if (extra.id !== void 0) out.id = extra.id;
|
|
1697
|
+
if (extra.src !== void 0) out.src = extra.src;
|
|
1698
|
+
if (extra.index !== void 0) out.index = extra.index;
|
|
1699
|
+
return out;
|
|
1700
|
+
}
|
|
1701
|
+
function named(id, text) {
|
|
1702
|
+
return id === void 0 ? text : `${id}: ${text}`;
|
|
1703
|
+
}
|
|
1704
|
+
function subjectOf(entry) {
|
|
1705
|
+
const out = {};
|
|
1706
|
+
if (!isRecord2(entry)) return out;
|
|
1707
|
+
if (typeof entry.id === "string") out.id = entry.id;
|
|
1708
|
+
if (isRecord2(entry.source) && typeof entry.source.file === "string") out.src = entry.source.file;
|
|
1709
|
+
return out;
|
|
1710
|
+
}
|
|
1711
|
+
function pickExtractor(file, format) {
|
|
1712
|
+
if (format !== void 0) {
|
|
1713
|
+
const forced = extractorByName(format);
|
|
1714
|
+
if (!forced?.implemented) {
|
|
1715
|
+
throw new CiteError(
|
|
1716
|
+
`Unknown format "${format}". Supported extensions: ${supportedExtensions().join(", ")}.`
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
return forced;
|
|
1720
|
+
}
|
|
1721
|
+
const extension = extname2(file);
|
|
1722
|
+
const byExt = extractorForExtension(extension);
|
|
1723
|
+
if (!byExt) {
|
|
1724
|
+
throw new CiteError(
|
|
1725
|
+
`Unsupported file type "${extension}" for "${file}". Supported: ${supportedExtensions().join(", ")}. Use --as to override.`
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
1728
|
+
return byExt;
|
|
1729
|
+
}
|
|
1730
|
+
function byLine(a, b) {
|
|
1731
|
+
return (a.line ?? Number.MAX_SAFE_INTEGER) - (b.line ?? Number.MAX_SAFE_INTEGER);
|
|
1732
|
+
}
|
|
1733
|
+
function bodyLineOf(content, bodyOffset) {
|
|
1734
|
+
if (bodyOffset <= 0) return 1;
|
|
1735
|
+
const line = lineAt(content, bodyOffset);
|
|
1736
|
+
return content.charCodeAt(bodyOffset - 1) === 10 ? line : line + 1;
|
|
1737
|
+
}
|
|
1738
|
+
function readPage(file, content, opts) {
|
|
1739
|
+
const extractor = pickExtractor(file, opts?.format);
|
|
1740
|
+
const format = extractor.name;
|
|
1741
|
+
const extracted = extractor.extract(content, file);
|
|
1742
|
+
const data = extracted.data;
|
|
1743
|
+
const bodyOffset = ELEMENT_BACKED.has(format) ? 0 : locateFrontmatter(content)?.closeEnd ?? 0;
|
|
1744
|
+
const bodyLine = bodyLineOf(content, bodyOffset);
|
|
1745
|
+
const findings = [];
|
|
1746
|
+
const citations = [];
|
|
1747
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1748
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1749
|
+
const markedAt = /* @__PURE__ */ new Map();
|
|
1750
|
+
for (const key of Object.keys(data)) {
|
|
1751
|
+
if (!key.startsWith("citation-")) continue;
|
|
1752
|
+
findings.push(
|
|
1753
|
+
finding(
|
|
1754
|
+
"entry-invalid",
|
|
1755
|
+
`"${key}" is not a citations key; the page root reserves the citation- prefix`,
|
|
1756
|
+
{ line: extracted.lineFor(`/${key}`) }
|
|
1757
|
+
)
|
|
1758
|
+
);
|
|
1759
|
+
}
|
|
1760
|
+
const injected = opts?.citations;
|
|
1761
|
+
const rawCitations = data.citations;
|
|
1762
|
+
if (opts?.owned !== void 0 && rawCitations !== void 0) {
|
|
1763
|
+
findings.push(
|
|
1764
|
+
finding("entry-invalid", ownedMessage(opts.owned), {
|
|
1765
|
+
line: extracted.lineFor("/citations")
|
|
1766
|
+
})
|
|
1767
|
+
);
|
|
1768
|
+
}
|
|
1769
|
+
if (injected === void 0 && rawCitations !== void 0 && !Array.isArray(rawCitations)) {
|
|
1770
|
+
findings.push(
|
|
1771
|
+
finding("entry-invalid", "citations must be an array of entries", {
|
|
1772
|
+
line: extracted.lineFor("/citations")
|
|
1773
|
+
})
|
|
1774
|
+
);
|
|
1775
|
+
}
|
|
1776
|
+
const inputs = injected !== void 0 ? [...injected] : (Array.isArray(rawCitations) ? rawCitations : []).map((entry, index) => {
|
|
1777
|
+
const origin = { kind: "frontmatter", file };
|
|
1778
|
+
const line = extracted.lineFor(`/citations/${String(index)}`);
|
|
1779
|
+
if (line !== void 0) origin.line = line;
|
|
1780
|
+
return { entry, origin };
|
|
1781
|
+
});
|
|
1782
|
+
inputs.forEach((input, index) => {
|
|
1783
|
+
const origin = { ...input.origin, index };
|
|
1784
|
+
const where = { index };
|
|
1785
|
+
if (origin.line !== void 0) where.line = origin.line;
|
|
1786
|
+
if (origin.kind === "manifest") where.file = origin.file;
|
|
1787
|
+
const entry = input.entry;
|
|
1788
|
+
const error = validateEntry(entry);
|
|
1789
|
+
if (error !== void 0 || !isRecord2(entry)) {
|
|
1790
|
+
findings.push(finding("entry-invalid", error ?? "must be object", { ...where, ...subjectOf(entry) }));
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
const citation = toCitation(entry);
|
|
1794
|
+
const subject = { ...where, src: spellSource(citation.source) };
|
|
1795
|
+
if (citation.id !== void 0) subject.id = citation.id;
|
|
1796
|
+
const bad = ["claim", "source"].find((end) => {
|
|
1797
|
+
const lines = end === "claim" ? citation.claim?.lines : citation.source.lines;
|
|
1798
|
+
return lines !== void 0 && parseLines(lines) === void 0;
|
|
1799
|
+
});
|
|
1800
|
+
if (bad !== void 0) {
|
|
1801
|
+
const lines = bad === "claim" ? citation.claim?.lines : citation.source.lines;
|
|
1802
|
+
findings.push(
|
|
1803
|
+
finding(
|
|
1804
|
+
"entry-invalid",
|
|
1805
|
+
named(citation.id, `${bad}.lines "${String(lines)}" ends before it starts`),
|
|
1806
|
+
subject
|
|
1807
|
+
)
|
|
1808
|
+
);
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
const encrypted = isEncryptedValue(citation.source.file);
|
|
1812
|
+
if (encrypted !== isKeyedPin(citation.source.integrity)) {
|
|
1813
|
+
findings.push(
|
|
1814
|
+
finding(
|
|
1815
|
+
"entry-invalid",
|
|
1816
|
+
named(
|
|
1817
|
+
citation.id,
|
|
1818
|
+
encrypted ? "an encrypted source is pinned with hmac-sha256-, not sha256-." : "a plain source is pinned with sha256-, not hmac-sha256-."
|
|
1819
|
+
),
|
|
1820
|
+
subject
|
|
1821
|
+
)
|
|
1822
|
+
);
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
const page = { citation, origin };
|
|
1826
|
+
citations.push(page);
|
|
1827
|
+
if (citation.id !== void 0) {
|
|
1828
|
+
if (ids.has(citation.id)) {
|
|
1829
|
+
findings.push(finding("entry-invalid", `duplicate id "${citation.id}"`, subject));
|
|
1830
|
+
} else {
|
|
1831
|
+
ids.add(citation.id);
|
|
1832
|
+
byId.set(citation.id, page);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
});
|
|
1836
|
+
const body = content.slice(bodyOffset);
|
|
1837
|
+
const statements = parseStatements(body, format, {
|
|
1838
|
+
offset: bodyOffset,
|
|
1839
|
+
line: bodyLine
|
|
1840
|
+
});
|
|
1841
|
+
if (statements.length > MAX_MARKERS_PER_PAGE) {
|
|
1842
|
+
findings.push(
|
|
1843
|
+
finding(
|
|
1844
|
+
"marker-invalid",
|
|
1845
|
+
`more than ${String(MAX_MARKERS_PER_PAGE)} markers on one page (${String(statements.length)}); the rest are not read`,
|
|
1846
|
+
{ line: statements[MAX_MARKERS_PER_PAGE]?.line }
|
|
1847
|
+
)
|
|
1848
|
+
);
|
|
1849
|
+
}
|
|
1850
|
+
for (const statement of statements.slice(0, MAX_MARKERS_PER_PAGE)) {
|
|
1851
|
+
const { payload } = statement;
|
|
1852
|
+
if (payload.kind === "bad") {
|
|
1853
|
+
findings.push(
|
|
1854
|
+
finding(
|
|
1855
|
+
"marker-invalid",
|
|
1856
|
+
payload.json === true ? MARKER_JSON : `invalid marker: ${payload.reason}`,
|
|
1857
|
+
{ line: statement.line }
|
|
1858
|
+
)
|
|
1859
|
+
);
|
|
1860
|
+
continue;
|
|
1861
|
+
}
|
|
1862
|
+
const target = byId.get(payload.id);
|
|
1863
|
+
if (target === void 0) {
|
|
1864
|
+
findings.push(
|
|
1865
|
+
finding("marker-orphan", `no entry has id "${payload.id}"`, {
|
|
1866
|
+
line: statement.line,
|
|
1867
|
+
id: payload.id
|
|
1868
|
+
})
|
|
1869
|
+
);
|
|
1870
|
+
continue;
|
|
1871
|
+
}
|
|
1872
|
+
const first = markedAt.get(target);
|
|
1873
|
+
if (first !== void 0) {
|
|
1874
|
+
findings.push(
|
|
1875
|
+
finding(
|
|
1876
|
+
"marker-repeated",
|
|
1877
|
+
`${payload.id} is named by markers at lines ${String(first)} and ${String(statement.line)}; the first anchors it.`,
|
|
1878
|
+
{
|
|
1879
|
+
line: statement.line,
|
|
1880
|
+
id: payload.id,
|
|
1881
|
+
index: target.origin.index,
|
|
1882
|
+
src: spellSource(target.citation.source)
|
|
1883
|
+
}
|
|
1884
|
+
)
|
|
1885
|
+
);
|
|
1886
|
+
continue;
|
|
1887
|
+
}
|
|
1888
|
+
markedAt.set(target, statement.line);
|
|
1889
|
+
target.marker = statement;
|
|
1890
|
+
}
|
|
1891
|
+
for (const entry of citations) {
|
|
1892
|
+
const { citation, origin, marker } = entry;
|
|
1893
|
+
const subject = { index: origin.index, src: spellSource(citation.source) };
|
|
1894
|
+
if (citation.id !== void 0) subject.id = citation.id;
|
|
1895
|
+
if (origin.line !== void 0) subject.line = origin.line;
|
|
1896
|
+
if (origin.kind === "manifest") subject.file = origin.file;
|
|
1897
|
+
if (citation.claim?.lines !== void 0 && marker !== void 0) {
|
|
1898
|
+
const onPage = { index: origin.index, line: marker.line, src: subject.src };
|
|
1899
|
+
if (citation.id !== void 0) onPage.id = citation.id;
|
|
1900
|
+
findings.push(
|
|
1901
|
+
finding(
|
|
1902
|
+
"anchor-invalid",
|
|
1903
|
+
`${citation.id ?? "the entry"} has claim lines and a marker. Keep one.`,
|
|
1904
|
+
onPage
|
|
1905
|
+
)
|
|
1906
|
+
);
|
|
1907
|
+
continue;
|
|
1908
|
+
}
|
|
1909
|
+
if (citation.quote === true && citation.claim?.lines === void 0 && marker === void 0) {
|
|
1910
|
+
findings.push(
|
|
1911
|
+
finding("anchor-invalid", named(citation.id, "quote needs a claim or a marker."), subject)
|
|
1912
|
+
);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
return {
|
|
1916
|
+
file,
|
|
1917
|
+
format,
|
|
1918
|
+
content,
|
|
1919
|
+
bodyOffset,
|
|
1920
|
+
bodyLine,
|
|
1921
|
+
citations,
|
|
1922
|
+
statements,
|
|
1923
|
+
findings: findings.sort(byLine)
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
// src/cite/core/sidecar.ts
|
|
1928
|
+
import { isAbsolute, relative, resolve, sep as sep2 } from "node:path";
|
|
1929
|
+
var CITATIONS_KEY2 = "citations";
|
|
1930
|
+
var toPosix = (path) => path.split(sep2).join("/");
|
|
1931
|
+
function reportedPath(abs, base) {
|
|
1932
|
+
const rel = relative(base, abs);
|
|
1933
|
+
return rel === "" ? "." : toPosix(rel);
|
|
1934
|
+
}
|
|
1935
|
+
function urlManifestRefusal(source, collection) {
|
|
1936
|
+
return `${source}: collection ${collection}: ${CITATIONS_KEY2} cannot come from a URL manifest, because cite writes them.`;
|
|
1937
|
+
}
|
|
1938
|
+
function twoManifestsRefusal(label, a, b) {
|
|
1939
|
+
return `${label} is in collections ${a} and ${b}, and both keep citations in a manifest.`;
|
|
1940
|
+
}
|
|
1941
|
+
function ownsCitations(manifest) {
|
|
1942
|
+
return manifest.keys.includes(CITATIONS_KEY2);
|
|
1943
|
+
}
|
|
1944
|
+
async function loadCitationSidecars(opts) {
|
|
1945
|
+
const toError = opts.toError ?? ((message) => new CiteError(message));
|
|
1946
|
+
const manifests = [];
|
|
1947
|
+
const scoped = [];
|
|
1948
|
+
for (const collection of opts.collections) {
|
|
1949
|
+
const owning = collection.externalMetadata.filter(ownsCitations);
|
|
1950
|
+
if (owning.length === 0) continue;
|
|
1951
|
+
for (const manifest of owning) {
|
|
1952
|
+
if (classifyRef(manifest.file).kind === "url") {
|
|
1953
|
+
throw toError(urlManifestRefusal(opts.configSource, collection.name));
|
|
1954
|
+
}
|
|
1955
|
+
const path = isAbsolute(manifest.file) ? manifest.file : resolve(opts.configDir, manifest.file);
|
|
1956
|
+
manifests.push({
|
|
1957
|
+
collection: collection.name,
|
|
1958
|
+
path,
|
|
1959
|
+
file: reportedPath(path, opts.base),
|
|
1960
|
+
join: externalMetadataJoin(manifest)
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1963
|
+
scoped.push({ ...collection, externalMetadata: owning });
|
|
1964
|
+
}
|
|
1965
|
+
if (manifests.length === 0) return null;
|
|
1966
|
+
const index = await loadExternalMetadata(scoped, {
|
|
1967
|
+
configDir: opts.configDir,
|
|
1968
|
+
base: opts.base
|
|
1969
|
+
});
|
|
1970
|
+
if (index === null) return null;
|
|
1971
|
+
return sidecars(index, manifests, scoped, opts);
|
|
1972
|
+
}
|
|
1973
|
+
function sidecars(index, manifests, collections, opts) {
|
|
1974
|
+
const toError = opts.toError ?? ((message) => new CiteError(message));
|
|
1975
|
+
const { configDir, base } = opts;
|
|
1976
|
+
const forPage = (label, content, format) => {
|
|
1977
|
+
const members = memberOf(collections, configDir, base, label);
|
|
1978
|
+
if (members.length === 0) return { joins: [] };
|
|
1979
|
+
const mine = manifests.filter((m) => members.includes(m.collection));
|
|
1980
|
+
const [owner, second] = mine;
|
|
1981
|
+
if (owner === void 0) return { joins: [] };
|
|
1982
|
+
if (second !== void 0) {
|
|
1983
|
+
throw toError(twoManifestsRefusal(label, owner.collection, second.collection));
|
|
1984
|
+
}
|
|
1985
|
+
const extractor = pickExtractor(label, format);
|
|
1986
|
+
const extracted = extractor.extract(content, label);
|
|
1987
|
+
const merged = mergeExternalMetadata(label, extracted, index, members, base, {
|
|
1988
|
+
encryptionKey: () => opts.key
|
|
1989
|
+
});
|
|
1990
|
+
const out = { owner, joins: merged.joins };
|
|
1991
|
+
if (owner.join === PATH_JOIN) {
|
|
1992
|
+
out.entry = toPosix(relative(configDir, resolve(base, label)));
|
|
1993
|
+
} else {
|
|
1994
|
+
const value = merged.joins.find((j) => j.field === owner.join)?.value;
|
|
1995
|
+
const raw = value ?? extracted.data[owner.join];
|
|
1996
|
+
if (typeof raw === "string" && raw !== "") out.entry = raw;
|
|
1997
|
+
else if (typeof raw === "number" || typeof raw === "boolean") out.entry = String(raw);
|
|
1998
|
+
}
|
|
1999
|
+
const where = merged.locate(`/${CITATIONS_KEY2}`);
|
|
2000
|
+
if (where !== void 0) {
|
|
2001
|
+
const supplied = merged.extracted.data[CITATIONS_KEY2];
|
|
2002
|
+
const list = Array.isArray(supplied) ? supplied : [supplied];
|
|
2003
|
+
out.citations = list.map((entry, i) => {
|
|
2004
|
+
const at2 = merged.locate(`/${CITATIONS_KEY2}/${String(i)}`);
|
|
2005
|
+
const line = (Array.isArray(supplied) ? at2?.line : void 0) ?? where.line;
|
|
2006
|
+
return {
|
|
2007
|
+
entry,
|
|
2008
|
+
origin: {
|
|
2009
|
+
kind: "manifest",
|
|
2010
|
+
file: where.file,
|
|
2011
|
+
...line === void 0 ? {} : { line }
|
|
2012
|
+
}
|
|
2013
|
+
};
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
return out;
|
|
2017
|
+
};
|
|
2018
|
+
return {
|
|
2019
|
+
manifests,
|
|
2020
|
+
configDir,
|
|
2021
|
+
base,
|
|
2022
|
+
forPage,
|
|
2023
|
+
orphans: (loaded, covered) => orphanEntries(index, loaded, base, covered),
|
|
2024
|
+
orphanJoins: (matched, covered) => orphanJoins(index, matched, covered)
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
function orphanRefusal(orphans, toError = (m) => new CiteError(m)) {
|
|
2028
|
+
return toError(orphanError(orphans).message);
|
|
2029
|
+
}
|
|
2030
|
+
function duplicateJoinRefusal(field, value, file, labels) {
|
|
2031
|
+
return `${String(labels.length)} pages carry ${field} "${value}"; ${file} cannot tell them apart (${labels.join(", ")}).`;
|
|
2032
|
+
}
|
|
2033
|
+
function sidecarsFor(run3, over = {}) {
|
|
2034
|
+
const declared = run3.configFile?.collections ?? [];
|
|
2035
|
+
if (run3.configDir === void 0 || declared.length === 0) return Promise.resolve(null);
|
|
2036
|
+
return loadCitationSidecars({
|
|
2037
|
+
// Every declared collection, whatever `--collection` or the paths chose.
|
|
2038
|
+
collections: declared,
|
|
2039
|
+
configDir: run3.configDir,
|
|
2040
|
+
base: over.base ?? run3.base,
|
|
2041
|
+
configSource: run3.configSource ?? run3.configPath ?? "the config",
|
|
2042
|
+
...run3.key === void 0 ? {} : { key: run3.key },
|
|
2043
|
+
...over.toError === void 0 ? {} : { toError: over.toError }
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
// src/cite/core/manifest.ts
|
|
2048
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
2049
|
+
import { posix as posix2 } from "node:path";
|
|
2050
|
+
import { LineCounter as LineCounter2, isMap as isMap2, isNode as isNode2, isScalar as isScalar2, isSeq as isSeq2, parseDocument as parseDocument2 } from "yaml";
|
|
2051
|
+
|
|
2052
|
+
// src/cite/core/write.ts
|
|
2053
|
+
function extractorFor(format) {
|
|
2054
|
+
const extractor = extractorByName(format);
|
|
2055
|
+
if (!extractor) throw new CiteError(`Unknown format "${format}".`);
|
|
2056
|
+
return extractor;
|
|
2057
|
+
}
|
|
2058
|
+
function readsFencedFrontmatter(extractor, label) {
|
|
2059
|
+
return extractor.extract("---\nprobe: 1\n---\n", label).fenced === true;
|
|
2060
|
+
}
|
|
2061
|
+
function appendFrontmatterCitation(content, format, citation, filePath) {
|
|
2062
|
+
const label = filePath ?? "the page";
|
|
2063
|
+
const extractor = extractorFor(format);
|
|
2064
|
+
if (!extractor.apply || !readsFencedFrontmatter(extractor, label)) {
|
|
2065
|
+
throw new CiteError(
|
|
2066
|
+
`${label} has no frontmatter to write to; keep its citations in a manifest instead.`
|
|
2067
|
+
);
|
|
2068
|
+
}
|
|
2069
|
+
const extracted = extractor.extract(content, label);
|
|
2070
|
+
if (locateFrontmatter(content)?.flavor === "toml") {
|
|
2071
|
+
throw new CiteError(
|
|
2072
|
+
`${label} has TOML frontmatter; manni cannot add a citations table without re-emitting the whole block, which would drop its comments. Add the entry by hand.`
|
|
2073
|
+
);
|
|
2074
|
+
}
|
|
2075
|
+
const raw = extracted.data.citations ?? [];
|
|
2076
|
+
if (!Array.isArray(raw)) {
|
|
2077
|
+
throw new CiteError(`${label}: \`citations\` is not a list; edit it by hand.`);
|
|
2078
|
+
}
|
|
2079
|
+
const existing = raw;
|
|
2080
|
+
const entry = entryObject(citation);
|
|
2081
|
+
try {
|
|
2082
|
+
return extractor.apply(content, { citations: [...existing, entry] });
|
|
2083
|
+
} catch (e) {
|
|
2084
|
+
if (e instanceof DocmetaError) throw new CiteError(`${label}: ${e.message}`);
|
|
2085
|
+
throw e;
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
function entryObject(citation) {
|
|
2089
|
+
const source = { file: citation.source.file };
|
|
2090
|
+
if (citation.source.lines !== void 0) source.lines = citation.source.lines;
|
|
2091
|
+
source.integrity = citation.source.integrity;
|
|
2092
|
+
if (citation.source["commit-sha"] !== void 0) {
|
|
2093
|
+
source["commit-sha"] = citation.source["commit-sha"];
|
|
2094
|
+
}
|
|
2095
|
+
const entry = {};
|
|
2096
|
+
if (citation.id !== void 0) entry.id = citation.id;
|
|
2097
|
+
if (citation.claim !== void 0) {
|
|
2098
|
+
const claim = {};
|
|
2099
|
+
if (citation.claim.lines !== void 0) claim.lines = citation.claim.lines;
|
|
2100
|
+
claim.integrity = citation.claim.integrity;
|
|
2101
|
+
entry.claim = claim;
|
|
2102
|
+
}
|
|
2103
|
+
entry.source = source;
|
|
2104
|
+
if (citation.quote !== void 0) entry.quote = citation.quote;
|
|
2105
|
+
return entry;
|
|
2106
|
+
}
|
|
2107
|
+
function needsYamlQuotes(value) {
|
|
2108
|
+
return value === "" || /^[\s!&*?|>'"%@`{}[\],#]/.test(value) || /^-(\s|$)/.test(value) || /:\s|\s#|:$|\s$/.test(value) || /^(?:true|false|null|~|yes|no|on|off|[-+]?(?:\d[\d_]*\.?\d*|\.\d+)(?:e[-+]?\d+)?)$/i.test(value);
|
|
2109
|
+
}
|
|
2110
|
+
var doubleQuote = (value) => JSON.stringify(value);
|
|
2111
|
+
var singleQuote = (value) => `'${value.replace(/'/g, "''")}'`;
|
|
2112
|
+
function splitScalarLine(line, field) {
|
|
2113
|
+
const keyed = new RegExp(`^(\\s*(?:-\\s+)?"?${field}"?\\s*:\\s*)(.*)$`).exec(line);
|
|
2114
|
+
const lead = keyed?.[1];
|
|
2115
|
+
const rest = keyed?.[2];
|
|
2116
|
+
if (lead === void 0 || rest === void 0) return void 0;
|
|
2117
|
+
const quote = rest.charAt(0);
|
|
2118
|
+
let end;
|
|
2119
|
+
if (quote === '"' || quote === "'") {
|
|
2120
|
+
end = -1;
|
|
2121
|
+
for (let i = 1; i < rest.length; i++) {
|
|
2122
|
+
const ch = rest.charAt(i);
|
|
2123
|
+
if (quote === '"' && ch === "\\") {
|
|
2124
|
+
i++;
|
|
2125
|
+
} else if (ch === quote) {
|
|
2126
|
+
if (quote === "'" && rest.charAt(i + 1) === "'") {
|
|
2127
|
+
i++;
|
|
2128
|
+
continue;
|
|
2129
|
+
}
|
|
2130
|
+
end = i + 1;
|
|
2131
|
+
break;
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
if (end < 0) return void 0;
|
|
2135
|
+
} else {
|
|
2136
|
+
const comment = /\s#/.exec(rest);
|
|
2137
|
+
end = comment ? comment.index : rest.length;
|
|
2138
|
+
while (end > 0 && /\s/.test(rest.charAt(end - 1))) end--;
|
|
2139
|
+
}
|
|
2140
|
+
return { lead, scalar: rest.slice(0, end), trail: rest.slice(end) };
|
|
2141
|
+
}
|
|
2142
|
+
function spliceEntryField(content, format, index, path, value) {
|
|
2143
|
+
const [which, field] = path;
|
|
2144
|
+
const refuse = () => new CiteError(
|
|
2145
|
+
`Cannot rewrite citations[${String(index)}].${which}.${field} in ${format} frontmatter; edit it by hand.`
|
|
2146
|
+
);
|
|
2147
|
+
const extractor = extractorFor(format);
|
|
2148
|
+
const pointer = `/citations/${String(index)}/${which}/${field}`;
|
|
2149
|
+
const line = extractor.extract(content, "page").lineFor(pointer);
|
|
2150
|
+
if (line === void 0) throw refuse();
|
|
2151
|
+
const start = offsetOfLine(content, line);
|
|
2152
|
+
const nl = content.indexOf("\n", start);
|
|
2153
|
+
let end = nl === -1 ? content.length : nl;
|
|
2154
|
+
if (end > start && content.charAt(end - 1) === "\r") end--;
|
|
2155
|
+
const parts = splitScalarLine(content.slice(start, end), field);
|
|
2156
|
+
if (!parts) throw refuse();
|
|
2157
|
+
const quote = parts.scalar.charAt(0);
|
|
2158
|
+
const spelled = typeof value === "number" ? String(value) : quote === '"' ? doubleQuote(value) : quote === "'" ? singleQuote(value) : needsYamlQuotes(value) ? doubleQuote(value) : value;
|
|
2159
|
+
const out = content.slice(0, start) + parts.lead + spelled + parts.trail + content.slice(end);
|
|
2160
|
+
const check = extractor.extract(out, "page").data.citations;
|
|
2161
|
+
const entry = Array.isArray(check) ? check[index] : void 0;
|
|
2162
|
+
const block = isRecord3(entry) ? entry[which] : void 0;
|
|
2163
|
+
const readBack2 = isRecord3(block) ? block[field] : void 0;
|
|
2164
|
+
if (readBack2 !== value) throw refuse();
|
|
2165
|
+
return out;
|
|
2166
|
+
}
|
|
2167
|
+
function isRecord3(value) {
|
|
2168
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2169
|
+
}
|
|
2170
|
+
function insertStatementBefore(content, offset, statement) {
|
|
2171
|
+
const start = offsetOfLine(content, lineAt(content, offset));
|
|
2172
|
+
return content.slice(0, start) + statement + detectEol(content) + content.slice(start);
|
|
2173
|
+
}
|
|
2174
|
+
function unifiedDiff(label, before, after) {
|
|
2175
|
+
const a = splitLines(before);
|
|
2176
|
+
const b = splitLines(after);
|
|
2177
|
+
let head = 0;
|
|
2178
|
+
while (head < a.length && head < b.length && a[head] === b[head]) head++;
|
|
2179
|
+
let tail = 0;
|
|
2180
|
+
while (tail < a.length - head && tail < b.length - head && a[a.length - 1 - tail] === b[b.length - 1 - tail]) tail++;
|
|
2181
|
+
const x = a.slice(head, a.length - tail);
|
|
2182
|
+
const y = b.slice(head, b.length - tail);
|
|
2183
|
+
if (x.length === 0 && y.length === 0) return "";
|
|
2184
|
+
const width = y.length + 1;
|
|
2185
|
+
const lcs = new Uint32Array((x.length + 1) * width);
|
|
2186
|
+
for (let i2 = x.length - 1; i2 >= 0; i2--) {
|
|
2187
|
+
for (let j2 = y.length - 1; j2 >= 0; j2--) {
|
|
2188
|
+
lcs[i2 * width + j2] = x[i2] === y[j2] ? (lcs[(i2 + 1) * width + j2 + 1] ?? 0) + 1 : Math.max(lcs[(i2 + 1) * width + j2] ?? 0, lcs[i2 * width + j2 + 1] ?? 0);
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
const edits = a.slice(0, head).map((text) => ({ op: " ", text }));
|
|
2192
|
+
let i = 0;
|
|
2193
|
+
let j = 0;
|
|
2194
|
+
while (i < x.length || j < y.length) {
|
|
2195
|
+
const xi = x[i];
|
|
2196
|
+
const yj = y[j];
|
|
2197
|
+
if (xi !== void 0 && yj !== void 0 && xi === yj) {
|
|
2198
|
+
edits.push({ op: " ", text: xi });
|
|
2199
|
+
i++;
|
|
2200
|
+
j++;
|
|
2201
|
+
} else if (xi !== void 0 && (yj === void 0 || (lcs[(i + 1) * width + j] ?? 0) >= (lcs[i * width + j + 1] ?? 0))) {
|
|
2202
|
+
edits.push({ op: "-", text: xi });
|
|
2203
|
+
i++;
|
|
2204
|
+
} else if (yj !== void 0) {
|
|
2205
|
+
edits.push({ op: "+", text: yj });
|
|
2206
|
+
j++;
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
edits.push(...a.slice(a.length - tail).map((text) => ({ op: " ", text })));
|
|
2210
|
+
const CONTEXT2 = 1;
|
|
2211
|
+
const out = [`--- ${label}`, `+++ ${label}`];
|
|
2212
|
+
let pos = 0;
|
|
2213
|
+
while (pos < edits.length) {
|
|
2214
|
+
if (edits[pos]?.op === " ") {
|
|
2215
|
+
pos++;
|
|
2216
|
+
continue;
|
|
2217
|
+
}
|
|
2218
|
+
const from = Math.max(0, pos - CONTEXT2);
|
|
2219
|
+
let to = pos;
|
|
2220
|
+
for (let k = pos; k < edits.length; k++) {
|
|
2221
|
+
if (edits[k]?.op !== " ") to = k;
|
|
2222
|
+
else if (k - to > CONTEXT2 * 2) break;
|
|
2223
|
+
}
|
|
2224
|
+
to = Math.min(edits.length - 1, to + CONTEXT2);
|
|
2225
|
+
let oldStart = 1;
|
|
2226
|
+
let newStart = 1;
|
|
2227
|
+
for (let k = 0; k < from; k++) {
|
|
2228
|
+
const op = edits[k]?.op;
|
|
2229
|
+
if (op !== "+") oldStart++;
|
|
2230
|
+
if (op !== "-") newStart++;
|
|
2231
|
+
}
|
|
2232
|
+
const slice = edits.slice(from, to + 1);
|
|
2233
|
+
const oldCount = slice.filter((e) => e.op !== "+").length;
|
|
2234
|
+
const newCount = slice.filter((e) => e.op !== "-").length;
|
|
2235
|
+
out.push(
|
|
2236
|
+
`@@ -${oldCount === 0 ? oldStart - 1 : oldStart},${oldCount} +${newCount === 0 ? newStart - 1 : newStart},${newCount} @@`,
|
|
2237
|
+
...slice.map((e) => e.op + e.text)
|
|
2238
|
+
);
|
|
2239
|
+
pos = to + 1;
|
|
2240
|
+
}
|
|
2241
|
+
return out.join("\n") + "\n";
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
// src/cite/core/manifest.ts
|
|
2245
|
+
var ManifestSet = class {
|
|
2246
|
+
held = /* @__PURE__ */ new Map();
|
|
2247
|
+
/** Read a manifest, or hand back the copy this run has already rewritten. */
|
|
2248
|
+
async hold(manifest) {
|
|
2249
|
+
const already = this.held.get(manifest.path);
|
|
2250
|
+
if (already !== void 0) return already;
|
|
2251
|
+
let before;
|
|
2252
|
+
try {
|
|
2253
|
+
before = await readFile2(manifest.path, "utf8");
|
|
2254
|
+
} catch (error) {
|
|
2255
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2256
|
+
throw new CiteError(`Manifest ${manifest.file} could not be read: ${reason}`);
|
|
2257
|
+
}
|
|
2258
|
+
const fresh = { path: manifest.path, file: manifest.file, before, text: before };
|
|
2259
|
+
this.held.set(manifest.path, fresh);
|
|
2260
|
+
return fresh;
|
|
2261
|
+
}
|
|
2262
|
+
/**
|
|
2263
|
+
* Write one page's whole `citations` list into its entry, and answer where
|
|
2264
|
+
* the entry at `index` now sits. The manifest is not saved: `changed()`
|
|
2265
|
+
* lists what the run would write.
|
|
2266
|
+
*/
|
|
2267
|
+
async write(manifest, entry, citations, index) {
|
|
2268
|
+
const held = await this.hold(manifest);
|
|
2269
|
+
const spliced = splice(held.text, {
|
|
2270
|
+
entry,
|
|
2271
|
+
key: CITATIONS_KEY2,
|
|
2272
|
+
value: [...citations],
|
|
2273
|
+
join: manifest.join,
|
|
2274
|
+
file: manifest.file
|
|
2275
|
+
});
|
|
2276
|
+
held.text = spliced.text;
|
|
2277
|
+
return { file: manifest.file, line: itemLine(spliced.text, entry, index, manifest.join) ?? spliced.line };
|
|
2278
|
+
}
|
|
2279
|
+
/** The manifests this run rewrote, with the diff of each. */
|
|
2280
|
+
changed() {
|
|
2281
|
+
return [...this.held.values()].filter((m) => m.text !== m.before).map((m) => ({ ...m, diff: unifiedDiff(m.file, m.before, m.text) }));
|
|
2282
|
+
}
|
|
2283
|
+
};
|
|
2284
|
+
function splice(text, options) {
|
|
2285
|
+
try {
|
|
2286
|
+
return spliceManifestValue(text, options);
|
|
2287
|
+
} catch (error) {
|
|
2288
|
+
if (error instanceof DocmetaError) throw new CiteError(error.message);
|
|
2289
|
+
throw error;
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
function itemLine(text, entry, index, join4) {
|
|
2293
|
+
const lc = new LineCounter2();
|
|
2294
|
+
const doc = parseDocument2(text, { lineCounter: lc, uniqueKeys: false });
|
|
2295
|
+
if (doc.errors.length > 0) return void 0;
|
|
2296
|
+
const root = doc.contents;
|
|
2297
|
+
if (!isMap2(root)) return void 0;
|
|
2298
|
+
const same = (key) => {
|
|
2299
|
+
const spelled = isScalar2(key) ? String(key.value) : String(key);
|
|
2300
|
+
return join4 === "path" ? posix2.normalize(spelled) === posix2.normalize(entry) : spelled === entry;
|
|
2301
|
+
};
|
|
2302
|
+
const pair = root.items.find((p) => same(p.key));
|
|
2303
|
+
const value = pair?.value;
|
|
2304
|
+
if (!isMap2(value)) return void 0;
|
|
2305
|
+
const kv = value.items.find(
|
|
2306
|
+
(p) => (isScalar2(p.key) ? String(p.key.value) : String(p.key)) === CITATIONS_KEY2
|
|
2307
|
+
);
|
|
2308
|
+
const list = kv?.value;
|
|
2309
|
+
if (!isSeq2(list)) return void 0;
|
|
2310
|
+
const item = list.items[index];
|
|
2311
|
+
const range = isNode2(item) ? item.range : void 0;
|
|
2312
|
+
return range ? lc.linePos(range[0]).line : void 0;
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
// src/cite/core/adapt.ts
|
|
2316
|
+
import { isAbsolute as isAbsolute2 } from "node:path";
|
|
2317
|
+
function short(commit) {
|
|
2318
|
+
return commit.slice(0, 7);
|
|
2319
|
+
}
|
|
2320
|
+
function plural(n, noun) {
|
|
2321
|
+
return `${String(n)} ${noun}${n === 1 ? "" : "s"}`;
|
|
2322
|
+
}
|
|
2323
|
+
function listOf(values) {
|
|
2324
|
+
if (values.length <= 1) return values.join("");
|
|
2325
|
+
return `${values.slice(0, -1).join(", ")} and ${values[values.length - 1] ?? ""}`;
|
|
2326
|
+
}
|
|
2327
|
+
function citesEncrypted(src) {
|
|
2328
|
+
try {
|
|
2329
|
+
return parseSrc(src).encrypted;
|
|
2330
|
+
} catch {
|
|
2331
|
+
return false;
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
function missingMessage(source) {
|
|
2335
|
+
if (!citesEncrypted(source.src)) return "missing";
|
|
2336
|
+
switch (source.missingReason) {
|
|
2337
|
+
case "no-key":
|
|
2338
|
+
return "missing (no encryption key is available to decrypt it)";
|
|
2339
|
+
case "undecryptable":
|
|
2340
|
+
return "missing (does not decrypt under the current key)";
|
|
2341
|
+
case "untracked":
|
|
2342
|
+
return "missing (no tracked file matches; wrong --root?)";
|
|
2343
|
+
case "unreadable":
|
|
2344
|
+
case void 0:
|
|
2345
|
+
return "missing";
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
function messageFor(source) {
|
|
2349
|
+
switch (source.status) {
|
|
2350
|
+
case "current":
|
|
2351
|
+
return "current";
|
|
2352
|
+
case "skipped":
|
|
2353
|
+
return "skipped";
|
|
2354
|
+
case "moved":
|
|
2355
|
+
return `moved -> ${source.newSrc ?? "?"}`;
|
|
2356
|
+
case "moved-ambiguous": {
|
|
2357
|
+
const candidates2 = source.candidates ?? [];
|
|
2358
|
+
return `moved, ${plural(candidates2.length, "candidate")} (${candidates2.join(", ")}); widen the range`;
|
|
2359
|
+
}
|
|
2360
|
+
case "changed": {
|
|
2361
|
+
if (source.commitSha === void 0) return "changed";
|
|
2362
|
+
const at2 = short(source.commitSha);
|
|
2363
|
+
if (source.historyAvailable === false) {
|
|
2364
|
+
return `changed (history unavailable: commit ${at2} not found; fetch-depth: 0)`;
|
|
2365
|
+
}
|
|
2366
|
+
if (source.commitsSince === void 0) return `changed since ${at2}`;
|
|
2367
|
+
return `changed since ${at2}, ${plural(source.commitsSince.length, "commit")}`;
|
|
2368
|
+
}
|
|
2369
|
+
case "never-true":
|
|
2370
|
+
return source.commitSha === void 0 ? "never true: the pin does not match at the recorded commit" : `never true: the pin does not match at ${short(source.commitSha)}`;
|
|
2371
|
+
case "missing":
|
|
2372
|
+
return missingMessage(source);
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
function named2(id, text) {
|
|
2376
|
+
return id === void 0 ? text : `${id}: ${text}`;
|
|
2377
|
+
}
|
|
2378
|
+
function at(spec) {
|
|
2379
|
+
return spec.includes("-") ? `lines ${spec}` : `line ${spec}`;
|
|
2380
|
+
}
|
|
2381
|
+
function claimMessageFor(result) {
|
|
2382
|
+
const claim = result.claim;
|
|
2383
|
+
const id = result.citation.id;
|
|
2384
|
+
if (claim === null) return "";
|
|
2385
|
+
const where = claim.fileLines ?? (result.markerLine === void 0 ? void 0 : String(result.markerLine));
|
|
2386
|
+
switch (claim.status) {
|
|
2387
|
+
case "moved":
|
|
2388
|
+
return named2(
|
|
2389
|
+
id,
|
|
2390
|
+
`the claim moved from ${at(where ?? "?")} to ${at(claim.newFileLines ?? "?")}.`
|
|
2391
|
+
);
|
|
2392
|
+
case "moved-ambiguous":
|
|
2393
|
+
return named2(
|
|
2394
|
+
id,
|
|
2395
|
+
`the claim at ${at(where ?? "?")} now appears at lines ${listOf(claim.candidateFileLines ?? [])}.`
|
|
2396
|
+
);
|
|
2397
|
+
case "changed":
|
|
2398
|
+
return where === void 0 ? named2(id, "the claim has no lines and no marker names the entry, so its pin anchors nothing.") : named2(id, `the claim at ${at(where)} has changed since it was pinned.`);
|
|
2399
|
+
case "current":
|
|
2400
|
+
case "skipped":
|
|
2401
|
+
return "";
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
function sourceRuleOf(status) {
|
|
2405
|
+
if (status === "skipped" || status === "current") return void 0;
|
|
2406
|
+
return `source-${status}`;
|
|
2407
|
+
}
|
|
2408
|
+
function claimRuleOf(claim) {
|
|
2409
|
+
const status = claim.status;
|
|
2410
|
+
if (status === "skipped" || status === "current") return void 0;
|
|
2411
|
+
return `claim-${status}`;
|
|
2412
|
+
}
|
|
2413
|
+
function entrySite(result) {
|
|
2414
|
+
const site = {};
|
|
2415
|
+
if (result.origin.line !== void 0) site.line = result.origin.line;
|
|
2416
|
+
if (result.origin.kind === "manifest") site.file = result.origin.file;
|
|
2417
|
+
return site;
|
|
2418
|
+
}
|
|
2419
|
+
function anchorSite(result) {
|
|
2420
|
+
if (result.anchorLine !== void 0) return { line: result.anchorLine };
|
|
2421
|
+
return entrySite(result);
|
|
2422
|
+
}
|
|
2423
|
+
function findingsFor(results, severity) {
|
|
2424
|
+
const out = [];
|
|
2425
|
+
const push = (result, rule, message, site, extra) => {
|
|
2426
|
+
const level = severity[rule];
|
|
2427
|
+
if (level === "off") return;
|
|
2428
|
+
const finding2 = {
|
|
2429
|
+
rule,
|
|
2430
|
+
ruleId: ruleId(rule),
|
|
2431
|
+
severity: level,
|
|
2432
|
+
message,
|
|
2433
|
+
src: result.source.src,
|
|
2434
|
+
index: result.origin.index
|
|
2435
|
+
};
|
|
2436
|
+
if (site.line !== void 0) finding2.line = site.line;
|
|
2437
|
+
if (site.file !== void 0) finding2.file = site.file;
|
|
2438
|
+
if (result.citation.id !== void 0) finding2.id = result.citation.id;
|
|
2439
|
+
if (extra?.newSrc !== void 0) finding2.newSrc = extra.newSrc;
|
|
2440
|
+
out.push(finding2);
|
|
2441
|
+
};
|
|
2442
|
+
for (const result of results) {
|
|
2443
|
+
const claim = result.claim;
|
|
2444
|
+
if (claim !== null) {
|
|
2445
|
+
const rule2 = claimRuleOf(claim);
|
|
2446
|
+
if (rule2 !== void 0) {
|
|
2447
|
+
const site = claimLine(claim) === void 0 && result.markerLine === void 0 ? entrySite(result) : { line: claimLine(claim) ?? result.markerLine };
|
|
2448
|
+
push(result, rule2, claimMessageFor(result), site);
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
const rule = sourceRuleOf(result.source.status);
|
|
2452
|
+
if (rule === void 0) continue;
|
|
2453
|
+
const extra = result.source.newSrc === void 0 ? void 0 : { newSrc: result.source.newSrc };
|
|
2454
|
+
push(result, rule, messageFor(result.source), anchorSite(result), extra);
|
|
2455
|
+
}
|
|
2456
|
+
return out;
|
|
2457
|
+
}
|
|
2458
|
+
function integrityFor(finding2, results) {
|
|
2459
|
+
if (finding2.index === void 0) return void 0;
|
|
2460
|
+
return results.find((r) => r.origin.index === finding2.index)?.citation.source.integrity;
|
|
2461
|
+
}
|
|
2462
|
+
function errorSite(finding2) {
|
|
2463
|
+
if (finding2.file === void 0) {
|
|
2464
|
+
return finding2.line === void 0 ? {} : { line: finding2.line };
|
|
2465
|
+
}
|
|
2466
|
+
if (finding2.file.startsWith("../") || isAbsolute2(finding2.file)) return {};
|
|
2467
|
+
return {
|
|
2468
|
+
file: finding2.file,
|
|
2469
|
+
...finding2.line === void 0 ? {} : { line: finding2.line }
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2472
|
+
function toValidationResult(report) {
|
|
2473
|
+
const errors = report.findings.map((finding2) => {
|
|
2474
|
+
const error = {
|
|
2475
|
+
schema: RULE_ID_PREFIX,
|
|
2476
|
+
instancePath: finding2.index === void 0 ? "" : `/citations/${String(finding2.index)}`,
|
|
2477
|
+
message: finding2.message,
|
|
2478
|
+
keyword: finding2.rule,
|
|
2479
|
+
severity: finding2.severity
|
|
2480
|
+
};
|
|
2481
|
+
const subject = finding2.id ?? integrityFor(finding2, report.citations);
|
|
2482
|
+
if (subject !== void 0) error.subject = subject;
|
|
2483
|
+
const site = errorSite(finding2);
|
|
2484
|
+
if (site.file !== void 0) error.file = site.file;
|
|
2485
|
+
if (site.line !== void 0) error.line = site.line;
|
|
2486
|
+
return error;
|
|
2487
|
+
});
|
|
2488
|
+
return {
|
|
2489
|
+
file: report.file,
|
|
2490
|
+
format: report.format,
|
|
2491
|
+
ok: !errors.some(isErrorSeverity),
|
|
2492
|
+
schemas: [RULE_ID_PREFIX],
|
|
2493
|
+
errors
|
|
2494
|
+
};
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
// src/cite/core/check-page.ts
|
|
2498
|
+
function byLine2(a, b) {
|
|
2499
|
+
return (a.line ?? Number.MAX_SAFE_INTEGER) - (b.line ?? Number.MAX_SAFE_INTEGER);
|
|
2500
|
+
}
|
|
2501
|
+
function relevel(finding2, severity) {
|
|
2502
|
+
const level = severity[finding2.rule];
|
|
2503
|
+
if (level === "off") return void 0;
|
|
2504
|
+
return { ...finding2, severity: level };
|
|
2505
|
+
}
|
|
2506
|
+
async function citedNow(input, source, entry) {
|
|
2507
|
+
const range = sourceRange(entry.citation.source);
|
|
2508
|
+
const read = await readSource(input.root, input.index, range, input.key);
|
|
2509
|
+
if (read.kind === "missing") return void 0;
|
|
2510
|
+
const at2 = source.status === "moved" && source.newSrc !== void 0 ? parseSrc(source.newSrc) : range;
|
|
2511
|
+
try {
|
|
2512
|
+
return sliceLines(splitLines(read.text), at2);
|
|
2513
|
+
} catch {
|
|
2514
|
+
return void 0;
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
function faithful(blockText, pin, key, now) {
|
|
2518
|
+
return hashLines(splitLines(blockText).join("\n"), key) === pin || now !== void 0 && blockMatches(blockText, now);
|
|
2519
|
+
}
|
|
2520
|
+
function insideOf(lines, block) {
|
|
2521
|
+
return lines.slice(block.start, block.end - 1).join("\n");
|
|
2522
|
+
}
|
|
2523
|
+
async function quoteFindings(input, entry, result) {
|
|
2524
|
+
const { citation } = entry;
|
|
2525
|
+
if (citation.quote !== true || result.anchor === null) return [];
|
|
2526
|
+
const id = citation.id;
|
|
2527
|
+
const named3 = (text) => id === void 0 ? text : `${id}: ${text}`;
|
|
2528
|
+
const { page } = input;
|
|
2529
|
+
let block;
|
|
2530
|
+
if (result.anchor === "marker" && entry.marker !== void 0) {
|
|
2531
|
+
block = anchoredLines(page.content, entry.marker.end, page.format, true);
|
|
2532
|
+
if (block === void 0) {
|
|
2533
|
+
return [
|
|
2534
|
+
{
|
|
2535
|
+
rule: "quote-drift",
|
|
2536
|
+
message: "quote: true, but no fenced block follows the marker",
|
|
2537
|
+
...result.markerLine === void 0 ? {} : { line: result.markerLine }
|
|
2538
|
+
}
|
|
2539
|
+
];
|
|
2540
|
+
}
|
|
2541
|
+
} else {
|
|
2542
|
+
const at2 = result.claim === null ? void 0 : claimLinesNow(result.claim);
|
|
2543
|
+
if (at2 === void 0) return [];
|
|
2544
|
+
const span = fenceSpanAt(page.content, at2.start, page.format);
|
|
2545
|
+
if (span === void 0 || span.end !== at2.end) {
|
|
2546
|
+
return [
|
|
2547
|
+
{
|
|
2548
|
+
rule: "anchor-invalid",
|
|
2549
|
+
message: named3(
|
|
2550
|
+
`the quote's claim lines ${at2.start === at2.end ? String(at2.start) : `${String(at2.start)}-${String(at2.end)}`} are no longer a fenced block.`
|
|
2551
|
+
),
|
|
2552
|
+
line: at2.start
|
|
2553
|
+
}
|
|
2554
|
+
];
|
|
2555
|
+
}
|
|
2556
|
+
block = span;
|
|
2557
|
+
}
|
|
2558
|
+
if (!input.checkSources || result.source.status === "missing") return [];
|
|
2559
|
+
const key = sourceRange(citation.source).encrypted ? input.key : void 0;
|
|
2560
|
+
const now = await citedNow(input, result.source, entry);
|
|
2561
|
+
const inside = insideOf(input.lines, block);
|
|
2562
|
+
if (faithful(inside, citation.source.integrity, key, now)) return [];
|
|
2563
|
+
return [
|
|
2564
|
+
{
|
|
2565
|
+
rule: "quote-drift",
|
|
2566
|
+
message: "quote: true, but the fenced block does not reproduce the cited lines",
|
|
2567
|
+
line: block.start
|
|
2568
|
+
}
|
|
2569
|
+
];
|
|
2570
|
+
}
|
|
2571
|
+
function anchorOf(entry, claim, page) {
|
|
2572
|
+
const { citation, marker } = entry;
|
|
2573
|
+
const hasLines = citation.claim?.lines !== void 0;
|
|
2574
|
+
const out = { anchor: null };
|
|
2575
|
+
if (marker !== void 0) out.markerLine = marker.line;
|
|
2576
|
+
if (hasLines && marker !== void 0) return out;
|
|
2577
|
+
if (hasLines) {
|
|
2578
|
+
out.anchor = "claim";
|
|
2579
|
+
const at2 = claim === null ? void 0 : claimLineNow(claim);
|
|
2580
|
+
if (at2 !== void 0) out.anchorLine = at2;
|
|
2581
|
+
return out;
|
|
2582
|
+
}
|
|
2583
|
+
if (marker !== void 0) {
|
|
2584
|
+
out.anchor = "marker";
|
|
2585
|
+
const unit = anchoredLines(page.content, marker.end, page.format, citation.quote === true);
|
|
2586
|
+
out.anchorLine = unit?.start ?? marker.line;
|
|
2587
|
+
}
|
|
2588
|
+
return out;
|
|
2589
|
+
}
|
|
2590
|
+
async function checkCitations(page, opts) {
|
|
2591
|
+
const readOptions = {};
|
|
2592
|
+
if (page.format !== void 0) readOptions.format = page.format;
|
|
2593
|
+
if (opts.citations !== void 0) readOptions.citations = opts.citations;
|
|
2594
|
+
if (opts.owned !== void 0) readOptions.owned = opts.owned;
|
|
2595
|
+
const read = readPage(page.file, page.content, readOptions);
|
|
2596
|
+
const severity = resolveSeverity(opts.severity);
|
|
2597
|
+
const client = opts.gitClient ?? gitClient(opts.root);
|
|
2598
|
+
const checkSources = opts.checkSources !== false;
|
|
2599
|
+
const lines = splitLines(read.content);
|
|
2600
|
+
const results = [];
|
|
2601
|
+
const findings = [];
|
|
2602
|
+
const notices = [];
|
|
2603
|
+
for (const finding2 of read.findings) {
|
|
2604
|
+
const kept = relevel(finding2, severity);
|
|
2605
|
+
if (kept !== void 0) findings.push(kept);
|
|
2606
|
+
}
|
|
2607
|
+
const index = checkSources ? opts.sourceIndex ?? await buildSourceIndex(opts.root, { gitClient: client }) : void 0;
|
|
2608
|
+
const classifyOpts = index === void 0 ? void 0 : { root: opts.root, index, git: client, ...opts.key === void 0 ? {} : { key: opts.key } };
|
|
2609
|
+
const quoteInput = {
|
|
2610
|
+
page: read,
|
|
2611
|
+
lines,
|
|
2612
|
+
root: opts.root,
|
|
2613
|
+
index: index ?? emptyIndex,
|
|
2614
|
+
checkSources,
|
|
2615
|
+
...opts.key === void 0 ? {} : { key: opts.key }
|
|
2616
|
+
};
|
|
2617
|
+
for (const entry of read.citations) {
|
|
2618
|
+
const claim = claimEnd(read, entry, lines);
|
|
2619
|
+
const source = classifyOpts === void 0 ? skippedSource(entry) : await classifyCitation(entry, classifyOpts);
|
|
2620
|
+
const result = {
|
|
2621
|
+
citation: entry.citation,
|
|
2622
|
+
origin: entry.origin,
|
|
2623
|
+
...anchorOf(entry, claim, read),
|
|
2624
|
+
claim,
|
|
2625
|
+
source
|
|
2626
|
+
};
|
|
2627
|
+
results.push(result);
|
|
2628
|
+
for (const found of await quoteFindings(quoteInput, entry, result)) {
|
|
2629
|
+
const level = severity[found.rule];
|
|
2630
|
+
if (level === "off") continue;
|
|
2631
|
+
const finding2 = {
|
|
2632
|
+
rule: found.rule,
|
|
2633
|
+
ruleId: ruleId(found.rule),
|
|
2634
|
+
severity: level,
|
|
2635
|
+
message: found.message,
|
|
2636
|
+
src: source.src,
|
|
2637
|
+
index: entry.origin.index
|
|
2638
|
+
};
|
|
2639
|
+
if (found.line !== void 0) finding2.line = found.line;
|
|
2640
|
+
if (entry.citation.id !== void 0) finding2.id = entry.citation.id;
|
|
2641
|
+
findings.push(finding2);
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
findings.push(...findingsFor(results, severity));
|
|
2645
|
+
const wantsHistory = checkSources && read.citations.some(({ citation }) => citation.source["commit-sha"] !== void 0);
|
|
2646
|
+
if (wantsHistory && !await client.available()) notices.push(GIT_UNAVAILABLE_HISTORY);
|
|
2647
|
+
const unavailable = results.find((r) => r.source.historyAvailable === false);
|
|
2648
|
+
if (unavailable?.source.commitSha !== void 0) {
|
|
2649
|
+
notices.push(
|
|
2650
|
+
`commit ${unavailable.source.commitSha.slice(0, 7)} not found in history; use fetch-depth: 0 to enable never-true and diffs`
|
|
2651
|
+
);
|
|
2652
|
+
}
|
|
2653
|
+
if (results.some((r) => r.source.truncatedSearch === true)) {
|
|
2654
|
+
notices.push(
|
|
2655
|
+
`a move search hit its ${String(MOVE_BUDGET_BYTES / (1024 * 1024))} MiB budget before covering the file; a citation may read changed rather than moved`
|
|
2656
|
+
);
|
|
2657
|
+
}
|
|
2658
|
+
return {
|
|
2659
|
+
file: read.file,
|
|
2660
|
+
format: read.format,
|
|
2661
|
+
citations: results,
|
|
2662
|
+
findings: findings.sort(byLine2),
|
|
2663
|
+
notices
|
|
2664
|
+
};
|
|
2665
|
+
}
|
|
2666
|
+
function skippedSource(entry) {
|
|
2667
|
+
const { source } = entry.citation;
|
|
2668
|
+
const out = { src: spellSource(source), status: "skipped" };
|
|
2669
|
+
if (source["commit-sha"] !== void 0) out.commitSha = source["commit-sha"];
|
|
2670
|
+
return out;
|
|
2671
|
+
}
|
|
2672
|
+
var emptyIndex = { files: () => [], has: () => false };
|
|
2673
|
+
|
|
2674
|
+
// src/cite/core/mint.ts
|
|
2675
|
+
var COMMIT_PATTERN = /^[0-9a-f]{7,64}$/;
|
|
2676
|
+
async function commitFor(opts) {
|
|
2677
|
+
if (opts.commitSha === false) return void 0;
|
|
2678
|
+
if (typeof opts.commitSha === "string") {
|
|
2679
|
+
if (!COMMIT_PATTERN.test(opts.commitSha)) {
|
|
2680
|
+
throw new CiteError(
|
|
2681
|
+
`Invalid commit-sha "${opts.commitSha}": expected 7 to 64 lowercase hex digits.`
|
|
2682
|
+
);
|
|
2683
|
+
}
|
|
2684
|
+
return opts.commitSha;
|
|
2685
|
+
}
|
|
2686
|
+
const client = opts.gitClient;
|
|
2687
|
+
if (client === void 0 || !await client.available()) return void 0;
|
|
2688
|
+
return await client.head() ?? void 0;
|
|
2689
|
+
}
|
|
2690
|
+
function refusalFor(reason, path, encrypted) {
|
|
2691
|
+
switch (reason) {
|
|
2692
|
+
case "untracked":
|
|
2693
|
+
return new CiteError(
|
|
2694
|
+
encrypted ? `No tracked file matches ${path} (wrong --root?).` : `Source not found: ${path} is not a tracked file under the root.`
|
|
2695
|
+
);
|
|
2696
|
+
case "no-key":
|
|
2697
|
+
return new CiteError(`${path} is encrypted, and no encryption key is available to decrypt it.`);
|
|
2698
|
+
case "undecryptable":
|
|
2699
|
+
return new CiteError(`${path} does not decrypt under the current key.`);
|
|
2700
|
+
case "unreadable":
|
|
2701
|
+
return new CiteError(`Source not readable: ${path} could not be read.`);
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
async function mintCitation(opts) {
|
|
2705
|
+
const range = parseSrc(opts.src);
|
|
2706
|
+
const { key } = opts;
|
|
2707
|
+
const encrypt = opts.encrypt === true || range.encrypted;
|
|
2708
|
+
if (encrypt && !range.encrypted && key === void 0) {
|
|
2709
|
+
throw new CiteError(
|
|
2710
|
+
`${opts.src} must be encrypted, and no encryption key is available. Run \`manni key set\`, or set ${ENCRYPTION_KEY_ENV}.`
|
|
2711
|
+
);
|
|
2712
|
+
}
|
|
2713
|
+
const index = opts.sourceIndex ?? await buildSourceIndex(opts.root, { gitClient: opts.gitClient });
|
|
2714
|
+
const read = await readSource(opts.root, index, range, key);
|
|
2715
|
+
if (read.kind === "missing") throw refusalFor(read.reason, range.path, range.encrypted);
|
|
2716
|
+
const joined = sliceLines(splitLines(read.text), range, range.path);
|
|
2717
|
+
const pinKey = encrypt ? key : void 0;
|
|
2718
|
+
const file = pinKey !== void 0 && !range.encrypted ? encryptSourcePath(read.resolvedPath, pinKey) : range.path;
|
|
2719
|
+
const lines = rangeLines(range);
|
|
2720
|
+
const commitSha = await commitFor(opts);
|
|
2721
|
+
const source = { file, integrity: hashLines(joined, pinKey) };
|
|
2722
|
+
const ordered = {
|
|
2723
|
+
file: source.file,
|
|
2724
|
+
...lines === void 0 ? {} : { lines },
|
|
2725
|
+
integrity: source.integrity,
|
|
2726
|
+
...commitSha === void 0 ? {} : { "commit-sha": commitSha }
|
|
2727
|
+
};
|
|
2728
|
+
return {
|
|
2729
|
+
...opts.id === void 0 ? {} : { id: opts.id },
|
|
2730
|
+
...opts.claim === void 0 ? {} : { claim: opts.claim },
|
|
2731
|
+
source: ordered,
|
|
2732
|
+
...opts.quote === void 0 ? {} : { quote: opts.quote }
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
// src/cite/core/config.ts
|
|
2737
|
+
import { resolve as resolve2 } from "node:path";
|
|
2738
|
+
|
|
2739
|
+
// src/shared/severity.ts
|
|
2740
|
+
var SEVERITIES = ["notice", "warning", "error"];
|
|
2741
|
+
var SEVERITY_LIST = SEVERITIES.join(" | ");
|
|
2742
|
+
function isSeverity(value) {
|
|
2743
|
+
return SEVERITIES.includes(value);
|
|
2744
|
+
}
|
|
2745
|
+
function meetsSeverity(severity, min) {
|
|
2746
|
+
return SEVERITIES.indexOf(severity) >= SEVERITIES.indexOf(min);
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
// src/cite/core/config.ts
|
|
2750
|
+
var CITE_SECTION = "cite";
|
|
2751
|
+
var DEFAULT_CITE_BASELINE_PATH = ".manni-cite-baseline.json";
|
|
2752
|
+
var CONFIG_KEYS = [
|
|
2753
|
+
"allowEmpty",
|
|
2754
|
+
"respectGitignore",
|
|
2755
|
+
"root",
|
|
2756
|
+
"baseline",
|
|
2757
|
+
"checkSources",
|
|
2758
|
+
"severity"
|
|
2759
|
+
];
|
|
2760
|
+
var SALT_KEY = "salt";
|
|
2761
|
+
var saltRefusal = (source) => `${source}: "salt" is no longer a cite key. Values are encrypted with a family key: a top-level encryptionKey:, or MANNI_ENCRYPTION_KEY. Run \`manni key set\`.`;
|
|
2762
|
+
var SEVERITY_LEVELS = [...[...SEVERITIES].reverse(), "off"];
|
|
2763
|
+
function isLevel(value) {
|
|
2764
|
+
return typeof value === "string" && SEVERITY_LEVELS.includes(value);
|
|
2765
|
+
}
|
|
2766
|
+
var CONFIG_REF = "https://hawkeyexl.github.io/manni/meta/reference/configuration/";
|
|
2767
|
+
var MOVED_KEYS = ["paths", "exclude"];
|
|
2768
|
+
function assertNoMovedKeys(raw, source) {
|
|
2769
|
+
for (const key of MOVED_KEYS) {
|
|
2770
|
+
if (Object.hasOwn(raw, key)) {
|
|
2771
|
+
throw new CiteError(
|
|
2772
|
+
`${source}: "${key}" is no longer a cite key. Document sets are declared once for every tool, under a top-level collections: list. See ${CONFIG_REF}#collections`
|
|
2773
|
+
);
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
var CONFIG_FILE = {
|
|
2778
|
+
section: CITE_SECTION,
|
|
2779
|
+
// The citation tool never had a file of its own before the family file.
|
|
2780
|
+
legacyNames: [],
|
|
2781
|
+
toError: (message) => new CiteError(message)
|
|
2782
|
+
};
|
|
2783
|
+
function rejectUnknownKeys(obj, allowed, where, source) {
|
|
2784
|
+
for (const key of Object.keys(obj)) {
|
|
2785
|
+
if (!allowed.includes(key)) {
|
|
2786
|
+
throw new CiteError(
|
|
2787
|
+
`Unknown key "${key}" under ${where}: in ${source}. Supported keys: ${allowed.join(", ")}.`
|
|
2788
|
+
);
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
function wrongType(key, expected, source) {
|
|
2793
|
+
return new CiteError(`cite.${key} in ${source} must be ${expected}.`);
|
|
2794
|
+
}
|
|
2795
|
+
function asBoolean(value, key, source) {
|
|
2796
|
+
if (typeof value !== "boolean") throw wrongType(key, "a boolean", source);
|
|
2797
|
+
return value;
|
|
2798
|
+
}
|
|
2799
|
+
function asString(value, key, source) {
|
|
2800
|
+
if (typeof value !== "string") throw wrongType(key, "a string", source);
|
|
2801
|
+
return value;
|
|
2802
|
+
}
|
|
2803
|
+
function isMapping(value) {
|
|
2804
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2805
|
+
}
|
|
2806
|
+
function parseSeverity(value, source) {
|
|
2807
|
+
if (!isMapping(value)) throw wrongType("severity", "a mapping", source);
|
|
2808
|
+
rejectUnknownKeys(value, CITE_RULES, "cite.severity", source);
|
|
2809
|
+
const severity = {};
|
|
2810
|
+
for (const [rule, level] of Object.entries(value)) {
|
|
2811
|
+
if (!isCiteRule(rule)) continue;
|
|
2812
|
+
if (!isLevel(level)) {
|
|
2813
|
+
const got = typeof level === "string" ? `, not "${level}"` : "";
|
|
2814
|
+
throw new CiteError(
|
|
2815
|
+
`cite.severity.${rule} in ${source} must be one of ${SEVERITY_LEVELS.join(", ")}${got}.`
|
|
2816
|
+
);
|
|
2817
|
+
}
|
|
2818
|
+
severity[rule] = level;
|
|
2819
|
+
}
|
|
2820
|
+
return severity;
|
|
2821
|
+
}
|
|
2822
|
+
function parseCiteConfig(raw, source) {
|
|
2823
|
+
if (raw == null) return {};
|
|
2824
|
+
if (!isMapping(raw)) {
|
|
2825
|
+
throw new CiteError(`cite: in ${source} must be a mapping.`);
|
|
2826
|
+
}
|
|
2827
|
+
assertNoMovedKeys(raw, source);
|
|
2828
|
+
if (Object.hasOwn(raw, SALT_KEY)) throw new CiteError(saltRefusal(source));
|
|
2829
|
+
rejectUnknownKeys(raw, CONFIG_KEYS, "cite", source);
|
|
2830
|
+
const config = {};
|
|
2831
|
+
if (raw.allowEmpty !== void 0)
|
|
2832
|
+
config.allowEmpty = asBoolean(raw.allowEmpty, "allowEmpty", source);
|
|
2833
|
+
if (raw.respectGitignore !== void 0)
|
|
2834
|
+
config.respectGitignore = asBoolean(raw.respectGitignore, "respectGitignore", source);
|
|
2835
|
+
if (raw.root !== void 0) config.root = asString(raw.root, "root", source);
|
|
2836
|
+
if (raw.baseline !== void 0)
|
|
2837
|
+
config.baseline = asString(raw.baseline, "baseline", source);
|
|
2838
|
+
if (raw.checkSources !== void 0)
|
|
2839
|
+
config.checkSources = asBoolean(raw.checkSources, "checkSources", source);
|
|
2840
|
+
if (raw.severity !== void 0) config.severity = parseSeverity(raw.severity, source);
|
|
2841
|
+
return config;
|
|
2842
|
+
}
|
|
2843
|
+
function readCiteConfigFile(explicitPath, cwd) {
|
|
2844
|
+
return explicitPath ? readConfigFile(explicitPath, cwd, CONFIG_FILE) : findConfigFile(cwd, CONFIG_FILE);
|
|
2845
|
+
}
|
|
2846
|
+
async function loadCiteConfig(explicitPath, cwd = process.cwd()) {
|
|
2847
|
+
const file = await readCiteConfigFile(explicitPath, cwd);
|
|
2848
|
+
return file === null ? null : toLoaded(file);
|
|
2849
|
+
}
|
|
2850
|
+
function toLoaded(file) {
|
|
2851
|
+
return {
|
|
2852
|
+
config: parseCiteConfig(file.value, file.source),
|
|
2853
|
+
path: file.path,
|
|
2854
|
+
dir: file.dir,
|
|
2855
|
+
source: file.source,
|
|
2856
|
+
collections: file.collections
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2859
|
+
var STDIN = "-";
|
|
2860
|
+
async function resolveCiteRun(opts) {
|
|
2861
|
+
const cwd = resolve2(opts.cwd);
|
|
2862
|
+
const env = opts.env ?? process.env;
|
|
2863
|
+
const wanted = opts.collection ?? [];
|
|
2864
|
+
if (wanted.length > 0 && opts.inputs.some((input) => input !== STDIN)) {
|
|
2865
|
+
throw new CiteError(
|
|
2866
|
+
"--collection selects a configured collection; it cannot be combined with paths."
|
|
2867
|
+
);
|
|
2868
|
+
}
|
|
2869
|
+
const file = opts.noConfig ? null : await readCiteConfigFile(opts.configPath, cwd);
|
|
2870
|
+
const loaded = file === null ? null : toLoaded(file);
|
|
2871
|
+
if (wanted.length > 0 && loaded === null) {
|
|
2872
|
+
throw new CiteError("--collection needs a config file to select from.");
|
|
2873
|
+
}
|
|
2874
|
+
if (loaded) opts.onConfigLoaded?.({ path: loaded.path, dir: loaded.dir });
|
|
2875
|
+
const config = loaded ? loaded.config : null;
|
|
2876
|
+
const collections = loaded ? selectCollections(loaded.collections, wanted, loaded.source, (message) => new CiteError(message)) : [];
|
|
2877
|
+
const fromCollections = opts.inputs.length === 0;
|
|
2878
|
+
const inputs = fromCollections ? collections.flatMap((c) => c.paths) : opts.inputs;
|
|
2879
|
+
const base = fromCollections && inputs.length > 0 && loaded ? loaded.dir : cwd;
|
|
2880
|
+
let root;
|
|
2881
|
+
if (opts.root !== void 0) {
|
|
2882
|
+
root = resolve2(cwd, opts.root);
|
|
2883
|
+
} else if (loaded && config?.root !== void 0) {
|
|
2884
|
+
root = resolve2(loaded.dir, config.root);
|
|
2885
|
+
} else {
|
|
2886
|
+
const gitRoot = findGitRoot(cwd);
|
|
2887
|
+
if (gitRoot !== null) {
|
|
2888
|
+
root = gitRoot;
|
|
2889
|
+
} else {
|
|
2890
|
+
root = cwd;
|
|
2891
|
+
opts.onNotice?.(`No git root found; resolving src: paths from ${cwd}`);
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
const { key, source: keySource } = resolveEncryptionKey({
|
|
2895
|
+
env,
|
|
2896
|
+
file,
|
|
2897
|
+
toError: (message) => new CiteError(message)
|
|
2898
|
+
});
|
|
2899
|
+
return {
|
|
2900
|
+
config,
|
|
2901
|
+
inputs,
|
|
2902
|
+
base,
|
|
2903
|
+
collections,
|
|
2904
|
+
fromCollections,
|
|
2905
|
+
...loaded ? { configDir: loaded.dir, configPath: loaded.path, configSource: loaded.source } : {},
|
|
2906
|
+
root,
|
|
2907
|
+
key,
|
|
2908
|
+
keySource,
|
|
2909
|
+
...file === null ? {} : { configFile: file }
|
|
2910
|
+
};
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
// src/cite/commands/check.ts
|
|
2914
|
+
import { existsSync } from "node:fs";
|
|
2915
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
2916
|
+
import { resolve as resolve3 } from "node:path";
|
|
2917
|
+
function joinHits() {
|
|
2918
|
+
const matched = /* @__PURE__ */ new Map();
|
|
2919
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2920
|
+
return {
|
|
2921
|
+
matched,
|
|
2922
|
+
record(page, label) {
|
|
2923
|
+
for (const join4 of page.joins) {
|
|
2924
|
+
const values = matched.get(join4.field) ?? /* @__PURE__ */ new Set();
|
|
2925
|
+
values.add(join4.value);
|
|
2926
|
+
matched.set(join4.field, values);
|
|
2927
|
+
const id = `${join4.field}\0${join4.value}`;
|
|
2928
|
+
const hit = seen.get(id) ?? { file: join4.file, labels: [] };
|
|
2929
|
+
if (!hit.labels.includes(label)) hit.labels.push(label);
|
|
2930
|
+
seen.set(id, hit);
|
|
2931
|
+
if (hit.labels.length > 1) {
|
|
2932
|
+
throw new CiteError(
|
|
2933
|
+
duplicateJoinRefusal(join4.field, join4.value, join4.file, hit.labels)
|
|
2934
|
+
);
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
};
|
|
2939
|
+
}
|
|
2940
|
+
function isEnoent(error) {
|
|
2941
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
2942
|
+
}
|
|
2943
|
+
async function indexFor(root, client) {
|
|
2944
|
+
try {
|
|
2945
|
+
return await buildSourceIndex(root, { gitClient: client });
|
|
2946
|
+
} catch (error) {
|
|
2947
|
+
if (isEnoent(error)) throw new CiteError(`Root directory not found: ${root}.`);
|
|
2948
|
+
throw error;
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
async function prepareRun(opts, action, verb, requireSources = false) {
|
|
2952
|
+
const cwd = resolve3(opts.cwd ?? process.cwd());
|
|
2953
|
+
const run3 = await resolveCiteRun({
|
|
2954
|
+
cwd,
|
|
2955
|
+
configPath: opts.configPath,
|
|
2956
|
+
noConfig: opts.noConfig,
|
|
2957
|
+
inputs: opts.inputs,
|
|
2958
|
+
collection: opts.collection,
|
|
2959
|
+
root: opts.root,
|
|
2960
|
+
onConfigLoaded: opts.onConfigLoaded,
|
|
2961
|
+
onNotice: opts.onNotice,
|
|
2962
|
+
env: opts.env
|
|
2963
|
+
});
|
|
2964
|
+
const { config, inputs, base } = run3;
|
|
2965
|
+
const checkSources = opts.checkSources ?? config?.checkSources ?? true;
|
|
2966
|
+
if (requireSources && !checkSources) {
|
|
2967
|
+
throw new CiteError(`${verb} needs the sources: drop --no-check-sources (or \`checkSources: false\`).`);
|
|
2968
|
+
}
|
|
2969
|
+
if (inputs.length === 0) {
|
|
2970
|
+
throw new CiteError(
|
|
2971
|
+
`No files to ${verb}. Pass paths/globs, or declare a collection under \`collections:\` in manni.config.yaml.`
|
|
2972
|
+
);
|
|
2973
|
+
}
|
|
2974
|
+
const usingStdin = inputs.includes(STDIN_TOKEN);
|
|
2975
|
+
const forced = opts.as === void 0 ? void 0 : extractorByName(opts.as);
|
|
2976
|
+
if (opts.as !== void 0 && forced?.implemented !== true) {
|
|
2977
|
+
throw new CiteError(
|
|
2978
|
+
`Unknown format "${opts.as}". Supported extensions: ${supportedExtensions().join(", ")}.`
|
|
2979
|
+
);
|
|
2980
|
+
}
|
|
2981
|
+
if (usingStdin && forced === void 0) {
|
|
2982
|
+
throw new CiteError("Reading from stdin (`-`) requires --as <format> to choose an extractor.");
|
|
2983
|
+
}
|
|
2984
|
+
const exts = opts.exts ?? forced?.extensions;
|
|
2985
|
+
const fileInputs = inputs.filter((input) => input !== STDIN_TOKEN);
|
|
2986
|
+
const allowEmpty = opts.allowEmpty ?? config?.allowEmpty;
|
|
2987
|
+
const exclude = [
|
|
2988
|
+
.../* @__PURE__ */ new Set([
|
|
2989
|
+
...opts.exclude ?? [],
|
|
2990
|
+
...run3.fromCollections ? run3.collections.flatMap((c) => c.exclude) : []
|
|
2991
|
+
])
|
|
2992
|
+
];
|
|
2993
|
+
const { files, gitignoreSkipped } = await resolveTargetSet({
|
|
2994
|
+
inputs: fileInputs,
|
|
2995
|
+
exts,
|
|
2996
|
+
exclude,
|
|
2997
|
+
cwd: base,
|
|
2998
|
+
allowEmpty,
|
|
2999
|
+
...gitignoreOptions({
|
|
3000
|
+
flag: opts.respectGitignore,
|
|
3001
|
+
configured: config?.respectGitignore,
|
|
3002
|
+
onNotice: opts.onNotice
|
|
3003
|
+
})
|
|
3004
|
+
});
|
|
3005
|
+
assertNonEmpty({ files, inputs: fileInputs, usingStdin, allowEmpty, exclude, exts, gitignoreSkipped, action });
|
|
3006
|
+
const git = opts.gitClient ?? gitClient(run3.root);
|
|
3007
|
+
const pageOptions = {
|
|
3008
|
+
root: run3.root,
|
|
3009
|
+
checkSources,
|
|
3010
|
+
key: run3.key,
|
|
3011
|
+
severity: config?.severity,
|
|
3012
|
+
gitClient: git
|
|
3013
|
+
};
|
|
3014
|
+
if (checkSources) pageOptions.sourceIndex = await indexFor(run3.root, git);
|
|
3015
|
+
const sidecars2 = await sidecarsFor(run3);
|
|
3016
|
+
const setupFor = (label, content) => {
|
|
3017
|
+
const page = sidecars2?.forPage(label, content, forced?.name);
|
|
3018
|
+
if (page?.owner === void 0) return { options: pageOptions };
|
|
3019
|
+
return {
|
|
3020
|
+
sidecar: page,
|
|
3021
|
+
options: {
|
|
3022
|
+
...pageOptions,
|
|
3023
|
+
owned: { file: page.owner.file, collection: page.owner.collection },
|
|
3024
|
+
...page.citations === void 0 ? {} : { citations: page.citations }
|
|
3025
|
+
}
|
|
3026
|
+
};
|
|
3027
|
+
};
|
|
3028
|
+
const scopedByFlags = (
|
|
3029
|
+
// The inputs the operator typed, not the collection globs they resolved
|
|
3030
|
+
// to: a corpus run has none of the first and all of the second.
|
|
3031
|
+
opts.inputs.length > 0 || usingStdin || opts.as !== void 0 || opts.exts !== void 0 || opts.exclude !== void 0 && opts.exclude.length > 0 || opts.respectGitignore !== void 0
|
|
3032
|
+
);
|
|
3033
|
+
const covered = (opts.collection?.length ?? 0) > 0 ? run3.collections.map((c) => c.name) : void 0;
|
|
3034
|
+
return {
|
|
3035
|
+
cwd,
|
|
3036
|
+
run: run3,
|
|
3037
|
+
files,
|
|
3038
|
+
gitignoreSkipped,
|
|
3039
|
+
usingStdin,
|
|
3040
|
+
forced,
|
|
3041
|
+
pageOptions,
|
|
3042
|
+
git,
|
|
3043
|
+
sidecars: sidecars2,
|
|
3044
|
+
setupFor,
|
|
3045
|
+
scopedByFlags,
|
|
3046
|
+
...covered === void 0 ? {} : { covered }
|
|
3047
|
+
};
|
|
3048
|
+
}
|
|
3049
|
+
function assertNoOrphans(prepared) {
|
|
3050
|
+
const { sidecars: sidecars2 } = prepared;
|
|
3051
|
+
if (sidecars2 === null || prepared.scopedByFlags) return;
|
|
3052
|
+
const orphans = sidecars2.orphans(prepared.files, prepared.covered);
|
|
3053
|
+
if (orphans.length > 0) throw orphanRefusal(orphans);
|
|
3054
|
+
}
|
|
3055
|
+
function assertNoOrphanJoins(prepared, hits) {
|
|
3056
|
+
const { sidecars: sidecars2 } = prepared;
|
|
3057
|
+
if (sidecars2 === null || prepared.scopedByFlags) return;
|
|
3058
|
+
const orphans = sidecars2.orphanJoins(hits.matched, prepared.covered);
|
|
3059
|
+
if (orphans.length > 0) throw orphanRefusal(orphans);
|
|
3060
|
+
}
|
|
3061
|
+
function readTarget(run3, file) {
|
|
3062
|
+
return readFile3(resolve3(run3.base, file), "utf8");
|
|
3063
|
+
}
|
|
3064
|
+
function sayNotices(pages, onNotice, also = []) {
|
|
3065
|
+
const said = /* @__PURE__ */ new Set();
|
|
3066
|
+
for (const notice of [...pages.flatMap((page) => page.notices), ...also]) {
|
|
3067
|
+
if (said.has(notice)) continue;
|
|
3068
|
+
said.add(notice);
|
|
3069
|
+
onNotice?.(notice);
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
async function runCheck(opts) {
|
|
3073
|
+
const prepared = await prepareRun(opts, "checked", "check");
|
|
3074
|
+
const { cwd, run: run3, files, gitignoreSkipped, usingStdin, forced, pageOptions, git } = prepared;
|
|
3075
|
+
assertNoOrphans(prepared);
|
|
3076
|
+
const hits = joinHits();
|
|
3077
|
+
const pages = [];
|
|
3078
|
+
const checkOne = async (label, content) => {
|
|
3079
|
+
const setup = prepared.setupFor(label, content);
|
|
3080
|
+
if (setup.sidecar !== void 0) hits.record(setup.sidecar, label);
|
|
3081
|
+
pages.push(await checkCitations({ file: label, content, format: forced?.name }, setup.options));
|
|
3082
|
+
};
|
|
3083
|
+
if (usingStdin) await checkOne(STDIN_LABEL, opts.stdinContent ?? "");
|
|
3084
|
+
for (const file of files) await checkOne(file, await readTarget(run3, file));
|
|
3085
|
+
assertNoOrphanJoins(prepared, hits);
|
|
3086
|
+
const wantsDiff = opts.showDiff === true && pageOptions.checkSources !== false && !await git.available();
|
|
3087
|
+
sayNotices(pages, opts.onNotice, wantsDiff ? [GIT_UNAVAILABLE_HISTORY] : []);
|
|
3088
|
+
const results = pages.map(toValidationResult);
|
|
3089
|
+
const frame = { cwd, base: run3.configDir ?? cwd, runBase: run3.base };
|
|
3090
|
+
const request = resolveBaselineRequest(opts, run3.config?.baseline, run3.configDir, cwd, {
|
|
3091
|
+
current: DEFAULT_CITE_BASELINE_PATH
|
|
3092
|
+
});
|
|
3093
|
+
if (request !== null && !request.write && !existsSync(request.absPath)) {
|
|
3094
|
+
throw new CiteError(
|
|
3095
|
+
`Baseline "${request.label}" not found. Record one with \`manni cite check --write-baseline\`, or drop --baseline.`
|
|
3096
|
+
);
|
|
3097
|
+
}
|
|
3098
|
+
const { results: reported, baseline } = await settleBaseline(results, request, frame);
|
|
3099
|
+
const failed = reported.filter((r) => !r.ok).length;
|
|
3100
|
+
const count = (keep) => reported.reduce((n, r) => n + r.errors.filter(keep).length, 0);
|
|
3101
|
+
const warnings = count((e) => e.severity === "warning");
|
|
3102
|
+
const notices = count((e) => e.severity === "notice");
|
|
3103
|
+
const summary = {
|
|
3104
|
+
files: reported.length,
|
|
3105
|
+
passed: reported.length - failed,
|
|
3106
|
+
failed,
|
|
3107
|
+
errors: count(isErrorSeverity),
|
|
3108
|
+
// Omitted at zero, as meta's summary has them, so the JSON shape of a
|
|
3109
|
+
// clean run is the one every consumer of `meta validate` already reads.
|
|
3110
|
+
...warnings > 0 ? { warnings } : {},
|
|
3111
|
+
...notices > 0 ? { notices } : {},
|
|
3112
|
+
...gitignoreSkipped > 0 ? { gitignoreSkipped } : {},
|
|
3113
|
+
...baseline ? { baseline } : {}
|
|
3114
|
+
};
|
|
3115
|
+
return { results: reported, summary, frame, pages, warnings, notices };
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
// src/cite/commands/add.ts
|
|
3119
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
3120
|
+
import { relative as relative2, resolve as resolve4 } from "node:path";
|
|
3121
|
+
var ID2 = /^[a-z0-9][a-z0-9-]*$/;
|
|
3122
|
+
var toPosix2 = (path) => path.replace(/\\/g, "/");
|
|
3123
|
+
function isEnoent2(error) {
|
|
3124
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
3125
|
+
}
|
|
3126
|
+
async function readPageFile(path, label) {
|
|
3127
|
+
try {
|
|
3128
|
+
return await readFile4(path, "utf8");
|
|
3129
|
+
} catch (error) {
|
|
3130
|
+
if (isEnoent2(error)) throw new CiteError(`File not found: "${label}".`);
|
|
3131
|
+
throw error;
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
async function citedText(root, index, src, key) {
|
|
3135
|
+
const range = parseSrc(src);
|
|
3136
|
+
const source = await readSource(root, index, range, key);
|
|
3137
|
+
if (source.kind === "missing") {
|
|
3138
|
+
throw new CiteError(`Source not readable: ${range.path} could not be read.`);
|
|
3139
|
+
}
|
|
3140
|
+
return sliceLines(splitLines(source.text), range, range.path);
|
|
3141
|
+
}
|
|
3142
|
+
async function runAdd(opts) {
|
|
3143
|
+
const cwd = resolve4(opts.cwd ?? process.cwd());
|
|
3144
|
+
const run3 = await resolveCiteRun({
|
|
3145
|
+
cwd,
|
|
3146
|
+
configPath: opts.configPath,
|
|
3147
|
+
noConfig: opts.noConfig,
|
|
3148
|
+
inputs: [],
|
|
3149
|
+
root: opts.root,
|
|
3150
|
+
onConfigLoaded: opts.onConfigLoaded,
|
|
3151
|
+
onNotice: opts.onNotice,
|
|
3152
|
+
env: opts.env
|
|
3153
|
+
});
|
|
3154
|
+
const { root } = run3;
|
|
3155
|
+
const usingStdin = opts.page === STDIN_TOKEN;
|
|
3156
|
+
if (usingStdin && opts.as === void 0) {
|
|
3157
|
+
throw new CiteError("Reading from stdin (`-`) requires --as <format> to choose an extractor.");
|
|
3158
|
+
}
|
|
3159
|
+
const path = usingStdin ? void 0 : resolve4(cwd, opts.page);
|
|
3160
|
+
const label = path === void 0 ? STDIN_LABEL : toPosix2(relative2(cwd, path));
|
|
3161
|
+
const content = path === void 0 ? opts.stdinContent ?? "" : await readPageFile(path, label);
|
|
3162
|
+
const sidecars2 = await sidecarsFor(run3, { base: cwd });
|
|
3163
|
+
const firstManifest = sidecars2?.manifests[0];
|
|
3164
|
+
if (usingStdin && firstManifest !== void 0) {
|
|
3165
|
+
const keyedBy = firstManifest.join === "path" ? "path" : firstManifest.join;
|
|
3166
|
+
throw new CiteError(
|
|
3167
|
+
`A page read from stdin has no path, and its citations live in ${firstManifest.file}, which is keyed by ${keyedBy}.`
|
|
3168
|
+
);
|
|
3169
|
+
}
|
|
3170
|
+
const sidecar = usingStdin ? void 0 : sidecars2?.forPage(label, content, opts.as);
|
|
3171
|
+
const owner = sidecar?.owner;
|
|
3172
|
+
const page = readPage(label, content, {
|
|
3173
|
+
...opts.as === void 0 ? {} : { format: opts.as },
|
|
3174
|
+
// The entries already there are the manifest's, so `--id` uniqueness and
|
|
3175
|
+
// the marker checks are measured against them and not against a page
|
|
3176
|
+
// that carries none.
|
|
3177
|
+
...sidecar?.citations === void 0 ? {} : { citations: sidecar.citations },
|
|
3178
|
+
...owner === void 0 ? {} : { owned: { file: owner.file, collection: owner.collection } }
|
|
3179
|
+
});
|
|
3180
|
+
const { format } = page;
|
|
3181
|
+
const pageLines = opts.pageLines;
|
|
3182
|
+
const marker = opts.marker === true;
|
|
3183
|
+
const quote = opts.quote === true;
|
|
3184
|
+
const at2 = pageLines === void 0 ? "" : `${label}:${spellLines(pageLines)}`;
|
|
3185
|
+
if (marker && pageLines === void 0) {
|
|
3186
|
+
throw new CiteError(`--marker needs the page lines to anchor: ${label}:L.`);
|
|
3187
|
+
}
|
|
3188
|
+
if (marker && opts.id === void 0) {
|
|
3189
|
+
throw new CiteError("--marker needs --id: the marker names the entry.");
|
|
3190
|
+
}
|
|
3191
|
+
if (quote && pageLines === void 0) {
|
|
3192
|
+
throw new CiteError(`--quote needs the block's lines: ${label}:L1-L2.`);
|
|
3193
|
+
}
|
|
3194
|
+
if (opts.id !== void 0 && !ID2.test(opts.id)) {
|
|
3195
|
+
throw new CiteError(
|
|
3196
|
+
`Invalid id "${opts.id}": use lowercase letters, digits and hyphens, starting with a letter or digit.`
|
|
3197
|
+
);
|
|
3198
|
+
}
|
|
3199
|
+
if (opts.id !== void 0 && page.citations.some((c) => c.citation.id === opts.id)) {
|
|
3200
|
+
throw new CiteError(`${label} already has an entry ${opts.id}.`);
|
|
3201
|
+
}
|
|
3202
|
+
if (marker && opts.id !== void 0) {
|
|
3203
|
+
const already = page.statements.find(
|
|
3204
|
+
(s) => s.payload.kind === "ref" && s.payload.id === opts.id
|
|
3205
|
+
);
|
|
3206
|
+
if (already !== void 0) {
|
|
3207
|
+
throw new CiteError(
|
|
3208
|
+
`${label} already has a marker ${opts.id} at line ${String(already.line)}.`
|
|
3209
|
+
);
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
const lines = splitLines(content);
|
|
3213
|
+
if (pageLines !== void 0) {
|
|
3214
|
+
if (pageLines.end > lines.length) {
|
|
3215
|
+
throw new CiteError(`${at2} is past the end of the page (${String(lines.length)} lines).`);
|
|
3216
|
+
}
|
|
3217
|
+
if (pageLines.start < page.bodyLine) {
|
|
3218
|
+
throw new CiteError(`${at2} is in the frontmatter. A claim is body text.`);
|
|
3219
|
+
}
|
|
3220
|
+
if (quote) {
|
|
3221
|
+
const span = fenceSpanAt(content, pageLines.start, format);
|
|
3222
|
+
if (span === void 0 || span.end !== pageLines.end) {
|
|
3223
|
+
throw new CiteError(`${at2} is not a fenced block, so it cannot be a quote.`);
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
const client = opts.gitClient ?? gitClient(root);
|
|
3228
|
+
const sourceIndex = await buildSourceIndex(root, { gitClient: client });
|
|
3229
|
+
let body = content;
|
|
3230
|
+
let markerAt;
|
|
3231
|
+
let claimSpan;
|
|
3232
|
+
let claim;
|
|
3233
|
+
if (marker && pageLines !== void 0) {
|
|
3234
|
+
const statement = formatStatement(format, { kind: "ref", id: opts.id ?? "" });
|
|
3235
|
+
const insertAt = offsetOfLine(content, pageLines.start);
|
|
3236
|
+
body = insertStatementBefore(content, insertAt, statement);
|
|
3237
|
+
markerAt = pageLines.start;
|
|
3238
|
+
const unit = anchoredLines(body, insertAt + statement.length, format, quote);
|
|
3239
|
+
const pin = unit === void 0 ? void 0 : pinOfLines(splitLines(body), unit);
|
|
3240
|
+
if (unit === void 0 || pin === void 0) {
|
|
3241
|
+
throw new CiteError(`${at2} has no paragraph or block for a marker to anchor.`);
|
|
3242
|
+
}
|
|
3243
|
+
claimSpan = unit;
|
|
3244
|
+
claim = { integrity: pin };
|
|
3245
|
+
} else if (pageLines !== void 0) {
|
|
3246
|
+
const pin = pinOfLines(lines, pageLines);
|
|
3247
|
+
if (pin === void 0) {
|
|
3248
|
+
throw new CiteError(`${at2} is past the end of the page (${String(lines.length)} lines).`);
|
|
3249
|
+
}
|
|
3250
|
+
claimSpan = pageLines;
|
|
3251
|
+
claim = { lines: lineSpec(toBodyLines(pageLines, page.bodyLine)), integrity: pin };
|
|
3252
|
+
}
|
|
3253
|
+
const mint = (key, encrypt) => mintCitation({
|
|
3254
|
+
root,
|
|
3255
|
+
src: opts.src,
|
|
3256
|
+
...opts.id === void 0 ? {} : { id: opts.id },
|
|
3257
|
+
...claim === void 0 ? {} : { claim },
|
|
3258
|
+
...quote ? { quote: true } : {},
|
|
3259
|
+
...opts.commitSha === false ? { commitSha: false } : {},
|
|
3260
|
+
encrypt,
|
|
3261
|
+
...key === void 0 ? {} : { key },
|
|
3262
|
+
gitClient: client,
|
|
3263
|
+
sourceIndex
|
|
3264
|
+
});
|
|
3265
|
+
const needsKey = opts.encrypt === true && run3.key === void 0;
|
|
3266
|
+
let citation = await mint(run3.key, run3.key !== void 0);
|
|
3267
|
+
if (quote && claimSpan !== void 0) {
|
|
3268
|
+
const cited = await citedText(root, sourceIndex, opts.src, run3.key);
|
|
3269
|
+
const inside = splitLines(body).slice(claimSpan.start, claimSpan.end - 1).join("\n");
|
|
3270
|
+
if (!blockMatches(inside, cited)) {
|
|
3271
|
+
throw new CiteError(`The block at ${at2} does not reproduce ${opts.src}.`);
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
if (needsKey) {
|
|
3275
|
+
const { key } = await ensureEncryptionKey({
|
|
3276
|
+
subject: opts.src,
|
|
3277
|
+
cwd,
|
|
3278
|
+
file: run3.configFile ?? null,
|
|
3279
|
+
env: opts.env,
|
|
3280
|
+
confirm: opts.confirm,
|
|
3281
|
+
notice: (message) => {
|
|
3282
|
+
opts.onNotice?.(message);
|
|
3283
|
+
},
|
|
3284
|
+
toError: (message) => new CiteError(message)
|
|
3285
|
+
});
|
|
3286
|
+
citation = await mint(key, true);
|
|
3287
|
+
}
|
|
3288
|
+
const manifests = new ManifestSet();
|
|
3289
|
+
let placed;
|
|
3290
|
+
if (owner !== void 0) {
|
|
3291
|
+
if (sidecar?.entry === void 0) {
|
|
3292
|
+
throw new CiteError(
|
|
3293
|
+
`${label} carries no ${owner.join}: value, so its citations cannot be keyed in ${owner.file}.`
|
|
3294
|
+
);
|
|
3295
|
+
}
|
|
3296
|
+
const existing = (sidecar.citations ?? []).map((input) => input.entry);
|
|
3297
|
+
const list = [...existing, entryObject(citation)];
|
|
3298
|
+
const at3 = await manifests.write(owner, sidecar.entry, list, list.length - 1);
|
|
3299
|
+
const [changed] = manifests.changed();
|
|
3300
|
+
placed = {
|
|
3301
|
+
file: at3.file,
|
|
3302
|
+
line: at3.line,
|
|
3303
|
+
content: changed?.text ?? "",
|
|
3304
|
+
diff: changed?.diff ?? "",
|
|
3305
|
+
written: false
|
|
3306
|
+
};
|
|
3307
|
+
}
|
|
3308
|
+
const after = owner === void 0 ? appendFrontmatterCitation(body, format, citation, label) : body;
|
|
3309
|
+
const shift = bodyLineOf(after, locateFrontmatter(after)?.closeEnd ?? 0) - page.bodyLine;
|
|
3310
|
+
const result = {
|
|
3311
|
+
file: label,
|
|
3312
|
+
citation,
|
|
3313
|
+
placed: owner === void 0 ? "frontmatter" : "manifest",
|
|
3314
|
+
content: after,
|
|
3315
|
+
diff: unifiedDiff(label, content, after),
|
|
3316
|
+
written: false,
|
|
3317
|
+
...placed === void 0 ? {} : { manifest: placed }
|
|
3318
|
+
};
|
|
3319
|
+
if (markerAt !== void 0) result.markerLine = markerAt + shift;
|
|
3320
|
+
if (claimSpan !== void 0) {
|
|
3321
|
+
result.claimLines = { start: claimSpan.start + shift, end: claimSpan.end + shift };
|
|
3322
|
+
}
|
|
3323
|
+
result.written = path !== void 0 && opts.dryRun !== true && after !== content;
|
|
3324
|
+
if (result.written && path !== void 0) await writeFileAtomic(path, after);
|
|
3325
|
+
if (result.manifest !== void 0 && opts.dryRun !== true) {
|
|
3326
|
+
for (const changed of manifests.changed()) await writeFileAtomic(changed.path, changed.text);
|
|
3327
|
+
result.manifest.written = true;
|
|
3328
|
+
}
|
|
3329
|
+
if (opts.commitSha !== false && !await client.available()) {
|
|
3330
|
+
opts.onNotice?.(GIT_UNAVAILABLE_COMMIT);
|
|
3331
|
+
}
|
|
3332
|
+
return result;
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
// src/cite/commands/update.ts
|
|
3336
|
+
import { resolve as resolve5 } from "node:path";
|
|
3337
|
+
function settles(plan) {
|
|
3338
|
+
switch (plan.kind) {
|
|
3339
|
+
case "claim-moved":
|
|
3340
|
+
return "claim-moved";
|
|
3341
|
+
case "source-moved":
|
|
3342
|
+
return "source-moved";
|
|
3343
|
+
case "claim-accepted":
|
|
3344
|
+
return "claim-changed";
|
|
3345
|
+
case "source-accepted":
|
|
3346
|
+
return plan.result.source.status === "never-true" ? "source-never-true" : "source-changed";
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
function apply(content, format, plan) {
|
|
3350
|
+
const index = plan.result.origin.index;
|
|
3351
|
+
switch (plan.kind) {
|
|
3352
|
+
case "claim-moved":
|
|
3353
|
+
return spliceEntryField(content, format, index, ["claim", "lines"], plan.lines);
|
|
3354
|
+
case "source-moved":
|
|
3355
|
+
return spliceEntryField(content, format, index, ["source", "lines"], plan.lines);
|
|
3356
|
+
case "claim-accepted": {
|
|
3357
|
+
let out = spliceEntryField(content, format, index, ["claim", "integrity"], plan.pin);
|
|
3358
|
+
if (plan.lines !== void 0) {
|
|
3359
|
+
out = spliceEntryField(out, format, index, ["claim", "lines"], plan.lines);
|
|
3360
|
+
}
|
|
3361
|
+
return out;
|
|
3362
|
+
}
|
|
3363
|
+
case "source-accepted": {
|
|
3364
|
+
let out = spliceEntryField(
|
|
3365
|
+
content,
|
|
3366
|
+
format,
|
|
3367
|
+
index,
|
|
3368
|
+
["source", "integrity"],
|
|
3369
|
+
plan.minted.source.integrity
|
|
3370
|
+
);
|
|
3371
|
+
const commit = plan.minted.source["commit-sha"];
|
|
3372
|
+
if (commit !== void 0) {
|
|
3373
|
+
out = spliceEntryField(out, format, index, ["source", "commit-sha"], commit);
|
|
3374
|
+
}
|
|
3375
|
+
return out;
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
function isRecord4(value) {
|
|
3380
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3381
|
+
}
|
|
3382
|
+
function applyToEntry(entry, plan) {
|
|
3383
|
+
if (!isRecord4(entry)) return false;
|
|
3384
|
+
const end = (name) => isRecord4(entry[name]) ? entry[name] : void 0;
|
|
3385
|
+
switch (plan.kind) {
|
|
3386
|
+
case "claim-moved": {
|
|
3387
|
+
const claim = end("claim");
|
|
3388
|
+
if (claim === void 0) return false;
|
|
3389
|
+
claim.lines = plan.lines;
|
|
3390
|
+
return true;
|
|
3391
|
+
}
|
|
3392
|
+
case "source-moved": {
|
|
3393
|
+
const source = end("source");
|
|
3394
|
+
if (source === void 0) return false;
|
|
3395
|
+
source.lines = plan.lines;
|
|
3396
|
+
return true;
|
|
3397
|
+
}
|
|
3398
|
+
case "claim-accepted": {
|
|
3399
|
+
const claim = end("claim");
|
|
3400
|
+
if (claim === void 0) return false;
|
|
3401
|
+
claim.integrity = plan.pin;
|
|
3402
|
+
if (plan.lines !== void 0) claim.lines = plan.lines;
|
|
3403
|
+
return true;
|
|
3404
|
+
}
|
|
3405
|
+
case "source-accepted": {
|
|
3406
|
+
const source = end("source");
|
|
3407
|
+
if (source === void 0) return false;
|
|
3408
|
+
source.integrity = plan.minted.source.integrity;
|
|
3409
|
+
const commit = plan.minted.source["commit-sha"];
|
|
3410
|
+
if (commit !== void 0) source["commit-sha"] = commit;
|
|
3411
|
+
return true;
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
function rewriteOf(plan) {
|
|
3416
|
+
const { citation, origin, source } = plan.result;
|
|
3417
|
+
const base = { index: origin.index };
|
|
3418
|
+
if (citation.id !== void 0) base.id = citation.id;
|
|
3419
|
+
if (origin.line !== void 0) base.line = origin.line;
|
|
3420
|
+
switch (plan.kind) {
|
|
3421
|
+
case "claim-moved":
|
|
3422
|
+
return { ...base, end: "claim", reason: "moved", status: "moved", from: plan.from, to: plan.to };
|
|
3423
|
+
case "source-moved":
|
|
3424
|
+
return {
|
|
3425
|
+
...base,
|
|
3426
|
+
end: "source",
|
|
3427
|
+
reason: "moved",
|
|
3428
|
+
status: "moved",
|
|
3429
|
+
from: source.src,
|
|
3430
|
+
to: plan.to
|
|
3431
|
+
};
|
|
3432
|
+
case "claim-accepted": {
|
|
3433
|
+
const out = {
|
|
3434
|
+
...base,
|
|
3435
|
+
end: "claim",
|
|
3436
|
+
reason: "accepted",
|
|
3437
|
+
status: "changed",
|
|
3438
|
+
from: citation.claim?.integrity ?? "",
|
|
3439
|
+
to: plan.pin,
|
|
3440
|
+
at: plan.unit.lines.start,
|
|
3441
|
+
text: normalizeWhitespace(plan.unit.text.join("\n"))
|
|
3442
|
+
};
|
|
3443
|
+
return out;
|
|
3444
|
+
}
|
|
3445
|
+
case "source-accepted": {
|
|
3446
|
+
const out = {
|
|
3447
|
+
...base,
|
|
3448
|
+
end: "source",
|
|
3449
|
+
reason: "accepted",
|
|
3450
|
+
status: source.status === "never-true" ? "never-true" : "changed",
|
|
3451
|
+
from: citation.source.integrity,
|
|
3452
|
+
to: plan.minted.source.integrity,
|
|
3453
|
+
src: source.src
|
|
3454
|
+
};
|
|
3455
|
+
const commit = plan.minted.source["commit-sha"];
|
|
3456
|
+
if (commit !== void 0) out.commitSha = commit;
|
|
3457
|
+
return out;
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
async function runUpdate(opts) {
|
|
3462
|
+
const prepared = await prepareRun(opts, "updated", "update", true);
|
|
3463
|
+
const { run: run3, files, usingStdin, forced, pageOptions, git } = prepared;
|
|
3464
|
+
assertNoOrphans(prepared);
|
|
3465
|
+
const hits = joinHits();
|
|
3466
|
+
const manifests = new ManifestSet();
|
|
3467
|
+
const only = opts.only !== void 0 && opts.only.length > 0 ? new Set(opts.only) : void 0;
|
|
3468
|
+
const accept = opts.accept === true;
|
|
3469
|
+
const plansFor = async (result, entry, page, lines) => {
|
|
3470
|
+
const out = [];
|
|
3471
|
+
const claim = result.claim;
|
|
3472
|
+
if (claim !== null && claim.status === "moved" && claim.newLines !== void 0) {
|
|
3473
|
+
const at2 = parseLines(claim.newLines);
|
|
3474
|
+
if (at2 !== void 0) {
|
|
3475
|
+
out.push({
|
|
3476
|
+
kind: "claim-moved",
|
|
3477
|
+
result,
|
|
3478
|
+
lines: lineSpec(at2),
|
|
3479
|
+
from: claim.fileLines ?? "",
|
|
3480
|
+
to: claim.newFileLines ?? ""
|
|
3481
|
+
});
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
if (result.source.status === "moved" && result.source.newLines !== void 0) {
|
|
3485
|
+
const at2 = parseLines(result.source.newLines);
|
|
3486
|
+
if (at2 !== void 0 && result.source.newSrc !== void 0) {
|
|
3487
|
+
out.push({ kind: "source-moved", result, lines: lineSpec(at2), to: result.source.newSrc });
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
if (!accept) return out;
|
|
3491
|
+
if (claim !== null && claim.status === "changed" && entry !== void 0) {
|
|
3492
|
+
const unit = result.anchor === "marker" ? markerUnit(page, entry, lines) : (() => {
|
|
3493
|
+
const at2 = claimLine(claim);
|
|
3494
|
+
return at2 === void 0 ? void 0 : unitAt(page, at2, lines);
|
|
3495
|
+
})();
|
|
3496
|
+
const wantsBlock = entry.citation.quote === true;
|
|
3497
|
+
const pin = unit === void 0 ? void 0 : pinOfLines(lines, unit.lines);
|
|
3498
|
+
if (unit !== void 0 && pin !== void 0 && (!wantsBlock || unit.kind === "block")) {
|
|
3499
|
+
const plan = { kind: "claim-accepted", result, unit, pin };
|
|
3500
|
+
if (entry.citation.claim?.lines !== void 0) {
|
|
3501
|
+
const body = toBodyLines(unit.lines, page.bodyLine);
|
|
3502
|
+
const recorded = parseLines(entry.citation.claim.lines);
|
|
3503
|
+
if (recorded === void 0 || recorded.start !== body.start || recorded.end !== body.end) {
|
|
3504
|
+
plan.lines = lineSpec(body);
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
out.push(plan);
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
if (result.source.status === "changed" || result.source.status === "never-true") {
|
|
3511
|
+
try {
|
|
3512
|
+
const minted = await mintCitation({
|
|
3513
|
+
root: run3.root,
|
|
3514
|
+
src: result.source.src,
|
|
3515
|
+
...run3.key === void 0 ? {} : { key: run3.key },
|
|
3516
|
+
...result.citation.source["commit-sha"] === void 0 ? { commitSha: false } : {},
|
|
3517
|
+
gitClient: git,
|
|
3518
|
+
...pageOptions.sourceIndex === void 0 ? {} : { sourceIndex: pageOptions.sourceIndex }
|
|
3519
|
+
});
|
|
3520
|
+
out.push({ kind: "source-accepted", result, minted });
|
|
3521
|
+
} catch (error) {
|
|
3522
|
+
if (!(error instanceof CiteError)) throw error;
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
return out;
|
|
3526
|
+
};
|
|
3527
|
+
const pages = [];
|
|
3528
|
+
const updateOne = async (label, content, path) => {
|
|
3529
|
+
const setup = prepared.setupFor(label, content);
|
|
3530
|
+
if (setup.sidecar !== void 0) hits.record(setup.sidecar, label);
|
|
3531
|
+
const report = await checkCitations({ file: label, content, format: forced?.name }, setup.options);
|
|
3532
|
+
const { format } = report;
|
|
3533
|
+
const page = readPage(label, content, {
|
|
3534
|
+
...forced === void 0 ? {} : { format: forced.name },
|
|
3535
|
+
...setup.options.citations === void 0 ? {} : { citations: setup.options.citations },
|
|
3536
|
+
...setup.options.owned === void 0 ? {} : { owned: setup.options.owned }
|
|
3537
|
+
});
|
|
3538
|
+
const lines = splitLines(content);
|
|
3539
|
+
const rewritten2 = [];
|
|
3540
|
+
const settled = /* @__PURE__ */ new Set();
|
|
3541
|
+
let after = content;
|
|
3542
|
+
const owner = setup.sidecar?.owner;
|
|
3543
|
+
const entries = setup.sidecar?.citations === void 0 ? void 0 : JSON.parse(JSON.stringify(setup.sidecar.citations.map((c) => c.entry)));
|
|
3544
|
+
let manifestDirty = false;
|
|
3545
|
+
for (const result of report.citations) {
|
|
3546
|
+
if (only !== void 0 && (result.citation.id === void 0 || !only.has(result.citation.id))) {
|
|
3547
|
+
continue;
|
|
3548
|
+
}
|
|
3549
|
+
const entry = page.citations.find((c) => c.origin.index === result.origin.index);
|
|
3550
|
+
for (const plan of await plansFor(result, entry, page, lines)) {
|
|
3551
|
+
if (result.origin.kind === "manifest") {
|
|
3552
|
+
if (entries === void 0 || !applyToEntry(entries[result.origin.index], plan)) continue;
|
|
3553
|
+
manifestDirty = true;
|
|
3554
|
+
} else {
|
|
3555
|
+
after = apply(after, format, plan);
|
|
3556
|
+
}
|
|
3557
|
+
rewritten2.push(rewriteOf(plan));
|
|
3558
|
+
settled.add(`${String(result.origin.index)}\0${settles(plan)}`);
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
if (manifestDirty && owner !== void 0 && entries !== void 0) {
|
|
3562
|
+
if (setup.sidecar?.entry === void 0) {
|
|
3563
|
+
throw new CiteError(
|
|
3564
|
+
`${label} carries no ${owner.join}: value, so its citations cannot be keyed in ${owner.file}.`
|
|
3565
|
+
);
|
|
3566
|
+
}
|
|
3567
|
+
await manifests.write(owner, setup.sidecar.entry, entries, 0);
|
|
3568
|
+
}
|
|
3569
|
+
const skipped2 = report.findings.filter(
|
|
3570
|
+
(finding2) => (only === void 0 || finding2.id !== void 0 && only.has(finding2.id)) && !settled.has(`${String(finding2.index ?? -1)}\0${finding2.rule}`)
|
|
3571
|
+
);
|
|
3572
|
+
const diff = after === content ? "" : unifiedDiff(label, content, after);
|
|
3573
|
+
const written = after !== content && path !== void 0 && opts.dryRun !== true;
|
|
3574
|
+
if (written) await writeFileAtomic(path, after);
|
|
3575
|
+
const out = { file: label, rewritten: rewritten2, skipped: skipped2, diff, written };
|
|
3576
|
+
if (path === void 0) out.content = after;
|
|
3577
|
+
pages.push(out);
|
|
3578
|
+
return report;
|
|
3579
|
+
};
|
|
3580
|
+
const reports = [];
|
|
3581
|
+
if (usingStdin) reports.push(await updateOne(STDIN_LABEL, opts.stdinContent ?? ""));
|
|
3582
|
+
for (const file of files) {
|
|
3583
|
+
reports.push(await updateOne(file, await readTarget(run3, file), resolve5(run3.base, file)));
|
|
3584
|
+
}
|
|
3585
|
+
assertNoOrphanJoins(prepared, hits);
|
|
3586
|
+
const changedManifests = manifests.changed();
|
|
3587
|
+
const rewrittenManifests = [];
|
|
3588
|
+
for (const changed of changedManifests) {
|
|
3589
|
+
const write = opts.dryRun !== true;
|
|
3590
|
+
if (write) await writeFileAtomic(changed.path, changed.text);
|
|
3591
|
+
rewrittenManifests.push({ file: changed.file, diff: changed.diff, written: write });
|
|
3592
|
+
}
|
|
3593
|
+
const reminted = pages.some(
|
|
3594
|
+
(page) => page.rewritten.some((r) => r.reason === "accepted" && r.end === "source")
|
|
3595
|
+
);
|
|
3596
|
+
sayNotices(reports, opts.onNotice, reminted && !await git.available() ? [GIT_UNAVAILABLE_COMMIT] : []);
|
|
3597
|
+
const rewritten = pages.reduce((n, page) => n + page.rewritten.length, 0);
|
|
3598
|
+
const skipped = pages.reduce((n, page) => n + page.skipped.length, 0);
|
|
3599
|
+
const undone = pages.some((page) => page.skipped.some((finding2) => finding2.severity === "error"));
|
|
3600
|
+
return {
|
|
3601
|
+
pages,
|
|
3602
|
+
rewritten,
|
|
3603
|
+
skipped,
|
|
3604
|
+
...rewrittenManifests.length > 0 ? { manifests: rewrittenManifests } : {},
|
|
3605
|
+
exitCode: undone ? 1 : 0
|
|
3606
|
+
};
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
// src/cite/core/reencrypt.ts
|
|
3610
|
+
var UNDECRYPTABLE = "does not decrypt under the current key";
|
|
3611
|
+
var MISSING = "missing (no tracked file matches)";
|
|
3612
|
+
var CHANGED = "changed; run `manni cite update --accept` before rotating";
|
|
3613
|
+
function whereOf(citation, origin) {
|
|
3614
|
+
const where = { index: origin.index };
|
|
3615
|
+
if (citation.id !== void 0) where.id = citation.id;
|
|
3616
|
+
if (origin.line !== void 0) where.line = origin.line;
|
|
3617
|
+
return where;
|
|
3618
|
+
}
|
|
3619
|
+
async function pinnedLines(ctx, citation, range, path, key) {
|
|
3620
|
+
const source = await readSource(ctx.root, ctx.index, { ...range, path, encrypted: false });
|
|
3621
|
+
if (source.kind === "missing") return { skip: MISSING };
|
|
3622
|
+
const pin = citation.source.integrity;
|
|
3623
|
+
let joined;
|
|
3624
|
+
try {
|
|
3625
|
+
joined = sliceLines(splitLines(source.text), range);
|
|
3626
|
+
} catch {
|
|
3627
|
+
joined = void 0;
|
|
3628
|
+
}
|
|
3629
|
+
if (joined !== void 0 && hashLines(joined, key) === pin) return { joined };
|
|
3630
|
+
const commit = citation.source["commit-sha"];
|
|
3631
|
+
if (commit !== void 0 && await ctx.git.available()) {
|
|
3632
|
+
const history = await historyOf(ctx.git, commit, path, range, pin, key, void 0);
|
|
3633
|
+
if (history.kind === "original") return { joined: history.lines.join("\n") };
|
|
3634
|
+
}
|
|
3635
|
+
return { skip: CHANGED };
|
|
3636
|
+
}
|
|
3637
|
+
async function verdictFor(ctx, citation, opts) {
|
|
3638
|
+
const range = sourceRange(citation.source);
|
|
3639
|
+
const underNew = decryptSourcePath(range.path, opts.toKey);
|
|
3640
|
+
if (underNew !== void 0) {
|
|
3641
|
+
const held = await pinnedLines(ctx, citation, range, underNew, opts.toKey);
|
|
3642
|
+
if ("joined" in held) return { kind: "done" };
|
|
3643
|
+
}
|
|
3644
|
+
const path = underNew ?? decryptSourcePath(range.path, opts.fromKey);
|
|
3645
|
+
if (path === void 0) return { kind: "skip", message: UNDECRYPTABLE };
|
|
3646
|
+
const lines = await pinnedLines(ctx, citation, range, path, opts.fromKey);
|
|
3647
|
+
if ("skip" in lines) return { kind: "skip", message: lines.skip };
|
|
3648
|
+
const file = underNew === void 0 ? encryptSourcePath(path, opts.toKey) : range.path;
|
|
3649
|
+
return {
|
|
3650
|
+
kind: "write",
|
|
3651
|
+
file,
|
|
3652
|
+
integrity: hashLines(lines.joined, opts.toKey),
|
|
3653
|
+
to: formatSrc({ ...range, path: file })
|
|
3654
|
+
};
|
|
3655
|
+
}
|
|
3656
|
+
async function contextFor(opts) {
|
|
3657
|
+
const git = opts.gitClient ?? gitClient(opts.root);
|
|
3658
|
+
const index = opts.sourceIndex ?? await buildSourceIndex(opts.root, { gitClient: git });
|
|
3659
|
+
return { root: opts.root, index, git };
|
|
3660
|
+
}
|
|
3661
|
+
async function reencryptCitations(page, opts) {
|
|
3662
|
+
const read = readPage(page.file, page.content, page.format === void 0 ? void 0 : { format: page.format });
|
|
3663
|
+
const result = { content: page.content, rewritten: [], skipped: [] };
|
|
3664
|
+
const encrypted = read.citations.filter(({ citation }) => sourceRange(citation.source).encrypted);
|
|
3665
|
+
if (encrypted.length === 0) return result;
|
|
3666
|
+
const ctx = await contextFor(opts);
|
|
3667
|
+
let after = page.content;
|
|
3668
|
+
for (const { citation, origin } of encrypted) {
|
|
3669
|
+
const range = sourceRange(citation.source);
|
|
3670
|
+
const where = whereOf(citation, origin);
|
|
3671
|
+
const verdict = await verdictFor(ctx, citation, opts);
|
|
3672
|
+
if (verdict.kind === "done") continue;
|
|
3673
|
+
if (verdict.kind === "skip") {
|
|
3674
|
+
result.skipped.push({ ...where, message: verdict.message });
|
|
3675
|
+
continue;
|
|
3676
|
+
}
|
|
3677
|
+
if (verdict.file !== range.path) {
|
|
3678
|
+
after = spliceEntryField(after, read.format, origin.index, ["source", "file"], verdict.file);
|
|
3679
|
+
}
|
|
3680
|
+
after = spliceEntryField(
|
|
3681
|
+
after,
|
|
3682
|
+
read.format,
|
|
3683
|
+
origin.index,
|
|
3684
|
+
["source", "integrity"],
|
|
3685
|
+
verdict.integrity
|
|
3686
|
+
);
|
|
3687
|
+
result.rewritten.push({ ...where, from: spellSource(citation.source), to: verdict.to });
|
|
3688
|
+
}
|
|
3689
|
+
result.content = after;
|
|
3690
|
+
return result;
|
|
3691
|
+
}
|
|
3692
|
+
async function reencryptCitationEntries(page, opts) {
|
|
3693
|
+
const entries = JSON.parse(
|
|
3694
|
+
JSON.stringify(page.citations.map((input) => input.entry))
|
|
3695
|
+
);
|
|
3696
|
+
const out = { entries, changed: false, rewritten: [], skipped: [] };
|
|
3697
|
+
const read = readPage(page.file, page.content, {
|
|
3698
|
+
...page.format === void 0 ? {} : { format: page.format },
|
|
3699
|
+
citations: page.citations
|
|
3700
|
+
});
|
|
3701
|
+
const encrypted = read.citations.filter(({ citation }) => sourceRange(citation.source).encrypted);
|
|
3702
|
+
if (encrypted.length === 0) return out;
|
|
3703
|
+
const ctx = await contextFor(opts);
|
|
3704
|
+
for (const { citation, origin } of encrypted) {
|
|
3705
|
+
const where = whereOf(citation, origin);
|
|
3706
|
+
const verdict = await verdictFor(ctx, citation, opts);
|
|
3707
|
+
if (verdict.kind === "done") continue;
|
|
3708
|
+
if (verdict.kind === "skip") {
|
|
3709
|
+
out.skipped.push({ ...where, message: verdict.message });
|
|
3710
|
+
continue;
|
|
3711
|
+
}
|
|
3712
|
+
const entry = entries[origin.index];
|
|
3713
|
+
const source = typeof entry === "object" && entry !== null && !Array.isArray(entry) ? entry.source : void 0;
|
|
3714
|
+
if (typeof source !== "object" || source === null || Array.isArray(source)) {
|
|
3715
|
+
out.skipped.push({ ...where, message: UNDECRYPTABLE });
|
|
3716
|
+
continue;
|
|
3717
|
+
}
|
|
3718
|
+
const fields = source;
|
|
3719
|
+
fields.file = verdict.file;
|
|
3720
|
+
fields.integrity = verdict.integrity;
|
|
3721
|
+
out.changed = true;
|
|
3722
|
+
out.rewritten.push({ ...where, from: spellSource(citation.source), to: verdict.to });
|
|
3723
|
+
}
|
|
3724
|
+
return out;
|
|
3725
|
+
}
|
|
3726
|
+
|
|
3727
|
+
// src/cite/reporters/pretty.ts
|
|
3728
|
+
var DIFF_LINE_CAP = 60;
|
|
3729
|
+
var plural2 = (n, one, many = `${one}s`) => `${String(n)} ${n === 1 ? one : many}`;
|
|
3730
|
+
function severityMark(severity, c) {
|
|
3731
|
+
switch (severity) {
|
|
3732
|
+
case "error":
|
|
3733
|
+
return c.red("\u2717");
|
|
3734
|
+
case "warning":
|
|
3735
|
+
return c.yellow("\u2195");
|
|
3736
|
+
case "notice":
|
|
3737
|
+
return c.dim("\u2139");
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
3740
|
+
function splitBaselined(page, result) {
|
|
3741
|
+
if (result === void 0) return { reported: [...page.findings], baselined: [] };
|
|
3742
|
+
const pool = [...result.errors];
|
|
3743
|
+
const out = { reported: [], baselined: [] };
|
|
3744
|
+
for (const finding2 of page.findings) {
|
|
3745
|
+
const instancePath = finding2.index === void 0 ? "" : `/citations/${String(finding2.index)}`;
|
|
3746
|
+
const line = errorSite(finding2).line;
|
|
3747
|
+
const at2 = pool.findIndex(
|
|
3748
|
+
(e) => e.keyword === finding2.rule && e.instancePath === instancePath && e.message === finding2.message && (e.line ?? null) === (line ?? null)
|
|
3749
|
+
);
|
|
3750
|
+
if (at2 === -1) {
|
|
3751
|
+
out.baselined.push(finding2);
|
|
3752
|
+
} else {
|
|
3753
|
+
pool.splice(at2, 1);
|
|
3754
|
+
out.reported.push(finding2);
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
return out;
|
|
3758
|
+
}
|
|
3759
|
+
function resultFor(run3, index) {
|
|
3760
|
+
const page = run3.pages[index];
|
|
3761
|
+
if (page === void 0) return void 0;
|
|
3762
|
+
const aligned = run3.results[index];
|
|
3763
|
+
if (aligned !== void 0 && aligned.file === page.file) return aligned;
|
|
3764
|
+
return run3.results.find((r) => r.file === page.file);
|
|
3765
|
+
}
|
|
3766
|
+
function belongsTo(finding2, result) {
|
|
3767
|
+
return finding2.index !== void 0 && finding2.index === result.origin.index;
|
|
3768
|
+
}
|
|
3769
|
+
function labelOf(result) {
|
|
3770
|
+
return result.citation.id ?? "";
|
|
3771
|
+
}
|
|
3772
|
+
function labelOfFinding(finding2) {
|
|
3773
|
+
if (finding2.id !== void 0) return finding2.id;
|
|
3774
|
+
if (finding2.index !== void 0) return `#${String(finding2.index)}`;
|
|
3775
|
+
return finding2.src ?? "page";
|
|
3776
|
+
}
|
|
3777
|
+
function citesEncrypted2(src) {
|
|
3778
|
+
try {
|
|
3779
|
+
return parseSrc(src).encrypted;
|
|
3780
|
+
} catch {
|
|
3781
|
+
return false;
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
function claimColumn(result) {
|
|
3785
|
+
const { claim } = result;
|
|
3786
|
+
const where = result.anchor === "marker" && result.markerLine !== void 0 ? `marker :${String(result.markerLine)}` : claim?.fileLines === void 0 ? "" : `:${claim.fileLines}`;
|
|
3787
|
+
if (claim === null) return where;
|
|
3788
|
+
let status;
|
|
3789
|
+
switch (claim.status) {
|
|
3790
|
+
case "moved":
|
|
3791
|
+
status = `moved -> :${claim.newFileLines ?? "?"}`;
|
|
3792
|
+
break;
|
|
3793
|
+
case "moved-ambiguous": {
|
|
3794
|
+
const at2 = (claim.candidateFileLines ?? []).map((lines) => `:${lines}`);
|
|
3795
|
+
status = `moved, ${plural2(at2.length, "candidate")} (${at2.join(", ")})`;
|
|
3796
|
+
break;
|
|
3797
|
+
}
|
|
3798
|
+
default:
|
|
3799
|
+
status = claim.status;
|
|
3800
|
+
}
|
|
3801
|
+
return where === "" ? status : `${where} ${status}`;
|
|
3802
|
+
}
|
|
3803
|
+
function sourceColumn(result, opts, dim) {
|
|
3804
|
+
const { source } = result;
|
|
3805
|
+
const src = shortSrc(source.src);
|
|
3806
|
+
const revealed = opts.reveal && source.resolvedPath !== void 0 && citesEncrypted2(source.src) ? ` ${dim(`(${source.resolvedPath})`)}` : "";
|
|
3807
|
+
return `${src}${revealed} ${messageFor(source)}`;
|
|
3808
|
+
}
|
|
3809
|
+
function diffLines(result, dim) {
|
|
3810
|
+
const out = [];
|
|
3811
|
+
for (const subject of result.source.commitsSince ?? []) out.push(dim(` ${subject}`));
|
|
3812
|
+
const diff = result.source.diff;
|
|
3813
|
+
if (diff === void 0 || diff === "") return out;
|
|
3814
|
+
const lines = diff.split(/\r?\n/);
|
|
3815
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
3816
|
+
const shown = lines.slice(0, DIFF_LINE_CAP);
|
|
3817
|
+
for (const line of shown) out.push(dim(` ${line}`));
|
|
3818
|
+
const more = lines.length - shown.length;
|
|
3819
|
+
if (more > 0) out.push(dim(` \u2026 (${plural2(more, "more line")})`));
|
|
3820
|
+
return out;
|
|
3821
|
+
}
|
|
3822
|
+
function claimLines(result, dim) {
|
|
3823
|
+
const text = result.claim?.text ?? [];
|
|
3824
|
+
const shown = text.slice(0, DIFF_LINE_CAP);
|
|
3825
|
+
const out = shown.map((line) => dim(` ${line}`));
|
|
3826
|
+
const more = text.length - shown.length;
|
|
3827
|
+
if (more > 0) out.push(dim(` \u2026 (${plural2(more, "more line")})`));
|
|
3828
|
+
return out;
|
|
3829
|
+
}
|
|
3830
|
+
function padTo(text, width, plain) {
|
|
3831
|
+
return width <= plain ? text : text + " ".repeat(width - plain);
|
|
3832
|
+
}
|
|
3833
|
+
function renderCheckPretty(run3, opts) {
|
|
3834
|
+
const c = palette(opts.color);
|
|
3835
|
+
const lines = [];
|
|
3836
|
+
const quiet = opts.quiet ?? false;
|
|
3837
|
+
const location = (finding2) => {
|
|
3838
|
+
const { file, line } = errorSite(finding2);
|
|
3839
|
+
if (line === void 0) return "";
|
|
3840
|
+
if (file === void 0) return c.dim(` (line ${String(line)})`);
|
|
3841
|
+
return c.dim(` (${file}:${String(line)})`);
|
|
3842
|
+
};
|
|
3843
|
+
run3.pages.forEach((page, index) => {
|
|
3844
|
+
const { reported, baselined: baselined2 } = splitBaselined(page, resultFor(run3, index));
|
|
3845
|
+
const errors = reported.filter((f) => f.severity === "error").length;
|
|
3846
|
+
const warned = reported.some((f) => f.severity === "warning");
|
|
3847
|
+
const forgiven = baselined2.length > 0 ? c.dim(` (${String(baselined2.length)} baselined)`) : "";
|
|
3848
|
+
if (reported.length === 0) {
|
|
3849
|
+
if (quiet) return;
|
|
3850
|
+
lines.push(`${c.green("\u2713")} ${page.file}${forgiven}`);
|
|
3851
|
+
} else {
|
|
3852
|
+
const mark = errors > 0 ? c.red("\u2717") : warned ? c.yellow("\u26A0") : c.dim("\u2139");
|
|
3853
|
+
lines.push(`${mark} ${page.file}${forgiven}`);
|
|
3854
|
+
}
|
|
3855
|
+
const isBaselined = new Set(baselined2);
|
|
3856
|
+
const placed = /* @__PURE__ */ new Set();
|
|
3857
|
+
const rows = [];
|
|
3858
|
+
for (const result of page.citations) {
|
|
3859
|
+
const own = page.findings.filter((f) => belongsTo(f, result) && !placed.has(f));
|
|
3860
|
+
for (const finding2 of own) placed.add(finding2);
|
|
3861
|
+
const live = own.filter((f) => !isBaselined.has(f));
|
|
3862
|
+
if (own.length === 0 && quiet) continue;
|
|
3863
|
+
const worst = live.find((f) => f.severity === "error") ?? live.find((f) => f.severity === "warning") ?? live[0];
|
|
3864
|
+
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");
|
|
3865
|
+
const endRules = /* @__PURE__ */ new Set(["claim-", "source-"]);
|
|
3866
|
+
const under = [];
|
|
3867
|
+
for (const finding2 of own) {
|
|
3868
|
+
const isEnd = [...endRules].some((prefix) => finding2.rule.startsWith(prefix));
|
|
3869
|
+
if (isEnd) continue;
|
|
3870
|
+
const text = isBaselined.has(finding2) ? c.dim(`${finding2.message} (baselined)`) : finding2.message;
|
|
3871
|
+
under.push(` ${severityMark(finding2.severity, c)} ${text}${location(finding2)}`);
|
|
3872
|
+
}
|
|
3873
|
+
if (opts.showDiff) {
|
|
3874
|
+
if (own.some((f) => f.rule === "source-changed")) under.push(...diffLines(result, c.dim));
|
|
3875
|
+
if (own.some((f) => f.rule === "claim-changed")) under.push(...claimLines(result, c.dim));
|
|
3876
|
+
}
|
|
3877
|
+
const forgivenEnd = own.length > 0 && live.length === 0 ? c.dim(" (baselined)") : "";
|
|
3878
|
+
rows.push({
|
|
3879
|
+
mark,
|
|
3880
|
+
label: labelOf(result),
|
|
3881
|
+
claim: claimColumn(result),
|
|
3882
|
+
source: `${sourceColumn(result, opts, c.dim)}${forgivenEnd}`,
|
|
3883
|
+
where: result.origin.kind === "manifest" ? `${result.origin.file}${result.origin.line === void 0 ? "" : `:${String(result.origin.line)}`}` : "",
|
|
3884
|
+
under
|
|
3885
|
+
});
|
|
3886
|
+
}
|
|
3887
|
+
const width = (pick) => rows.reduce((n, row) => Math.max(n, pick(row).length), 0);
|
|
3888
|
+
const labelWidth = width((row) => row.label);
|
|
3889
|
+
const claimWidth = width((row) => row.claim);
|
|
3890
|
+
const anyWhere = rows.some((row) => row.where !== "");
|
|
3891
|
+
const sourceWidth = anyWhere ? width((row) => row.source) : 0;
|
|
3892
|
+
for (const row of rows) {
|
|
3893
|
+
const label = padTo(c.cyan(row.label), labelWidth, row.label.length);
|
|
3894
|
+
const source = padTo(row.source, sourceWidth, row.source.length);
|
|
3895
|
+
const text = ` ${row.mark} ${label} ${padTo(row.claim, claimWidth, row.claim.length)} ${source}${row.where === "" ? "" : ` ${c.dim(row.where)}`}`;
|
|
3896
|
+
lines.push(text.replace(/\s+$/, ""));
|
|
3897
|
+
lines.push(...row.under);
|
|
3898
|
+
}
|
|
3899
|
+
for (const finding2 of page.findings) {
|
|
3900
|
+
if (placed.has(finding2)) continue;
|
|
3901
|
+
const label = c.cyan(labelOfFinding(finding2));
|
|
3902
|
+
lines.push(
|
|
3903
|
+
isBaselined.has(finding2) ? ` ${c.dim("\xB7")} ${label} ${c.dim(`${finding2.message} (baselined)`)}${location(finding2)}` : ` ${severityMark(finding2.severity, c)} ${label} ${finding2.message}${location(finding2)}`
|
|
3904
|
+
);
|
|
3905
|
+
}
|
|
3906
|
+
});
|
|
3907
|
+
const { summary } = run3;
|
|
3908
|
+
const warnings = summary.warnings ?? 0;
|
|
3909
|
+
const notices = summary.notices ?? 0;
|
|
3910
|
+
const baselined = summary.baseline?.suppressed ?? 0;
|
|
3911
|
+
const findings = summary.errors + warnings + notices + baselined;
|
|
3912
|
+
const parts = [plural2(findings, "finding")];
|
|
3913
|
+
if (warnings > 0) parts.push(`(${plural2(warnings, "warning")})`);
|
|
3914
|
+
if (notices > 0) parts.push(`(${plural2(notices, "notice")})`);
|
|
3915
|
+
if (baselined > 0) parts.push(`(${String(baselined)} baselined)`);
|
|
3916
|
+
const summaryText = `${plural2(summary.files, "file")} checked, ${String(summary.passed)} passed, ${String(summary.failed)} failed, ${parts.join(" ")}`;
|
|
3917
|
+
if (lines.length > 0) lines.push("");
|
|
3918
|
+
lines.push(summary.failed > 0 ? c.red(summaryText) : c.green(summaryText));
|
|
3919
|
+
if (summary.baseline?.written) {
|
|
3920
|
+
lines.push(c.dim(`Baseline written to ${summary.baseline.path}`));
|
|
3921
|
+
}
|
|
3922
|
+
return lines.join("\n");
|
|
3923
|
+
}
|
|
3924
|
+
function rewriteLine(rewrite) {
|
|
3925
|
+
const word = rewrite.from.includes("-") ? "lines" : "line";
|
|
3926
|
+
const status = rewrite.status === "never-true" ? "never true" : rewrite.status;
|
|
3927
|
+
if (rewrite.reason === "moved") {
|
|
3928
|
+
return rewrite.end === "claim" ? `claim ${word} ${rewrite.from} -> ${rewrite.to} (moved)` : `source ${shortSrc(rewrite.from)} -> ${shortSrc(rewrite.to)} (moved)`;
|
|
3929
|
+
}
|
|
3930
|
+
if (rewrite.end === "claim") {
|
|
3931
|
+
return `claim at line ${String(rewrite.at ?? 0)} re-pinned (${status}; now "${rewrite.text ?? ""}")`;
|
|
3932
|
+
}
|
|
3933
|
+
const at2 = rewrite.commitSha === void 0 ? "" : ` at ${shortCommit(rewrite.commitSha)}`;
|
|
3934
|
+
return `source ${shortSrc(rewrite.src ?? "")} re-pinned${at2} (${status}; ${shortPin(rewrite.from)} -> ${shortPin(rewrite.to)})`;
|
|
3935
|
+
}
|
|
3936
|
+
function renderUpdatePretty(run3, opts) {
|
|
3937
|
+
const c = palette(opts.color);
|
|
3938
|
+
const lines = [];
|
|
3939
|
+
let files = 0;
|
|
3940
|
+
for (const page of run3.pages) {
|
|
3941
|
+
if (page.rewritten.length > 0) files += 1;
|
|
3942
|
+
if (opts.showDiff && page.diff !== "") {
|
|
3943
|
+
for (const line of page.diff.split(/\r?\n/)) {
|
|
3944
|
+
if (line !== "") lines.push(c.dim(line));
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3947
|
+
for (const rewrite of page.rewritten) {
|
|
3948
|
+
const label = rewrite.id ?? `#${String(rewrite.index)}`;
|
|
3949
|
+
lines.push(`${page.file}: ${c.cyan(label)} ${rewriteLine(rewrite)}`);
|
|
3950
|
+
}
|
|
3951
|
+
for (const finding2 of page.skipped) {
|
|
3952
|
+
const mark = severityMark(finding2.severity, c);
|
|
3953
|
+
lines.push(
|
|
3954
|
+
`${page.file}: ${c.cyan(labelOfFinding(finding2))} ${mark} skipped: ${finding2.message}`
|
|
3955
|
+
);
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
if (opts.showDiff) {
|
|
3959
|
+
for (const manifest of run3.manifests ?? []) {
|
|
3960
|
+
for (const line of manifest.diff.split(/\r?\n/)) {
|
|
3961
|
+
if (line !== "") lines.push(c.dim(line));
|
|
3962
|
+
}
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3965
|
+
const summary = `${plural2(run3.rewritten, "citation")} rewritten in ${plural2(files, "file")}, ${String(run3.skipped)} skipped`;
|
|
3966
|
+
lines.push(run3.exitCode === 0 ? c.green(summary) : c.red(summary));
|
|
3967
|
+
return lines.join("\n");
|
|
3968
|
+
}
|
|
3969
|
+
|
|
3970
|
+
// src/cite/reporters/json.ts
|
|
3971
|
+
function publicCitation(result) {
|
|
3972
|
+
const origin = {
|
|
3973
|
+
kind: result.origin.kind,
|
|
3974
|
+
file: result.origin.file
|
|
3975
|
+
};
|
|
3976
|
+
if (result.origin.line !== void 0) origin.line = result.origin.line;
|
|
3977
|
+
const source = { src: result.source.src, status: result.source.status };
|
|
3978
|
+
if (result.source.newLines !== void 0) source.newLines = result.source.newLines;
|
|
3979
|
+
if (result.source.newSrc !== void 0) source.newSrc = result.source.newSrc;
|
|
3980
|
+
if (result.source.candidates !== void 0) source.candidates = result.source.candidates;
|
|
3981
|
+
if (result.source.commitSha !== void 0) source.commitSha = result.source.commitSha;
|
|
3982
|
+
if (result.source.historyAvailable !== void 0) {
|
|
3983
|
+
source.historyAvailable = result.source.historyAvailable;
|
|
3984
|
+
}
|
|
3985
|
+
if (result.source.truncatedSearch !== void 0) {
|
|
3986
|
+
source.truncatedSearch = result.source.truncatedSearch;
|
|
3987
|
+
}
|
|
3988
|
+
let claim = null;
|
|
3989
|
+
if (result.claim !== null) {
|
|
3990
|
+
claim = { status: result.claim.status };
|
|
3991
|
+
if (result.claim.lines !== void 0) claim.lines = result.claim.lines;
|
|
3992
|
+
if (result.claim.fileLines !== void 0) claim.fileLines = result.claim.fileLines;
|
|
3993
|
+
if (result.claim.newLines !== void 0) claim.newLines = result.claim.newLines;
|
|
3994
|
+
if (result.claim.candidates !== void 0) claim.candidates = result.claim.candidates;
|
|
3995
|
+
}
|
|
3996
|
+
return {
|
|
3997
|
+
...result.citation.id === void 0 ? {} : { id: result.citation.id },
|
|
3998
|
+
origin,
|
|
3999
|
+
anchor: result.anchor,
|
|
4000
|
+
claim,
|
|
4001
|
+
source
|
|
4002
|
+
};
|
|
4003
|
+
}
|
|
4004
|
+
function publicPage(page) {
|
|
4005
|
+
return { ...page, citations: page.citations.map(publicCitation) };
|
|
4006
|
+
}
|
|
4007
|
+
function renderCheckJson(run3) {
|
|
4008
|
+
return JSON.stringify({ summary: run3.summary, pages: run3.pages.map(publicPage) }, null, 2);
|
|
4009
|
+
}
|
|
4010
|
+
function renderUpdateJson(run3) {
|
|
4011
|
+
return JSON.stringify(run3, null, 2);
|
|
4012
|
+
}
|
|
4013
|
+
|
|
4014
|
+
// src/cite/reporters/github.ts
|
|
4015
|
+
var NAMES_ITS_SUBJECT = /* @__PURE__ */ new Set([
|
|
4016
|
+
"claim-moved",
|
|
4017
|
+
"claim-moved-ambiguous",
|
|
4018
|
+
"claim-changed",
|
|
4019
|
+
"marker-orphan",
|
|
4020
|
+
"marker-invalid",
|
|
4021
|
+
"marker-repeated",
|
|
4022
|
+
"anchor-invalid"
|
|
4023
|
+
]);
|
|
4024
|
+
function annotationMessage(finding2) {
|
|
4025
|
+
const subject = finding2.id ?? finding2.src;
|
|
4026
|
+
if (subject === void 0) return finding2.message;
|
|
4027
|
+
if (NAMES_ITS_SUBJECT.has(finding2.rule)) return finding2.message;
|
|
4028
|
+
if (finding2.id !== void 0 && finding2.message.startsWith(finding2.id)) return finding2.message;
|
|
4029
|
+
const where = finding2.id !== void 0 && finding2.src !== void 0 ? ` (${finding2.src})` : "";
|
|
4030
|
+
return `${subject}${where}: ${finding2.message}`;
|
|
4031
|
+
}
|
|
4032
|
+
function renderCheckGithub(run3) {
|
|
4033
|
+
const lines = [];
|
|
4034
|
+
run3.pages.forEach((page, index) => {
|
|
4035
|
+
const { reported } = splitBaselined(page, resultFor(run3, index));
|
|
4036
|
+
for (const finding2 of reported) {
|
|
4037
|
+
const site = errorSite(finding2);
|
|
4038
|
+
const params = [`file=${escapeWorkflowCommandProperty(site.file ?? page.file)}`];
|
|
4039
|
+
if (site.line !== void 0) params.push(`line=${String(site.line)}`);
|
|
4040
|
+
params.push(`title=${escapeWorkflowCommandProperty(finding2.ruleId)}`);
|
|
4041
|
+
lines.push(
|
|
4042
|
+
`::${finding2.severity} ${params.join(",")}::${escapeWorkflowCommandMessage(annotationMessage(finding2))}`
|
|
4043
|
+
);
|
|
4044
|
+
}
|
|
4045
|
+
});
|
|
4046
|
+
return lines.join("\n");
|
|
4047
|
+
}
|
|
4048
|
+
|
|
4049
|
+
// src/key/errors.ts
|
|
4050
|
+
var KeyError = class extends ToolError {
|
|
4051
|
+
constructor(message) {
|
|
4052
|
+
super(message);
|
|
4053
|
+
this.name = "KeyError";
|
|
4054
|
+
}
|
|
4055
|
+
};
|
|
4056
|
+
|
|
4057
|
+
// src/key/commands/rotate.ts
|
|
4058
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
4059
|
+
import { readFile as readFile5 } from "node:fs/promises";
|
|
4060
|
+
import { isAbsolute as isAbsolute3, join as join2, relative as relative3, resolve as resolve6, sep as sep3 } from "node:path";
|
|
4061
|
+
import { parse as parseYaml } from "yaml";
|
|
4062
|
+
|
|
4063
|
+
// src/key/core/config.ts
|
|
4064
|
+
var toKeyError = (message) => new KeyError(message);
|
|
4065
|
+
var KEY_CONFIG = {
|
|
4066
|
+
section: "key",
|
|
4067
|
+
// The key never had a file of its own.
|
|
4068
|
+
legacyNames: [],
|
|
4069
|
+
toError: toKeyError
|
|
4070
|
+
};
|
|
4071
|
+
function readRotateConfig(configPath, cwd) {
|
|
4072
|
+
return configPath === void 0 ? findConfigFile(cwd, KEY_CONFIG) : readConfigFile(configPath, cwd, KEY_CONFIG);
|
|
4073
|
+
}
|
|
4074
|
+
function readSetConfig(configPath, cwd) {
|
|
4075
|
+
return configPath === void 0 ? findFamilyConfigFile(cwd, toKeyError) : readFamilyConfigFile(configPath, cwd, toKeyError);
|
|
4076
|
+
}
|
|
4077
|
+
var unfinishedRotation = (source) => `A rotation is unfinished in ${source}. Run \`manni key rotate\` with no --to to finish it.`;
|
|
4078
|
+
|
|
4079
|
+
// src/key/commands/rotate.ts
|
|
4080
|
+
var BAD_TO = "--to must be at least 32 hex or base64url characters.";
|
|
4081
|
+
var NO_STDIN = "key rotate reads and writes files, so it takes no stdin (`-`). Name the files instead.";
|
|
4082
|
+
var WITH_PATHS = "--collection selects a configured collection; it cannot be combined with paths.";
|
|
4083
|
+
var NEEDS_CONFIG = "--collection needs a config file to select from.";
|
|
4084
|
+
var NO_KEY = "No encryption key is available, so nothing can be re-encrypted. Run `manni key set` first.";
|
|
4085
|
+
var ENV_NEEDS_TO = "The key comes from MANNI_ENCRYPTION_KEY; pass --to <value>, re-encrypt with it, then update the secret. Nothing is written to config.";
|
|
4086
|
+
var NARROWED_NEEDS_TO = "A run over part of the family needs --to, and never writes the key.";
|
|
4087
|
+
var NO_FILES = "No files to re-encrypt. Pass paths/globs, or declare a collection under `collections:` in manni.config.yaml.";
|
|
4088
|
+
var ANY_CIPHERTEXT = /~[A-Za-z0-9_-]{82,}/;
|
|
4089
|
+
function citeSection(file) {
|
|
4090
|
+
if (file === null) return {};
|
|
4091
|
+
const doc = parseYaml(file.text);
|
|
4092
|
+
if (typeof doc !== "object" || doc === null || !("cite" in doc)) return {};
|
|
4093
|
+
return parseCiteConfig(doc.cite, file.source);
|
|
4094
|
+
}
|
|
4095
|
+
function rootFor(flag, cite, file, cwd) {
|
|
4096
|
+
if (flag !== void 0) return { root: resolve6(cwd, flag), fellBack: false };
|
|
4097
|
+
if (file !== null && cite.root !== void 0) {
|
|
4098
|
+
return { root: resolve6(file.dir, cite.root), fellBack: false };
|
|
4099
|
+
}
|
|
4100
|
+
const gitRoot = findGitRoot(cwd);
|
|
4101
|
+
return gitRoot === null ? { root: cwd, fellBack: true } : { root: gitRoot, fellBack: false };
|
|
4102
|
+
}
|
|
4103
|
+
function isEnoent3(error) {
|
|
4104
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
4105
|
+
}
|
|
4106
|
+
async function indexFor2(root, client) {
|
|
4107
|
+
try {
|
|
4108
|
+
return await buildSourceIndex(root, { gitClient: client });
|
|
4109
|
+
} catch (error) {
|
|
4110
|
+
if (isEnoent3(error)) throw new KeyError(`Root directory not found: ${root}.`);
|
|
4111
|
+
throw error;
|
|
4112
|
+
}
|
|
4113
|
+
}
|
|
4114
|
+
function encryptedCitations(file, content, format) {
|
|
4115
|
+
const read = readPage(file, content, format === void 0 ? void 0 : { format });
|
|
4116
|
+
const encrypted = read.citations.filter(({ citation }) => sourceRange(citation.source).encrypted);
|
|
4117
|
+
return {
|
|
4118
|
+
any: encrypted.length > 0,
|
|
4119
|
+
withCommit: encrypted.some(({ citation }) => citation.source["commit-sha"] !== void 0)
|
|
4120
|
+
};
|
|
4121
|
+
}
|
|
4122
|
+
async function rotateManifestCitations(sidecars2, manifests, page, opts) {
|
|
4123
|
+
const none = { rewritten: [], skipped: [], wantsHistory: false };
|
|
4124
|
+
if (sidecars2 === null) return none;
|
|
4125
|
+
const sidecar = sidecars2.forPage(page.file, page.content, page.format);
|
|
4126
|
+
const owner = sidecar.owner;
|
|
4127
|
+
if (owner === void 0 || sidecar.citations === void 0) return none;
|
|
4128
|
+
if (!sidecar.citations.some((input) => ANY_CIPHERTEXT.test(JSON.stringify(input.entry)))) {
|
|
4129
|
+
return none;
|
|
4130
|
+
}
|
|
4131
|
+
const result = await reencryptCitationEntries(
|
|
4132
|
+
{ ...page, citations: sidecar.citations },
|
|
4133
|
+
{
|
|
4134
|
+
root: opts.root,
|
|
4135
|
+
fromKey: opts.fromKey,
|
|
4136
|
+
toKey: opts.toKey,
|
|
4137
|
+
gitClient: opts.gitClient,
|
|
4138
|
+
sourceIndex: await opts.sourceIndex()
|
|
4139
|
+
}
|
|
4140
|
+
);
|
|
4141
|
+
if (result.changed && sidecar.entry !== void 0) {
|
|
4142
|
+
await manifests.write(owner, sidecar.entry, result.entries, 0);
|
|
4143
|
+
}
|
|
4144
|
+
return {
|
|
4145
|
+
rewritten: result.rewritten.map((r) => ({ kind: "citation", ...r })),
|
|
4146
|
+
skipped: result.skipped.map((s) => ({ kind: "citation", ...s })),
|
|
4147
|
+
// A pin that no longer holds is re-keyed from the lines at its commit.
|
|
4148
|
+
wantsHistory: sidecar.citations.some(
|
|
4149
|
+
(input) => /"commit-sha"\s*:/.test(JSON.stringify(input.entry))
|
|
4150
|
+
)
|
|
4151
|
+
};
|
|
4152
|
+
}
|
|
4153
|
+
function reportedPath2(abs, base) {
|
|
4154
|
+
const rel = relative3(base, abs);
|
|
4155
|
+
return rel === "" ? "." : rel.split(sep3).join("/");
|
|
4156
|
+
}
|
|
4157
|
+
function manifestCollections(collections, opts) {
|
|
4158
|
+
if (opts.fromCollections) return [...collections];
|
|
4159
|
+
const rels = opts.files.map(
|
|
4160
|
+
(rel) => reportedPath2(resolve6(opts.base, rel), opts.configDir)
|
|
4161
|
+
);
|
|
4162
|
+
return collections.filter((c) => rels.some((rel) => isMember(c, rel)));
|
|
4163
|
+
}
|
|
4164
|
+
function valuesOf(index, entry) {
|
|
4165
|
+
const empty = /* @__PURE__ */ new Map();
|
|
4166
|
+
if (entry.join === PATH_JOIN) {
|
|
4167
|
+
return entry.abs === void 0 ? empty : index.byPath.get(entry.abs) ?? empty;
|
|
4168
|
+
}
|
|
4169
|
+
return index.byField.get(entry.join)?.get(entry.spelled) ?? empty;
|
|
4170
|
+
}
|
|
4171
|
+
async function planManifests(opts) {
|
|
4172
|
+
const local = opts.collections.map((c) => ({
|
|
4173
|
+
...c,
|
|
4174
|
+
externalMetadata: c.externalMetadata.filter(
|
|
4175
|
+
(m) => classifyRef(m.file).kind !== "url"
|
|
4176
|
+
)
|
|
4177
|
+
})).filter((c) => c.externalMetadata.length > 0);
|
|
4178
|
+
if (local.length === 0) return [];
|
|
4179
|
+
const index = await loadExternalMetadata(local, {
|
|
4180
|
+
configDir: opts.configDir,
|
|
4181
|
+
base: opts.base
|
|
4182
|
+
});
|
|
4183
|
+
if (index === null) return [];
|
|
4184
|
+
const plans = /* @__PURE__ */ new Map();
|
|
4185
|
+
const located = /* @__PURE__ */ new Map();
|
|
4186
|
+
for (const c of local) {
|
|
4187
|
+
for (const m of c.externalMetadata) {
|
|
4188
|
+
const join4 = externalMetadataJoin(m);
|
|
4189
|
+
const abs = isAbsolute3(m.file) ? m.file : resolve6(opts.configDir, m.file);
|
|
4190
|
+
const file = reportedPath2(abs, opts.base);
|
|
4191
|
+
located.set(`${c.name}\0${file}\0${join4}`, abs);
|
|
4192
|
+
if (plans.has(abs)) continue;
|
|
4193
|
+
plans.set(abs, {
|
|
4194
|
+
manifest: { file, collection: c.name, rewritten: [], skipped: [], written: false },
|
|
4195
|
+
path: abs,
|
|
4196
|
+
text: await readFile5(abs, "utf8"),
|
|
4197
|
+
changed: false
|
|
4198
|
+
});
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
for (const entry of index.entries) {
|
|
4202
|
+
const abs = located.get(`${entry.collection}\0${entry.file}\0${entry.join}`);
|
|
4203
|
+
const plan = abs === void 0 ? void 0 : plans.get(abs);
|
|
4204
|
+
if (plan === void 0) continue;
|
|
4205
|
+
const data = {};
|
|
4206
|
+
for (const [key, supplied] of valuesOf(index, entry)) {
|
|
4207
|
+
if (supplied.file === entry.file && supplied.collection === entry.collection) {
|
|
4208
|
+
data[key] = supplied.value;
|
|
4209
|
+
}
|
|
4210
|
+
}
|
|
4211
|
+
const done = reencryptData(data, { fromKey: opts.fromKey, toKey: opts.toKey });
|
|
4212
|
+
for (const r of done.rewritten) {
|
|
4213
|
+
plan.manifest.rewritten.push({ entry: entry.spelled, ...r });
|
|
4214
|
+
}
|
|
4215
|
+
for (const s of done.skipped) {
|
|
4216
|
+
plan.manifest.skipped.push({ entry: entry.spelled, ...s });
|
|
4217
|
+
}
|
|
4218
|
+
for (const key of done.keys) {
|
|
4219
|
+
plan.text = spliceManifestValue(plan.text, {
|
|
4220
|
+
entry: entry.spelled,
|
|
4221
|
+
key,
|
|
4222
|
+
value: done.data[key],
|
|
4223
|
+
join: entry.join,
|
|
4224
|
+
file: plan.manifest.file
|
|
4225
|
+
}).text;
|
|
4226
|
+
plan.changed = true;
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
4229
|
+
return [...plans.values()].filter(
|
|
4230
|
+
(p) => p.manifest.rewritten.length > 0 || p.manifest.skipped.length > 0
|
|
4231
|
+
);
|
|
4232
|
+
}
|
|
4233
|
+
async function runKeyRotate(opts) {
|
|
4234
|
+
const cwd = resolve6(opts.cwd ?? process.cwd());
|
|
4235
|
+
const env = opts.env ?? process.env;
|
|
4236
|
+
if (opts.to !== void 0 && !isValidEncryptionKey(opts.to)) throw new KeyError(BAD_TO);
|
|
4237
|
+
if (opts.inputs.includes(STDIN_TOKEN)) throw new KeyError(NO_STDIN);
|
|
4238
|
+
const wanted = opts.collection ?? [];
|
|
4239
|
+
if (wanted.length > 0 && opts.inputs.length > 0) throw new KeyError(WITH_PATHS);
|
|
4240
|
+
const file = await readRotateConfig(opts.configPath, cwd);
|
|
4241
|
+
if (wanted.length > 0 && file === null) throw new KeyError(NEEDS_CONFIG);
|
|
4242
|
+
const { key: current, source: keySource } = resolveEncryptionKey({
|
|
4243
|
+
env,
|
|
4244
|
+
file,
|
|
4245
|
+
toError: toKeyError
|
|
4246
|
+
});
|
|
4247
|
+
if (current === void 0) throw new KeyError(NO_KEY);
|
|
4248
|
+
const narrowed = opts.inputs.length > 0 || wanted.length > 0;
|
|
4249
|
+
const keyFile = keySource === "config" ? file : null;
|
|
4250
|
+
const previous = keyFile?.encryptionKeyPrevious;
|
|
4251
|
+
if (keyFile !== null && previous !== void 0 && (narrowed || opts.to !== void 0 && opts.to !== current)) {
|
|
4252
|
+
throw new KeyError(unfinishedRotation(keyFile.source));
|
|
4253
|
+
}
|
|
4254
|
+
if (keySource === "env" && opts.to === void 0) throw new KeyError(ENV_NEEDS_TO);
|
|
4255
|
+
if (narrowed && opts.to === void 0) throw new KeyError(NARROWED_NEEDS_TO);
|
|
4256
|
+
const fromKey = previous ?? current;
|
|
4257
|
+
const toKey = previous === void 0 ? opts.to ?? generateEncryptionKey() : current;
|
|
4258
|
+
const collections = file === null ? [] : selectCollections(file.collections, wanted, file.source, toKeyError);
|
|
4259
|
+
const fromCollections = opts.inputs.length === 0;
|
|
4260
|
+
const inputs = fromCollections ? collections.flatMap((c) => c.paths) : opts.inputs;
|
|
4261
|
+
if (inputs.length === 0) throw new KeyError(NO_FILES);
|
|
4262
|
+
const base = fromCollections && file !== null ? file.dir : cwd;
|
|
4263
|
+
const forced = opts.as === void 0 ? void 0 : extractorByName(opts.as);
|
|
4264
|
+
if (opts.as !== void 0 && forced?.implemented !== true) {
|
|
4265
|
+
throw new KeyError(
|
|
4266
|
+
`Unknown format "${opts.as}". Supported extensions: ${supportedExtensions().join(", ")}.`
|
|
4267
|
+
);
|
|
4268
|
+
}
|
|
4269
|
+
const format = forced?.name;
|
|
4270
|
+
const exts = opts.exts ?? forced?.extensions;
|
|
4271
|
+
const exclude = [
|
|
4272
|
+
.../* @__PURE__ */ new Set([
|
|
4273
|
+
...opts.exclude ?? [],
|
|
4274
|
+
...fromCollections ? collections.flatMap((c) => c.exclude) : []
|
|
4275
|
+
])
|
|
4276
|
+
];
|
|
4277
|
+
const { files, gitignoreSkipped } = await resolveTargetSet({
|
|
4278
|
+
inputs,
|
|
4279
|
+
exts,
|
|
4280
|
+
exclude,
|
|
4281
|
+
cwd: base,
|
|
4282
|
+
allowEmpty: opts.allowEmpty,
|
|
4283
|
+
...gitignoreOptions({ flag: opts.respectGitignore, onNotice: opts.onNotice })
|
|
4284
|
+
});
|
|
4285
|
+
assertNonEmpty({
|
|
4286
|
+
files,
|
|
4287
|
+
inputs,
|
|
4288
|
+
usingStdin: false,
|
|
4289
|
+
allowEmpty: opts.allowEmpty,
|
|
4290
|
+
exclude,
|
|
4291
|
+
exts,
|
|
4292
|
+
gitignoreSkipped,
|
|
4293
|
+
action: "re-encrypted"
|
|
4294
|
+
});
|
|
4295
|
+
const cite = citeSection(file);
|
|
4296
|
+
const { root, fellBack } = rootFor(opts.root, cite, file, cwd);
|
|
4297
|
+
const client = opts.gitClient ?? gitClient(root);
|
|
4298
|
+
let index;
|
|
4299
|
+
const sourceIndex = () => {
|
|
4300
|
+
if (index === void 0) {
|
|
4301
|
+
if (fellBack) opts.onNotice?.(`No git root found; resolving src: paths from ${cwd}`);
|
|
4302
|
+
index = indexFor2(root, client);
|
|
4303
|
+
}
|
|
4304
|
+
return index;
|
|
4305
|
+
};
|
|
4306
|
+
const sidecars2 = file === null ? null : await loadCitationSidecars({
|
|
4307
|
+
collections: file.collections,
|
|
4308
|
+
configDir: file.dir,
|
|
4309
|
+
base,
|
|
4310
|
+
configSource: file.source,
|
|
4311
|
+
key: current,
|
|
4312
|
+
toError: toKeyError
|
|
4313
|
+
});
|
|
4314
|
+
const manifests = new ManifestSet();
|
|
4315
|
+
const planned = [];
|
|
4316
|
+
let wantsHistory = false;
|
|
4317
|
+
for (const rel of files) {
|
|
4318
|
+
const path = resolve6(base, rel);
|
|
4319
|
+
const before = await readFile5(path, "utf8");
|
|
4320
|
+
const page = { file: rel, content: before, ...format === void 0 ? {} : { format } };
|
|
4321
|
+
const sidecarWork = await rotateManifestCitations(sidecars2, manifests, page, {
|
|
4322
|
+
root,
|
|
4323
|
+
fromKey,
|
|
4324
|
+
toKey,
|
|
4325
|
+
gitClient: client,
|
|
4326
|
+
sourceIndex
|
|
4327
|
+
});
|
|
4328
|
+
if (sidecarWork.wantsHistory) wantsHistory = true;
|
|
4329
|
+
if (!ANY_CIPHERTEXT.test(before)) {
|
|
4330
|
+
if (sidecarWork.rewritten.length === 0 && sidecarWork.skipped.length === 0) continue;
|
|
4331
|
+
planned.push({
|
|
4332
|
+
page: { file: rel, rewritten: sidecarWork.rewritten, skipped: sidecarWork.skipped, written: false },
|
|
4333
|
+
path,
|
|
4334
|
+
content: before,
|
|
4335
|
+
changed: false
|
|
4336
|
+
});
|
|
4337
|
+
continue;
|
|
4338
|
+
}
|
|
4339
|
+
const meta = reencryptMetadata(page, { fromKey, toKey });
|
|
4340
|
+
const encrypted = encryptedCitations(rel, meta.content, format);
|
|
4341
|
+
if (encrypted.withCommit) wantsHistory = true;
|
|
4342
|
+
const cited = encrypted.any ? await reencryptCitations(
|
|
4343
|
+
{ ...page, content: meta.content },
|
|
4344
|
+
{ root, fromKey, toKey, gitClient: client, sourceIndex: await sourceIndex() }
|
|
4345
|
+
) : { content: meta.content, rewritten: [], skipped: [] };
|
|
4346
|
+
const rewritten = [
|
|
4347
|
+
...meta.rewritten.map((r) => ({ kind: "metadata", ...r })),
|
|
4348
|
+
...cited.rewritten.map((r) => ({ kind: "citation", ...r })),
|
|
4349
|
+
...sidecarWork.rewritten
|
|
4350
|
+
];
|
|
4351
|
+
const skipped2 = [
|
|
4352
|
+
...meta.skipped.map((s) => ({ kind: "metadata", ...s })),
|
|
4353
|
+
...cited.skipped.map((s) => ({ kind: "citation", ...s })),
|
|
4354
|
+
...sidecarWork.skipped
|
|
4355
|
+
];
|
|
4356
|
+
if (rewritten.length === 0 && skipped2.length === 0) continue;
|
|
4357
|
+
planned.push({
|
|
4358
|
+
page: { file: rel, rewritten, skipped: skipped2, written: false },
|
|
4359
|
+
path,
|
|
4360
|
+
content: cited.content,
|
|
4361
|
+
changed: cited.content !== before
|
|
4362
|
+
});
|
|
4363
|
+
}
|
|
4364
|
+
if (wantsHistory && !await client.available()) opts.onNotice?.(GIT_UNAVAILABLE_HISTORY);
|
|
4365
|
+
const valueManifests = file === null ? [] : await planManifests({
|
|
4366
|
+
collections: manifestCollections(collections, {
|
|
4367
|
+
fromCollections,
|
|
4368
|
+
files,
|
|
4369
|
+
base,
|
|
4370
|
+
configDir: file.dir
|
|
4371
|
+
}),
|
|
4372
|
+
configDir: file.dir,
|
|
4373
|
+
base,
|
|
4374
|
+
fromKey,
|
|
4375
|
+
toKey
|
|
4376
|
+
});
|
|
4377
|
+
const reencrypted = planned.reduce((n, p) => n + p.page.rewritten.length, 0) + valueManifests.reduce((n, m) => n + m.manifest.rewritten.length, 0);
|
|
4378
|
+
const skipped = planned.reduce((n, p) => n + p.page.skipped.length, 0) + valueManifests.reduce((n, m) => n + m.manifest.skipped.length, 0);
|
|
4379
|
+
const writePage = opts.writePage ?? ((path, content) => writeFileAtomic(path, content));
|
|
4380
|
+
let outcome;
|
|
4381
|
+
if (skipped > 0) {
|
|
4382
|
+
outcome = "skipped";
|
|
4383
|
+
} else if (opts.dryRun === true) {
|
|
4384
|
+
outcome = "dry-run";
|
|
4385
|
+
} else {
|
|
4386
|
+
const writesKey = !narrowed && keyFile !== null;
|
|
4387
|
+
if (!narrowed && keyFile !== null && previous === void 0) {
|
|
4388
|
+
await writeEncryptionKey({ file: keyFile, key: toKey, previous: fromKey, cwd, toError: toKeyError });
|
|
4389
|
+
}
|
|
4390
|
+
for (const p of planned) {
|
|
4391
|
+
if (!p.changed) continue;
|
|
4392
|
+
await writePage(p.path, p.content);
|
|
4393
|
+
p.page.written = true;
|
|
4394
|
+
}
|
|
4395
|
+
for (const changed of manifests.changed()) await writePage(changed.path, changed.text);
|
|
4396
|
+
for (const m of valueManifests) {
|
|
4397
|
+
if (!m.changed) continue;
|
|
4398
|
+
await writePage(m.path, m.text);
|
|
4399
|
+
m.manifest.written = true;
|
|
4400
|
+
}
|
|
4401
|
+
if (!narrowed && keyFile !== null) {
|
|
4402
|
+
await writeEncryptionKey({ file: keyFile, key: toKey, previous: null, cwd, toError: toKeyError });
|
|
4403
|
+
}
|
|
4404
|
+
if (narrowed) outcome = "narrowed";
|
|
4405
|
+
else if (!writesKey) outcome = "env";
|
|
4406
|
+
else outcome = previous === void 0 ? "written" : "finished";
|
|
4407
|
+
}
|
|
4408
|
+
const wrote = outcome !== "skipped" && outcome !== "dry-run";
|
|
4409
|
+
const baseline = file !== null && cite.baseline !== void 0 ? resolve6(file.dir, cite.baseline) : join2(file?.dir ?? cwd, DEFAULT_CITE_BASELINE_PATH);
|
|
4410
|
+
return {
|
|
4411
|
+
pages: planned.map((p) => p.page),
|
|
4412
|
+
manifests: valueManifests.map((m) => m.manifest),
|
|
4413
|
+
reencrypted,
|
|
4414
|
+
skipped,
|
|
4415
|
+
keyWritten: outcome === "written" || outcome === "finished",
|
|
4416
|
+
outcome,
|
|
4417
|
+
...keyFile === null ? {} : { configSource: keyFile.source },
|
|
4418
|
+
baselineStale: wrote && existsSync2(baseline),
|
|
4419
|
+
exitCode: skipped > 0 ? 1 : 0
|
|
4420
|
+
};
|
|
4421
|
+
}
|
|
4422
|
+
|
|
4423
|
+
// src/key/commands/set.ts
|
|
4424
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
4425
|
+
import { basename, dirname, join as join3, relative as relative4, resolve as resolve7 } from "node:path";
|
|
4426
|
+
var BAD_VALUE = "The key must be at least 32 hex or base64url characters. Run `manni key set` with no value to generate one.";
|
|
4427
|
+
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.`;
|
|
4428
|
+
var alreadyConfigured = (source) => `An encryption key is already configured in ${source}. Run \`manni key rotate\` to replace it and re-encrypt every value.`;
|
|
4429
|
+
function refuseBesideLegacy(path, cwd) {
|
|
4430
|
+
const dir = dirname(path);
|
|
4431
|
+
const name = basename(path);
|
|
4432
|
+
for (const legacy of LEGACY_CONFIG_NAMES) {
|
|
4433
|
+
const found = join3(dir, legacy);
|
|
4434
|
+
if (!existsSync3(found)) continue;
|
|
4435
|
+
const source = relative4(cwd, found).replace(/\\/g, "/");
|
|
4436
|
+
throw new KeyError(
|
|
4437
|
+
`${source} is a single-tool config; a ${name} beside it would hide it. Move its keys under meta: in ${name} first.`
|
|
4438
|
+
);
|
|
4439
|
+
}
|
|
4440
|
+
}
|
|
4441
|
+
async function runKeySet(opts) {
|
|
4442
|
+
const cwd = resolve7(opts.cwd ?? process.cwd());
|
|
4443
|
+
const env = opts.env ?? process.env;
|
|
4444
|
+
if (opts.value !== void 0 && !isValidEncryptionKey(opts.value)) {
|
|
4445
|
+
throw new KeyError(BAD_VALUE);
|
|
4446
|
+
}
|
|
4447
|
+
const fromEnv = env[ENCRYPTION_KEY_ENV];
|
|
4448
|
+
if (fromEnv !== void 0 && fromEnv !== "") throw new KeyError(ENV_SET);
|
|
4449
|
+
const file = await readSetConfig(opts.configPath, cwd);
|
|
4450
|
+
if (file?.encryptionKeyPrevious !== void 0) {
|
|
4451
|
+
throw new KeyError(unfinishedRotation(file.source));
|
|
4452
|
+
}
|
|
4453
|
+
if (file?.encryptionKey !== void 0) {
|
|
4454
|
+
throw new KeyError(alreadyConfigured(file.source));
|
|
4455
|
+
}
|
|
4456
|
+
const generated = opts.value === void 0;
|
|
4457
|
+
const key = opts.value ?? generateEncryptionKey();
|
|
4458
|
+
const dryRun = opts.dryRun === true;
|
|
4459
|
+
const target = await writeEncryptionKey({ file, key, cwd, toError: toKeyError, dryRun: true });
|
|
4460
|
+
if (target.created) refuseBesideLegacy(target.path, cwd);
|
|
4461
|
+
if (isIgnoredByGit(target.path) === false) {
|
|
4462
|
+
opts.onNotice?.(committedKeyWarning(target.source));
|
|
4463
|
+
}
|
|
4464
|
+
if (!dryRun) await writeEncryptionKey({ file, key, cwd, toError: toKeyError });
|
|
4465
|
+
return { path: target.path, source: target.source, created: target.created, generated, dryRun };
|
|
4466
|
+
}
|
|
4467
|
+
|
|
4468
|
+
export {
|
|
4469
|
+
SEVERITIES,
|
|
4470
|
+
SEVERITY_LIST,
|
|
4471
|
+
isSeverity,
|
|
4472
|
+
meetsSeverity,
|
|
4473
|
+
reencryptMetadata,
|
|
4474
|
+
CiteError,
|
|
4475
|
+
spellSource,
|
|
4476
|
+
runAdd,
|
|
4477
|
+
runCheck,
|
|
4478
|
+
runUpdate,
|
|
4479
|
+
shortPin,
|
|
4480
|
+
shortCommit,
|
|
4481
|
+
shortSrc,
|
|
4482
|
+
renderCheckPretty,
|
|
4483
|
+
renderUpdatePretty,
|
|
4484
|
+
renderCheckGithub,
|
|
4485
|
+
renderCheckJson,
|
|
4486
|
+
renderUpdateJson,
|
|
4487
|
+
cite_exports,
|
|
4488
|
+
KeyError,
|
|
4489
|
+
runKeyRotate,
|
|
4490
|
+
runKeySet
|
|
4491
|
+
};
|
|
4492
|
+
//# sourceMappingURL=chunk-QLVNSAHR.js.map
|