@erdemtuna/doc-review 0.8.1 → 0.10.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 (82) hide show
  1. package/README.md +52 -137
  2. package/{src → lib}/SKILL.md +28 -4
  3. package/lib/anchor-text.js +155 -0
  4. package/lib/atomic-write.js +54 -0
  5. package/lib/chrome-api.js +78 -0
  6. package/lib/chrome-client.js +2255 -0
  7. package/lib/chrome-session.js +76 -0
  8. package/{src → lib}/chrome.css +253 -8
  9. package/lib/chrome.html +196 -0
  10. package/lib/cli.js +255 -0
  11. package/lib/click-target.js +28 -0
  12. package/lib/comment-anchor.js +23 -0
  13. package/lib/comment-target.js +164 -0
  14. package/lib/comparison-view.js +380 -0
  15. package/lib/contracts/feedback.js +1 -0
  16. package/lib/contracts/frame.js +7 -0
  17. package/lib/contracts/history.js +1 -0
  18. package/lib/contracts/index.js +2 -0
  19. package/lib/contracts/page.js +23 -0
  20. package/lib/document-execution.js +115 -0
  21. package/lib/document-trust.js +2 -0
  22. package/lib/edit-limits.js +24 -0
  23. package/lib/editing.js +107 -0
  24. package/lib/execution-client.js +69 -0
  25. package/lib/feedback-controller.js +106 -0
  26. package/lib/frame-channel.js +42 -0
  27. package/lib/frame-controller.js +313 -0
  28. package/lib/frame-host.js +136 -0
  29. package/lib/frame-policy.js +50 -0
  30. package/lib/history-client.js +112 -0
  31. package/lib/history-coordinator.js +210 -0
  32. package/lib/history-policy.js +43 -0
  33. package/lib/history-server.js +464 -0
  34. package/lib/html-transform.js +52 -0
  35. package/lib/icons.js +249 -0
  36. package/{src → lib}/markdown.js +25 -26
  37. package/lib/paths.js +83 -0
  38. package/lib/poll-transport.js +220 -0
  39. package/lib/positioning.js +97 -0
  40. package/lib/review-controller.js +93 -0
  41. package/lib/review-mode.js +66 -0
  42. package/lib/revision-diff.js +482 -0
  43. package/lib/revision-schema.js +235 -0
  44. package/lib/revision-store.js +190 -0
  45. package/lib/save-controller.js +300 -0
  46. package/lib/sdk.js +2629 -0
  47. package/lib/semantic-snapshot.js +268 -0
  48. package/lib/serialize.js +32 -0
  49. package/lib/server-entry.js +26 -0
  50. package/lib/server-lock.js +109 -0
  51. package/lib/server.js +1501 -0
  52. package/lib/setup-guidance.js +119 -0
  53. package/{src → lib}/setup.js +44 -55
  54. package/lib/state.js +733 -0
  55. package/lib/view-identity.js +112 -0
  56. package/package.json +24 -12
  57. package/src/anchor-text.js +0 -160
  58. package/src/atomic-write.js +0 -53
  59. package/src/chrome-client.js +0 -2006
  60. package/src/chrome-session.js +0 -85
  61. package/src/chrome.html +0 -129
  62. package/src/cli.js +0 -267
  63. package/src/click-target.js +0 -26
  64. package/src/comment-anchor.js +0 -22
  65. package/src/comment-target.js +0 -77
  66. package/src/edit-limits.js +0 -23
  67. package/src/editing.js +0 -99
  68. package/src/frame-channel.js +0 -44
  69. package/src/frame-policy.js +0 -16
  70. package/src/html-transform.js +0 -62
  71. package/src/icons.js +0 -251
  72. package/src/paths.js +0 -91
  73. package/src/poll-transport.js +0 -222
  74. package/src/positioning.js +0 -107
  75. package/src/review-mode.js +0 -59
  76. package/src/sdk.js +0 -2243
  77. package/src/serialize.js +0 -32
  78. package/src/server-entry.js +0 -26
  79. package/src/server-lock.js +0 -101
  80. package/src/server.js +0 -1298
  81. package/src/setup-guidance.js +0 -128
  82. package/src/state.js +0 -507
