papilot 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -30
- package/README.zh-CN.md +54 -30
- package/bin/doctor.mjs +53 -6
- package/bin/installation.d.mts +2 -0
- package/bin/installation.mjs +7 -1
- package/dist/assets/{_virtual_papilot-licenses-BWIsfrQh.js → _virtual_papilot-licenses-D7ro6ecN.js} +54 -3
- package/dist/assets/index-Cc-awdNn.css +1 -0
- package/dist/assets/index-D_VX51no.js +265 -0
- package/dist/assets/latex-BarqJFtQ.svg +1 -0
- package/dist/assets/{math-BsEl3u6P.js → math-V9uUaUzA.js} +1 -1
- package/dist/assets/pdf-view-Bnb2uIBE.js +598 -0
- package/dist/index.html +2 -2
- package/examples/papilot-paper/main.tex +1 -1
- package/examples/papilot-paper/sections/02-using.tex +3 -1
- package/examples/papilot-paper/sections/09-agents.tex +8 -5
- package/package.json +10 -6
- package/skills/ai-review.md +18 -0
- package/skills/latex-warnings.md +1 -1
- package/src/conversation.ts +6 -0
- package/src/server/acp.ts +354 -0
- package/src/server/agents.ts +220 -43
- package/src/server/ai-review-routes.ts +72 -0
- package/src/server/ai-review.ts +38 -0
- package/src/server/app.ts +53 -11
- package/src/server/build-problems.ts +329 -0
- package/src/server/changes.ts +25 -2
- package/src/server/codex.ts +2 -1
- package/src/server/cursor.ts +153 -0
- package/src/server/latex-installation.ts +43 -0
- package/src/server/manuscript.ts +165 -23
- package/src/server/paths.ts +1 -1
- package/src/server/references.ts +70 -0
- package/src/server/review.ts +3 -3
- package/src/server/skills.ts +5 -8
- package/src/server/store.ts +20 -2
- package/src/server/update-install.ts +53 -2
- package/src/server/update-worker.ts +6 -6
- package/src/server/updates.ts +43 -14
- package/src/server/workspace.ts +183 -29
- package/src/shared.ts +78 -7
- package/src/skill-icons.ts +18 -0
- package/src/skill-metadata.ts +45 -0
- package/vendor/latex-review/dist/web/assets/index-7VKAte5x.js +19 -0
- package/vendor/latex-review/dist/web/assets/index-DEz-LsGl.css +1 -0
- package/vendor/latex-review/dist/web/index.html +2 -2
- package/vendor/latex-review/src/core/bib.ts +692 -0
- package/vendor/latex-review/src/core/changes.ts +39 -0
- package/vendor/latex-review/src/core/review.ts +1 -0
- package/vendor/latex-review/src/core/verify.ts +784 -0
- package/vendor/latex-review/src/core/view.ts +204 -0
- package/vendor/latex-review/src/render/round.ts +11 -0
- package/vendor/latex-review/src/server.ts +65 -0
- package/vendor/latex-review/src/types.ts +125 -0
- package/vendor/latex-review/src/web/effect-preferences.ts +10 -0
- package/vendor/latex-review/src/web/references.tsx +499 -0
- package/vendor/latex-review/src/web/review.tsx +259 -131
- package/vendor/latex-review/src/web/style.css +285 -0
- package/vendor/latex-review/src/web/zh-CN.ts +75 -0
- package/dist/assets/index-CIJEYlP7.js +0 -252
- package/dist/assets/index-hOJEruFc.css +0 -1
- package/dist/assets/pdf-view-Cz9_vK5U.js +0 -598
- package/vendor/latex-review/dist/web/assets/index-Be9CtgcZ.css +0 -1
- package/vendor/latex-review/dist/web/assets/index-TvMWVSaw.js +0 -19
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { Builder, BuildError } from "./build.ts";
|
|
5
|
+
import { cleanPath, within } from "./git.ts";
|
|
6
|
+
import { run } from "./process.ts";
|
|
7
|
+
import { mupdf } from "../render/pdf.ts";
|
|
8
|
+
import type { App } from "../server.ts";
|
|
9
|
+
import type { BuildInfo } from "../types.ts";
|
|
10
|
+
// A task's agent looks at its typeset pages: the task worktree as it is now,
|
|
11
|
+
// or the accepted text, compiled like a review round (only the chapters the
|
|
12
|
+
// edits and the asked-for places need, when the paper allows partial builds,
|
|
13
|
+
// so a later publication of the same text reuses the build) and drawn as PNG
|
|
14
|
+
// files the agent opens with its own image viewer. Places are source lines
|
|
15
|
+
// or printed page numbers.
|
|
16
|
+
export interface ViewRequest {
|
|
17
|
+
locations?: string[];
|
|
18
|
+
pages?: string[];
|
|
19
|
+
accepted?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface ViewedPage {
|
|
22
|
+
/** The page's position in the PDF that was built, from 1. */
|
|
23
|
+
index: number;
|
|
24
|
+
/** The PDF's page label, or null when no numbering metadata is available. */
|
|
25
|
+
label: string | null;
|
|
26
|
+
/** What asked for this page. */
|
|
27
|
+
shows: string[];
|
|
28
|
+
/** The drawn page. */
|
|
29
|
+
file: string;
|
|
30
|
+
}
|
|
31
|
+
export interface ViewResult {
|
|
32
|
+
pages: ViewedPage[];
|
|
33
|
+
/** Requests no page answered, with the reason. */
|
|
34
|
+
missing: string[];
|
|
35
|
+
targets: string[];
|
|
36
|
+
error?: string;
|
|
37
|
+
details?: string[];
|
|
38
|
+
}
|
|
39
|
+
// Pages drawn per request, and how far from an asked-for line SyncTeX may
|
|
40
|
+
// look for one that printed something (blank lines and comments print nothing).
|
|
41
|
+
const PAGES = 4,
|
|
42
|
+
NEARBY = 10,
|
|
43
|
+
SCALE = 1.5,
|
|
44
|
+
// Drawn pages are kept this long, for an agent that opens them later in its turn.
|
|
45
|
+
KEEP = 6 * 3600 * 1000;
|
|
46
|
+
// A new folder for one view's pages, after removing the task's old ones.
|
|
47
|
+
function folder(app: App, name: string) {
|
|
48
|
+
const views = path.join(app.project.state, "views", name);
|
|
49
|
+
for (const entry of fs.existsSync(views) ? fs.readdirSync(views) : []) {
|
|
50
|
+
const old = path.join(views, entry);
|
|
51
|
+
if (Date.now() - fs.statSync(old).mtimeMs > KEEP)
|
|
52
|
+
fs.rmSync(old, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
const created = path.join(views, `${Date.now()}-${randomBytes(3).toString("hex")}`);
|
|
55
|
+
fs.mkdirSync(created, { recursive: true });
|
|
56
|
+
return created;
|
|
57
|
+
}
|
|
58
|
+
interface Location {
|
|
59
|
+
asked: string;
|
|
60
|
+
file: string;
|
|
61
|
+
line: number;
|
|
62
|
+
}
|
|
63
|
+
function location(app: App, worktree: string, value: string): Location {
|
|
64
|
+
const match = /^(.*?):(\d+)$/.exec(value.trim());
|
|
65
|
+
if (!match) throw new Error(`Give a location as file:line, such as main.tex:12: ${value}`);
|
|
66
|
+
let file = match[1];
|
|
67
|
+
if (path.isAbsolute(file)) file = path.relative(worktree, file);
|
|
68
|
+
file = cleanPath(file);
|
|
69
|
+
if (!within(file, [app.project.source_dir]))
|
|
70
|
+
throw new Error(`${file} is outside the paper's source folder ${app.project.source_dir}.`);
|
|
71
|
+
return { asked: value.trim(), file, line: Number(match[2]) };
|
|
72
|
+
}
|
|
73
|
+
// The pages SyncTeX gives for a source line, or for the nearest line that
|
|
74
|
+
// printed something.
|
|
75
|
+
function pagesOf(pdf: string, file: string, line: number) {
|
|
76
|
+
for (let distance = 0; distance <= NEARBY; distance++)
|
|
77
|
+
for (const candidate of distance ? [line - distance, line + distance] : [line]) {
|
|
78
|
+
if (candidate < 1) continue;
|
|
79
|
+
const output = run(
|
|
80
|
+
"synctex",
|
|
81
|
+
["view", "-i", `${candidate}:0:${file}`, "-o", pdf],
|
|
82
|
+
{ check: false, timeout: 20000 },
|
|
83
|
+
).stdout.toString();
|
|
84
|
+
const pages = [
|
|
85
|
+
...new Set(
|
|
86
|
+
[...output.matchAll(/^Page:(\d+)$/gm)].map((m) => Number(m[1])),
|
|
87
|
+
),
|
|
88
|
+
];
|
|
89
|
+
if (pages.length) return pages;
|
|
90
|
+
}
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
export async function view(
|
|
94
|
+
app: App,
|
|
95
|
+
name: string,
|
|
96
|
+
request: ViewRequest,
|
|
97
|
+
): Promise<ViewResult> {
|
|
98
|
+
const task = app.tasks.get(name),
|
|
99
|
+
project = app.project;
|
|
100
|
+
if (!fs.existsSync(task.worktree))
|
|
101
|
+
throw new Error(`The task worktree ${task.worktree} no longer exists.`);
|
|
102
|
+
const locations = (request.locations ?? []).map((l) =>
|
|
103
|
+
location(app, task.worktree, l),
|
|
104
|
+
),
|
|
105
|
+
labels = (request.pages ?? []).map((p) => p.trim()).filter(Boolean);
|
|
106
|
+
if (!locations.length && !labels.length)
|
|
107
|
+
throw new Error("Ask for at least one source location or page number.");
|
|
108
|
+
const after = app.tasks.snapshot(task),
|
|
109
|
+
before = task.accepted,
|
|
110
|
+
builder = new Builder(project, app.repo),
|
|
111
|
+
targets = builder.plan(before, after, [
|
|
112
|
+
...new Set([
|
|
113
|
+
...app.repo.changedFiles(before, after),
|
|
114
|
+
...locations.map((l) => l.file),
|
|
115
|
+
]),
|
|
116
|
+
]);
|
|
117
|
+
let build: BuildInfo;
|
|
118
|
+
try {
|
|
119
|
+
const seed = targets[0] !== "full" ? await builder.build(task.base) : null;
|
|
120
|
+
build = await builder.build(request.accepted ? before : after, targets, seed);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (!(error instanceof BuildError)) throw error;
|
|
123
|
+
return {
|
|
124
|
+
pages: [],
|
|
125
|
+
missing: [],
|
|
126
|
+
targets,
|
|
127
|
+
error: request.accepted
|
|
128
|
+
? "The accepted text could not be compiled."
|
|
129
|
+
: "The paper in the task worktree could not be compiled.",
|
|
130
|
+
details: error.details,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const pdf = build.pdf,
|
|
134
|
+
document = mupdf.Document.openDocument(fs.readFileSync(pdf), "application/pdf");
|
|
135
|
+
try {
|
|
136
|
+
// MuPDF synthesizes 1, 2, ... when /PageLabels is absent. Those are
|
|
137
|
+
// physical positions, not the printed numbers (front matter may be i, ii).
|
|
138
|
+
const trailer = document.asPDF()?.getTrailer(),
|
|
139
|
+
pageLabels = trailer?.get("Root", "PageLabels"),
|
|
140
|
+
hasLabels = pageLabels?.isDictionary() ?? false;
|
|
141
|
+
pageLabels?.destroy();
|
|
142
|
+
trailer?.destroy();
|
|
143
|
+
const wanted = new Map<number, string[]>(),
|
|
144
|
+
missing: string[] = [],
|
|
145
|
+
ask = (index: number, asked: string) =>
|
|
146
|
+
wanted.set(index, [...(wanted.get(index) ?? []), asked]);
|
|
147
|
+
for (const l of locations) {
|
|
148
|
+
// The build compiles an export of the source folder.
|
|
149
|
+
const file = path.join(
|
|
150
|
+
build.root,
|
|
151
|
+
path.posix.relative(project.source_dir || ".", l.file),
|
|
152
|
+
),
|
|
153
|
+
found = fs.existsSync(file) ? pagesOf(pdf, file, l.line) : [];
|
|
154
|
+
if (found.length) found.forEach((page) => ask(page, l.asked));
|
|
155
|
+
else missing.push(`${l.asked}: SyncTeX places no typeset text at or near this line.`);
|
|
156
|
+
}
|
|
157
|
+
if (labels.length && !hasLabels) {
|
|
158
|
+
missing.push("This PDF has no page-label metadata, so printed page numbers cannot be located reliably. Ask for source locations (file:line) instead.");
|
|
159
|
+
} else if (labels.length) {
|
|
160
|
+
const printed = Array.from({ length: document.countPages() }, (_, i) => {
|
|
161
|
+
const page = document.loadPage(i),
|
|
162
|
+
label = page.getLabel();
|
|
163
|
+
page.destroy();
|
|
164
|
+
return label;
|
|
165
|
+
});
|
|
166
|
+
for (const label of labels) {
|
|
167
|
+
const index = printed.findIndex(
|
|
168
|
+
(p) => p.toLowerCase() === label.toLowerCase(),
|
|
169
|
+
);
|
|
170
|
+
if (index >= 0) ask(index + 1, label);
|
|
171
|
+
else
|
|
172
|
+
missing.push(
|
|
173
|
+
targets[0] === "full"
|
|
174
|
+
? `Page ${label}: no page has this printed number.`
|
|
175
|
+
: `Page ${label}: not in the chapters compiled for this view (${targets.join(", ")}). Ask for a source line on that page instead.`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const indices = [...wanted.keys()].sort((a, b) => a - b);
|
|
180
|
+
if (indices.length > PAGES)
|
|
181
|
+
missing.push(
|
|
182
|
+
`Only the first ${PAGES} pages are shown; ask for more specific source locations to see the remaining pages (physical positions ${indices.slice(PAGES).join(", ")} in this build).`,
|
|
183
|
+
);
|
|
184
|
+
const out = indices.length ? folder(app, name) : "";
|
|
185
|
+
const pages = indices.slice(0, PAGES).map((index) => {
|
|
186
|
+
const page = document.loadPage(index - 1),
|
|
187
|
+
pixmap = page.toPixmap(
|
|
188
|
+
[SCALE, 0, 0, SCALE, 0, 0],
|
|
189
|
+
mupdf.ColorSpace.DeviceRGB,
|
|
190
|
+
false,
|
|
191
|
+
true,
|
|
192
|
+
),
|
|
193
|
+
label = hasLabels ? page.getLabel() || null : null,
|
|
194
|
+
file = path.join(out, `page-${label && /^[\w-]{1,16}$/u.test(label) ? label : index}-${index}.png`);
|
|
195
|
+
fs.writeFileSync(file, pixmap.asPNG());
|
|
196
|
+
pixmap.destroy();
|
|
197
|
+
page.destroy();
|
|
198
|
+
return { index, label, shows: wanted.get(index)!, file };
|
|
199
|
+
});
|
|
200
|
+
return { pages, missing, targets };
|
|
201
|
+
} finally {
|
|
202
|
+
document.destroy();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { readJson, writeJson } from "../core/files.ts";
|
|
4
4
|
import { within } from "../core/git.ts";
|
|
5
5
|
import { deepestNode } from "../core/outline.ts";
|
|
6
|
+
import { referenceChanges } from "../core/bib.ts";
|
|
6
7
|
import { changesOf, placesOf, renderPlace } from "./effects.ts";
|
|
7
8
|
import { Build } from "./locate.ts";
|
|
8
9
|
import { renderSection, snap, Strips } from "./inline.ts";
|
|
@@ -54,8 +55,11 @@ export function classify(
|
|
|
54
55
|
/\\(?:verb\b|lstinline\b|mintinline\b|begin\{(?:verbatim\*?|Verbatim|lstlisting|minted)\})/.test(
|
|
55
56
|
(old.read(name) ?? "") + (next.read(name) ?? ""),
|
|
56
57
|
);
|
|
58
|
+
// A .bib file is not typeset where it is written, and % in a URL is not
|
|
59
|
+
// a comment there: its edits are compared in the PDF.
|
|
57
60
|
if (
|
|
58
61
|
unit.kind !== "binary" &&
|
|
62
|
+
!name.endsWith(".bib") &&
|
|
59
63
|
!whitespaceSensitive &&
|
|
60
64
|
!unit.truncated &&
|
|
61
65
|
unit.status === "modified" &&
|
|
@@ -286,6 +290,13 @@ export function renderRound(
|
|
|
286
290
|
if (!placed.has(index))
|
|
287
291
|
result.loose.push(looseBlock(card, strips, old, next));
|
|
288
292
|
});
|
|
293
|
+
try {
|
|
294
|
+
const references = referenceChanges(request.units, old, next);
|
|
295
|
+
if (references) result.references = references;
|
|
296
|
+
} catch (error) {
|
|
297
|
+
// The PDF review stands without the bibliography's entry view.
|
|
298
|
+
console.error("Could not read the bibliography changes:", error);
|
|
299
|
+
}
|
|
289
300
|
fs.writeFileSync(path.join(out, "round.json"), JSON.stringify(result));
|
|
290
301
|
return result;
|
|
291
302
|
} finally {
|
|
@@ -12,8 +12,10 @@ import { Tasks } from "./core/tasks.ts";
|
|
|
12
12
|
import { Review } from "./core/review.ts";
|
|
13
13
|
import { Deliveries } from "./core/delivery.ts";
|
|
14
14
|
import { more, PublishError, Publisher } from "./core/publish.ts";
|
|
15
|
+
import { view, type ViewRequest } from "./core/view.ts";
|
|
15
16
|
import { acquireLock, now, readJson, writeJson } from "./core/files.ts";
|
|
16
17
|
import { stopAll } from "./core/process.ts";
|
|
18
|
+
import { checkInputs, Confirmations, ReferenceChecker } from "./core/verify.ts";
|
|
17
19
|
import type { ActionKind } from "./types.ts";
|
|
18
20
|
export class App {
|
|
19
21
|
root: string;
|
|
@@ -28,6 +30,8 @@ export class App {
|
|
|
28
30
|
review: Review;
|
|
29
31
|
delivery: Deliveries;
|
|
30
32
|
publisher: Publisher;
|
|
33
|
+
references: ReferenceChecker;
|
|
34
|
+
confirmations: Confirmations;
|
|
31
35
|
private cachedProject?: Project;
|
|
32
36
|
private stamp?: number;
|
|
33
37
|
constructor(root: string) {
|
|
@@ -39,6 +43,12 @@ export class App {
|
|
|
39
43
|
this.review = new Review(this);
|
|
40
44
|
this.delivery = new Deliveries(this);
|
|
41
45
|
this.publisher = new Publisher(this);
|
|
46
|
+
this.references = new ReferenceChecker(
|
|
47
|
+
path.join(this.state, "references.json"),
|
|
48
|
+
);
|
|
49
|
+
this.confirmations = new Confirmations(
|
|
50
|
+
path.join(this.state, "references-confirmed.json"),
|
|
51
|
+
);
|
|
42
52
|
this.review.recover();
|
|
43
53
|
this.delivery.recover();
|
|
44
54
|
}
|
|
@@ -210,6 +220,55 @@ export function createServer(app: App) {
|
|
|
210
220
|
}
|
|
211
221
|
},
|
|
212
222
|
);
|
|
223
|
+
// Checks of the bibliography entries a round adds or modifies. The page
|
|
224
|
+
// asks with check=1 to look up entries online, and keeps asking until
|
|
225
|
+
// none is pending; without it only results already known are returned.
|
|
226
|
+
server.get<{
|
|
227
|
+
Params: { name: string; number: string };
|
|
228
|
+
Querystring: { check?: string; retry?: string };
|
|
229
|
+
}>("/api/tasks/:name/rounds/:number/references", (request) => {
|
|
230
|
+
const inputs = checkInputs(
|
|
231
|
+
app.review.roundDetail(request.params.name, Number(request.params.number))
|
|
232
|
+
.references,
|
|
233
|
+
);
|
|
234
|
+
return {
|
|
235
|
+
...app.references.lookup(
|
|
236
|
+
inputs,
|
|
237
|
+
request.query.check === "1",
|
|
238
|
+
request.query.retry === "1",
|
|
239
|
+
),
|
|
240
|
+
confirmed: app.confirmations.of(inputs),
|
|
241
|
+
};
|
|
242
|
+
});
|
|
243
|
+
// The author confirms, or takes back, that they checked an entry by hand.
|
|
244
|
+
server.post<{
|
|
245
|
+
Params: { name: string; number: string };
|
|
246
|
+
Body: { id: string; confirmed: boolean };
|
|
247
|
+
}>(
|
|
248
|
+
"/api/tasks/:name/rounds/:number/references/confirm",
|
|
249
|
+
{
|
|
250
|
+
schema: {
|
|
251
|
+
body: {
|
|
252
|
+
...{ type: "object" as const },
|
|
253
|
+
required: ["id", "confirmed"],
|
|
254
|
+
properties: { id: { type: "string" }, confirmed: { type: "boolean" } },
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
(request, reply) => {
|
|
259
|
+
const inputs = checkInputs(
|
|
260
|
+
app.review.roundDetail(request.params.name, Number(request.params.number))
|
|
261
|
+
.references,
|
|
262
|
+
),
|
|
263
|
+
input = inputs.find((i) => i.id === request.body.id);
|
|
264
|
+
if (!input) return reply.code(404).send({ error: "No such entry in this round." });
|
|
265
|
+
app.confirmations.set(input, request.body.confirmed);
|
|
266
|
+
return {
|
|
267
|
+
...app.references.lookup(inputs),
|
|
268
|
+
confirmed: app.confirmations.of(inputs),
|
|
269
|
+
};
|
|
270
|
+
},
|
|
271
|
+
);
|
|
213
272
|
server.get<{ Params: { name: string; number: string; file: string } }>(
|
|
214
273
|
"/files/:name/:number/:file",
|
|
215
274
|
(request, reply) => {
|
|
@@ -272,6 +331,12 @@ export function createServer(app: App) {
|
|
|
272
331
|
return { job: job.id, status: job.status };
|
|
273
332
|
},
|
|
274
333
|
);
|
|
334
|
+
// The task's typeset pages as images, for its agent to look at.
|
|
335
|
+
server.post<{ Params: { name: string }; Body: ViewRequest }>(
|
|
336
|
+
"/api/tasks/:name/view",
|
|
337
|
+
{ schema: { body: { ...objectSchema, additionalProperties: false, properties: { locations: { type: "array", maxItems: 8, items: { type: "string", maxLength: 1000 } }, pages: { type: "array", maxItems: 8, items: { type: "string", maxLength: 40 } }, accepted: { type: "boolean" } } } } },
|
|
338
|
+
(request) => view(app, request.params.name, request.body),
|
|
339
|
+
);
|
|
275
340
|
server.post<{ Params: { name: string }; Body: { mode: "workspace" | "branch"; message?: string } }>(
|
|
276
341
|
"/api/tasks/:name/deliver",
|
|
277
342
|
{ schema: { body: { ...objectSchema, required: ["mode"], properties: { mode: { enum: ["workspace", "branch"] }, message: { type: "string", maxLength: 5000 } } } } },
|
|
@@ -239,6 +239,130 @@ export interface RenderedRound {
|
|
|
239
239
|
units: Record<string, VisualUnit>;
|
|
240
240
|
sections: { node: string; number: string; title: string; blocks: Block[] }[];
|
|
241
241
|
loose: ChangeBlock[];
|
|
242
|
+
references?: References;
|
|
243
|
+
}
|
|
244
|
+
/** One field of a changed bibliography entry, before and after; null
|
|
245
|
+
* where the entry lacks it. `resolved` is the new value with @string
|
|
246
|
+
* macros expanded, when that differs; `oldResolved` is its old expansion. */
|
|
247
|
+
export interface ReferenceField {
|
|
248
|
+
name: string;
|
|
249
|
+
old: string | null;
|
|
250
|
+
new: string | null;
|
|
251
|
+
resolved?: string;
|
|
252
|
+
oldResolved?: string;
|
|
253
|
+
}
|
|
254
|
+
export interface ReferenceCitation {
|
|
255
|
+
file: string;
|
|
256
|
+
line: number;
|
|
257
|
+
section: string | null;
|
|
258
|
+
}
|
|
259
|
+
/** still-cited: a removed key that the document still cites.
|
|
260
|
+
* not-in-bibliography: the document does not read this .bib file. */
|
|
261
|
+
export type ReferenceProblem =
|
|
262
|
+
| "still-cited"
|
|
263
|
+
| "duplicate-key"
|
|
264
|
+
| "unreadable"
|
|
265
|
+
| "not-in-bibliography";
|
|
266
|
+
/** A bibliography entry that a round adds, removes or modifies, shown whole
|
|
267
|
+
* under the edit it belongs to. */
|
|
268
|
+
export interface ReferenceEntry {
|
|
269
|
+
id: string;
|
|
270
|
+
unit: string;
|
|
271
|
+
/** All edits affecting this entry when macros span multiple files. */
|
|
272
|
+
units?: string[];
|
|
273
|
+
file: string;
|
|
274
|
+
key: string;
|
|
275
|
+
old_key?: string;
|
|
276
|
+
type: string;
|
|
277
|
+
old_type?: string;
|
|
278
|
+
status: "added" | "removed" | "modified";
|
|
279
|
+
fields: ReferenceField[];
|
|
280
|
+
lines: Span;
|
|
281
|
+
/** Where the revised document cites the key. */
|
|
282
|
+
cited: ReferenceCitation[];
|
|
283
|
+
/** Where it still cites a renamed entry's old key. */
|
|
284
|
+
old_cited?: ReferenceCitation[];
|
|
285
|
+
problems: ReferenceProblem[];
|
|
286
|
+
error?: string;
|
|
287
|
+
}
|
|
288
|
+
/** A key the revised document newly cites with no bibliography entry. */
|
|
289
|
+
export interface MissingCitation {
|
|
290
|
+
key: string;
|
|
291
|
+
file: string;
|
|
292
|
+
line: number;
|
|
293
|
+
unit: string | null;
|
|
294
|
+
section: string | null;
|
|
295
|
+
}
|
|
296
|
+
export interface References {
|
|
297
|
+
entries: ReferenceEntry[];
|
|
298
|
+
missing: MissingCitation[];
|
|
299
|
+
}
|
|
300
|
+
/** A published record a bibliography entry was compared with. */
|
|
301
|
+
export interface CheckRecord {
|
|
302
|
+
source: "doi" | "crossref" | "arxiv" | "dblp";
|
|
303
|
+
title: string;
|
|
304
|
+
authors: string[];
|
|
305
|
+
year: number | null;
|
|
306
|
+
venue: string | null;
|
|
307
|
+
doi: string | null;
|
|
308
|
+
url: string;
|
|
309
|
+
}
|
|
310
|
+
export interface CheckDifference {
|
|
311
|
+
field: "title" | "author" | "year" | "doi" | "arxiv";
|
|
312
|
+
bib: string;
|
|
313
|
+
record: string;
|
|
314
|
+
/** Shown, but not a reason to doubt the entry: punctuation in a title,
|
|
315
|
+
* or a preprint's year before the published version's. */
|
|
316
|
+
minor?: boolean;
|
|
317
|
+
}
|
|
318
|
+
/** verified: a record agrees. mismatch: a record of the same work differs,
|
|
319
|
+
* or the DOI names another work. unverified: no record was found.
|
|
320
|
+
* failed: a source could not be reached. skipped: nothing to look up. */
|
|
321
|
+
export interface ReferenceCheck {
|
|
322
|
+
status: "verified" | "mismatch" | "unverified" | "failed" | "skipped";
|
|
323
|
+
record?: CheckRecord;
|
|
324
|
+
differences: CheckDifference[];
|
|
325
|
+
/** What the entry was looked up by. */
|
|
326
|
+
via: "doi" | "arxiv" | "title" | null;
|
|
327
|
+
reason?: "doi-not-found" | "arxiv-not-found" | "no-match" | "nothing-to-check";
|
|
328
|
+
/** Sources that could not be reached for this check. */
|
|
329
|
+
unavailable: string[];
|
|
330
|
+
checked: number;
|
|
331
|
+
}
|
|
332
|
+
export interface ReferenceChecks {
|
|
333
|
+
checks: Record<string, ReferenceCheck>;
|
|
334
|
+
/** Entries still being looked up. */
|
|
335
|
+
pending: number;
|
|
336
|
+
/** Entries the author has checked by hand, with when (seconds). A
|
|
337
|
+
* confirmation holds while the entry's looked-up fields stay the same. */
|
|
338
|
+
confirmed?: Record<string, number>;
|
|
339
|
+
}
|
|
340
|
+
/** An entry of the paper's bibliography as it is now. */
|
|
341
|
+
export interface BibliographyEntry {
|
|
342
|
+
id: string;
|
|
343
|
+
file: string;
|
|
344
|
+
key: string;
|
|
345
|
+
type: string;
|
|
346
|
+
fields: { name: string; value: string; resolved?: string }[];
|
|
347
|
+
lines: Span;
|
|
348
|
+
cited: ReferenceCitation[];
|
|
349
|
+
error?: string;
|
|
350
|
+
}
|
|
351
|
+
/** The whole bibliography of a paper: its entries, where each is cited,
|
|
352
|
+
* and what is wrong with them without looking anything up. */
|
|
353
|
+
export interface Bibliography {
|
|
354
|
+
/** The .bib files the paper names, and those of them that do not exist. */
|
|
355
|
+
bibs: string[];
|
|
356
|
+
absent: string[];
|
|
357
|
+
entries: BibliographyEntry[];
|
|
358
|
+
/** Citations of keys no entry defines, each place. */
|
|
359
|
+
missing: (ReferenceCitation & { key: string })[];
|
|
360
|
+
/** Entries sharing a key. */
|
|
361
|
+
duplicates: string[][];
|
|
362
|
+
/** Entries under different keys with the same DOI or title. */
|
|
363
|
+
similar: { ids: string[]; by: "doi" | "title" }[];
|
|
364
|
+
/** \nocite{*} puts every entry in the bibliography. */
|
|
365
|
+
all_cited: boolean;
|
|
242
366
|
}
|
|
243
367
|
export interface RenderRequest {
|
|
244
368
|
main: string;
|
|
@@ -273,6 +397,7 @@ export interface RoundDetail extends Omit<RenderedRound, "units"> {
|
|
|
273
397
|
units: (Unit & Partial<VisualUnit> & { history?: History[] })[];
|
|
274
398
|
warnings: string[];
|
|
275
399
|
files: string;
|
|
400
|
+
references?: References;
|
|
276
401
|
}
|
|
277
402
|
export type TaskState =
|
|
278
403
|
"editing" | "building" | "build_failed" | "reviewing" | "revise" | "done";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Shared with the host settings; storage events also update standalone tabs.
|
|
2
|
+
export const EFFECTS_STORAGE = "latex-review:effects";
|
|
3
|
+
export const EFFECTS_EVENT = "latex-review:effects-changed";
|
|
4
|
+
export function readExpandedEffects() {
|
|
5
|
+
try {
|
|
6
|
+
return localStorage.getItem(EFFECTS_STORAGE) === "on";
|
|
7
|
+
} catch {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
}
|