papilot 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +126 -2
- package/README.zh-CN.md +127 -0
- package/bin/papilot.mjs +113 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/Inconsolatazi4-Regular-Dbtq7t_r.otf +0 -0
- package/dist/assets/TeXGyrePagellaX-Bold-CEtYdOXj.otf +0 -0
- package/dist/assets/TeXGyrePagellaX-Italic-CSuYIstn.otf +0 -0
- package/dist/assets/TeXGyrePagellaX-Regular-DQJjZE_L.otf +0 -0
- package/dist/assets/cursor-editorTextHighlight-kG3uUOwL.svg +8 -0
- package/dist/assets/index-BhQFCMTx.js +131 -0
- package/dist/assets/index-CAV3-87U.css +1 -0
- package/dist/assets/pdf-view-DJo0X38w.js +598 -0
- package/dist/assets/pdf.worker.min-CjEcRF4W.mjs +28 -0
- package/dist/favicon.png +0 -0
- package/dist/index.html +4 -0
- package/examples/papilot-paper/.latex-review.json +4 -0
- package/examples/papilot-paper/appendix/proofs.tex +60 -0
- package/examples/papilot-paper/appendix/reference.tex +54 -0
- package/examples/papilot-paper/main.tex +57 -0
- package/examples/papilot-paper/preamble.tex +53 -0
- package/examples/papilot-paper/references.bib +158 -0
- package/examples/papilot-paper/sections/01-introduction.tex +66 -0
- package/examples/papilot-paper/sections/02-using.tex +134 -0
- package/examples/papilot-paper/sections/03-background.tex +97 -0
- package/examples/papilot-paper/sections/04-model.tex +185 -0
- package/examples/papilot-paper/sections/05-conflicts.tex +99 -0
- package/examples/papilot-paper/sections/06-building.tex +70 -0
- package/examples/papilot-paper/sections/07-geometry.tex +193 -0
- package/examples/papilot-paper/sections/08-notes.tex +104 -0
- package/examples/papilot-paper/sections/09-agents.tex +106 -0
- package/examples/papilot-paper/sections/10-usage.tex +56 -0
- package/examples/papilot-paper/sections/11-git.tex +62 -0
- package/examples/papilot-paper/sections/12-security.tex +41 -0
- package/examples/papilot-paper/sections/13-evaluation.tex +106 -0
- package/examples/papilot-paper/sections/14-related.tex +38 -0
- package/examples/papilot-paper/sections/15-conclusion.tex +27 -0
- package/package.json +85 -4
- package/scripts/demo.ts +21 -0
- package/scripts/service.ts +35 -0
- package/src/notes.ts +40 -0
- package/src/server/agents.ts +343 -0
- package/src/server/codex.ts +63 -0
- package/src/server/frontend.ts +15 -0
- package/src/server/main.ts +202 -0
- package/src/server/manuscript.ts +337 -0
- package/src/server/paths.ts +10 -0
- package/src/server/pdf.ts +27 -0
- package/src/server/review.ts +53 -0
- package/src/server/security.ts +15 -0
- package/src/server/store.ts +60 -0
- package/src/server/workspace.ts +529 -0
- package/src/shared.ts +28 -0
- package/vendor/latex-review/LICENSE +21 -0
- package/vendor/latex-review/dist/web/assets/TeXGyrePagellaX-Bold-CEtYdOXj.otf +0 -0
- package/vendor/latex-review/dist/web/assets/TeXGyrePagellaX-Regular-DQJjZE_L.otf +0 -0
- package/vendor/latex-review/dist/web/assets/index-DyzYnIGk.css +1 -0
- package/vendor/latex-review/dist/web/assets/index-HxBK-dls.js +19 -0
- package/vendor/latex-review/dist/web/index.html +18 -0
- package/vendor/latex-review/package-lock.json +3114 -0
- package/vendor/latex-review/package.json +49 -0
- package/vendor/latex-review/src/cli.ts +494 -0
- package/vendor/latex-review/src/client.ts +198 -0
- package/vendor/latex-review/src/core/build.ts +296 -0
- package/vendor/latex-review/src/core/changes.ts +126 -0
- package/vendor/latex-review/src/core/config.ts +270 -0
- package/vendor/latex-review/src/core/diff.ts +54 -0
- package/vendor/latex-review/src/core/files.ts +96 -0
- package/vendor/latex-review/src/core/git.ts +317 -0
- package/vendor/latex-review/src/core/outline.ts +314 -0
- package/vendor/latex-review/src/core/process.ts +111 -0
- package/vendor/latex-review/src/core/publish.ts +339 -0
- package/vendor/latex-review/src/core/review.ts +734 -0
- package/vendor/latex-review/src/core/store.ts +66 -0
- package/vendor/latex-review/src/core/tasks.ts +237 -0
- package/vendor/latex-review/src/render/inline.ts +805 -0
- package/vendor/latex-review/src/render/locate.ts +481 -0
- package/vendor/latex-review/src/render/pdf.ts +281 -0
- package/vendor/latex-review/src/render/pdfdiff.ts +272 -0
- package/vendor/latex-review/src/render/round.ts +236 -0
- package/vendor/latex-review/src/render/worker.ts +10 -0
- package/vendor/latex-review/src/runtime.ts +23 -0
- package/vendor/latex-review/src/server.ts +401 -0
- package/vendor/latex-review/src/types.ts +283 -0
- package/vendor/latex-review/src/web/ChangeMap.tsx +212 -0
- package/vendor/latex-review/src/web/PdfStrip.tsx +77 -0
- package/vendor/latex-review/src/web/fonts/GUST-FONT-LICENSE.txt +29 -0
- package/vendor/latex-review/src/web/fonts/LPPL.txt +415 -0
- package/vendor/latex-review/src/web/fonts/MANIFEST-newpx.txt +20 -0
- package/vendor/latex-review/src/web/fonts/README-newpx.txt +473 -0
- package/vendor/latex-review/src/web/fonts/README.md +12 -0
- package/vendor/latex-review/src/web/fonts/TeXGyrePagellaX-Bold.otf +0 -0
- package/vendor/latex-review/src/web/fonts/TeXGyrePagellaX-Regular.otf +0 -0
- package/vendor/latex-review/src/web/index.html +17 -0
- package/vendor/latex-review/src/web/main.tsx +10 -0
- package/vendor/latex-review/src/web/model.ts +153 -0
- package/vendor/latex-review/src/web/overview.tsx +146 -0
- package/vendor/latex-review/src/web/review.tsx +991 -0
- package/vendor/latex-review/src/web/style.css +1249 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
spawn,
|
|
3
|
+
spawnSync,
|
|
4
|
+
type SpawnSyncOptionsWithBufferEncoding,
|
|
5
|
+
} from "node:child_process";
|
|
6
|
+
export class ProcessError extends Error {
|
|
7
|
+
constructor(
|
|
8
|
+
message: string,
|
|
9
|
+
public stdout = Buffer.alloc(0),
|
|
10
|
+
public stderr = Buffer.alloc(0),
|
|
11
|
+
public code = 1,
|
|
12
|
+
) {
|
|
13
|
+
super(message);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function run(
|
|
17
|
+
command: string,
|
|
18
|
+
args: string[],
|
|
19
|
+
options: SpawnSyncOptionsWithBufferEncoding & { check?: boolean } = {},
|
|
20
|
+
) {
|
|
21
|
+
const { check = true, ...rest } = options;
|
|
22
|
+
const result = spawnSync(command, args, {
|
|
23
|
+
timeout: 300000,
|
|
24
|
+
maxBuffer: 128 * 1024 * 1024,
|
|
25
|
+
...rest,
|
|
26
|
+
});
|
|
27
|
+
if (result.error) throw result.error;
|
|
28
|
+
if (check && result.status !== 0)
|
|
29
|
+
throw new ProcessError(
|
|
30
|
+
result.stderr?.toString().trim() ||
|
|
31
|
+
`${command} failed (${result.status})`,
|
|
32
|
+
result.stdout,
|
|
33
|
+
result.stderr,
|
|
34
|
+
result.status ?? 1,
|
|
35
|
+
);
|
|
36
|
+
return {
|
|
37
|
+
stdout: result.stdout ?? Buffer.alloc(0),
|
|
38
|
+
stderr: result.stderr ?? Buffer.alloc(0),
|
|
39
|
+
status: result.status ?? 1,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const running = new Set<ReturnType<typeof spawn>>();
|
|
43
|
+
export function stopAll() {
|
|
44
|
+
for (const child of running) {
|
|
45
|
+
try {
|
|
46
|
+
if (child.pid) process.kill(-child.pid, "SIGTERM");
|
|
47
|
+
} catch {
|
|
48
|
+
child.kill();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function runAsync(
|
|
53
|
+
command: string,
|
|
54
|
+
args: string[],
|
|
55
|
+
options: {
|
|
56
|
+
cwd?: string;
|
|
57
|
+
env?: NodeJS.ProcessEnv;
|
|
58
|
+
timeout?: number;
|
|
59
|
+
output?: (chunk: Buffer) => void;
|
|
60
|
+
} = {},
|
|
61
|
+
) {
|
|
62
|
+
return new Promise<Buffer>((resolve, reject) => {
|
|
63
|
+
const child = spawn(command, args, {
|
|
64
|
+
cwd: options.cwd,
|
|
65
|
+
env: options.env,
|
|
66
|
+
detached: true,
|
|
67
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
68
|
+
});
|
|
69
|
+
running.add(child);
|
|
70
|
+
const chunks: Buffer[] = [];
|
|
71
|
+
let timedOut = false;
|
|
72
|
+
const timer = setTimeout(() => {
|
|
73
|
+
timedOut = true;
|
|
74
|
+
try {
|
|
75
|
+
if (child.pid) process.kill(-child.pid, "SIGKILL");
|
|
76
|
+
} catch {
|
|
77
|
+
child.kill("SIGKILL");
|
|
78
|
+
}
|
|
79
|
+
}, options.timeout ?? 300000);
|
|
80
|
+
const receive = (data: Buffer) => {
|
|
81
|
+
chunks.push(data);
|
|
82
|
+
options.output?.(data);
|
|
83
|
+
};
|
|
84
|
+
child.stdout.on("data", receive);
|
|
85
|
+
child.stderr.on("data", receive);
|
|
86
|
+
child.on("error", (error) => {
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
running.delete(child);
|
|
89
|
+
reject(error);
|
|
90
|
+
});
|
|
91
|
+
child.on("close", (code) => {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
running.delete(child);
|
|
94
|
+
const data = Buffer.concat(chunks);
|
|
95
|
+
if (code === 0 && !timedOut) resolve(data);
|
|
96
|
+
else
|
|
97
|
+
reject(
|
|
98
|
+
new ProcessError(
|
|
99
|
+
timedOut ? `${command} timed out` : `${command} failed (${code})`,
|
|
100
|
+
data,
|
|
101
|
+
data,
|
|
102
|
+
code ?? 1,
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
export function which(command: string): string | null {
|
|
109
|
+
const found = run("which", [command], { check: false });
|
|
110
|
+
return found.status === 0 ? found.stdout.toString().trim() : null;
|
|
111
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { ROOT } from "../runtime.ts";
|
|
5
|
+
import { Builder, BuildError } from "./build.ts";
|
|
6
|
+
import { diffUnits, scopeViolations } from "./changes.ts";
|
|
7
|
+
import { isFigure } from "./config.ts";
|
|
8
|
+
import { now, readJson, writeJson } from "./files.ts";
|
|
9
|
+
import { runAsync } from "./process.ts";
|
|
10
|
+
import type { App } from "../server.ts";
|
|
11
|
+
import type {
|
|
12
|
+
Job,
|
|
13
|
+
JobResult,
|
|
14
|
+
RenderedRound,
|
|
15
|
+
RenderRequest,
|
|
16
|
+
Round,
|
|
17
|
+
} from "../types.ts";
|
|
18
|
+
export class PublishError extends Error {
|
|
19
|
+
constructor(
|
|
20
|
+
message: string,
|
|
21
|
+
public details: string[] = [],
|
|
22
|
+
public next = "",
|
|
23
|
+
public log?: string,
|
|
24
|
+
) {
|
|
25
|
+
super(message);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
class Stale extends Error {}
|
|
29
|
+
export async function render(output: string, request: RenderRequest) {
|
|
30
|
+
const file = path.join(output, "request.json");
|
|
31
|
+
writeJson(file, request);
|
|
32
|
+
try {
|
|
33
|
+
await runAsync(
|
|
34
|
+
process.execPath,
|
|
35
|
+
["--import", "tsx", path.join(ROOT, "src/render/worker.ts"), file],
|
|
36
|
+
{ cwd: ROOT, timeout: 1200000 },
|
|
37
|
+
);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
throw new PublishError(
|
|
40
|
+
"Rendering the PDF comparison failed.",
|
|
41
|
+
[
|
|
42
|
+
(error as Error).message,
|
|
43
|
+
...String((error as { stderr?: Buffer }).stderr ?? "")
|
|
44
|
+
.trim()
|
|
45
|
+
.split("\n")
|
|
46
|
+
.slice(-12),
|
|
47
|
+
],
|
|
48
|
+
"Fix the renderer error, then publish again.",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
fs.rmSync(file, { force: true });
|
|
52
|
+
}
|
|
53
|
+
export class Publisher {
|
|
54
|
+
private pruning: Promise<void> | null = null;
|
|
55
|
+
pending = new Map<string, string>();
|
|
56
|
+
running = new Map<string, Promise<void>>();
|
|
57
|
+
queue: string[] = [];
|
|
58
|
+
stopping = false;
|
|
59
|
+
constructor(public app: App) {
|
|
60
|
+
app.store.execute(
|
|
61
|
+
"UPDATE jobs SET status='failed', finished=?, result=? WHERE status IN ('queued','running')",
|
|
62
|
+
now(),
|
|
63
|
+
JSON.stringify({
|
|
64
|
+
error: "The review server restarted during the build.",
|
|
65
|
+
next: "Publish again.",
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
submit(name: string) {
|
|
70
|
+
this.app.tasks.get(name);
|
|
71
|
+
if (this.stopping)
|
|
72
|
+
throw new Error(
|
|
73
|
+
"The review server is stopping. Publish again after it restarts.",
|
|
74
|
+
);
|
|
75
|
+
const current = this.pending.get(name);
|
|
76
|
+
if (current) {
|
|
77
|
+
const job = this.job(current);
|
|
78
|
+
if (job.status === "queued") return job;
|
|
79
|
+
}
|
|
80
|
+
const id = randomBytes(8).toString("hex");
|
|
81
|
+
this.app.store.execute(
|
|
82
|
+
"INSERT INTO jobs(id,task,status,created) VALUES(?,?,'queued',?)",
|
|
83
|
+
id,
|
|
84
|
+
name,
|
|
85
|
+
now(),
|
|
86
|
+
);
|
|
87
|
+
this.pending.set(name, id);
|
|
88
|
+
if (!this.running.has(name)) this.queue.push(id);
|
|
89
|
+
queueMicrotask(() => this.drain());
|
|
90
|
+
return this.job(id);
|
|
91
|
+
}
|
|
92
|
+
job(id: string) {
|
|
93
|
+
const job = this.app.store.one<Job>("SELECT * FROM jobs WHERE id=?", id);
|
|
94
|
+
if (!job) throw new Error("Unknown job " + id);
|
|
95
|
+
return job;
|
|
96
|
+
}
|
|
97
|
+
current(name: string) {
|
|
98
|
+
return this.app.store.one<Job>(
|
|
99
|
+
"SELECT * FROM jobs WHERE task=? ORDER BY created DESC,rowid DESC LIMIT 1",
|
|
100
|
+
name,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
busy(name?: string) {
|
|
104
|
+
return name ? this.pending.has(name) : this.pending.size > 0;
|
|
105
|
+
}
|
|
106
|
+
ahead(job: Job) {
|
|
107
|
+
return this.app.store.one<{ n: number }>(
|
|
108
|
+
"SELECT COUNT(*) AS n FROM jobs WHERE status IN ('queued','running') AND created<?",
|
|
109
|
+
job.created,
|
|
110
|
+
)!.n;
|
|
111
|
+
}
|
|
112
|
+
update(
|
|
113
|
+
id: string,
|
|
114
|
+
fields: Partial<Pick<Job, "phase" | "status" | "finished">> & {
|
|
115
|
+
result?: JobResult;
|
|
116
|
+
},
|
|
117
|
+
) {
|
|
118
|
+
const entries = Object.entries(fields);
|
|
119
|
+
this.app.store.execute(
|
|
120
|
+
"UPDATE jobs SET " +
|
|
121
|
+
entries.map(([key]) => key + "=?").join(",") +
|
|
122
|
+
" WHERE id=?",
|
|
123
|
+
...entries.map(([key, value]) =>
|
|
124
|
+
key === "result" ? JSON.stringify(value) : (value as string | number),
|
|
125
|
+
),
|
|
126
|
+
id,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
stop() {
|
|
130
|
+
this.stopping = true;
|
|
131
|
+
}
|
|
132
|
+
async join() {
|
|
133
|
+
await Promise.resolve();
|
|
134
|
+
while (this.running.size || this.pruning)
|
|
135
|
+
await Promise.allSettled([
|
|
136
|
+
...this.running.values(),
|
|
137
|
+
...(this.pruning ? [this.pruning] : []),
|
|
138
|
+
]);
|
|
139
|
+
}
|
|
140
|
+
private drain() {
|
|
141
|
+
if (this.pruning) return;
|
|
142
|
+
while (
|
|
143
|
+
!this.stopping &&
|
|
144
|
+
this.queue.length &&
|
|
145
|
+
this.running.size < this.app.project.build_workers
|
|
146
|
+
) {
|
|
147
|
+
const id = this.queue.shift()!,
|
|
148
|
+
job = this.job(id);
|
|
149
|
+
this.update(id, { status: "running" });
|
|
150
|
+
const promise = Promise.resolve().then(() => this.work(job));
|
|
151
|
+
this.running.set(job.task, promise);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
private async work(job: Job) {
|
|
155
|
+
try {
|
|
156
|
+
let result: JobResult;
|
|
157
|
+
for (;;) {
|
|
158
|
+
try {
|
|
159
|
+
result = await this.publish(job);
|
|
160
|
+
break;
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (!(error instanceof Stale)) throw error;
|
|
163
|
+
if (this.stopping)
|
|
164
|
+
throw new Error("Server stopped during publication.");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
this.update(job.id, { status: "done", result, finished: now() });
|
|
168
|
+
} catch (error) {
|
|
169
|
+
const result: JobResult =
|
|
170
|
+
error instanceof PublishError
|
|
171
|
+
? {
|
|
172
|
+
error: error.message,
|
|
173
|
+
details: error.details,
|
|
174
|
+
next: error.next,
|
|
175
|
+
...(error.log ? { log: error.log } : {}),
|
|
176
|
+
}
|
|
177
|
+
: { error: (error as Error).message, next: "Publish again." };
|
|
178
|
+
this.update(job.id, { status: "failed", result, finished: now() });
|
|
179
|
+
} finally {
|
|
180
|
+
this.running.delete(job.task);
|
|
181
|
+
if (this.pending.get(job.task) === job.id) this.pending.delete(job.task);
|
|
182
|
+
else if (!this.stopping) this.queue.push(this.pending.get(job.task)!);
|
|
183
|
+
this.drain();
|
|
184
|
+
if (!this.pending.size && !this.stopping) await this.pruneCache();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async publish(job: Pick<Job, "id" | "task">): Promise<JobResult> {
|
|
188
|
+
const app = this.app,
|
|
189
|
+
name = job.task,
|
|
190
|
+
project = app.project;
|
|
191
|
+
let task = app.tasks.get(name);
|
|
192
|
+
if (!fs.existsSync(task.worktree))
|
|
193
|
+
throw new PublishError(
|
|
194
|
+
`The task worktree ${task.worktree} no longer exists.`,
|
|
195
|
+
[],
|
|
196
|
+
"Run `finish --force`, then start a new task.",
|
|
197
|
+
);
|
|
198
|
+
this.update(job.id, { phase: "snapshot" });
|
|
199
|
+
const after = app.tasks.snapshot(task);
|
|
200
|
+
task = app.tasks.get(name);
|
|
201
|
+
const before = task.accepted,
|
|
202
|
+
latest = app.review.latestRound(name),
|
|
203
|
+
decided = app.review.lastChange(name),
|
|
204
|
+
violations = scopeViolations(app.repo, task.base, after, task.scope);
|
|
205
|
+
if (violations.length)
|
|
206
|
+
throw new PublishError(
|
|
207
|
+
"Some edits are outside the task scope: " +
|
|
208
|
+
violations.slice(0, 10).join(", "),
|
|
209
|
+
[
|
|
210
|
+
"Allowed: " +
|
|
211
|
+
(task.scope.sections.length
|
|
212
|
+
? task.scope.sections.map((s) => `${s.number} ${s.title}`)
|
|
213
|
+
: task.scope.paths
|
|
214
|
+
).join(", "),
|
|
215
|
+
],
|
|
216
|
+
"Undo edits outside the scope, then publish again.",
|
|
217
|
+
);
|
|
218
|
+
if (
|
|
219
|
+
latest &&
|
|
220
|
+
latest.before === before &&
|
|
221
|
+
app.repo.treeId(latest.after) === app.repo.treeId(after)
|
|
222
|
+
)
|
|
223
|
+
return {
|
|
224
|
+
outcome: "unchanged",
|
|
225
|
+
round: latest.number,
|
|
226
|
+
changes: latest.units.length,
|
|
227
|
+
};
|
|
228
|
+
const files = app.repo.changedFiles(before, after);
|
|
229
|
+
if (!files.length) return { outcome: "empty", round: null, changes: 0 };
|
|
230
|
+
const units = diffUnits(app.repo, before, after, (f) =>
|
|
231
|
+
isFigure(project, f),
|
|
232
|
+
);
|
|
233
|
+
this.update(job.id, { phase: "build" });
|
|
234
|
+
const builder = new Builder(project, app.repo),
|
|
235
|
+
targets = builder.plan(before, after, files);
|
|
236
|
+
let seed = null,
|
|
237
|
+
old,
|
|
238
|
+
next;
|
|
239
|
+
try {
|
|
240
|
+
seed = targets[0] !== "full" ? await builder.build(task.base) : null;
|
|
241
|
+
old = await builder.build(before, targets, seed);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (error instanceof BuildError)
|
|
244
|
+
throw new PublishError(
|
|
245
|
+
"The accepted text does not compile, so there is nothing to compare against.",
|
|
246
|
+
error.details,
|
|
247
|
+
"Fix the accepted document or undo the change that broke it, then publish again.",
|
|
248
|
+
error.log,
|
|
249
|
+
);
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
next = await builder.build(after, targets, seed);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (error instanceof BuildError)
|
|
256
|
+
throw new PublishError(
|
|
257
|
+
"Your edits do not compile.",
|
|
258
|
+
error.details,
|
|
259
|
+
"Fix the LaTeX errors in the worktree, then publish again.",
|
|
260
|
+
error.log,
|
|
261
|
+
);
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
if (app.review.lastChange(name) !== decided) throw new Stale();
|
|
265
|
+
const warnings = next.warnings.filter((w) => !old.warnings.includes(w));
|
|
266
|
+
this.update(job.id, { phase: "render" });
|
|
267
|
+
const number = (latest?.number ?? 0) + 1,
|
|
268
|
+
rounds = path.join(app.state, "rounds", name),
|
|
269
|
+
final = path.join(rounds, String(number).padStart(3, "0")),
|
|
270
|
+
output = final + ".pending-" + randomBytes(4).toString("hex");
|
|
271
|
+
fs.mkdirSync(output, { recursive: true });
|
|
272
|
+
const summary = {
|
|
273
|
+
warnings,
|
|
274
|
+
targets,
|
|
275
|
+
builds: [seed, old, next].filter((b) => b !== null).map((b) => b.key),
|
|
276
|
+
};
|
|
277
|
+
try {
|
|
278
|
+
await render(output, {
|
|
279
|
+
main: project.main,
|
|
280
|
+
source_dir: project.source_dir,
|
|
281
|
+
figures: project.figures,
|
|
282
|
+
old,
|
|
283
|
+
new: next,
|
|
284
|
+
units,
|
|
285
|
+
});
|
|
286
|
+
if (app.review.lastChange(name) !== decided) throw new Stale();
|
|
287
|
+
fs.rmSync(final, { recursive: true, force: true });
|
|
288
|
+
fs.renameSync(output, final);
|
|
289
|
+
app.store.execute(
|
|
290
|
+
"INSERT INTO rounds(task,number,before,after,units,summary,created) VALUES(?,?,?,?,?,?,?)",
|
|
291
|
+
name,
|
|
292
|
+
number,
|
|
293
|
+
before,
|
|
294
|
+
after,
|
|
295
|
+
JSON.stringify(units),
|
|
296
|
+
JSON.stringify(summary),
|
|
297
|
+
now(),
|
|
298
|
+
);
|
|
299
|
+
app.repo.updateRef(`refs/latex-review/${name}/round-${number}`, after);
|
|
300
|
+
} finally {
|
|
301
|
+
fs.rmSync(output, { recursive: true, force: true });
|
|
302
|
+
}
|
|
303
|
+
const visual = readJson<RenderedRound>(path.join(final, "round.json"), {
|
|
304
|
+
units: {},
|
|
305
|
+
sections: [],
|
|
306
|
+
loose: [],
|
|
307
|
+
}),
|
|
308
|
+
kinds: Record<string, number> = {};
|
|
309
|
+
for (const unit of Object.values(visual.units))
|
|
310
|
+
kinds[unit.visual] = (kinds[unit.visual] ?? 0) + 1;
|
|
311
|
+
return {
|
|
312
|
+
outcome: "published",
|
|
313
|
+
round: number,
|
|
314
|
+
changes: units.length,
|
|
315
|
+
kinds,
|
|
316
|
+
warnings,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
async pruneCache() {
|
|
320
|
+
if (this.pruning) return this.pruning;
|
|
321
|
+
if (this.busy()) return;
|
|
322
|
+
const keep = new Set(
|
|
323
|
+
this.app.store
|
|
324
|
+
.all<Pick<Round, "summary">>(
|
|
325
|
+
"SELECT summary FROM rounds JOIN tasks ON rounds.task=tasks.name WHERE tasks.closed IS NULL",
|
|
326
|
+
)
|
|
327
|
+
.flatMap((r) => r.summary.builds ?? []),
|
|
328
|
+
);
|
|
329
|
+
this.pruning = new Builder(this.app.project, this.app.repo).prune(keep);
|
|
330
|
+
try {
|
|
331
|
+
await this.pruning;
|
|
332
|
+
} catch (error) {
|
|
333
|
+
console.error("Build cache cleanup:", error);
|
|
334
|
+
} finally {
|
|
335
|
+
this.pruning = null;
|
|
336
|
+
this.drain();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|