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.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +126 -2
  3. package/README.zh-CN.md +127 -0
  4. package/bin/papilot.mjs +113 -0
  5. package/dist/apple-touch-icon.png +0 -0
  6. package/dist/assets/Inconsolatazi4-Regular-Dbtq7t_r.otf +0 -0
  7. package/dist/assets/TeXGyrePagellaX-Bold-CEtYdOXj.otf +0 -0
  8. package/dist/assets/TeXGyrePagellaX-Italic-CSuYIstn.otf +0 -0
  9. package/dist/assets/TeXGyrePagellaX-Regular-DQJjZE_L.otf +0 -0
  10. package/dist/assets/cursor-editorTextHighlight-kG3uUOwL.svg +8 -0
  11. package/dist/assets/index-BhQFCMTx.js +131 -0
  12. package/dist/assets/index-CAV3-87U.css +1 -0
  13. package/dist/assets/pdf-view-DJo0X38w.js +598 -0
  14. package/dist/assets/pdf.worker.min-CjEcRF4W.mjs +28 -0
  15. package/dist/favicon.png +0 -0
  16. package/dist/index.html +4 -0
  17. package/examples/papilot-paper/.latex-review.json +4 -0
  18. package/examples/papilot-paper/appendix/proofs.tex +60 -0
  19. package/examples/papilot-paper/appendix/reference.tex +54 -0
  20. package/examples/papilot-paper/main.tex +57 -0
  21. package/examples/papilot-paper/preamble.tex +53 -0
  22. package/examples/papilot-paper/references.bib +158 -0
  23. package/examples/papilot-paper/sections/01-introduction.tex +66 -0
  24. package/examples/papilot-paper/sections/02-using.tex +134 -0
  25. package/examples/papilot-paper/sections/03-background.tex +97 -0
  26. package/examples/papilot-paper/sections/04-model.tex +185 -0
  27. package/examples/papilot-paper/sections/05-conflicts.tex +99 -0
  28. package/examples/papilot-paper/sections/06-building.tex +70 -0
  29. package/examples/papilot-paper/sections/07-geometry.tex +193 -0
  30. package/examples/papilot-paper/sections/08-notes.tex +104 -0
  31. package/examples/papilot-paper/sections/09-agents.tex +106 -0
  32. package/examples/papilot-paper/sections/10-usage.tex +56 -0
  33. package/examples/papilot-paper/sections/11-git.tex +62 -0
  34. package/examples/papilot-paper/sections/12-security.tex +41 -0
  35. package/examples/papilot-paper/sections/13-evaluation.tex +106 -0
  36. package/examples/papilot-paper/sections/14-related.tex +38 -0
  37. package/examples/papilot-paper/sections/15-conclusion.tex +27 -0
  38. package/package.json +85 -4
  39. package/scripts/demo.ts +21 -0
  40. package/scripts/service.ts +35 -0
  41. package/src/notes.ts +40 -0
  42. package/src/server/agents.ts +343 -0
  43. package/src/server/codex.ts +63 -0
  44. package/src/server/frontend.ts +15 -0
  45. package/src/server/main.ts +202 -0
  46. package/src/server/manuscript.ts +337 -0
  47. package/src/server/paths.ts +10 -0
  48. package/src/server/pdf.ts +27 -0
  49. package/src/server/review.ts +53 -0
  50. package/src/server/security.ts +15 -0
  51. package/src/server/store.ts +60 -0
  52. package/src/server/workspace.ts +529 -0
  53. package/src/shared.ts +28 -0
  54. package/vendor/latex-review/LICENSE +21 -0
  55. package/vendor/latex-review/dist/web/assets/TeXGyrePagellaX-Bold-CEtYdOXj.otf +0 -0
  56. package/vendor/latex-review/dist/web/assets/TeXGyrePagellaX-Regular-DQJjZE_L.otf +0 -0
  57. package/vendor/latex-review/dist/web/assets/index-DyzYnIGk.css +1 -0
  58. package/vendor/latex-review/dist/web/assets/index-HxBK-dls.js +19 -0
  59. package/vendor/latex-review/dist/web/index.html +18 -0
  60. package/vendor/latex-review/package-lock.json +3114 -0
  61. package/vendor/latex-review/package.json +49 -0
  62. package/vendor/latex-review/src/cli.ts +494 -0
  63. package/vendor/latex-review/src/client.ts +198 -0
  64. package/vendor/latex-review/src/core/build.ts +296 -0
  65. package/vendor/latex-review/src/core/changes.ts +126 -0
  66. package/vendor/latex-review/src/core/config.ts +270 -0
  67. package/vendor/latex-review/src/core/diff.ts +54 -0
  68. package/vendor/latex-review/src/core/files.ts +96 -0
  69. package/vendor/latex-review/src/core/git.ts +317 -0
  70. package/vendor/latex-review/src/core/outline.ts +314 -0
  71. package/vendor/latex-review/src/core/process.ts +111 -0
  72. package/vendor/latex-review/src/core/publish.ts +339 -0
  73. package/vendor/latex-review/src/core/review.ts +734 -0
  74. package/vendor/latex-review/src/core/store.ts +66 -0
  75. package/vendor/latex-review/src/core/tasks.ts +237 -0
  76. package/vendor/latex-review/src/render/inline.ts +805 -0
  77. package/vendor/latex-review/src/render/locate.ts +481 -0
  78. package/vendor/latex-review/src/render/pdf.ts +281 -0
  79. package/vendor/latex-review/src/render/pdfdiff.ts +272 -0
  80. package/vendor/latex-review/src/render/round.ts +236 -0
  81. package/vendor/latex-review/src/render/worker.ts +10 -0
  82. package/vendor/latex-review/src/runtime.ts +23 -0
  83. package/vendor/latex-review/src/server.ts +401 -0
  84. package/vendor/latex-review/src/types.ts +283 -0
  85. package/vendor/latex-review/src/web/ChangeMap.tsx +212 -0
  86. package/vendor/latex-review/src/web/PdfStrip.tsx +77 -0
  87. package/vendor/latex-review/src/web/fonts/GUST-FONT-LICENSE.txt +29 -0
  88. package/vendor/latex-review/src/web/fonts/LPPL.txt +415 -0
  89. package/vendor/latex-review/src/web/fonts/MANIFEST-newpx.txt +20 -0
  90. package/vendor/latex-review/src/web/fonts/README-newpx.txt +473 -0
  91. package/vendor/latex-review/src/web/fonts/README.md +12 -0
  92. package/vendor/latex-review/src/web/fonts/TeXGyrePagellaX-Bold.otf +0 -0
  93. package/vendor/latex-review/src/web/fonts/TeXGyrePagellaX-Regular.otf +0 -0
  94. package/vendor/latex-review/src/web/index.html +17 -0
  95. package/vendor/latex-review/src/web/main.tsx +10 -0
  96. package/vendor/latex-review/src/web/model.ts +153 -0
  97. package/vendor/latex-review/src/web/overview.tsx +146 -0
  98. package/vendor/latex-review/src/web/review.tsx +991 -0
  99. package/vendor/latex-review/src/web/style.css +1249 -0