@@ -1,222 +0,0 @@
1
- import http from "node:http";
2
- import { performance } from "node:perf_hooks";
3
-
4
- export const DEFAULT_POLL_SECONDS = 12 * 60 * 60;
5
- const MAX_TIMER_MS = 2 ** 31 - 1;
6
- const clock = {
7
- now: () => performance.now(),
8
- setTimeout: (fn, ms) => setTimeout(fn, ms),
9
- clearTimeout: (timer) => clearTimeout(timer),
10
- };
11
-
12
- function codedError(message, code) {
13
- return Object.assign(new Error(message), { code });
14
- }
15
-
16
- export function createDeadline(seconds = DEFAULT_POLL_SECONDS, time = clock) {
17
- const end = time.now() + seconds * 1000;
18
- const remaining = () => Math.max(0, end - time.now());
19
- const check = () => {
20
- if (remaining() <= 0) throw codedError("Polling deadline reached.", "POLL_DEADLINE");
21
- };
22
- return {
23
- seconds,
24
- time,
25
- remaining,
26
- check,
27
- async sleep(ms) {
28
- check();
29
- await new Promise((resolve) => time.setTimeout(resolve, Math.min(ms, remaining(), MAX_TIMER_MS)));
30
- check();
31
- },
32
- };
33
- }
34
-
35
- export function isRecoverableTransportError(err) {
36
- return new Set([
37
- "ECONNRESET", "ECONNREFUSED", "EPIPE", "ETIMEDOUT", "EHOSTUNREACH",
38
- "ENETUNREACH", "ENETDOWN", "ECONNABORTED", "ERR_STREAM_PREMATURE_CLOSE",
39
- "SERVER_START_PENDING",
40
- ]).has(err?.code);
41
- }
42
-
43
- /**
44
- * An absolute response deadline, not a socket-idle timeout: heartbeats and
45
- * partially received JSON must not extend the caller's waiting budget.
46
- */
47
- export function requestRaw(server, options, body, {
48
- timeoutMs = options.timeout,
49
- time = clock,
50
- signal,
51
- timeoutCode = "ETIMEDOUT",
52
- makeRequest = http.request,
53
- } = {}) {
54
- return new Promise((resolve, reject) => {
55
- let req;
56
- let res;
57
- let timer;
58
- let done = false;
59
- let ended = false;
60
- let raw = "";
61
- const expires = timeoutMs == null ? null : time.now() + timeoutMs;
62
- const premature = () => codedError("The server closed an incomplete response.", "ERR_STREAM_PREMATURE_CLOSE");
63
- const cleanupResponse = () => {
64
- res?.removeListener("data", onData);
65
- res?.removeListener("end", onEnd);
66
- res?.removeListener("aborted", onAborted);
67
- };
68
- const settle = (err, value) => {
69
- if (done) return;
70
- done = true;
71
- time.clearTimeout(timer);
72
- signal?.removeEventListener("abort", onAbort);
73
- cleanupResponse();
74
- // Keep error handlers until close: destroying a socket may emit one last
75
- // error asynchronously. Close removes the remaining owned listeners.
76
- res?.destroy();
77
- req?.destroy();
78
- if (err) reject(err);
79
- else resolve(value);
80
- };
81
- const onError = (err) => settle(err);
82
- const onData = (chunk) => { raw += chunk; };
83
- const onEnd = () => {
84
- ended = true;
85
- if (!res.complete) return settle(premature());
86
- settle(null, { status: res.statusCode, raw });
87
- };
88
- const onAborted = () => settle(premature());
89
- const onResponseClose = () => {
90
- if (!ended) settle(premature());
91
- cleanupResponse();
92
- res.removeListener("error", onError);
93
- res.removeListener("close", onResponseClose);
94
- };
95
- const onRequestClose = () => {
96
- if (!done) settle(premature());
97
- req.removeListener("error", onError);
98
- req.removeListener("close", onRequestClose);
99
- req.removeListener("response", onResponse);
100
- };
101
- const onAbort = () => settle(codedError("Polling cancelled.", "ABORT_ERR"));
102
- const armTimer = () => {
103
- const left = expires - time.now();
104
- if (left <= 0) return settle(codedError("Polling request timed out.", timeoutCode));
105
- timer = time.setTimeout(armTimer, Math.min(left, MAX_TIMER_MS));
106
- };
107
- const onResponse = (response) => {
108
- res = response;
109
- if (done) {
110
- res.destroy();
111
- return;
112
- }
113
- res.setEncoding("utf8");
114
- res.on("data", onData);
115
- res.on("end", onEnd);
116
- res.on("error", onError);
117
- res.on("aborted", onAborted);
118
- res.on("close", onResponseClose);
119
- };
120
- try {
121
- if (signal?.aborted) return onAbort();
122
- const port = typeof server === "number" ? server : server.port;
123
- const token = typeof server === "number" ? "" : server.token || "";
124
- const { timeout: _timeout, ...requestOptions } = options;
125
- req = makeRequest({
126
- host: "127.0.0.1",
127
- port,
128
- ...requestOptions,
129
- headers: { ...(token ? { "x-doc-review-token": token } : {}), ...(options.headers || {}) },
130
- });
131
- req.on("response", onResponse);
132
- req.on("error", onError);
133
- req.on("close", onRequestClose);
134
- signal?.addEventListener("abort", onAbort, { once: true });
135
- if (expires !== null) armTimer();
136
- if (done) return;
137
- if (body) req.write(JSON.stringify(body));
138
- req.end();
139
- } catch (err) {
140
- settle(err);
141
- }
142
- });
143
- }
144
-
145
- export function parseServerResponse(response) {
146
- if (response.status !== 200) {
147
- let detail = "";
148
- try { detail = JSON.parse(response.raw).error || ""; } catch { /* HTTP status is sufficient. */ }
149
- throw codedError(
150
- `Doc-review server returned HTTP ${response.status}${detail ? `: ${detail}` : "."}`,
151
- "SERVER_RESPONSE_ERROR",
152
- );
153
- }
154
- try {
155
- const parsed = JSON.parse(response.raw);
156
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error();
157
- return parsed;
158
- } catch {
159
- throw codedError("Malformed response from the doc-review server. End the review and restart the server.", "SERVER_RESPONSE_INVALID");
160
- }
161
- }
162
-
163
- export async function pollOnce(server, target, ackId, deadline) {
164
- deadline.check();
165
- const query = `target=${encodeURIComponent(target)}${ackId ? `&ack=${encodeURIComponent(ackId)}` : ""}`;
166
- const response = await requestRaw(server, {
167
- method: "GET",
168
- path: `/api/poll?${query}`,
169
- }, undefined, {
170
- timeoutMs: deadline.remaining(),
171
- time: deadline.time,
172
- timeoutCode: "POLL_DEADLINE",
173
- });
174
- // Graceful server disposal ends the space heartbeats without a JSON payload.
175
- // HTTP is complete, but this poll was interrupted just like a dropped socket.
176
- if (response.status === 200 && /^ +$/.test(response.raw)) {
177
- throw codedError("The server ended the poll before sending feedback.", "ERR_STREAM_PREMATURE_CLOSE");
178
- }
179
- const batch = parseServerResponse(response);
180
- if (!["feedback", "closed", "timeout"].includes(batch.status) ||
181
- (batch.status === "feedback" && (typeof batch.batch_id !== "string" || !batch.batch_id || !Array.isArray(batch.pages)))) {
182
- throw codedError("Malformed polling response from the doc-review server.", "SERVER_RESPONSE_INVALID");
183
- }
184
- return batch;
185
- }
186
-
187
- export async function pollUntilDeadline({
188
- target,
189
- ackId = "",
190
- deadline = createDeadline(),
191
- discover,
192
- poll = pollOnce,
193
- diagnostic = () => {},
194
- }) {
195
- let failures = 0;
196
- for (;;) {
197
- try {
198
- deadline.check();
199
- const server = await discover(deadline);
200
- deadline.check();
201
- // An uncertain send must repeat the supplied receipt, never a newer ID.
202
- const batch = await poll(server, target, ackId, deadline);
203
- deadline.check();
204
- return batch;
205
- } catch (err) {
206
- if (err.code === "POLL_DEADLINE" || deadline.remaining() <= 0) {
207
- return {
208
- status: "timeout",
209
- waited_seconds: deadline.seconds,
210
- next_step: "No feedback yet. Run the same poll command again to keep waiting, or `doc-review status <target>` to check without blocking.",
211
- };
212
- }
213
- if (!isRecoverableTransportError(err)) throw err;
214
- diagnostic(`Lost the connection (${err.message}); retrying.\n`);
215
- try {
216
- await deadline.sleep(Math.min(250 * 2 ** Math.min(failures++, 5), 5000));
217
- } catch (sleepError) {
218
- if (sleepError.code !== "POLL_DEADLINE") throw sleepError;
219
- }
220
- }
221
- }
222
- }
@@ -1,107 +0,0 @@
1
- const GAP = 12;
2
-
3
- export function visibleViewport(win = window) {
4
- const viewport = win.visualViewport;
5
- return viewport
6
- ? { left: viewport.offsetLeft, top: viewport.offsetTop, width: viewport.width, height: viewport.height }
7
- : { left: 0, top: 0, width: win.innerWidth, height: win.innerHeight };
8
- }
9
-
10
- export function lastVisibleRect(rects) {
11
- if (!Array.isArray(rects)) return null;
12
- for (let index = rects.length - 1; index >= 0; index -= 1) {
13
- const rect = rects[index];
14
- if (rect && rect.width >= 0 && rect.height >= 0) return rect;
15
- }
16
- return null;
17
- }
18
-
19
- const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
20
-
21
- function frameRectToChrome(rect, frameRect) {
22
- return {
23
- left: frameRect.left + rect.left,
24
- right: frameRect.left + rect.right,
25
- top: frameRect.top + rect.top,
26
- bottom: frameRect.top + rect.bottom,
27
- };
28
- }
29
-
30
- export function placeContextualSurface(geometry, options) {
31
- const {
32
- frameRect,
33
- viewport,
34
- surfaceWidth = 340,
35
- surfaceHeight = 220,
36
- narrow = false,
37
- toolbarHeight = 48,
38
- } = options;
39
- const bottomSheet = () => ({
40
- kind: "sheet",
41
- left: viewport.left,
42
- top: viewport.top + viewport.height - Math.min(surfaceHeight, viewport.height),
43
- width: viewport.width,
44
- });
45
- if (!geometry || geometry.relation === "unavailable" || !geometry.clip) return { kind: "hidden" };
46
- if (narrow) return bottomSheet();
47
- const clip = frameRectToChrome(geometry.clip, frameRect);
48
- const minLeft = viewport.left + GAP;
49
- const maxLeft = viewport.left + viewport.width - surfaceWidth - GAP;
50
- const viewportMinTop = viewport.top + toolbarHeight + GAP;
51
- const minTop = Math.max(viewportMinTop, clip.top + GAP);
52
- const maxTop = viewport.top + viewport.height - surfaceHeight - GAP;
53
- const clipBottomTop = clip.bottom - surfaceHeight - GAP;
54
- const horizontal = Number.isFinite(geometry.horizontal)
55
- ? frameRect.left + geometry.horizontal
56
- : clip.left;
57
- const edgeLeft = clamp(horizontal + GAP, minLeft, maxLeft);
58
-
59
- if (geometry.relation === "above") {
60
- return { kind: "edge-top", left: edgeLeft, top: minTop, width: surfaceWidth };
61
- }
62
- if (geometry.relation === "below") {
63
- return {
64
- kind: "edge-bottom",
65
- left: edgeLeft,
66
- top: Math.max(viewportMinTop, Math.min(maxTop, clipBottomTop)),
67
- width: surfaceWidth,
68
- };
69
- }
70
-
71
- const target = lastVisibleRect(geometry.rects);
72
- if (!target) return { kind: "hidden" };
73
- const rect = frameRectToChrome(target, frameRect);
74
- const attachedMaxTop = Math.min(maxTop, clipBottomTop);
75
- const clampTop = (top) => clamp(top, minTop, Math.max(minTop, attachedMaxTop));
76
- const candidates = [
77
- { left: rect.right + GAP, top: clampTop(rect.bottom - surfaceHeight) },
78
- { left: rect.left - surfaceWidth - GAP, top: clampTop(rect.bottom - surfaceHeight) },
79
- { left: clamp(rect.left, minLeft, maxLeft), top: rect.bottom + GAP },
80
- { left: clamp(rect.left, minLeft, maxLeft), top: rect.top - surfaceHeight - GAP },
81
- ];
82
- const fit = candidates.find((candidate) =>
83
- candidate.left >= minLeft &&
84
- candidate.left <= maxLeft &&
85
- candidate.top >= minTop &&
86
- candidate.top <= attachedMaxTop &&
87
- candidate.left + surfaceWidth <= Math.min(viewport.left + viewport.width - GAP, clip.right) &&
88
- candidate.top + surfaceHeight <= clip.bottom
89
- );
90
- const fallback = fit || {
91
- left: clamp(rect.right + GAP, minLeft, maxLeft),
92
- top: clampTop(rect.top),
93
- };
94
- return { kind: "attached", ...fallback, width: surfaceWidth };
95
- }
96
-
97
- export function alignedCardPosition(rects, { frameRect, viewport, width = 300, height = 180 }) {
98
- const target = lastVisibleRect(rects);
99
- if (!target) return null;
100
- const top = frameRect.top + target.top;
101
- const right = frameRect.left + target.right;
102
- if (top < frameRect.top || top > frameRect.bottom || right < frameRect.left || right > frameRect.right) return null;
103
- return {
104
- left: Math.max(viewport.left + 12, Math.min(viewport.left + viewport.width - width - 12, right + 12)),
105
- top: Math.max(viewport.top + 12, Math.min(viewport.top + viewport.height - height - 12, top)),
106
- };
107
- }
@@ -1,59 +0,0 @@
1
- export const REVIEW_MODES = Object.freeze(["view", "edit"]);
2
-
3
- export function normalizeReviewMode(mode) {
4
- return mode === "edit" ? "edit" : "view";
5
- }
6
-
7
- export function savePolicyForPage(page) {
8
- return page && (page.kind === "url" || page.markdown || page.feedbackOnly)
9
- ? "feedback-only"
10
- : "writable";
11
- }
12
-
13
- export function reviewConfiguration(page, mode) {
14
- return {
15
- mode: normalizeReviewMode(mode),
16
- savePolicy: savePolicyForPage(page),
17
- };
18
- }
19
-
20
- export function applyBodyReviewMode(body, mode) {
21
- const normalized = normalizeReviewMode(mode);
22
- if (normalized === "edit") {
23
- if (body.getAttribute("contenteditable") !== "true") body.setAttribute("contenteditable", "true");
24
- if (body.getAttribute("spellcheck") !== "false") body.setAttribute("spellcheck", "false");
25
- } else {
26
- if (body.hasAttribute("contenteditable")) body.removeAttribute("contenteditable");
27
- if (body.hasAttribute("spellcheck")) body.removeAttribute("spellcheck");
28
- }
29
- return normalized;
30
- }
31
-
32
- /** Keep the active mode intact when an application hydrates and rewrites body attributes. */
33
- export function keepBodyInReviewMode(body, initialMode = "view") {
34
- let mode = normalizeReviewMode(initialMode);
35
- let applying = false;
36
- const enforce = () => {
37
- if (applying) return;
38
- applying = true;
39
- applyBodyReviewMode(body, mode);
40
- applying = false;
41
- };
42
- const Observer = body.ownerDocument.defaultView.MutationObserver;
43
- const observer = new Observer(enforce);
44
- observer.observe(body, { attributes: true, attributeFilter: ["contenteditable", "spellcheck"] });
45
- enforce();
46
- return {
47
- get mode() {
48
- return mode;
49
- },
50
- setMode(nextMode) {
51
- mode = normalizeReviewMode(nextMode);
52
- enforce();
53
- return mode;
54
- },
55
- disconnect() {
56
- observer.disconnect();
57
- },
58
- };
59
- }