@erdemtuna/doc-review 0.9.0 → 0.11.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 (103) hide show
  1. package/README.md +58 -222
  2. package/{src → lib}/SKILL.md +5 -2
  3. package/lib/anchor-text.js +155 -0
  4. package/lib/atomic-write.js +54 -0
  5. package/lib/changes-controller.js +184 -0
  6. package/lib/chrome-api.js +78 -0
  7. package/lib/chrome-client.js +1700 -0
  8. package/lib/chrome-session.js +73 -0
  9. package/lib/chrome.html +53 -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/comments-controller.js +109 -0
  15. package/lib/contextual-controller.js +33 -0
  16. package/lib/contracts/feedback.js +1 -0
  17. package/lib/contracts/frame.js +7 -0
  18. package/lib/contracts/history.js +1 -0
  19. package/lib/contracts/index.js +2 -0
  20. package/lib/contracts/page.js +23 -0
  21. package/lib/controller-store.js +96 -0
  22. package/lib/document-execution.js +115 -0
  23. package/lib/edit-limits.js +24 -0
  24. package/lib/editing.js +107 -0
  25. package/lib/execution-client.js +19 -0
  26. package/lib/feedback-controller.js +113 -0
  27. package/lib/feedback-panel-controller.js +161 -0
  28. package/lib/frame-channel.js +42 -0
  29. package/lib/frame-controller.js +313 -0
  30. package/lib/frame-host.js +136 -0
  31. package/lib/frame-policy.js +50 -0
  32. package/lib/history-client.js +112 -0
  33. package/lib/history-coordinator.js +217 -0
  34. package/lib/history-policy.js +43 -0
  35. package/lib/history-server.js +464 -0
  36. package/lib/html-transform.js +52 -0
  37. package/lib/icons.js +325 -0
  38. package/{src → lib}/markdown.js +25 -26
  39. package/lib/paths.js +83 -0
  40. package/lib/poll-transport.js +220 -0
  41. package/lib/positioning.js +97 -0
  42. package/lib/recovery-controller.js +123 -0
  43. package/lib/review-controller.js +93 -0
  44. package/lib/review-mode.js +66 -0
  45. package/lib/revision-diff.js +482 -0
  46. package/lib/revision-schema.js +235 -0
  47. package/lib/revision-store.js +190 -0
  48. package/lib/save-controller.js +303 -0
  49. package/lib/sdk.js +2629 -0
  50. package/lib/semantic-snapshot.js +268 -0
  51. package/lib/serialize.js +32 -0
  52. package/lib/server-entry.js +26 -0
  53. package/lib/server-lock.js +109 -0
  54. package/lib/server.js +1501 -0
  55. package/lib/setup-guidance.js +119 -0
  56. package/{src → lib}/setup.js +44 -55
  57. package/lib/state.js +733 -0
  58. package/lib/toolbar-controller.js +32 -0
  59. package/lib/ui/THIRD_PARTY_NOTICES.md +1254 -0
  60. package/lib/ui/chrome.css +4881 -0
  61. package/lib/ui/chrome.js +52 -0
  62. package/lib/view-identity.js +112 -0
  63. package/package.json +50 -14
  64. package/src/anchor-text.js +0 -160
  65. package/src/atomic-write.js +0 -53
  66. package/src/chrome-client.js +0 -2747
  67. package/src/chrome-session.js +0 -85
  68. package/src/chrome.css +0 -652
  69. package/src/chrome.html +0 -196
  70. package/src/cli.js +0 -271
  71. package/src/click-target.js +0 -26
  72. package/src/comment-anchor.js +0 -22
  73. package/src/comment-target.js +0 -164
  74. package/src/comparison-view.js +0 -337
  75. package/src/document-execution.js +0 -103
  76. package/src/edit-limits.js +0 -23
  77. package/src/editing.js +0 -99
  78. package/src/execution-client.js +0 -63
  79. package/src/frame-channel.js +0 -44
  80. package/src/frame-policy.js +0 -54
  81. package/src/history-client.js +0 -104
  82. package/src/history-coordinator.js +0 -186
  83. package/src/history-policy.js +0 -43
  84. package/src/history-server.js +0 -463
  85. package/src/html-transform.js +0 -62
  86. package/src/icons.js +0 -251
  87. package/src/paths.js +0 -91
  88. package/src/poll-transport.js +0 -222
  89. package/src/positioning.js +0 -107
  90. package/src/review-mode.js +0 -59
  91. package/src/revision-diff.js +0 -440
  92. package/src/revision-schema.js +0 -219
  93. package/src/revision-store.js +0 -177
  94. package/src/sdk.js +0 -2607
  95. package/src/semantic-snapshot.js +0 -230
  96. package/src/serialize.js +0 -32
  97. package/src/server-entry.js +0 -26
  98. package/src/server-lock.js +0 -101
  99. package/src/server.js +0 -1466
  100. package/src/setup-guidance.js +0 -128
  101. package/src/state.js +0 -732
  102. package/src/view-identity.js +0 -99
  103. /package/{src → lib}/document-trust.js +0 -0