@@ -0,0 +1,66 @@
1
+ import { DatabaseSync, type SQLInputValue } from "node:sqlite";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ const SCHEMA = `
5
+ CREATE TABLE IF NOT EXISTS tasks(name TEXT PRIMARY KEY,title TEXT NOT NULL,worktree TEXT NOT NULL,base TEXT NOT NULL,accepted TEXT NOT NULL,scope TEXT NOT NULL,created REAL NOT NULL,closed REAL);
6
+ CREATE TABLE IF NOT EXISTS rounds(task TEXT NOT NULL,number INTEGER NOT NULL,before TEXT NOT NULL,after TEXT NOT NULL,units TEXT NOT NULL,summary TEXT NOT NULL,created REAL NOT NULL,PRIMARY KEY(task,number));
7
+ CREATE TABLE IF NOT EXISTS actions(seq INTEGER PRIMARY KEY AUTOINCREMENT,id TEXT NOT NULL UNIQUE,task TEXT NOT NULL,round INTEGER NOT NULL,kind TEXT NOT NULL,changes TEXT NOT NULL,body TEXT NOT NULL DEFAULT '',status TEXT NOT NULL,detail TEXT NOT NULL DEFAULT '{}',created REAL NOT NULL);
8
+ CREATE TABLE IF NOT EXISTS writes(action TEXT NOT NULL,phase TEXT NOT NULL,path TEXT NOT NULL,expected BLOB,target BLOB,PRIMARY KEY(action,phase,path));
9
+ CREATE TABLE IF NOT EXISTS messages(id INTEGER PRIMARY KEY AUTOINCREMENT,task TEXT NOT NULL,round INTEGER,kind TEXT NOT NULL,action TEXT,body TEXT NOT NULL DEFAULT '',data TEXT NOT NULL DEFAULT '{}',created REAL NOT NULL);
10
+ CREATE TABLE IF NOT EXISTS jobs(id TEXT PRIMARY KEY,task TEXT NOT NULL,status TEXT NOT NULL,phase TEXT NOT NULL DEFAULT '',result TEXT NOT NULL DEFAULT '{}',created REAL NOT NULL,finished REAL);
11
+ CREATE INDEX IF NOT EXISTS actions_by_task ON actions(task,round);
12
+ CREATE INDEX IF NOT EXISTS messages_by_task ON messages(task,id);
13
+ CREATE INDEX IF NOT EXISTS jobs_by_task ON jobs(task,created);`;
14
+ const JSON_COLUMNS = new Set([
15
+ "scope",
16
+ "units",
17
+ "summary",
18
+ "changes",
19
+ "detail",
20
+ "data",
21
+ "result",
22
+ ]);
23
+ function row<T>(value: Record<string, unknown> | undefined): T | null {
24
+ if (!value) return null;
25
+ for (const key of Object.keys(value)) {
26
+ if (JSON_COLUMNS.has(key)) value[key] = JSON.parse(value[key] as string);
27
+ else if (value[key] instanceof Uint8Array)
28
+ value[key] = Buffer.from(value[key]);
29
+ }
30
+ return value as T;
31
+ }
32
+ export class Store {
33
+ db: DatabaseSync;
34
+ constructor(public file: string) {
35
+ fs.mkdirSync(path.dirname(file), { recursive: true });
36
+ this.db = new DatabaseSync(file);
37
+ this.db.exec("PRAGMA busy_timeout=30000; PRAGMA journal_mode=WAL;");
38
+ this.db.exec(SCHEMA);
39
+ }
40
+ transaction<T>(work: () => T): T {
41
+ this.db.exec("BEGIN IMMEDIATE");
42
+ try {
43
+ const value = work();
44
+ this.db.exec("COMMIT");
45
+ return value;
46
+ } catch (error) {
47
+ this.db.exec("ROLLBACK");
48
+ throw error;
49
+ }
50
+ }
51
+ one<T>(query: string, ...args: SQLInputValue[]): T | null {
52
+ return row<T>(this.db.prepare(query).get(...args));
53
+ }
54
+ all<T>(query: string, ...args: SQLInputValue[]): T[] {
55
+ return this.db
56
+ .prepare(query)
57
+ .all(...args)
58
+ .map((r) => row<T>(r)!);
59
+ }
60
+ execute(query: string, ...args: SQLInputValue[]) {
61
+ return Number(this.db.prepare(query).run(...args).lastInsertRowid);
62
+ }
63
+ close() {
64
+ this.db.close();
65
+ }
66
+ }
@@ -0,0 +1,237 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { randomBytes } from "node:crypto";
4
+ import { Repo, cleanPath } from "./git.ts";
5
+ import { readJson, writeJson, now } from "./files.ts";
6
+ import { buildOutline, resolveSections, selectedRanges } from "./outline.ts";
7
+ import type { Task, Scope } from "../types.ts";
8
+ import type { App } from "../server.ts";
9
+ export const GENERATED = [
10
+ ".aux",
11
+ ".log",
12
+ ".out",
13
+ ".toc",
14
+ ".lof",
15
+ ".lot",
16
+ ".loe",
17
+ ".fls",
18
+ ".fdb_latexmk",
19
+ ".synctex.gz",
20
+ ".bbl",
21
+ ".blg",
22
+ ".bcf",
23
+ ".run.xml",
24
+ ".xdv",
25
+ ".dvi",
26
+ ".nav",
27
+ ".snm",
28
+ ".vrb",
29
+ ".idx",
30
+ ".ilg",
31
+ ".ind",
32
+ ];
33
+ const NAME = /^[a-z0-9][a-z0-9-]{0,63}$/;
34
+ export const slug = (text: string) =>
35
+ text
36
+ .toLowerCase()
37
+ .replace(/[^a-z0-9]+/g, "-")
38
+ .replace(/^-+|-+$/g, "")
39
+ .slice(0, 40)
40
+ .replace(/-+$/g, "");
41
+ export function taskMarker(folder: string): [string, string | null] {
42
+ const repo = new Repo(folder),
43
+ marker = readJson<{ root: string; task: string } | null>(
44
+ path.join(repo.gitDir(), "latex-review.json"),
45
+ null,
46
+ );
47
+ return marker ? [marker.root, marker.task] : [repo.toplevel(), null];
48
+ }
49
+ export function revisionReader(repo: Repo, rev: string, folder: string) {
50
+ const files = repo.files(rev, folder).filter((p) => p.endsWith(".tex")),
51
+ blobs = repo.blobs(rev, files);
52
+ return (file: string) => blobs[file]?.toString() ?? null;
53
+ }
54
+ export class Tasks {
55
+ constructor(public app: App) {}
56
+ get(name: string, openOnly = true): Task {
57
+ const task = this.app.store.one<Task>(
58
+ "SELECT * FROM tasks WHERE name=?",
59
+ name,
60
+ );
61
+ if (!task || (openOnly && task.closed))
62
+ throw new Error(`No open review task named ${JSON.stringify(name)}.`);
63
+ return task;
64
+ }
65
+ list() {
66
+ return this.app.store.all<Task>(
67
+ "SELECT * FROM tasks WHERE closed IS NULL ORDER BY created",
68
+ );
69
+ }
70
+ snapshot(task: Task) {
71
+ return new Repo(task.worktree).snapshot(
72
+ [this.app.project.source_dir, ...task.scope.paths],
73
+ GENERATED,
74
+ );
75
+ }
76
+ create(
77
+ name?: string,
78
+ title?: string,
79
+ sections: string[] = [],
80
+ files: string[] = [],
81
+ ): Task {
82
+ const { app } = this,
83
+ project = app.project;
84
+ if (sections.length && files.length)
85
+ throw new Error("Choose sections or files for the scope, not both.");
86
+ title = (title || name || "LaTeX review").trim().slice(0, 120);
87
+ const explicit = Boolean(name);
88
+ name ||= slug(title) || "review-" + randomBytes(3).toString("hex");
89
+ if (!NAME.test(name))
90
+ throw new Error(
91
+ "Task names use lowercase letters, digits and hyphens (at most 64 characters).",
92
+ );
93
+ let candidate = name,
94
+ suffix = 1;
95
+ for (;;) {
96
+ const existing = app.store.one<Task>(
97
+ "SELECT * FROM tasks WHERE name=?",
98
+ candidate,
99
+ );
100
+ if (!existing || existing.closed) break;
101
+ if (this.matches(existing, sections, files)) return existing;
102
+ if (explicit)
103
+ throw new Error(
104
+ `Task ${JSON.stringify(name)} already exists with a different scope. Finish it first or choose another name.`,
105
+ );
106
+ candidate = `${name.slice(0, 60)}-${++suffix}`;
107
+ }
108
+ name = candidate;
109
+ if (!app.repo.hasHead())
110
+ throw new Error(
111
+ "The repository needs at least one commit before a review can start.",
112
+ );
113
+ if (
114
+ app.repo.text(["for-each-ref", "--count=1", `refs/latex-review/${name}/`])
115
+ )
116
+ throw new Error(
117
+ `Task name ${JSON.stringify(name)} is still used by another working tree.`,
118
+ );
119
+ const worktree = path.join(project.worktrees, name);
120
+ if (fs.existsSync(worktree))
121
+ throw new Error(
122
+ `The folder ${worktree} already exists. Remove it or choose another name.`,
123
+ );
124
+ const paths = files.map(cleanPath),
125
+ base = app.repo.snapshot([project.source_dir, ...paths], GENERATED);
126
+ let scope: Scope;
127
+ if (sections.length) {
128
+ const outline = buildOutline(
129
+ revisionReader(app.repo, base, project.doc_dir),
130
+ project.main,
131
+ );
132
+ const [ranges, chosen] = selectedRanges(
133
+ outline.nodes,
134
+ resolveSections(outline.nodes, sections),
135
+ );
136
+ scope = { sections: chosen, ranges, paths: Object.keys(ranges).sort() };
137
+ } else
138
+ scope = {
139
+ sections: [],
140
+ ranges: null,
141
+ paths: paths.length ? paths : [project.source_dir],
142
+ };
143
+ fs.mkdirSync(path.dirname(worktree), { recursive: true });
144
+ app.repo.run(["worktree", "add", "--detach", worktree, base]);
145
+ try {
146
+ writeJson(path.join(new Repo(worktree).gitDir(), "latex-review.json"), {
147
+ task: name,
148
+ root: project.root,
149
+ });
150
+ app.repo.updateRef(`refs/latex-review/${name}/base`, base);
151
+ app.store.transaction(() => {
152
+ app.store.execute(
153
+ "DELETE FROM writes WHERE action IN (SELECT id FROM actions WHERE task=?)",
154
+ name,
155
+ );
156
+ app.store.execute("DELETE FROM tasks WHERE name=?", name);
157
+ for (const table of ["rounds", "actions", "messages", "jobs"])
158
+ app.store.execute(`DELETE FROM ${table} WHERE task=?`, name);
159
+ app.store.execute(
160
+ "INSERT INTO tasks(name,title,worktree,base,accepted,scope,created) VALUES(?,?,?,?,?,?,?)",
161
+ name,
162
+ title,
163
+ worktree,
164
+ base,
165
+ base,
166
+ JSON.stringify(scope),
167
+ now(),
168
+ );
169
+ });
170
+ } catch (error) {
171
+ app.repo.run(["worktree", "remove", "--force", worktree], {
172
+ check: false,
173
+ });
174
+ app.repo.deleteRefs(`refs/latex-review/${name}/`);
175
+ throw error;
176
+ }
177
+ return this.get(name);
178
+ }
179
+ matches(task: Task, sections: string[], files: string[]) {
180
+ if (!sections.length && !files.length) return true;
181
+ if (sections.length) {
182
+ const known = new Set(
183
+ task.scope.sections.flatMap((s) => [
184
+ s.id,
185
+ s.id.replace(/^label:/, ""),
186
+ s.number,
187
+ s.title,
188
+ ]),
189
+ );
190
+ return sections.every((s) => known.has(s.trim().replace(/^label:/, "")));
191
+ }
192
+ return (
193
+ task.scope.ranges === null &&
194
+ JSON.stringify(files.map(cleanPath).sort()) ===
195
+ JSON.stringify([...task.scope.paths].sort())
196
+ );
197
+ }
198
+ async finish(name: string, force = false) {
199
+ const app = this.app,
200
+ task = this.get(name);
201
+ if (app.publisher.busy(name))
202
+ throw new Error(
203
+ "A round is still being built. Wait for it before finishing.",
204
+ );
205
+ const counts = app.review.counts(name),
206
+ problems: string[] = [];
207
+ if (counts.pending || counts.commented)
208
+ problems.push(
209
+ `${counts.pending} pending and ${counts.commented} commented changes`,
210
+ );
211
+ const leftover = fs.existsSync(task.worktree)
212
+ ? app.repo.changedFiles(task.accepted, this.snapshot(task))
213
+ : [];
214
+ if (leftover.length)
215
+ problems.push(
216
+ "edits not in the manuscript in " + leftover.slice(0, 5).join(", "),
217
+ );
218
+ if (problems.length && !force)
219
+ throw new Error(
220
+ `The task still has ${problems.join("; ")}. Publish and finish the review first, or use --force to discard them.`,
221
+ );
222
+ if (fs.existsSync(task.worktree))
223
+ app.repo.run(["worktree", "remove", "--force", task.worktree]);
224
+ app.repo.run(["worktree", "prune"], { check: false });
225
+ app.repo.deleteRefs(`refs/latex-review/${name}/`);
226
+ fs.rmSync(path.join(app.state, "rounds", name), {
227
+ recursive: true,
228
+ force: true,
229
+ });
230
+ app.store.execute("UPDATE tasks SET closed=? WHERE name=?", now(), name);
231
+ await app.publisher.pruneCache();
232
+ return {
233
+ changed: app.repo.changedFiles(task.base, task.accepted),
234
+ discarded: counts,
235
+ };
236
+ }
237
+ }