papilot 0.0.0-stage → 0.1.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.
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 +11 -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 +530 -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,104 @@
1
+ \section{Notes on the PDF}
2
+ \label{sec:notes}
3
+
4
+ An author can also ask for changes directly on the PDF: select text, or mark a
5
+ region around a figure or an equation, and write a note. All pending notes go
6
+ to the agent as one task. For that task to be useful, each note must name the
7
+ source it refers to.
8
+
9
+ \subsection{From a mark to source lines}
10
+
11
+ A note is a set of rectangles $\{r_1, \dots, r_q\}$ in the normalised
12
+ coordinates of \cref{eq:normalised} on page $P$, together with a comment. For
13
+ every printed line the selection touches, Papilot asks SyncTeX for the source
14
+ position of the first and the last marked character~\cite{laurens2008}:
15
+ \begin{equation}
16
+ \mathcal{S}(P, x, y) = (\text{file}, \text{line}),
17
+ \qquad
18
+ L = \{\, \mathcal{S}(P, c) : c \in C_{\text{first}} \cup C_{\text{last}} \,\},
19
+ \qquad
20
+ [\ell_{-}, \ell_{+}] = [\min L, \max L].
21
+ \label{eq:synctex-range}
22
+ \end{equation}
23
+
24
+ SyncTeX records boxes and glue rather than characters. The space before a word
25
+ that begins a printed line comes from the line break of the \emph{previous}
26
+ source line, so such a word is credited to that line. Papilot therefore
27
+ \emph{settles} each end of the selection by matching its printed context
28
+ against the source near SyncTeX's answer.
29
+
30
+ \subsection{Settling a selection}
31
+
32
+ Let $\letters(\cdot)$ map text to its lowercase letters and digits, after
33
+ Unicode compatibility normalisation, with \TeX{} control sequences and comments
34
+ removed from the source. For a source window $[\ell_{-} - 4, \ell_{+} + 4]$ we
35
+ form the string $S = \letters(\text{lines})$ and remember for each position the
36
+ line it came from, $\pi : \{1, \dots, \abs{S}\} \to \N$. The distance of a
37
+ line from SyncTeX's range is
38
+ \begin{equation}
39
+ \dist(\ell) = \max\bigl(0,\ \ell_{-} - \ell,\ \ell - \ell_{+}\bigr).
40
+ \label{eq:line-distance}
41
+ \end{equation}
42
+
43
+ \begin{algorithm}[t]
44
+ \caption{Settling one end of a selection}
45
+ \label{alg:settle}
46
+ \begin{algorithmic}[1]
47
+ \Require printed line $p$, index $\alpha$ of the marked letter in $\letters(p)$, source string $S$ with line map $\pi$
48
+ \For{$\rho \in (\infty, 24, 10)$} \Comment{whole line first, then shorter context}
49
+ \State $o_0 \gets \max(0, \alpha - \rho)$
50
+ \State $q \gets \letters(p)_{[o_0,\ \alpha + \rho]}$
51
+ \State $O \gets \{ o : S_{[o,\, o + \abs{q})} = q \}$
52
+ \If{$O \neq \emptyset$}
53
+ \State $o^{\star} \gets \argmin_{o \in O} \dist\bigl(\pi(o + \alpha - o_0)\bigr)$
54
+ \State \Return $\pi\bigl(o^{\star} + \alpha - o_0\bigr)$
55
+ \EndIf
56
+ \EndFor
57
+ \State \Return SyncTeX's line \Comment{formulas and other unmatched text}
58
+ \end{algorithmic}
59
+ \end{algorithm}
60
+
61
+ \Cref{alg:settle} runs once for the first and once for the last marked
62
+ character. If the settled range is empty or inverted, Papilot keeps SyncTeX's
63
+ range. Because the comparison uses letters only, subscripts, macros and
64
+ hyphenation do not prevent a match; formulas, whose printed form shares no
65
+ letters with their source, fall back to SyncTeX by design.
66
+
67
+ \subsection{When is a match unique?}
68
+
69
+ \Cref{alg:settle} picks the occurrence nearest to SyncTeX's answer, so a
70
+ spurious occurrence matters only if it is closer than the true one. Model the
71
+ letters of the source as a stationary source with entropy $\mathcal{H}$ bits per letter.
72
+ The expected number of chance occurrences of a context of $\abs{q}$ letters in
73
+ a window of $\abs{S}$ letters is about
74
+ \begin{equation}
75
+ \E[\text{spurious}] \approx \abs{S} \cdot 2^{-\mathcal{H} \abs{q}} .
76
+ \label{eq:spurious}
77
+ \end{equation}
78
+ Estimates of the entropy of English range from about one to four bits per
79
+ letter depending on how much context is used~\cite{shannon1951}. Even with the
80
+ pessimistic $\mathcal{H} = 1$, a context of $\abs{q} = 21$ letters (the shortest one
81
+ \cref{alg:settle} tries) in a window of $\abs{S} = 2000$ letters gives
82
+ $\E[\text{spurious}] \approx 2000 \cdot 2^{-21} \approx 10^{-3}$. Spurious
83
+ matches come instead from genuine repetition, such as a phrase used twice in
84
+ nearby paragraphs, which is exactly the case the distance
85
+ \cref{eq:line-distance} resolves.
86
+
87
+ \subsection{Selecting whole words}
88
+
89
+ Let a drag select characters $[s, t)$ of the per-word text layer of
90
+ \cref{thm:per-word}. Papilot widens the selection to whole words, but only at
91
+ a boundary that cuts through a word:
92
+ \begin{equation}
93
+ s' = \begin{cases}
94
+ \max\{ i \le s : \text{$i = 0$ or $c_{i-1}$ is not a letter} \}, &
95
+ c_s \text{ is a letter},\\
96
+ s, & \text{otherwise},
97
+ \end{cases}
98
+ \label{eq:whole-words}
99
+ \end{equation}
100
+ and symmetrically for $t$. A drag that ends at the start of the next word, as
101
+ browsers often report, therefore keeps exactly the words it covers. The note
102
+ stores the widened quote, one rectangle per printed line, and the source lines
103
+ of \cref{alg:settle} together with an excerpt of their \LaTeX{}, so that the
104
+ agent receives formulas as source rather than as extracted text.
@@ -0,0 +1,106 @@
1
+ \section{Agents and scheduling}
2
+ \label{sec:agents}
3
+
4
+ \subsection{One protocol for three agents}
5
+
6
+ Every agent is driven through the same small protocol: a thread is started or
7
+ resumed, a turn is started with the author's text, the agent streams items
8
+ (messages, commands, file changes, questions), and the turn completes, is
9
+ interrupted, or fails. Formally a task moves through the states of
10
+ \cref{fig:states}.
11
+
12
+ \begin{figure}[t]
13
+ \centering
14
+ \begin{tikzpicture}[
15
+ >={Stealth[length=2.2mm]}, node distance=2.1cm and 2.6cm,
16
+ every state/.style={draw=blue!50!black, thick, fill=blue!4, rounded corners=3pt,
17
+ rectangle, minimum width=1.9cm, minimum height=0.8cm, font=\small\sffamily},
18
+ every edge/.style={draw, ->, thick, gray!70!black},
19
+ lbl/.style={font=\scriptsize\sffamily, fill=white, inner sep=1pt}]
20
+ \node[state, initial, initial text={}] (idle) {idle};
21
+ \node[state, right=of idle] (queued) {queued};
22
+ \node[state, right=of queued] (running) {running};
23
+ \node[state, below=1.6cm of running] (building) {building};
24
+ \node[state, left=of building] (reviewing) {reviewing};
25
+ \node[state, above=1.5cm of running] (waiting) {waiting input};
26
+ \node[state, above=1.5cm of idle] (paused) {paused};
27
+ \path (idle) edge node[lbl, above] {instructions} (queued)
28
+ (queued) edge node[lbl, above] {slot free} (running)
29
+ (running) edge[bend right=28] node[lbl, right] {approval} (waiting)
30
+ (waiting) edge[bend right=28] node[lbl, left] {answer} (running)
31
+ (running) edge node[lbl, right] {turn done} (building)
32
+ (building) edge node[lbl, below] {published} (reviewing)
33
+ (reviewing) edge node[lbl, left] {feedback} (queued)
34
+ (running) edge[out=150, in=0, looseness=0.9] node[lbl, above, pos=0.45] {pause} (paused)
35
+ (paused) edge node[lbl, left] {resume} (idle);
36
+ \end{tikzpicture}
37
+ \caption{Task states. A turn that fails or whose outcome is uncertain leaves the
38
+ task in a recoverable state with a \emph{Check status} action (not shown).}
39
+ \label{fig:states}
40
+ \end{figure}
41
+
42
+ Codex speaks this protocol natively. Claude runs through the Claude Agent SDK,
43
+ and Antigravity runs the \texttt{agy} command line once per turn; both are
44
+ translated by small adapters into the same requests and notifications. Each
45
+ agent uses its own login on the host, and Papilot never reads, stores or
46
+ forwards a credential. An agent that cannot ask for permission, such as a
47
+ headless command line, is told in advance which tools it may not use.
48
+
49
+ \subsection{Effort and cost}
50
+
51
+ A reasoning effort $\epsilon$ from the ordered set
52
+ \begin{equation}
53
+ \mathcal{E} = \{\textsf{low} < \textsf{medium} < \textsf{high} <
54
+ \textsf{xhigh} < \textsf{max}\}
55
+ \end{equation}
56
+ is chosen per task and remembered per agent. If a turn at effort $\epsilon$
57
+ consumes on average $\kappa(\epsilon)$ tokens and produces an accepted change
58
+ with probability $q(\epsilon)$, the number of turns until acceptance is
59
+ geometric and the expected cost of an accepted change is
60
+ \begin{equation}
61
+ \operatorname{cost}(\epsilon) = \frac{\kappa(\epsilon)}{q(\epsilon)}
62
+ \cdot \operatorname{price}, \qquad
63
+ \epsilon^{\star} = \argmin_{\epsilon \in \mathcal{E}}
64
+ \operatorname{cost}(\epsilon).
65
+ \label{eq:effort-cost}
66
+ \end{equation}
67
+ A lower effort is cheaper only if
68
+ $\kappa(\epsilon) / \kappa(\epsilon') < q(\epsilon) / q(\epsilon')$: a failed
69
+ turn must be paid for again, and so must the author's review of it.
70
+
71
+ \subsection{Queueing}
72
+
73
+ Tasks arrive at rate $\nu$ and occupy a slot for a mean time $1/\omega$. With
74
+ $c$ slots, exponential times and offered load $A = \nu/\omega < c$, Papilot is
75
+ an $M/M/c$ queue~\cite{kleinrock1975}. The probability that a new task waits is
76
+ given by the Erlang~C formula~\cite{erlang1917}:
77
+ \begin{equation}
78
+ C(c, A) =
79
+ \frac{\dfrac{A^{c}}{c!}\,\dfrac{c}{c - A}}
80
+ {\displaystyle\sum_{k=0}^{c-1} \frac{A^{k}}{k!}
81
+ + \frac{A^{c}}{c!}\,\frac{c}{c - A}},
82
+ \qquad
83
+ \E[\text{wait}] = \frac{C(c, A)}{c\,\omega - \nu} .
84
+ \label{eq:erlang}
85
+ \end{equation}
86
+ For $c = 2$ this simplifies to $C(2, A) = A^2 / (2 + A)$. An author who starts
87
+ a task every ten minutes, each taking six minutes, has $A = 0.6$ and waits
88
+ with probability about $0.14$, for $0.14 / (2/6 - 1/10) \approx 0.6$ minutes on
89
+ average. By Little's law~\cite{little1961}, the mean number of tasks in the
90
+ system is
91
+ \begin{equation}
92
+ \bar{N} = \nu \Bigl( \E[\text{wait}] + \frac{1}{\omega} \Bigr) .
93
+ \label{eq:little}
94
+ \end{equation}
95
+
96
+ \subsection{Approvals}
97
+
98
+ A turn that asks for approval stops until the author answers. If approvals
99
+ arrive at rate $\zeta$ during a turn of working time $T_w$ and the author
100
+ answers after a mean delay $\Theta$, the expected wall-clock time of the turn is
101
+ \begin{equation}
102
+ \E[T] = T_w \bigl(1 + \zeta \Theta\bigr),
103
+ \label{eq:approvals}
104
+ \end{equation}
105
+ which is why Papilot lets agents edit and run sandboxed commands in the task's
106
+ worktree without asking, and asks only for what leaves it.
@@ -0,0 +1,56 @@
1
+ \section{Usage windows}
2
+ \label{sec:usage}
3
+
4
+ \subsection{Utilisation}
5
+
6
+ Agent subscriptions meter usage over rolling windows, typically a five-hour
7
+ window and a weekly one. If $r(\tau)$ is the rate at which a plan's allowance
8
+ is consumed, the utilisation of a window of length $w$ at time $t$ is
9
+ \begin{equation}
10
+ u_w(t) = \frac{1}{B_w} \int_{t-w}^{t} r(\tau)\, d\tau \in [0, 1],
11
+ \label{eq:utilisation}
12
+ \end{equation}
13
+ where $B_w$ is the window's budget. Papilot shows $u_w$ for each window the
14
+ agent reports, coloured by thresholds
15
+ \begin{equation}
16
+ \operatorname{colour}(u) =
17
+ \begin{cases}
18
+ \text{blue}, & u < 0.75,\\
19
+ \text{amber}, & 0.75 \le u < 0.9,\\
20
+ \text{red}, & u \ge 0.9 \ \text{or the limit is reached},
21
+ \end{cases}
22
+ \end{equation}
23
+ together with the reset time: as a countdown when it is less than a day away,
24
+ and as a day and time otherwise.
25
+
26
+ \subsection{Will the limit be reached?}
27
+
28
+ At a constant rate $\bar r$, the time left before the limit is
29
+ \begin{equation}
30
+ t_{\text{left}} = \frac{\bigl(1 - u_w(t)\bigr) B_w}{\bar r} ;
31
+ \end{equation}
32
+ with the weekly window at $u = 0.83$ and an even pace over the week,
33
+ $\bar r = B_w / 7$\,days and about $1.2$ days of work remain.
34
+
35
+ Usage is lumpy, however: it arrives in turns. Model turns as a Poisson process
36
+ of rate $\lambda$ with independent costs $J$ of mean $\mu_J$ and variance
37
+ $s_J^2$. The usage $S(t)$ over the next $t$ is compound Poisson, with
38
+ \begin{equation}
39
+ \E[S(t)] = \lambda t \mu_J, \qquad
40
+ \Var\bigl(S(t)\bigr) = \lambda t \bigl(\mu_J^2 + s_J^2\bigr)
41
+ \label{eq:compound}
42
+ \end{equation}
43
+ by Wald's identity and the law of total variance~\cite{feller1968}. With $t_r$ the time until the window
44
+ resets, the normal approximation gives the probability of reaching the limit
45
+ before the reset:
46
+ \begin{equation}
47
+ \Prob\bigl(S(t_r) > (1 - u) B_w\bigr) \approx
48
+ 1 - \Phi\!\left(
49
+ \frac{(1 - u) B_w - \lambda t_r \mu_J}
50
+ {\sqrt{\lambda t_r (\mu_J^2 + s_J^2)}}
51
+ \right),
52
+ \label{eq:limit-probability}
53
+ \end{equation}
54
+ where $\Phi$ is the standard normal distribution function. Large, rare turns
55
+ (high $s_J$) make the limit more likely than the mean alone suggests, which is
56
+ one more reason to prefer a lower effort for routine edits.
@@ -0,0 +1,62 @@
1
+ \section{Closing the loop with Git}
2
+ \label{sec:git}
3
+
4
+ Accepted changes land in the working tree without a commit, so that the author
5
+ decides what history to write. The Git panel shows the branch and how it
6
+ compares with its upstream, the last commit, and the uncommitted changes with
7
+ their diffs; from it the author commits chosen files, fetches, pulls and
8
+ pushes.
9
+
10
+ \subsection{Committing chosen files}
11
+
12
+ Write a tree as a map from paths to contents and, for a set of paths $P$, let
13
+ $X|_P$ be the restriction of $X$ to $P$ and $\overline{P}$ the complement. Let
14
+ $\mathrm{HEAD}$ have tree $H$, let $I$ be the index and $W$ the working tree,
15
+ and let $P$ be the set of paths the author ticks.
16
+
17
+ \begin{proposition}[Partial commits]
18
+ \label{prop:partial}
19
+ Committing $P$ produces the tree and leaves the index as
20
+ \begin{equation}
21
+ T_{c} = H|_{\overline{P}} \cup W|_{P},
22
+ \qquad
23
+ I' = I|_{\overline{P}} \cup W|_{P},
24
+ \label{eq:partial}
25
+ \end{equation}
26
+ that is, exactly the chosen files as they are on disk are committed, while
27
+ anything staged for other paths stays staged and out of the commit. A rename
28
+ from $\pi$ to $\pi'$ contributes both $\pi$ and $\pi'$ to $P$.
29
+ \end{proposition}
30
+
31
+ The proof, which follows the semantics of \texttt{git add} followed by
32
+ \texttt{git commit --only}, is in \cref{app:proofs}. Paths are passed to Git
33
+ literally, so that a file name cannot act as a pattern, and new \LaTeX{} build
34
+ output (\texttt{.aux}, \texttt{.log} and similar files) starts unticked.
35
+
36
+ \subsection{Synchronising with the remote}
37
+
38
+ Let $\anc(X)$ be the set of commits reachable from $X$, including $X$. With
39
+ local head $L$ and upstream $U$, the branch is ahead and behind by
40
+ \begin{equation}
41
+ \operatorname{ahead} = \abs{\anc(L) \setminus \anc(U)}, \qquad
42
+ \operatorname{behind} = \abs{\anc(U) \setminus \anc(L)},
43
+ \label{eq:ahead-behind}
44
+ \end{equation}
45
+ the two numbers shown beside the branch name. Pull is a fast-forward only. It
46
+ succeeds exactly when
47
+ \begin{equation}
48
+ \anc(L) \subseteq \anc(U)
49
+ \iff \operatorname{merge-base}(L, U) = L
50
+ \iff \operatorname{ahead} = 0,
51
+ \label{eq:fast-forward}
52
+ \end{equation}
53
+ in which case $L \leftarrow U$; otherwise Papilot reports the divergence and
54
+ never merges or rebases on the author's behalf. Push is the mirror image and
55
+ succeeds when $\operatorname{behind} = 0$. A branch without an upstream is
56
+ published with its first push.
57
+
58
+ \begin{remark}
59
+ Fetch, pull and push use the host's own Git credentials with prompts disabled
60
+ and a time limit, so that a missing credential produces Git's own error message
61
+ instead of a request that hangs.
62
+ \end{remark}
@@ -0,0 +1,41 @@
1
+ \section{Security}
2
+ \label{sec:security}
3
+
4
+ Papilot runs on the author's own computer and can be reached from other
5
+ devices over a private network. Its threat model is a web page in another tab
6
+ that tries to act on the author's behalf, and a device on the network that is
7
+ not the author's.
8
+
9
+ \subsection{Requests}
10
+
11
+ Every state-changing request must carry a token $\chi$ of $256$ random bits,
12
+ issued with the page and compared in constant time, which defends against
13
+ cross-site request forgery~\cite{barth2008}. A remote request must also come
14
+ from the configured origin and, over the private network, from the configured
15
+ owner. Writing $\rho$ for a request,
16
+ \begin{equation}
17
+ \operatorname{allow}(\rho) =
18
+ \bigl(\operatorname{safe}(\rho) \lor \operatorname{token}(\rho) = \chi\bigr)
19
+ \land \operatorname{origin}(\rho) \in \mathcal{O}
20
+ \land \operatorname{owner}(\rho) = o ,
21
+ \label{eq:allow}
22
+ \end{equation}
23
+ where $\operatorname{safe}(\rho)$ holds for requests that only read. A guess
24
+ succeeds with probability $2^{-256}$, and a constant-time comparison leaks no
25
+ prefix of $\chi$ through timing.
26
+
27
+ \subsection{Agents}
28
+
29
+ Agents run with the author's own logins but within limits Papilot sets:
30
+ \begin{itemize}
31
+ \item each task's agent works in its own worktree $w_i$ and is told never to
32
+ edit the manuscript checkout $w_0$, other worktrees, or Papilot itself;
33
+ \item commands run in the agent's sandbox, and anything that leaves it is
34
+ either brought to the author for approval or, for an agent that cannot
35
+ ask, declined in advance;
36
+ \item only the author accepts changes into $w_0$, through the review of
37
+ \cref{sec:model}.
38
+ \end{itemize}
39
+ The last point is the one that matters most: even an agent that ignored its
40
+ instructions could not change the manuscript without a decision the author
41
+ made while looking at the typeset result.
@@ -0,0 +1,106 @@
1
+ \section{A model-based evaluation}
2
+ \label{sec:evaluation}
3
+
4
+ This section evaluates the models of the previous sections numerically. Every
5
+ number below is computed from a formula in this paper; none is measured. The
6
+ purpose is to show what the formulas imply for realistic parameters, not to
7
+ report an empirical study.
8
+
9
+ \subsection{Rounds per task}
10
+
11
+ \Cref{tab:rounds} evaluates \cref{prop:rounds} and \cref{cor:rounds} for a
12
+ range of acceptance probabilities and numbers of changes. Two patterns stand
13
+ out. The expected number of rounds grows only logarithmically with the number
14
+ of changes, as the harmonic-number approximation predicts. And the
15
+ 95\,\% bound $r_{0.05}$ is at most two and a half times the mean in every case
16
+ shown, so that at $p = 0.7$ a task with five changes that is still open after
17
+ four rounds is among the slowest five per cent, and worth rewriting rather than
18
+ reviewing again.
19
+
20
+ \begin{table}[t]
21
+ \centering
22
+ \caption{Expected rounds $\E[R_{\max}]$ from \cref{eq:rounds}, and in
23
+ parentheses the rounds $r_{0.05}$ within which a task ends with probability
24
+ at least $0.95$ from \cref{eq:rounds-confidence}.}
25
+ \label{tab:rounds}
26
+ \begin{tabular}{@{}lccccc@{}}
27
+ \toprule
28
+ & \multicolumn{5}{c}{changes per task $m$} \\
29
+ \cmidrule(l){2-6}
30
+ acceptance $p$ & 1 & 2 & 5 & 10 & 20 \\
31
+ \midrule
32
+ $0.5$ & 2.00 (5) & 2.67 (6) & 3.79 (7) & 4.73 (8) & 5.69 (9) \\
33
+ $0.7$ & 1.43 (3) & 1.76 (4) & 2.39 (4) & 2.93 (5) & 3.49 (5) \\
34
+ $0.9$ & 1.11 (2) & 1.21 (2) & 1.46 (2) & 1.76 (3) & 2.08 (3) \\
35
+ \bottomrule
36
+ \end{tabular}
37
+ \end{table}
38
+
39
+ \subsection{Conflicts}
40
+
41
+ \Cref{tab:conflicts} evaluates \cref{eq:conflict} for two concurrent tasks with
42
+ edits of $\ell = 200$ characters. A $10$-page paper has about
43
+ $3 \times 10^{4}$ characters of source, a $40$-page paper about
44
+ $1.2 \times 10^{5}$ and a thesis about $4.8 \times 10^{5}$. By
45
+ \cref{cor:clustered}, confining both tasks to a quarter of a document moves a
46
+ row up one line.
47
+
48
+ \begin{table}[t]
49
+ \centering
50
+ \caption{Probability that two concurrent tasks with $k$ edits each conflict,
51
+ from \cref{eq:conflict} with $\ell = 200$.}
52
+ \label{tab:conflicts}
53
+ \begin{tabular}{@{}lccc@{}}
54
+ \toprule
55
+ & \multicolumn{3}{c}{edits per task $k$} \\
56
+ \cmidrule(l){2-4}
57
+ source length $n$ & 5 & 10 & 20 \\
58
+ \midrule
59
+ $3.0 \times 10^{4}$ (short paper) & 0.284 & 0.737 & 0.995 \\
60
+ $1.2 \times 10^{5}$ (long paper) & 0.080 & 0.284 & 0.737 \\
61
+ $4.8 \times 10^{5}$ (thesis) & 0.021 & 0.080 & 0.284 \\
62
+ \bottomrule
63
+ \end{tabular}
64
+ \end{table}
65
+
66
+ \subsection{Waiting for a slot}
67
+
68
+ \Cref{fig:erlang} plots the waiting probability \cref{eq:erlang} against the
69
+ offered load for one, two and three slots. At the load $A = 0.6$ of the example
70
+ in \cref{sec:agents}, a second slot lowers the probability of waiting from
71
+ $0.6$ to $0.14$, and a third would lower it to $0.025$. Two slots are a good
72
+ compromise between waiting and the conflicts of \cref{tab:conflicts}, which
73
+ grow with the number of tasks that run at once.
74
+
75
+ \begin{figure}[t]
76
+ \centering
77
+ \begin{tikzpicture}
78
+ \begin{axis}[
79
+ width=0.92\linewidth, height=5.4cm,
80
+ xlabel={offered load $A = \nu/\omega$}, ylabel={$C(c, A)$},
81
+ xmin=0, xmax=1.5, ymin=0, ymax=1,
82
+ legend style={at={(0.02,0.97)}, anchor=north west, draw=none, fill=white, font=\small},
83
+ grid=major, grid style={gray!20}, tick label style={font=\small}, label style={font=\small}]
84
+ \addplot[gray!70!black, thick, dashed, domain=0:0.999, samples=60] {x};
85
+ \addlegendentry{$c = 1$}
86
+ \addplot[blue!60!black, thick, domain=0:1.5, samples=60] {x^2/(2+x)};
87
+ \addlegendentry{$c = 2$}
88
+ \addplot[orange!80!black, thick, domain=0:1.5, samples=60]
89
+ {(x^3/6*3/(3-x))/(1+x+x^2/2+x^3/6*3/(3-x))};
90
+ \addlegendentry{$c = 3$}
91
+ \addplot[only marks, mark=*, mark size=1.6pt, blue!60!black] coordinates {(0.6,0.138)};
92
+ \end{axis}
93
+ \end{tikzpicture}
94
+ \caption{Probability that a new task waits for a slot, from the Erlang~C
95
+ formula \cref{eq:erlang}. The dot marks the example of \cref{sec:agents}.}
96
+ \label{fig:erlang}
97
+ \end{figure}
98
+
99
+ \subsection{Selection error}
100
+
101
+ For the text layer, \cref{fig:drift} already compares the two layouts. In
102
+ numbers: with $v = (0.4\,\mathrm{pt})^2$ per glyph, a line of $80$ glyphs has a
103
+ mid-line standard deviation of $0.4\sqrt{80/4} \approx 1.8$\,pt from
104
+ \cref{eq:bridge} alone, plus the bias of \cref{eq:bias}, which reaches several
105
+ points; per-word runs of seven glyphs bound it by $0.4\sqrt{7/4} \approx
106
+ 0.53$\,pt, well under the width of one letter.
@@ -0,0 +1,38 @@
1
+ \section{Related work}
2
+ \label{sec:related}
3
+
4
+ \paragraph{Differences between documents.}
5
+ Line-based differences go back to Hunt and McIlroy~\cite{hunt1976} and Myers's
6
+ $O(ND)$ algorithm~\cite{myers1986}, which Git uses by default.
7
+ \texttt{latexdiff}~\cite{latexdiff} marks up the differences between two
8
+ \LaTeX{} sources so that they can be typeset together. Papilot instead
9
+ typesets each version separately and shows the corresponding strips of the
10
+ two PDFs one above the other, which keeps the typeset result of each version
11
+ exact at the cost of a second build.
12
+
13
+ \paragraph{Concurrent editing.}
14
+ Operational transformation~\cite{ellis1989} and conflict-free replicated data
15
+ types~\cite{shapiro2011} let several writers edit a shared document and converge
16
+ without coordination. Papilot's setting is different: proposals are not applied
17
+ until an author accepts them, so convergence is a property of accepted edits
18
+ only, and \cref{lem:tp1} needs to hold only for compatible pairs. Incompatible
19
+ pairs are surfaced to the author rather than transformed.
20
+
21
+ \paragraph{Line breaking and text layers.}
22
+ The Knuth--Plass algorithm~\cite{knuth1981} is the reason \LaTeX{} spaces vary
23
+ from line to line, and with it the drift analysed in \cref{sec:geometry}. PDF
24
+ viewers such as PDF.js~\cite{pdfjs} place an invisible text layer over the
25
+ rendered page; Papilot uses PDF.js for rendering, search and links, and
26
+ replaces only the placement of its text with the per-word runs of
27
+ \cref{thm:per-word}.
28
+
29
+ \paragraph{Source synchronisation.}
30
+ SyncTeX~\cite{laurens2008} maps between typeset positions and source lines and
31
+ underlies forward and inverse search in most \TeX{} editors. Papilot uses its
32
+ reverse queries and corrects their line-boundary error with
33
+ \cref{alg:settle}.
34
+
35
+ \paragraph{Queueing.}
36
+ The waiting-time results of \cref{sec:agents} are classical; we use Erlang's
37
+ formula~\cite{erlang1917}, Little's law~\cite{little1961} and the treatment of
38
+ $M/M/c$ queues in Kleinrock~\cite{kleinrock1975}.
@@ -0,0 +1,27 @@
1
+ \section{Discussion and conclusion}
2
+ \label{sec:conclusion}
3
+
4
+ \subsection{Limitations}
5
+
6
+ Three limitations are worth stating. First, \cref{prop:conflict} assumes
7
+ uniformly placed edits; real revisions cluster around the parts of a paper an
8
+ author is working on, which by \cref{cor:clustered} raises the conflict rate
9
+ and argues for fewer, longer tasks over many short ones. Second,
10
+ \cref{alg:settle} cannot settle formulas, since their printed form shares no
11
+ letters with their source; for those, SyncTeX's lines are kept and the excerpt
12
+ shows the whole display. Third, an agent that cannot ask for permission, such
13
+ as a headless command line, must be restricted in advance; Papilot runs such
14
+ agents so that they edit only the task's worktree, which also means they cannot
15
+ compile or test their own changes.
16
+
17
+ \subsection{Conclusion}
18
+
19
+ Papilot treats agent editing as a sequence of reviewable rounds. Isolation
20
+ makes proposals safe, typeset review makes them legible, and explicit
21
+ acceptance keeps the author in charge of the paper. The models in this paper
22
+ make those properties checkable: compatible edits commute and rebase
23
+ (\cref{prop:commute,lem:tp1}), tasks need few rounds
24
+ (\cref{prop:rounds,cor:rounds}), undo never recompiles
25
+ (\cref{prop:undo-build}), per-word text bounds the selection error
26
+ (\cref{thm:per-word}), and partial commits keep unrelated staged work intact
27
+ (\cref{prop:partial}). They also give the reader something worth reviewing.
package/package.json CHANGED
@@ -1,6 +1,87 @@
1
1
  {
2
2
  "name": "papilot",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "A Cursor-like workspace for your papers: annotate and review PDFs directly without ever touching LaTeX.",
5
+ "keywords": [
6
+ "latex",
7
+ "pdf",
8
+ "diff",
9
+ "review",
10
+ "ai",
11
+ "agent",
12
+ "codex",
13
+ "claude",
14
+ "antigravity",
15
+ "writing",
16
+ "academic",
17
+ "manuscript"
18
+ ],
19
+ "homepage": "https://github.com/cu1ch3n/papilot#readme",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/cu1ch3n/papilot.git"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/cu1ch3n/papilot/issues"
26
+ },
27
+ "license": "MIT",
28
+ "author": "Cui Chen",
29
+ "type": "module",
30
+ "bin": {
31
+ "papilot": "bin/papilot.mjs"
32
+ },
33
+ "files": [
34
+ "bin",
35
+ "src/server",
36
+ "src/shared.ts",
37
+ "src/notes.ts",
38
+ "dist",
39
+ "scripts/demo.ts",
40
+ "scripts/service.ts",
41
+ "examples/papilot-paper",
42
+ "vendor/latex-review/src",
43
+ "vendor/latex-review/dist",
44
+ "vendor/latex-review/package.json",
45
+ "vendor/latex-review/package-lock.json",
46
+ "vendor/latex-review/LICENSE"
47
+ ],
48
+ "engines": {
49
+ "node": ">=22.22.0"
50
+ },
51
+ "scripts": {
52
+ "dev": "tsx watch src/server/main.ts",
53
+ "start": "tsx src/server/main.ts",
54
+ "build": "tsc --noEmit && vite build",
55
+ "typecheck": "tsc --noEmit",
56
+ "test": "tsx --test tests/*.test.ts",
57
+ "setup:review": "npm --prefix vendor/latex-review ci && npm --prefix vendor/latex-review run build",
58
+ "demo": "tsx scripts/demo.ts",
59
+ "service:install": "tsx scripts/service.ts install",
60
+ "service:stop": "tsx scripts/service.ts stop",
61
+ "test:ui": "npm run build && node scripts/check-ui.mjs",
62
+ "prepack": "npm run build && npm run setup:review"
63
+ },
64
+ "dependencies": {
65
+ "@fastify/compress": "^9.2.2",
66
+ "@fastify/static": "^10.1.7",
67
+ "commander": "^15.0.0",
68
+ "diff": "^9.0.0",
69
+ "fastify": "^5.12.7",
70
+ "mupdf": "^1.28.1",
71
+ "proper-lockfile": "^4.1.2",
72
+ "tsx": "^4.23.15"
73
+ },
74
+ "devDependencies": {
75
+ "@anthropic-ai/claude-agent-sdk": "^0.3.311",
76
+ "@openai/codex": "0.164.0",
77
+ "@types/node": "^22.20.5",
78
+ "@types/react": "^19.3.0",
79
+ "@types/react-dom": "^19.3.0",
80
+ "@vitejs/plugin-react": "^6.1.2",
81
+ "pdfjs-dist": "^6.4.299",
82
+ "react": "^19.3.1",
83
+ "react-dom": "^19.3.1",
84
+ "typescript": "^7.0.2",
85
+ "vite": "^8.3.5"
86
+ }
87
+ }
@@ -0,0 +1,21 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFileSync } from 'node:child_process';
4
+ import { DATA, ROOT } from '../src/server/paths.ts';
5
+ import { Store } from '../src/server/store.ts';
6
+ import { Workspace } from '../src/server/workspace.ts';
7
+
8
+ // The example is Papilot's own README written as a paper, with plenty of
9
+ // mathematics to review; it is copied into its own Git repository.
10
+ const folder = path.join(DATA, 'example-paper');
11
+ if (!fs.existsSync(folder)) {
12
+ fs.cpSync(path.join(ROOT, 'examples/papilot-paper'), folder, { recursive: true });
13
+ execFileSync('git', ['init', '-q', folder]);
14
+ execFileSync('git', ['-C', folder, 'add', '.']);
15
+ execFileSync('git', ['-C', folder, '-c', 'user.name=Papilot Example', '-c', 'user.email=papilot@localhost', 'commit', '-qm', 'Add example paper']);
16
+ }
17
+ const store = new Store(path.join(DATA, 'papilot.sqlite'));
18
+ const workspace = new Workspace(store);
19
+ const project = await workspace.addProject(folder, 'Papilot paper');
20
+ console.log(`Example paper ready: ${project.name} (${folder})`);
21
+ store.close();