@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.
- package/README.md +58 -222
- package/{src → lib}/SKILL.md +5 -2
- package/lib/anchor-text.js +155 -0
- package/lib/atomic-write.js +54 -0
- package/lib/changes-controller.js +184 -0
- package/lib/chrome-api.js +78 -0
- package/lib/chrome-client.js +1700 -0
- package/lib/chrome-session.js +73 -0
- package/lib/chrome.html +53 -0
- package/lib/cli.js +255 -0
- package/lib/click-target.js +28 -0
- package/lib/comment-anchor.js +23 -0
- package/lib/comment-target.js +164 -0
- package/lib/comments-controller.js +109 -0
- package/lib/contextual-controller.js +33 -0
- package/lib/contracts/feedback.js +1 -0
- package/lib/contracts/frame.js +7 -0
- package/lib/contracts/history.js +1 -0
- package/lib/contracts/index.js +2 -0
- package/lib/contracts/page.js +23 -0
- package/lib/controller-store.js +96 -0
- package/lib/document-execution.js +115 -0
- package/lib/edit-limits.js +24 -0
- package/lib/editing.js +107 -0
- package/lib/execution-client.js +19 -0
- package/lib/feedback-controller.js +113 -0
- package/lib/feedback-panel-controller.js +161 -0
- package/lib/frame-channel.js +42 -0
- package/lib/frame-controller.js +313 -0
- package/lib/frame-host.js +136 -0
- package/lib/frame-policy.js +50 -0
- package/lib/history-client.js +112 -0
- package/lib/history-coordinator.js +217 -0
- package/lib/history-policy.js +43 -0
- package/lib/history-server.js +464 -0
- package/lib/html-transform.js +52 -0
- package/lib/icons.js +325 -0
- package/{src → lib}/markdown.js +25 -26
- package/lib/paths.js +83 -0
- package/lib/poll-transport.js +220 -0
- package/lib/positioning.js +97 -0
- package/lib/recovery-controller.js +123 -0
- package/lib/review-controller.js +93 -0
- package/lib/review-mode.js +66 -0
- package/lib/revision-diff.js +482 -0
- package/lib/revision-schema.js +235 -0
- package/lib/revision-store.js +190 -0
- package/lib/save-controller.js +303 -0
- package/lib/sdk.js +2629 -0
- package/lib/semantic-snapshot.js +268 -0
- package/lib/serialize.js +32 -0
- package/lib/server-entry.js +26 -0
- package/lib/server-lock.js +109 -0
- package/lib/server.js +1501 -0
- package/lib/setup-guidance.js +119 -0
- package/{src → lib}/setup.js +44 -55
- package/lib/state.js +733 -0
- package/lib/toolbar-controller.js +32 -0
- package/lib/ui/THIRD_PARTY_NOTICES.md +1254 -0
- package/lib/ui/chrome.css +4881 -0
- package/lib/ui/chrome.js +52 -0
- package/lib/view-identity.js +112 -0
- package/package.json +50 -14
- package/src/anchor-text.js +0 -160
- package/src/atomic-write.js +0 -53
- package/src/chrome-client.js +0 -2747
- package/src/chrome-session.js +0 -85
- package/src/chrome.css +0 -652
- package/src/chrome.html +0 -196
- package/src/cli.js +0 -271
- package/src/click-target.js +0 -26
- package/src/comment-anchor.js +0 -22
- package/src/comment-target.js +0 -164
- package/src/comparison-view.js +0 -337
- package/src/document-execution.js +0 -103
- package/src/edit-limits.js +0 -23
- package/src/editing.js +0 -99
- package/src/execution-client.js +0 -63
- package/src/frame-channel.js +0 -44
- package/src/frame-policy.js +0 -54
- package/src/history-client.js +0 -104
- package/src/history-coordinator.js +0 -186
- package/src/history-policy.js +0 -43
- package/src/history-server.js +0 -463
- package/src/html-transform.js +0 -62
- package/src/icons.js +0 -251
- package/src/paths.js +0 -91
- package/src/poll-transport.js +0 -222
- package/src/positioning.js +0 -107
- package/src/review-mode.js +0 -59
- package/src/revision-diff.js +0 -440
- package/src/revision-schema.js +0 -219
- package/src/revision-store.js +0 -177
- package/src/sdk.js +0 -2607
- package/src/semantic-snapshot.js +0 -230
- package/src/serialize.js +0 -32
- package/src/server-entry.js +0 -26
- package/src/server-lock.js +0 -101
- package/src/server.js +0 -1466
- package/src/setup-guidance.js +0 -128
- package/src/state.js +0 -732
- package/src/view-identity.js +0 -99
- /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
|
+
}
|