@shadowclone/cli 0.0.28 → 0.0.29

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.
@@ -0,0 +1,195 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import {
3
+ reviewStyle,
4
+ findingMarkup,
5
+ setSourceLink,
6
+ sourceExcerptMarkup,
7
+ } from "../assets/presentation.mjs";
8
+ import { escapeHtml, jsonForHtml, markdown, fileCard, behaviorMarkup } from "./format.mjs";
9
+
10
+ function sourceLinks(options) {
11
+ return [...new Set(options.ids)]
12
+ .map((id) => {
13
+ const item = options.review.evidence.find((item) => item.id === id);
14
+ return (
15
+ '<button class="source-link" data-evidence="' +
16
+ id +
17
+ '" type="button" title="' +
18
+ escapeHtml(item.path) +
19
+ '">' +
20
+ escapeHtml(item.label ?? item.path.split("/").pop() + ":" + item.start) +
21
+ "</button>"
22
+ );
23
+ })
24
+ .join(" ");
25
+ }
26
+
27
+ function groupMarkup(options) {
28
+ const { group, review, index } = options;
29
+ const numbers = group.prs
30
+ .map((id) => "#" + review.pulls.find((pull) => pull.id === id).number)
31
+ .join(" + ");
32
+ const placedPaths = new Set();
33
+ const { flow, dryRuns } = behaviorMarkup({
34
+ group,
35
+ sources: (ids) => sourceLinks({ review, ids }),
36
+ });
37
+ const fileSlots = (paths) =>
38
+ [...new Set(paths)]
39
+ .filter((path) => !placedPaths.has(path))
40
+ .map((path) => {
41
+ placedPaths.add(path);
42
+ const related = group.files.filter((item) => item.path === path);
43
+ const combinedFile = review.combined?.files.find((file) => file.path === path);
44
+ const combinedCard = combinedFile
45
+ ? fileCard({
46
+ file: combinedFile,
47
+ open: true,
48
+ prLabel: "Combined stack",
49
+ url: review.pulls.find((pull) => pull.id === related.at(-1).pr).url,
50
+ })
51
+ : "";
52
+ const cards = related
53
+ .map((item) => {
54
+ const pull = review.pulls.find((pull) => pull.id === item.pr);
55
+ return fileCard({
56
+ file: pull.files.find((file) => file.path === path),
57
+ open: true,
58
+ prLabel: "#" + pull.number,
59
+ url: pull.url,
60
+ });
61
+ })
62
+ .join("");
63
+ return (
64
+ '<div class="file-slot" data-path="' +
65
+ escapeHtml(path) +
66
+ '" data-prs="' +
67
+ related.map((item) => item.pr).join(",") +
68
+ '">' +
69
+ combinedCard +
70
+ cards +
71
+ "</div>"
72
+ );
73
+ })
74
+ .join("");
75
+ const passages = group.steps
76
+ .map((step, stepIndex) => {
77
+ const files =
78
+ step.files ??
79
+ (step.evidence ?? [])
80
+ .map((id) => review.evidence.find((item) => item.id === id))
81
+ .filter((item) =>
82
+ group.files.some((file) => file.pr === item.pr && file.path === item.path),
83
+ );
84
+ const definitions = (step.definitions ?? [])
85
+ .map((id) =>
86
+ sourceExcerptMarkup({
87
+ item: review.evidence.find((item) => item.id === id),
88
+ pulls: review.pulls,
89
+ }),
90
+ )
91
+ .join("");
92
+ return `<section class="passage" data-anchor="${group.id}:passage:${stepIndex}" data-prs="${group.prs.join(",")}"><h3>${escapeHtml(step.title)}</h3>${markdown(step.text)}${definitions}<div class="sources">${sourceLinks({ review, ids: step.evidence ?? [] })}</div><div class="inline-notes" data-notes-at="${group.id}:passage:${stepIndex}"></div></section>${fileSlots(files.map((file) => file.path))}`;
93
+ })
94
+ .join("");
95
+ const remaining = fileSlots(group.files.map((file) => file.path));
96
+ return `<article class="reading-step" id="step-${group.id}" data-group="${group.id}" ${index ? "hidden" : ""}><div class="step-meta"><span>Step ${index + 1} of ${review.groups.length} · ${numbers}</span><label><input type="checkbox" data-reviewed="${group.id}"> Reviewed</label></div><h1>${escapeHtml(group.title)}</h1><div class="passage" data-anchor="${group.id}:summary" data-prs="${group.prs.join(",")}">${markdown(group.summary)}<div class="inline-notes" data-notes-at="${group.id}:summary"></div></div><p class="why">${escapeHtml(group.why)}</p><div class="before-after"><div><span>Before</span><p>${escapeHtml(group.before)}</p></div><div><span>After</span><p>${escapeHtml(group.after)}</p></div></div>${flow}${passages}${remaining ? '<div class="step-files"><h3>Remaining file changes</h3>' + remaining + "</div>" : ""}<div class="group-findings" data-findings-for="${group.id}"></div>${dryRuns}</article>`;
97
+ }
98
+
99
+ export async function renderReview(review) {
100
+ const asset = async (name) => readFile(new URL("../assets/" + name, import.meta.url), "utf8");
101
+ const count = review.pulls.reduce((total, pull) => total + pull.files.length, 0);
102
+ const findings = review.pulls.reduce((total, pull) => total + pull.findings.length, 0);
103
+ const prTabs =
104
+ review.pulls.length > 1
105
+ ? '<div class="pr-tabs">' +
106
+ (review.combined ? '<button data-pr="stack" type="button">Combined stack</button>' : "") +
107
+ review.pulls
108
+ .map(
109
+ (pull) =>
110
+ '<button data-pr="' +
111
+ pull.id +
112
+ '" type="button">#' +
113
+ pull.number +
114
+ " " +
115
+ escapeHtml(pull.title) +
116
+ "</button>",
117
+ )
118
+ .join("") +
119
+ "</div>"
120
+ : "";
121
+ const sidebar =
122
+ '<aside class="route"><h2>Reading route</h2>' +
123
+ review.groups
124
+ .map(
125
+ (group, index) =>
126
+ '<button class="route-step" data-step="' +
127
+ index +
128
+ '" type="button"><span>' +
129
+ (index + 1) +
130
+ "</span><span>" +
131
+ escapeHtml(group.title) +
132
+ "<small>" +
133
+ group.prs
134
+ .map((id) => "#" + review.pulls.find((pull) => pull.id === id).number)
135
+ .join(" + ") +
136
+ " · " +
137
+ group.files.length +
138
+ " file changes</small></span></button>",
139
+ )
140
+ .join("") +
141
+ '<div class="route-footer">' +
142
+ count +
143
+ " file changes across " +
144
+ review.pulls.length +
145
+ " PR" +
146
+ (review.pulls.length === 1 ? "" : "s") +
147
+ "<p>Read the code. Make your own notes. Then compare the agent findings.</p></div></aside>";
148
+ const ci = review.pulls
149
+ .map((pull) => {
150
+ const counts = new Map();
151
+ for (const check of pull.ci ?? [])
152
+ counts.set(
153
+ check.conclusion ?? check.state ?? "pending",
154
+ (counts.get(check.conclusion ?? check.state ?? "pending") ?? 0) + 1,
155
+ );
156
+ return (
157
+ '<p><a href="' +
158
+ escapeHtml(pull.url) +
159
+ '" target="_blank" rel="noreferrer">#' +
160
+ pull.number +
161
+ " " +
162
+ escapeHtml(pull.title) +
163
+ "</a><br><code>" +
164
+ pull.headSha.slice(0, 10) +
165
+ "</code> · " +
166
+ pull.files.length +
167
+ " files · " +
168
+ [...counts].map(([status, total]) => total + " " + status.toLowerCase()).join(", ") +
169
+ "</p>"
170
+ );
171
+ })
172
+ .join("");
173
+ const body = `<header><strong class="brand">◇ shadowclone <span>/ guided review</span></strong><div class="header-actions"><button id="toggle-findings" type="button">Agent findings <span>${findings}</span></button><button id="toggle-notes" type="button">Your notes <span id="note-count">0</span></button><button id="save-review" type="button">Save review</button><span id="save-status" role="status">Saved HTML</span></div></header><div class="document">${sidebar}<main><div class="overview"><h2>${escapeHtml(review.title)}</h2>${markdown(review.summary)}${review.status === "prototype" ? '<p class="prototype">Prototype: one part of the reading route. The full file inventory remains available.</p>' : ""}${prTabs}<div class="view-tabs"><button data-mode="guide" type="button">Guide</button><button data-mode="files" type="button">All changes (${count})</button></div><details class="pr-details"><summary>PR details and CI</summary>${ci}</details></div><div id="guide">${review.groups.map((group, index) => groupMarkup({ group, review, index })).join("")}<nav class="step-navigation"><button id="previous" type="button">Previous</button><button id="next" type="button">Next</button></nav></div><section id="files" hidden><h1>Every changed file</h1><p>These are the actual per-PR changes, including files outside the main reading step.</p><input id="file-search" type="search" placeholder="Find a file" aria-label="Find a changed file"><div id="file-list"></div></section></main></div><aside id="notes-panel" hidden><button id="close-notes" type="button">Close</button><section class="human-notes"><h2>Your review notes</h2><p>Save this HTML, then attach it to your chat and request staging.</p><div id="notes-list"></div></section><section class="agent-findings"><h2 id="findings-heading" hidden>Agent findings</h2><div id="findings-list"></div></section></aside><aside id="composer" hidden><form id="note-form"><div class="composer-title"><strong>Your note</strong><button id="cancel-note" type="button" aria-label="Cancel note">×</button></div><blockquote id="selected-quote"></blockquote><label id="target-label">Post on <select id="target-pr"></select></label><textarea id="note-body" aria-label="Your rough note" placeholder="Write the concern or question in your words." required></textarea><button type="submit">Save note</button></form></aside><button id="selection-comment" type="button" hidden>Comment</button><dialog id="source-dialog"><button id="close-source" type="button">Close</button><h2 id="source-title"></h2><p id="source-pin"></p><pre><code id="source-code"></code></pre><a id="source-link" target="_blank" rel="noreferrer">Open pinned source</a></dialog>`;
174
+ const format = (await readFile(new URL("./format.mjs", import.meta.url), "utf8")).replace(
175
+ /^export /gm,
176
+ "",
177
+ );
178
+ const scripts = [
179
+ format,
180
+ findingMarkup.toString(),
181
+ setSourceLink.toString(),
182
+ ...(await Promise.all(["document.mjs", "notes.mjs", "guide.mjs"].map(asset))),
183
+ ]
184
+ .map((script) => script.replace(/^export /gm, ""))
185
+ .join("\n");
186
+ return (await asset("review.html"))
187
+ .replace("{{TITLE}}", () => escapeHtml(review.title))
188
+ .replace("{{STYLE}}", () => reviewStyle)
189
+ .replace("{{BODY}}", () => body)
190
+ .replace("{{DATA}}", () => jsonForHtml(review))
191
+ .replace(
192
+ '<script type="module" id="review-runtime"></script>',
193
+ () => '<script type="module" id="review-runtime">' + scripts + "</script>",
194
+ );
195
+ }