@@ -0,0 +1,184 @@
1
+ import { createControllerStore } from "./controller-store.js";
2
+ import { historyPresentation } from "./history-coordinator.js";
3
+ import { changeKind, comparisonFreshness, excerpt } from "./history-client.js";
4
+ import { tidyMiddle } from "./anchor-text.js";
5
+ const roundId = (round) => round?.id || round?.roundId || "";
6
+ const timestamp = (value) => value == null ? NaN : new Date(value).getTime();
7
+ const formatTime = (value) => {
8
+ const time = timestamp(value);
9
+ return Number.isFinite(time) ? new Date(time).toLocaleString() : "Not available";
10
+ };
11
+ /** Pure normalization: reading or rendering never edits the authoritative history. */
12
+ export function changesSelection(history) {
13
+ const round = history.round && roundId(history.round) === history.selectedId ? history.round : null;
14
+ const targets = round?.targets || [];
15
+ const target = targets.find((item) => item.key === history.targetKey) || targets[0] || null;
16
+ const comparison = target?.comparison || {};
17
+ const modes = ["content", "source"].filter((mode) => comparison[mode]?.available === true);
18
+ const mode = modes.find((item) => item === history.preferredMode) || modes[0] || "content";
19
+ const value = comparison[mode] || {};
20
+ const items = value.changes || value.items || (mode === "content" ? comparison.changes || comparison.items : null) || [];
21
+ const index = Number.isSafeInteger(history.index) ? Math.max(0, Math.min(history.index, items.length - 1)) : 0;
22
+ return { round, target, targets, comparison, modes, mode, value, items, index, key: `${roundId(round)}:${target?.key || ""}` };
23
+ }
24
+ export function createChangesController(options) {
25
+ let disposed = false;
26
+ let diagnosticsOpen = false;
27
+ let limitationsOpen = false;
28
+ let pending = null;
29
+ let actionError = null;
30
+ let scrollSequence = 0;
31
+ let scrollRequest = null;
32
+ const getDetail = () => changesSelection(options.read().history);
33
+ let detailValue = getDetail().comparison[getDetail().mode] || null;
34
+ let detailVersion = 0;
35
+ function read() {
36
+ const context = options.read();
37
+ const history = context.history;
38
+ const selection = changesSelection(history);
39
+ const { round, target, comparison, modes, mode, value, items, index, key } = selection;
40
+ const failure = history.failures.get(key);
41
+ const presentation = historyPresentation({ ...history, round, target, comparison, failure });
42
+ const captureBusy = history.captureBusy || pending === "capture";
43
+ const finalizing = history.finalizing || pending === "finish";
44
+ const disabled = context.ended || !context.comparing;
45
+ const captureAllowed = !!target && !target.resultRevisionId && target.capture?.status !== "unavailable" &&
46
+ round?.feedbackStatus === "acknowledged";
47
+ const finishAllowed = !!target && !target.resultRevisionId && round?.feedbackStatus === "acknowledged" &&
48
+ ["pending", "failed"].includes(target.capture?.status || target.captureStatus || "pending");
49
+ const delay = timestamp(value.afterCapturedAt) - timestamp(round?.acknowledgedAt);
50
+ const view = mode === "content" ? value.viewComparison : null;
51
+ return {
52
+ disabled, loading: history.loading, key, mode, index, modes, detailVersion, scrollRequest, hasComparison: modes.length > 0,
53
+ rounds: history.rounds.map((item, position) => ({
54
+ value: roundId(item),
55
+ shortLabel: `Round ${item.ordinal || item.number || history.rounds.length - position}`,
56
+ label: `Round ${item.ordinal || item.number || history.rounds.length - position} · ${item.captureStatus === "ready" ? "completed" : item.captureStatus || item.status || item.feedbackStatus || "pending"}`,
57
+ })),
58
+ selectedId: history.selectedId || "",
59
+ targets: selection.targets.map((item) => ({ value: item.key, label: item.filename || item.label || item.key })),
60
+ targetKey: target?.key || "",
61
+ status: presentation.state,
62
+ message: history.loading ? round ? "Refreshing comparison…" : "Loading comparison…" : presentation.message,
63
+ error: actionError?.key === key ? actionError.message : history.error?.message || failure || target?.capture?.error || "",
64
+ canRetry: !!history.error && !history.loading,
65
+ counts: modes.length ? value.counts || null : null,
66
+ changes: items.map((item, position) => ({
67
+ value: String(position),
68
+ label: `${position + 1}. ${changeKind(item)} · ${tidyMiddle(item.label || excerpt(item.after) || excerpt(item.before) || "Structure", 65)}`,
69
+ })),
70
+ position: `${items.length ? index + 1 : 0} of ${items.length}`,
71
+ previousDisabled: disabled || history.loading || index === 0,
72
+ nextDisabled: disabled || history.loading || index >= items.length - 1,
73
+ captureAllowed, captureBusy,
74
+ captureDisabled: disabled || history.loading || context.sending || captureBusy || finalizing ||
75
+ target?.key !== context.current.key || !context.current.ready || context.current.pendingReload,
76
+ finishAllowed, finalizing,
77
+ finishDisabled: disabled || history.loading || context.sending || finalizing || captureBusy,
78
+ diagnosticsOpen, limitationsOpen,
79
+ unavailable: ["content", "source"].flatMap((item) => comparison[item]?.available === false
80
+ ? [`${item === "content" ? "Content" : "Source"} comparison unavailable: ${comparison[item]?.reason || "This representation could not be compared."}`] : []),
81
+ timing: round ? [
82
+ { label: "Before captured", value: formatTime(value.beforeCapturedAt) },
83
+ { label: "Agent acknowledged", value: formatTime(round.acknowledgedAt) },
84
+ { label: "After captured", value: formatTime(value.afterCapturedAt) },
85
+ ] : [],
86
+ delay: !Number.isFinite(delay) ? "" : delay >= 0
87
+ ? `Result captured ${Math.round(delay / 1000)} seconds after acknowledgment—not at the acknowledgment instant. A delayed capture can include later changes.`
88
+ : "This result snapshot predates acknowledgment. It is not proof of the page state at acknowledgment.",
89
+ freshness: target?.resultRevisionId || value.afterCapturedAt
90
+ ? comparisonFreshness(value, target?.key, context.current) : "",
91
+ view: view?.message ? view : null,
92
+ limitations: [...new Set([...(comparison.content?.limitations || []), ...(comparison.source?.limitations || [])])]
93
+ .map((item) => String(item).replaceAll("-", " ").replaceAll("_", " ")),
94
+ };
95
+ }
96
+ const store = createControllerStore(read);
97
+ const publish = () => {
98
+ if (disposed)
99
+ return;
100
+ const selection = getDetail();
101
+ const next = selection.comparison[selection.mode] || null;
102
+ if (detailValue !== next) {
103
+ detailValue = next;
104
+ detailVersion++;
105
+ }
106
+ store.publish();
107
+ };
108
+ const available = () => !disposed && !options.read().ended && options.read().comparing;
109
+ async function run(action, key) {
110
+ const current = read();
111
+ if (!available() || key !== current.key || pending ||
112
+ (action === "capture" ? !current.captureAllowed || current.captureDisabled : !current.finishAllowed || current.finishDisabled))
113
+ return;
114
+ pending = action;
115
+ actionError = null;
116
+ publish();
117
+ try {
118
+ await (action === "capture" ? options.capture() : options.finish());
119
+ }
120
+ catch (error) {
121
+ if (!disposed) {
122
+ const message = error instanceof Error ? error.message : String(error);
123
+ actionError = { key, message };
124
+ options.failed(message);
125
+ }
126
+ }
127
+ finally {
128
+ pending = null;
129
+ publish();
130
+ }
131
+ }
132
+ return {
133
+ getSnapshot: store.getSnapshot, subscribe: store.subscribe, publish, getDetail,
134
+ commands: {
135
+ selectRound(id) {
136
+ const history = options.read().history;
137
+ if (available() && history.selectedId !== id && history.rounds.some((round) => roundId(round) === id)) {
138
+ actionError = null;
139
+ scrollRequest = null;
140
+ return options.selectRound(id);
141
+ }
142
+ },
143
+ selectTarget(key) {
144
+ const selection = getDetail();
145
+ if (available() && !options.read().history.loading && key !== selection.target?.key && selection.targets.some((target) => target.key === key)) {
146
+ actionError = null;
147
+ scrollRequest = null;
148
+ return options.selectTarget(key);
149
+ }
150
+ },
151
+ selectMode(mode) {
152
+ const selection = getDetail();
153
+ if (available() && !options.read().history.loading && (mode === "content" || mode === "source") &&
154
+ mode !== selection.mode && selection.modes.includes(mode)) {
155
+ scrollRequest = null;
156
+ options.selectMode(mode);
157
+ }
158
+ },
159
+ jump(index, key) {
160
+ const selection = getDetail();
161
+ if (!available() || options.read().history.loading || key !== selection.key ||
162
+ !Number.isSafeInteger(index) || index < 0 || index >= selection.items.length)
163
+ return;
164
+ scrollRequest = { sequence: ++scrollSequence, key, mode: selection.mode, index };
165
+ options.selectIndex(index);
166
+ publish();
167
+ },
168
+ capture: (key) => run("capture", key),
169
+ finish: (key) => run("finish", key),
170
+ retry() { if (available() && !options.read().history.loading)
171
+ return options.refresh(); },
172
+ disclose(name, open) {
173
+ if (!available())
174
+ return;
175
+ if (name === "diagnostics")
176
+ diagnosticsOpen = open;
177
+ else
178
+ limitationsOpen = open;
179
+ publish();
180
+ },
181
+ },
182
+ dispose() { disposed = true; store.dispose(); },
183
+ };
184
+ }
@@ -0,0 +1,78 @@
1
+ export class ApiError extends Error {
2
+ status;
3
+ code;
4
+ targets;
5
+ constructor(message, status, code, targets) {
6
+ super(message);
7
+ this.status = status;
8
+ this.code = code;
9
+ this.targets = targets;
10
+ this.name = "ApiError";
11
+ }
12
+ }
13
+ export function record(value) {
14
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
15
+ throw new Error("The review server returned an invalid response.");
16
+ }
17
+ return value;
18
+ }
19
+ export function createReviewApi({ token, fetch: request = globalThis.fetch, }) {
20
+ const pending = new Set();
21
+ let disposed = false;
22
+ async function send(path, options = {}) {
23
+ if (disposed)
24
+ throw new Error("The review connection is closed.");
25
+ const controller = new AbortController();
26
+ const abort = () => controller.abort(options.signal?.reason);
27
+ if (options.signal?.aborted)
28
+ abort();
29
+ else
30
+ options.signal?.addEventListener("abort", abort, { once: true });
31
+ pending.add(controller);
32
+ try {
33
+ const headers = new Headers(options.headers);
34
+ if (!headers.has("content-type"))
35
+ headers.set("content-type", "application/json");
36
+ headers.set("x-doc-review-token", token);
37
+ const response = await request(path, { ...options, headers, signal: controller.signal });
38
+ if (!response.ok) {
39
+ let detail = {};
40
+ try {
41
+ detail = record(await response.json());
42
+ }
43
+ catch (error) {
44
+ if (controller.signal.aborted)
45
+ throw error;
46
+ // Preserve the HTTP failure even when the error body is not JSON.
47
+ }
48
+ throw new ApiError(typeof detail.error === "string" ? detail.error : `Request failed (${response.status})`, response.status, typeof detail.code === "string" ? detail.code : undefined, Array.isArray(detail.targets) ? detail.targets : []);
49
+ }
50
+ return await response.json();
51
+ }
52
+ finally {
53
+ pending.delete(controller);
54
+ options.signal?.removeEventListener("abort", abort);
55
+ }
56
+ }
57
+ async function requestJson(path, options, decode) {
58
+ const value = await send(path, options);
59
+ return decode ? decode(value) : value;
60
+ }
61
+ return {
62
+ request: requestJson,
63
+ dispose() {
64
+ if (disposed)
65
+ return;
66
+ disposed = true;
67
+ for (const controller of pending)
68
+ controller.abort(new Error("Review ended"));
69
+ pending.clear();
70
+ },
71
+ };
72
+ }
73
+ import { isPageResponse } from "./contracts/page.js";
74
+ export function decodePage(value) {
75
+ if (!isPageResponse(value))
76
+ throw new Error("The review server returned an invalid page response.");
77
+ return value;
78
+ }