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,97 @@
1
+ \section{Background}
2
+ \label{sec:background}
3
+
4
+ This section fixes notation for the four systems Papilot sits between: the
5
+ \TeX{} build, the PDF format, Git, and SyncTeX.
6
+
7
+ \subsection{The \LaTeX{} build as a fixed point}
8
+
9
+ \LaTeX{} resolves cross-references, citations and the table of contents by
10
+ reading auxiliary data written by the previous run~\cite{lamport1994}. Let
11
+ $x_k$ be the auxiliary state after run $k$ and $F$ the map computed by one run,
12
+ so that
13
+ \begin{equation}
14
+ x_{k+1} = F(x_k), \qquad x_0 = \varnothing .
15
+ \label{eq:fixed-point}
16
+ \end{equation}
17
+ A build tool such as \texttt{latexmk} repeats runs until a fixed point
18
+ $x^{\star} = F(x^{\star})$ is reached or a limit of $K$ runs is hit (five by
19
+ default). A single run suffices when $F(\varnothing) = \varnothing$, which is
20
+ the case only for documents without references. A typical paper converges in
21
+ two or three runs: the first records labels, the second resolves them, and a
22
+ third is needed only when resolving them moved a page break.
23
+
24
+ \begin{remark}
25
+ $F$ need not be a contraction. A reference whose resolution lengthens a line
26
+ can push a label onto the next page, whose page number shortens the line again;
27
+ the sequence $x_k$ then alternates between two states and the run limit ends
28
+ the build. Such documents are rare, but they are why a build key must include
29
+ the build settings and not only the sources (\cref{sec:build}).
30
+ \end{remark}
31
+
32
+ \subsection{Text in a PDF}
33
+
34
+ A PDF page places glyphs with a chain of matrices~\cite{iso32000}. The text
35
+ state contains the font size $T_{fs}$, the horizontal scaling $T_h$, the rise
36
+ $T_{\text{rise}}$, the character and word spacings $T_c$ and $T_w$, and the
37
+ text matrix $T_m$. A glyph is drawn in user space through the text rendering
38
+ matrix
39
+ \begin{equation}
40
+ T_{rm} =
41
+ \begin{pmatrix}
42
+ T_{fs} T_h & 0 & 0 \\
43
+ 0 & T_{fs} & 0 \\
44
+ 0 & T_{\text{rise}} & 1
45
+ \end{pmatrix}
46
+ \times T_m \times \mathrm{CTM},
47
+ \label{eq:trm}
48
+ \end{equation}
49
+ where $\mathrm{CTM}$ is the current transformation matrix. After each glyph
50
+ with horizontal advance $w_0$ (in thousandths of a text space unit) and a
51
+ positioning adjustment $T_j$, the text matrix moves by
52
+ \begin{equation}
53
+ t_x = \Bigl( \bigl(w_0 - \tfrac{T_j}{1000}\bigr) T_{fs} + T_c + T_w \Bigr) T_h ,
54
+ \qquad
55
+ T_m \leftarrow
56
+ \begin{pmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ t_x & 0 & 1 \end{pmatrix} T_m ,
57
+ \label{eq:advance}
58
+ \end{equation}
59
+ with $T_w$ applied only to the single-byte space character. \TeX{} rarely uses
60
+ $T_w$; it positions words with explicit adjustments $T_j$, which is why a PDF
61
+ produced from \TeX{} often contains no space characters at all.
62
+
63
+ \subsection{Git worktrees}
64
+
65
+ A Git repository is a store of immutable objects (blobs, trees and commits)
66
+ addressed by their hashes, together with a set of references~\cite{chacon2014}.
67
+ A \emph{worktree} is a checkout with its own working files $W_i$, index $I_i$
68
+ and head $\mathrm{HEAD}_i$, sharing the object store with every other worktree
69
+ of the same repository:
70
+ \begin{equation}
71
+ \mathcal{R} = \bigl(\mathcal{O},\ \mathrm{refs}\bigr), \qquad
72
+ w_i = \bigl(W_i,\ I_i,\ \mathrm{HEAD}_i\bigr), \quad i = 0, 1, \dots, t .
73
+ \end{equation}
74
+ The manuscript checkout is $w_0$; each task $i \ge 1$ gets its own $w_i$ on its
75
+ own branch. Because objects are shared, creating a worktree costs only the
76
+ working files, and because indexes are separate, a task can stage, diff and
77
+ build without touching $w_0$.
78
+
79
+ \subsection{SyncTeX}
80
+
81
+ SyncTeX records, while \TeX{} typesets, where each box, glue and kern came from:
82
+ an input file, a line and a column~\cite{laurens2008}. Positions are recorded
83
+ in \TeX{}'s scaled points. With
84
+ \begin{equation}
85
+ 1\,\mathrm{pt} = \tfrac{1}{72.27}\,\mathrm{in}, \qquad
86
+ 1\,\mathrm{bp} = \tfrac{1}{72}\,\mathrm{in}, \qquad
87
+ 1\,\mathrm{sp} = 2^{-16}\,\mathrm{pt},
88
+ \end{equation}
89
+ a horizontal position $h$ in scaled points becomes PDF points as
90
+ \begin{equation}
91
+ x = \bigl(h + h_{\text{origin}}\bigr) \cdot 2^{-16} \cdot \frac{72}{72.27},
92
+ \label{eq:sp-to-bp}
93
+ \end{equation}
94
+ where $h_{\text{origin}}$ is the one-inch offset of \TeX{}'s origin from the
95
+ page edge, expressed in scaled points. A reverse query (``edit'') returns the
96
+ record nearest to a point on the page; a forward query (``view'') returns the
97
+ boxes produced by a source line. Papilot uses only reverse queries.
@@ -0,0 +1,185 @@
1
+ \section{A model of review rounds}
2
+ \label{sec:model}
3
+
4
+ \subsection{Documents and edits}
5
+
6
+ We view a manuscript as a finite sequence of source characters over an
7
+ alphabet $\Sigma$, $\doc \in \Sigma^{*}$, of length $n = \abs{\doc}$. An
8
+ \emph{edit} replaces a half-open range of the source by new text.
9
+
10
+ \begin{definition}[Edit]
11
+ An edit is a triple $e = (a, b, w)$ with $0 \le a \le b \le n$ and
12
+ $w \in \Sigma^{*}$. Applying $e$ to $\doc$ gives
13
+ \begin{equation}
14
+ \doc \apply e = \doc_{[0,a)} \, w \, \doc_{[b,n)},
15
+ \label{eq:apply}
16
+ \end{equation}
17
+ whose length is $n + \delta(e)$ with \emph{shift} $\delta(e) = \abs{w} - (b - a)$.
18
+ \end{definition}
19
+
20
+ A task proposes a \emph{revision} $\edit = \{e_1, \dots, e_m\}$, a finite set
21
+ of edits against the same base document. Papilot groups nearby edits into
22
+ \emph{changes}, the units the author decides on.
23
+
24
+ \begin{definition}[Compatibility]
25
+ Two edits $e = (a, b, w)$ and $e' = (a', b', w')$ are \emph{compatible},
26
+ written $e \perp e'$, when their ranges are disjoint and not adjacent:
27
+ \begin{equation}
28
+ e \perp e' \iff b < a' \ \text{or}\ b' < a .
29
+ \label{eq:compatible}
30
+ \end{equation}
31
+ A revision is compatible when its edits are pairwise compatible.
32
+ \end{definition}
33
+
34
+ For a compatible revision we apply edits from right to left, so that earlier
35
+ offsets remain valid:
36
+ \begin{equation}
37
+ \doc \apply \edit
38
+ = \bigl(\cdots\bigl((\doc \apply e_{\sigma(1)}) \apply e_{\sigma(2)}\bigr)
39
+ \cdots\bigr) \apply e_{\sigma(m)},
40
+ \qquad a_{\sigma(1)} > a_{\sigma(2)} > \dots > a_{\sigma(m)} .
41
+ \label{eq:apply-revision}
42
+ \end{equation}
43
+
44
+ \begin{proposition}[Compatible edits commute]
45
+ \label{prop:commute}
46
+ If $e \perp e'$, then applying them right to left in either listing gives the
47
+ same document, and the result of \cref{eq:apply-revision} does not depend on
48
+ how the remaining edits of a compatible revision are ordered.
49
+ \end{proposition}
50
+
51
+ \begin{proof}
52
+ Assume without loss of generality $b < a'$. Applying $e'$ first changes only
53
+ positions at or after $a'$, so the range $[a, b)$ and every offset before it are
54
+ unchanged, and applying $e$ afterwards gives
55
+ $\doc_{[0,a)}\, w\, \doc_{[b,a')}\, w'\, \doc_{[b',n)}$. Applying $e$ first
56
+ shifts every later offset by $\delta(e)$; rewriting $e'$ as
57
+ $(a'+\delta(e), b'+\delta(e), w')$ yields the same string. Induction on the
58
+ number of edits, ordering them by start offset, gives the general statement.
59
+ \end{proof}
60
+
61
+ \subsection{Rebasing edits}
62
+
63
+ When one task's edit $e$ has been accepted, another task's edit $e'$, written
64
+ against the old document, must be moved onto the new one. The
65
+ \emph{transformation} of $e'$ past $e$ is
66
+ \begin{equation}
67
+ \tau(e', e) =
68
+ \begin{cases}
69
+ (a', b', w'), & b' < a,\\
70
+ \bigl(a' + \delta(e),\ b' + \delta(e),\ w'\bigr), & b < a',\\
71
+ \bot, & \text{otherwise,}
72
+ \end{cases}
73
+ \label{eq:transform}
74
+ \end{equation}
75
+ where $\bot$ signals a conflict that the author must resolve.
76
+
77
+ \begin{lemma}[Convergence of rebased edits]
78
+ \label{lem:tp1}
79
+ If $e \perp e'$, then
80
+ \begin{equation}
81
+ \doc \apply e \apply \tau(e', e) = \doc \apply e' \apply \tau(e, e') .
82
+ \label{eq:tp1}
83
+ \end{equation}
84
+ \end{lemma}
85
+
86
+ \begin{proof}
87
+ If $b < a'$, then $\tau(e, e') = e$ and $\tau(e', e)$ is $e'$ shifted by
88
+ $\delta(e)$, so the left side is the second computation in the proof of
89
+ \cref{prop:commute} and the right side the first. The case $b' < a$ is
90
+ symmetric.
91
+ \end{proof}
92
+
93
+ \Cref{eq:tp1} is the convergence property known from operational
94
+ transformation~\cite{ellis1989}. Papilot needs only the compatible case: an
95
+ incompatible pair is never merged automatically.
96
+
97
+ \subsection{Decisions and rounds}
98
+
99
+ Let $U$ be the set of changes in a round. A \emph{decision state} is a map
100
+ \begin{equation}
101
+ \theta : U \to \{\textsf{pending}, \textsf{accepted}, \textsf{rejected},
102
+ \textsf{commented}\},
103
+ \end{equation}
104
+ and the accepted part of a revision is
105
+ $\edit^{\theta} = \{ e \in \edit : \theta(u(e)) = \textsf{accepted} \}$.
106
+ The manuscript after round $t$ is
107
+ \begin{equation}
108
+ \doc_{t+1} =
109
+ \begin{cases}
110
+ \doc_t \apply \edit_t^{\theta_t}, & \text{if } \edit_t^{\theta_t}
111
+ \text{ applies cleanly to } \doc_t,\\[2pt]
112
+ \doc_t, & \text{otherwise, and the task is asked for a new proposal.}
113
+ \end{cases}
114
+ \label{eq:round}
115
+ \end{equation}
116
+ Rejected and commented changes return to the agent as feedback bound to their
117
+ round, file and lines; the agent must not reintroduce a rejected edit unless it
118
+ is explicitly asked to.
119
+
120
+ \paragraph{Actions and undo.}
121
+ An \emph{action} $\alpha = (\iota, u, d)$ carries an identifier $\iota$, a
122
+ change $u$ and a decision $d$. Applying it to a state gives
123
+ $\theta \cdot \alpha = \theta[u \mapsto d]$, and the history is a stack
124
+ $\alpha_1, \dots, \alpha_k$. Two properties make decisions safe over an
125
+ unreliable network:
126
+ \begin{align}
127
+ (\theta \cdot \alpha) \cdot \alpha &= \theta \cdot \alpha
128
+ && \text{(idempotence: a retried write changes nothing)},
129
+ \label{eq:idempotent}\\
130
+ \operatorname{undo}(\theta_0 \cdot \alpha_1 \cdots \alpha_k)
131
+ &= \theta_0 \cdot \alpha_1 \cdots \alpha_{k-1}
132
+ && \text{(undo reverses exactly the last action)}.
133
+ \label{eq:undo}
134
+ \end{align}
135
+ An undo request names the action it reverses, so a retried undo cannot reverse
136
+ a different one.
137
+
138
+ \subsection{How many rounds?}
139
+
140
+ Suppose that each change is accepted in a round independently with probability
141
+ $p \in (0,1]$, and otherwise revised and proposed again. The number of rounds
142
+ $R$ until a single change is accepted is geometric,
143
+ \begin{equation}
144
+ \Prob(R = r) = (1-p)^{r-1} p, \qquad
145
+ \E[R] = \frac{1}{p}, \qquad
146
+ \Var(R) = \frac{1-p}{p^{2}} .
147
+ \end{equation}
148
+ For $m$ independent changes the task ends when the last one is accepted, after
149
+ $R_{\max} = \max_i R_i$ rounds.
150
+
151
+ \begin{proposition}[Expected rounds]
152
+ \label{prop:rounds}
153
+ \begin{equation}
154
+ \E[R_{\max}]
155
+ = \sum_{k=1}^{m} (-1)^{k+1} \binom{m}{k} \frac{1}{1 - (1-p)^{k}} .
156
+ \label{eq:rounds}
157
+ \end{equation}
158
+ \end{proposition}
159
+
160
+ The proof is in \cref{app:proofs}. For small $p$ the sum behaves like
161
+ $\frac{1}{p} H_m$, where $H_m = \sum_{k=1}^{m} 1/k \approx \ln m + \gamma$ is
162
+ the $m$-th harmonic number: doubling the number of changes adds only about
163
+ $(\ln 2)/p$ rounds.
164
+
165
+ \begin{corollary}[Rounds with confidence]
166
+ \label{cor:rounds}
167
+ For every $\eta \in (0,1)$, the task ends within
168
+ \begin{equation}
169
+ r_{\eta} = \ceil*{ \frac{\ln(m/\eta)}{-\ln(1-p)} }
170
+ \label{eq:rounds-confidence}
171
+ \end{equation}
172
+ rounds with probability at least $1 - \eta$.
173
+ \end{corollary}
174
+
175
+ \begin{proof}
176
+ By the union bound, $\Prob(R_{\max} > r) \le m\, \Prob(R > r) = m (1-p)^{r}$,
177
+ which is at most $\eta$ as soon as $r \ge \ln(m/\eta) / (-\ln(1-p))$.
178
+ \end{proof}
179
+
180
+ \begin{example}
181
+ With $p = 0.8$ and $m = 5$, \cref{eq:rounds} gives
182
+ $\E[R_{\max}] \approx 1.91$ and \cref{eq:rounds-confidence} with $\eta = 0.05$
183
+ gives $r_{\eta} = 3$: most tasks finish in one or two rounds, and almost all
184
+ within three.
185
+ \end{example}
@@ -0,0 +1,99 @@
1
+ \section{Concurrent tasks and conflicts}
2
+ \label{sec:conflicts}
3
+
4
+ Papilot runs up to $c = 2$ tasks at once, each against the manuscript as it was
5
+ when the task started. Accepted text from one task can therefore overlap a
6
+ later proposal from another, and \cref{eq:transform} then returns $\bot$.
7
+ This section estimates how often that happens.
8
+
9
+ \subsection{Uniformly placed edits}
10
+
11
+ \begin{assumption}
12
+ \label{ass:uniform}
13
+ Each task proposes edits of a common length $\ell$ whose start positions are
14
+ independent and uniformly distributed over a document of length $n \gg \ell$.
15
+ \end{assumption}
16
+
17
+ \begin{proposition}[Conflict probability]
18
+ \label{prop:conflict}
19
+ Under \cref{ass:uniform}, let two tasks propose $k_1$ and $k_2$ edits. The
20
+ number $X$ of incompatible cross pairs has mean
21
+ \begin{equation}
22
+ \E[X] = \frac{(2\ell + 1)\, k_1 k_2}{n},
23
+ \label{eq:conflict-mean}
24
+ \end{equation}
25
+ and the probability that the tasks conflict at all is
26
+ \begin{equation}
27
+ \Prob(X > 0)
28
+ = 1 - \Bigl(1 - \frac{2\ell + 1}{n}\Bigr)^{k_1 k_2}
29
+ \approx 1 - \exp\Bigl(-\frac{(2\ell+1)\, k_1 k_2}{n}\Bigr).
30
+ \label{eq:conflict}
31
+ \end{equation}
32
+ \end{proposition}
33
+
34
+ \begin{proof}
35
+ Two edits of length $\ell$ starting at $s$ and $s'$ violate
36
+ \cref{eq:compatible} exactly when $\abs{s - s'} \le \ell$, an event of
37
+ probability $(2\ell+1)/n$ up to boundary effects of order $\ell/n$. Summing over
38
+ the $k_1 k_2$ cross pairs gives \cref{eq:conflict-mean}. Treating the pairs as
39
+ independent gives the product in \cref{eq:conflict}, and
40
+ $\log(1-x) = -x + O(x^2)$ gives the approximation, which is the Poisson
41
+ approximation $\Prob(X = 0) \approx e^{-\E[X]}$~\cite{feller1968}.
42
+ \end{proof}
43
+
44
+ For a $40$-page paper with $n \approx 1.2 \times 10^{5}$ characters, two tasks
45
+ of $k_1 = k_2 = 10$ edits of $\ell = 200$ characters have $\E[X] \approx 0.33$
46
+ and conflict with probability about $1 - e^{-0.33} \approx 0.28$.
47
+
48
+ \subsection{How much overlaps}
49
+
50
+ When two edits do conflict, the overlap is usually partial.
51
+
52
+ \begin{proposition}[Expected overlap]
53
+ \label{prop:overlap}
54
+ Conditioned on a conflict, the offset $d = s' - s$ is uniform on
55
+ $\{-\ell, \dots, \ell\}$ and the overlap $\ell - \abs{d}$ has mean
56
+ \begin{equation}
57
+ \E\bigl[\ell - \abs{d} \bigm| \abs{d} \le \ell\bigr]
58
+ = \frac{1}{2\ell + 1} \sum_{d=-\ell}^{\ell} \bigl(\ell - \abs{d}\bigr)
59
+ = \frac{\ell^{2}}{2\ell + 1}
60
+ \approx \frac{\ell}{2} .
61
+ \label{eq:overlap}
62
+ \end{equation}
63
+ \end{proposition}
64
+
65
+ \begin{proof}
66
+ The sum is $\ell + 2\sum_{j=1}^{\ell} (\ell - j) = \ell + \ell(\ell - 1) = \ell^2$.
67
+ \end{proof}
68
+
69
+ A typical conflict therefore involves about half of an edit, which matches the
70
+ experience that conflicting proposals usually touch the same sentence rather
71
+ than the same word.
72
+
73
+ \subsection{Clustered edits}
74
+
75
+ Real revisions are not uniform: an author working on the introduction asks for
76
+ several changes there. We model this by letting both tasks place their edits
77
+ uniformly within a common region covering a fraction $\nu \in (0, 1]$ of the
78
+ document.
79
+
80
+ \begin{corollary}[Clustering raises conflicts]
81
+ \label{cor:clustered}
82
+ If both tasks edit only within a region of length $\nu n$,
83
+ \begin{equation}
84
+ \Prob(X > 0) \approx 1 - \exp\Bigl(-\frac{(2\ell+1)\, k_1 k_2}{\nu n}\Bigr),
85
+ \label{eq:clustered}
86
+ \end{equation}
87
+ so that confining two tasks to a tenth of the paper multiplies the expected
88
+ number of conflicts by ten.
89
+ \end{corollary}
90
+
91
+ \begin{proof}
92
+ Apply \cref{prop:conflict} with $n$ replaced by $\nu n$.
93
+ \end{proof}
94
+
95
+ \Cref{cor:clustered} argues for fewer, longer tasks over many short ones that
96
+ touch the same part of a paper, and for finishing the review of one task
97
+ before starting another in the same section. It also explains why Papilot
98
+ surfaces conflicts from Git rather than resolving them: a resolution would
99
+ have to choose between two texts the author has not yet compared.
@@ -0,0 +1,70 @@
1
+ \section{Building the PDF}
2
+ \label{sec:build}
3
+
4
+ \subsection{Snapshots of the working tree}
5
+
6
+ The PDF view shows the manuscript exactly as it is on disk, including accepted
7
+ but uncommitted changes. Let $W$ be the working tree and $H$ the tree of
8
+ \texttt{HEAD}. A temporary index $I$ records the working tree without touching
9
+ the real index:
10
+ \begin{equation}
11
+ I \leftarrow \operatorname{read-tree}(H), \qquad
12
+ I \leftarrow I \cup \operatorname{add}(W), \qquad
13
+ T = \operatorname{write-tree}(I) .
14
+ \label{eq:snapshot}
15
+ \end{equation}
16
+ The tree $T$ is a content hash of every tracked and untracked, non-ignored
17
+ file. The build is cached under the key
18
+ \begin{equation}
19
+ \key(W) = h\bigl(T \,\|\, \text{build settings}\bigr),
20
+ \label{eq:key}
21
+ \end{equation}
22
+ where $h$ is a cryptographic hash and the settings name the engine and the
23
+ main file.
24
+
25
+ \begin{proposition}[Undo never recompiles]
26
+ \label{prop:undo-build}
27
+ Let $W_0$ be a working tree whose build has completed, and let a sequence of
28
+ decisions followed by their undos return the files to the same content. Then
29
+ the final working tree has the key of $W_0$ and its PDF is served from the
30
+ cache.
31
+ \end{proposition}
32
+
33
+ \begin{proof}
34
+ $T$ in \cref{eq:snapshot} depends only on file contents, so equal contents give
35
+ equal trees and, by \cref{eq:key}, equal keys.
36
+ \end{proof}
37
+
38
+ \subsection{When to build}
39
+
40
+ While the PDF view is open, Papilot checks the working tree every second and
41
+ a half during a build, every fifteen seconds otherwise, and whenever the window
42
+ regains focus. Let checks happen at rate at least $\varphi$, edits at rate
43
+ $\mu$ and builds take time $\beta$. A build that is running when the tree
44
+ changes is not cancelled; the first check after it finishes starts a build for
45
+ the newer key. An edit made just after a build started waits for that build,
46
+ then for the next check, then for its own build, so the PDF is never older than
47
+ \begin{equation}
48
+ \text{staleness} \le 2\beta + \frac{1}{\varphi} ,
49
+ \label{eq:staleness}
50
+ \end{equation}
51
+ and the number of builds per unit time is at most
52
+ $\min\bigl(\mu, \varphi, 1/\beta\bigr)$ in the long run: frequent edits do not
53
+ cause a backlog of builds, they only skip intermediate states.
54
+
55
+ \subsection{Reviewing changed pages}
56
+
57
+ A review compares the PDF of the round's base with the PDF after the task's
58
+ edits. Let the two documents have pages $P = (p_1, \dots, p_N)$ and
59
+ $P' = (p'_1, \dots, p'_M)$ with content hashes $h(p_i)$. Inserting a paragraph
60
+ shifts every later page, so comparing pages by index would mark almost
61
+ everything as changed. A robust comparison aligns the pages by a longest common
62
+ subsequence of their hashes,
63
+ \begin{equation}
64
+ \operatorname{LCS}(P, P') = \max\Bigl\{ \abs{S} : S \subseteq [N]\times[M]
65
+ \text{ increasing},\ h(p_i) = h(p'_j)\ \forall (i,j) \in S \Bigr\},
66
+ \end{equation}
67
+ computed in $O\bigl((N + M) D\bigr)$ time, where $D = N + M - 2\abs{S}$ is the
68
+ size of the edit script~\cite{myers1986}. Papilot's review goes further and
69
+ renders only the strips of each page around the changed source lines, so that
70
+ a one-word change is reviewed as one or two lines of typeset text.
@@ -0,0 +1,193 @@
1
+ \section{Geometry of the PDF view}
2
+ \label{sec:geometry}
3
+
4
+ \subsection{From a glyph to a pixel}
5
+
6
+ A PDF page lives in user space, in points ($1\,\mathrm{bp} = 1/72$\,in), with
7
+ the origin at the bottom left of its view box $[x_0, y_0, x_1, y_1]$. Following
8
+ the PDF convention we use row vectors. A glyph's origin is placed by the text
9
+ rendering matrix of \cref{eq:trm},
10
+ \begin{equation}
11
+ \begin{pmatrix} x & y & 1 \end{pmatrix}
12
+ = \begin{pmatrix} 0 & 0 & 1 \end{pmatrix} T_{rm} ,
13
+ \end{equation}
14
+ and the viewer draws the page at scale $s$ (device pixels per point) with the
15
+ origin at the top left, so for an unrotated page the viewport matrix is
16
+ \begin{equation}
17
+ V_s =
18
+ \begin{pmatrix}
19
+ s & 0 & 0 \\
20
+ 0 & -s & 0 \\
21
+ -s x_0 & s y_1 & 1
22
+ \end{pmatrix},
23
+ \qquad
24
+ \begin{pmatrix} u & v & 1 \end{pmatrix}
25
+ = \begin{pmatrix} x & y & 1 \end{pmatrix} V_s .
26
+ \label{eq:viewport}
27
+ \end{equation}
28
+ A page rotated by $\phi \in \{0, \tfrac{\pi}{2}, \pi, \tfrac{3\pi}{2}\}$ is
29
+ drawn with $V_s$ composed with a rotation about the page centre,
30
+ \begin{equation}
31
+ R_{\phi} =
32
+ \begin{pmatrix}
33
+ \cos\phi & \sin\phi & 0 \\
34
+ -\sin\phi & \cos\phi & 0 \\
35
+ 0 & 0 & 1
36
+ \end{pmatrix},
37
+ \end{equation}
38
+ so that the complete chain from a glyph to a pixel is
39
+ $(0\ 0\ 1)\, T_{rm}\, V_s\, R_{\phi}$ up to a translation. Zooming changes only
40
+ $s$, which is why Papilot stores positions on the page in normalised
41
+ coordinates $r = (\xi_0, \eta_0, \xi_1, \eta_1) \in [0,1]^4$ relative to the
42
+ view box:
43
+ \begin{equation}
44
+ \xi = \frac{x - x_0}{x_1 - x_0}, \qquad
45
+ \eta = \frac{y_1 - y}{y_1 - y_0} .
46
+ \label{eq:normalised}
47
+ \end{equation}
48
+
49
+ \subsection{How \TeX{} spaces a line}
50
+
51
+ \TeX{} sets a line of natural width $L_0$ in a box of width $L$ by stretching or
52
+ shrinking the glue between words~\cite{knuth1984}. If the line's glue has total
53
+ stretchability $Y$ and shrinkability $Z$, the \emph{adjustment ratio} is
54
+ \begin{equation}
55
+ r =
56
+ \begin{cases}
57
+ (L - L_0)/Y, & L \ge L_0,\\
58
+ (L - L_0)/Z, & L < L_0,
59
+ \end{cases}
60
+ \label{eq:ratio}
61
+ \end{equation}
62
+ and an interword space with natural width $g$, stretch $y$ and shrink $z$ is
63
+ set at width $g + r y$ or $g + r z$. The \emph{badness} of the line is
64
+ approximately
65
+ \begin{equation}
66
+ b(r) = \min\bigl(10000,\ 100\,\abs{r}^{3}\bigr),
67
+ \label{eq:badness}
68
+ \end{equation}
69
+ with $10000$ meaning infinitely bad. The Knuth--Plass algorithm chooses breaks
70
+ that minimise the total \emph{demerits}~\cite{knuth1981}, where a line with
71
+ badness $b$ ending at a break with penalty $p$ costs
72
+ \begin{equation}
73
+ d =
74
+ \begin{cases}
75
+ (l + b)^{2} + p^{2}, & 0 \le p < 10000,\\
76
+ (l + b)^{2} - p^{2}, & -10000 < p < 0,\\
77
+ (l + b)^{2}, & p \le -10000,
78
+ \end{cases}
79
+ \label{eq:demerits}
80
+ \end{equation}
81
+ and $l$ is the line penalty (ten by default). Because \cref{eq:demerits}
82
+ trades badness between lines, interword spaces vary from line to line, and
83
+ that variation is what a text layer must follow.
84
+
85
+ \subsection{Why a line-wide text layer drifts}
86
+
87
+ To make text selectable, a viewer overlays invisible text on the rendered page.
88
+ A common construction places one run per line in a stand-in font and stretches
89
+ it horizontally so that the run's ends meet the line's ends~\cite{pdfjs}. Let a
90
+ line consist of glyphs with true advances $a_1, \dots, a_N$ and stand-in
91
+ advances $b_1, \dots, b_N$. The stretch factor is
92
+ \begin{equation}
93
+ \sigma = \frac{\sum_{i=1}^{N} a_i}{\sum_{i=1}^{N} b_i},
94
+ \end{equation}
95
+ and the selectable position of glyph $k$ is off by
96
+ \begin{equation}
97
+ \varepsilon_k = \sum_{i<k} \delta_i, \qquad
98
+ \delta_i = \sigma b_i - a_i, \qquad
99
+ \varepsilon_1 = \varepsilon_{N+1} = 0 .
100
+ \label{eq:drift}
101
+ \end{equation}
102
+
103
+ \begin{theorem}[Mid-line drift]
104
+ \label{thm:drift}
105
+ If the discrepancies $\delta_1, \dots, \delta_N$ are exchangeable with
106
+ variance $v$, then $\sum_i \delta_i = 0$ forces
107
+ $\Cov(\delta_i, \delta_j) = -v/(N-1)$ for $i \ne j$, and
108
+ \begin{equation}
109
+ \Var(\varepsilon_k) = v\, \frac{(k-1)(N-k+1)}{N-1},
110
+ \qquad
111
+ \max_k \Var(\varepsilon_k) \approx \frac{v N}{4} .
112
+ \label{eq:bridge}
113
+ \end{equation}
114
+ \end{theorem}
115
+
116
+ The proof is in \cref{app:proofs}. \Cref{eq:bridge} is the variance of a
117
+ discrete Brownian bridge~\cite{billingsley1999}: the error vanishes at both
118
+ ends of the line, where the run is anchored, and is largest in the middle.
119
+
120
+ Justified lines add a deterministic bias. Every space is printed $g + r y$
121
+ wide but drawn $\sigma b_{\text{space}}$ wide in the stand-in font, so that
122
+ \begin{equation}
123
+ \beta_k = \bigl(g + r y - \sigma b_{\text{space}}\bigr) \cdot
124
+ \#\{\text{spaces before glyph } k\} .
125
+ \label{eq:bias}
126
+ \end{equation}
127
+ With \TeX{}'s interword glue at ten points, $g = 3.33$\,pt and $y = 1.67$\,pt,
128
+ a line set with $r = 0.6$ has spaces of $4.33$\,pt; if the stand-in font's
129
+ spaces come out at $3.0$\,pt after stretching, the bias grows by $1.33$\,pt per
130
+ space and reaches $8$\,pt, more than a letter and a half, after six words. Together,
131
+ \cref{eq:bridge,eq:bias} explain drifts of several characters in the middle of
132
+ a line, enough for a drag to select a neighbouring word.
133
+
134
+ \subsection{Per-word runs}
135
+
136
+ Papilot instead reads every word's printed place from the PDF: its left edge
137
+ $\lambda_j$, baseline $y_j$, size $f_j$ and width $w_j$. It places one run per
138
+ word, stretched to end where the next word begins, so that the run for word
139
+ $j$ covers glyphs $J_j$ and the space after them with its own factor
140
+ \begin{equation}
141
+ \sigma_j = \frac{\lambda_{j+1} - \lambda_j}
142
+ {\sum_{i \in J_j} b_i + b_{\text{space}}} .
143
+ \end{equation}
144
+
145
+ \begin{theorem}[Per-word drift]
146
+ \label{thm:per-word}
147
+ With per-word runs, every word starts exactly where it is printed, and the
148
+ error at a glyph $k$ of word $j$ satisfies
149
+ \begin{equation}
150
+ \abs{\varepsilon_k} \le \sum_{i \in J_j} \abs{\sigma_j b_i - a_i}
151
+ \le N_j \max_{i \in J_j} \abs{\sigma_j b_i - a_i},
152
+ \qquad
153
+ \Var(\varepsilon_k) \le \frac{v N_j}{4} ,
154
+ \label{eq:per-word}
155
+ \end{equation}
156
+ where $N_j = \abs{J_j}$. The bias \cref{eq:bias} does not accumulate across
157
+ words.
158
+ \end{theorem}
159
+
160
+ \begin{proof}
161
+ The run for word $j$ starts at $\lambda_j$, so $\varepsilon$ is zero at the
162
+ first glyph of every word. Within the word, \cref{eq:drift} applies with $N$
163
+ replaced by $N_j$ and $\sigma$ by $\sigma_j$, which gives the first two bounds
164
+ by the triangle inequality and the third by \cref{thm:drift}. Since every word
165
+ restarts at its printed position, no space contributes to the error of a later
166
+ word.
167
+ \end{proof}
168
+
169
+ For a line of $N = 80$ glyphs and words of $N_j \approx 6$, the typical error
170
+ falls by a factor of about $\sqrt{N/N_j} \approx 3.7$ and the bias disappears.
171
+ \Cref{fig:drift} compares the two layouts.
172
+
173
+ \begin{figure}[t]
174
+ \centering
175
+ \begin{tikzpicture}
176
+ \begin{axis}[
177
+ width=0.92\linewidth, height=5.2cm,
178
+ xlabel={glyph position $k$ in the line}, ylabel={$\sqrt{\Var(\varepsilon_k)/v}$},
179
+ xmin=0, xmax=80, ymin=0, ymax=5,
180
+ legend style={at={(0.5,1.03)}, anchor=south, legend columns=2, draw=none, font=\small, /tikz/every even column/.append style={column sep=1.2em}},
181
+ grid=major, grid style={gray!20}, tick label style={font=\small}, label style={font=\small}]
182
+ \addplot[blue!60!black, thick, domain=0:80, samples=161] {sqrt(x*(80-x)/79)};
183
+ \addlegendentry{one run per line}
184
+ \addplot[orange!80!black, thick, domain=0:80, samples=561] {sqrt(mod(x,7)*(7-mod(x,7))/6)};
185
+ \addlegendentry{one run per word}
186
+ \end{axis}
187
+ \end{tikzpicture}
188
+ \caption{Standard deviation of the selection error along a line of $80$ glyphs,
189
+ from \cref{eq:bridge}, for a single stretched run and for per-word runs with
190
+ words of six letters and a space. The per-word error returns to zero at every
191
+ word.}
192
+ \label{fig:drift}
193
+ \end{figure}